Merge branch 'develop' into sg721-updatable-integration
This commit is contained in:
commit
3dc1843d06
@ -1,9 +1,9 @@
|
||||
APP_VERSION=0.4.5
|
||||
APP_VERSION=0.4.8
|
||||
|
||||
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
NEXT_PUBLIC_SG721_CODE_ID=793
|
||||
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=275
|
||||
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1s48apjumprma0d64ge88dmu7lr8exu02tlw90xxupgds0s25gfasx447dn"
|
||||
NEXT_PUBLIC_SG721_CODE_ID=1702
|
||||
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1701
|
||||
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1xz4d6wzxqn3udgsm5qnr78y032xng4r2ycv7aw6mjtsuw59s2n9s93ec0v"
|
||||
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34tfq3zk87l78sdxngzv"
|
||||
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
||||
NEXT_PUBLIC_BASE_MINTER_CODE_ID=613
|
||||
|
@ -7,6 +7,7 @@ export interface TooltipProps extends ComponentProps<'div'> {
|
||||
label: ReactNode
|
||||
children: ReactElement
|
||||
placement?: 'top' | 'bottom' | 'left' | 'right'
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
||||
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||
@ -33,7 +34,11 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||
<div
|
||||
{...props}
|
||||
{...attributes.popper}
|
||||
className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)}
|
||||
className={clsx(
|
||||
'py-1 px-2 m-1 text-sm rounded shadow-md',
|
||||
props.backgroundColor ? props.backgroundColor : 'bg-slate-900',
|
||||
props.className,
|
||||
)}
|
||||
ref={setPopperElement}
|
||||
style={{ ...styles.popper, ...props.style }}
|
||||
>
|
||||
|
@ -3,21 +3,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
// import { AirdropUpload } from 'components/AirdropUpload'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { Alert } from 'components/Alert'
|
||||
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
|
||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
|
||||
import { ActionsCombobox } from 'components/badges/actions/Combobox'
|
||||
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { AddressList } from 'components/forms/AddressList'
|
||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
|
||||
import * as crypto from 'crypto'
|
||||
import sizeof from 'object-sizeof'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -25,11 +29,12 @@ import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
import { sha256 } from 'utils/hash'
|
||||
import { generateKeyPairs, sha256 } from 'utils/hash'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
|
||||
import { AddressInput, TextInput } from '../../forms/FormInput'
|
||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||
|
||||
interface BadgeActionsProps {
|
||||
@ -52,8 +57,10 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
||||
const [keyPairs, setKeyPairs] = useState<string[]>([])
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [signature, setSignature] = useState<string>('')
|
||||
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
||||
|
||||
const actionComboboxState = useActionsComboboxState()
|
||||
const type = actionComboboxState.value?.id
|
||||
@ -147,18 +154,26 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const pubkeyState = useInputState({
|
||||
id: 'pubkey',
|
||||
name: 'pubkey',
|
||||
title: 'Pubkey',
|
||||
subtitle: 'The public key to check whether it can be used to mint a badge',
|
||||
const ownerListState = useAddressListState()
|
||||
|
||||
const pubKeyState = useInputState({
|
||||
id: 'pubKey',
|
||||
name: 'pubKey',
|
||||
title: 'Public Key',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The whitelisted public key authorized to mint a badge'
|
||||
: 'The public key to check whether it can be used to mint a badge',
|
||||
})
|
||||
|
||||
const privateKeyState = useInputState({
|
||||
id: 'privateKey',
|
||||
name: 'privateKey',
|
||||
title: 'Private Key',
|
||||
subtitle: 'The private key that was generated during badge creation',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The corresponding private key for the whitelisted public key'
|
||||
: 'The private key that was generated during badge creation',
|
||||
})
|
||||
|
||||
const nftState = useInputState({
|
||||
@ -172,13 +187,16 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
id: 'limit',
|
||||
name: 'limit',
|
||||
title: 'Limit',
|
||||
subtitle: 'Number of keys/owners to execute the action for',
|
||||
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
|
||||
})
|
||||
|
||||
const showMetadataField = isEitherType(type, ['edit_badge'])
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'mint_by_minter'])
|
||||
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
|
||||
const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
|
||||
const showOwnerList = isEitherType(type, ['mint_by_minter'])
|
||||
const showPubKeyField = isEitherType(type, ['mint_by_keys'])
|
||||
const showLimitState = isEitherType(type, ['purge_keys', 'purge_owners'])
|
||||
|
||||
const payload: DispatchExecuteArgs = {
|
||||
badge: {
|
||||
@ -231,11 +249,18 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
id: badgeId,
|
||||
editFee,
|
||||
owner: resolvedOwnerAddress,
|
||||
pubkey: pubkeyState.value,
|
||||
pubkey: pubKeyState.value,
|
||||
signature,
|
||||
keys: [],
|
||||
limit: limitState.value,
|
||||
owners: [],
|
||||
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||
limit: limitState.value || undefined,
|
||||
owners: [
|
||||
...new Set(
|
||||
ownerListState.values
|
||||
.map((a) => a.address.trim())
|
||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||
.concat(ownerList),
|
||||
),
|
||||
],
|
||||
recipients: airdropAllocationArray,
|
||||
privateKey: privateKeyState.value,
|
||||
nft: nftState.value,
|
||||
@ -355,6 +380,21 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
|
||||
}, [privateKeyState.value, resolvedOwnerAddress])
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfKeys > 0) {
|
||||
setKeyPairs(generateKeyPairs(numberOfKeys))
|
||||
}
|
||||
}, [numberOfKeys])
|
||||
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeId.toString()}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const { isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent) => {
|
||||
if (!wallet.client) {
|
||||
@ -467,19 +507,6 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerateKeys = (amount: number) => {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
let privKey: Buffer
|
||||
do {
|
||||
privKey = crypto.randomBytes(32)
|
||||
} while (!secp256k1.privateKeyVerify(privKey))
|
||||
|
||||
const privateKey = privKey.toString('hex')
|
||||
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
|
||||
keyPairs.push(publicKey.concat(',', privateKey))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="grid grid-cols-2 mt-4">
|
||||
@ -515,7 +542,65 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
title="Owner"
|
||||
/>
|
||||
)}
|
||||
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
|
||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
||||
{showLimitState && <NumberInput className="mt-2" {...limitState} />}
|
||||
<Conditional test={isEitherType(type, ['purge_owners', 'purge_keys'])}>
|
||||
<Alert className="mt-4" type="info">
|
||||
This action is only available if the badge with the specified id is either minted out or expired.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={type === 'add_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of public keys to be whitelisted for minting badges
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
|
||||
<Alert type="info">
|
||||
<div className="pt-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted public keys together with their private key counterparts.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Key Pairs
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={showOwnerList}>
|
||||
<div className="mt-4">
|
||||
<AddressList
|
||||
entries={ownerListState.entries}
|
||||
isRequired
|
||||
onAdd={ownerListState.add}
|
||||
onChange={ownerListState.update}
|
||||
onRemove={ownerListState.remove}
|
||||
subtitle="Enter the owner addresses"
|
||||
title="Addresses"
|
||||
/>
|
||||
<Alert className="mt-8" type="info">
|
||||
You may optionally choose a text file of additional owner addresses.
|
||||
</Alert>
|
||||
<WhitelistUpload onChange={setOwnerList} />
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
{showAirdropFileField && (
|
||||
<FormGroup
|
||||
|
@ -28,11 +28,6 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_key',
|
||||
name: 'Mint by Key',
|
||||
@ -43,6 +38,11 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Airdrop by Key',
|
||||
description: `Airdrop badges to a list of specified addresses`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
@ -51,6 +51,11 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new badge with a whitelisted private key`,
|
||||
},
|
||||
{
|
||||
id: 'add_keys',
|
||||
name: 'Add Keys',
|
||||
@ -66,11 +71,6 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new badge with a whitelisted private key`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
||||
@ -79,16 +79,16 @@ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new badge to specified owner addresses`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new badge to the specified addresses`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
|
@ -3,24 +3,30 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Trait } from 'contracts/badgeHub'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { BADGE_HUB_ADDRESS } from 'utils/constants'
|
||||
|
||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||
import { MetadataAttributes } from '../../forms/MetadataAttributes'
|
||||
import { Tooltip } from '../../Tooltip'
|
||||
import type { MintRule, UploadMethod } from './ImageUploadDetails'
|
||||
|
||||
interface BadgeDetailsProps {
|
||||
onChange: (data: BadgeDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod | undefined
|
||||
mintRule: MintRule
|
||||
metadataSize: number
|
||||
}
|
||||
|
||||
export interface BadgeDetailsDataProps {
|
||||
@ -38,10 +44,14 @@ export interface BadgeDetailsDataProps {
|
||||
youtube_url?: string
|
||||
}
|
||||
|
||||
export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
||||
export const BadgeDetails = ({ metadataSize, onChange }: BadgeDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [transferrable, setTransferrable] = useState<boolean>(false)
|
||||
const [metadataFile, setMetadataFile] = useState<File>()
|
||||
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
||||
|
||||
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const managerState = useInputState({
|
||||
id: 'manager-address',
|
||||
@ -109,6 +119,79 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
||||
subtitle: 'YouTube URL for the badge',
|
||||
})
|
||||
|
||||
const parseMetadata = async () => {
|
||||
try {
|
||||
let parsedMetadata: any
|
||||
if (metadataFile) {
|
||||
attributesState.reset()
|
||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
|
||||
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
||||
attributesState.add({
|
||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
||||
value: parsedMetadata.attributes[i].value,
|
||||
})
|
||||
}
|
||||
}
|
||||
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
||||
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
||||
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
||||
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
||||
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
||||
backgroundColorState.onChange(parsedMetadata.background_color ? parsedMetadata.background_color : '')
|
||||
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
||||
} else {
|
||||
attributesState.reset()
|
||||
nameState.onChange('')
|
||||
descriptionState.onChange('')
|
||||
externalUrlState.onChange('')
|
||||
youtubeUrlState.onChange('')
|
||||
animationUrlState.onChange('')
|
||||
backgroundColorState.onChange('')
|
||||
imageDataState.onChange('')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error parsing metadata file: Invalid JSON format.')
|
||||
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
||||
setMetadataFile(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFile(undefined)
|
||||
if (event.target.files === null) return
|
||||
|
||||
let selectedFile: File
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'application/json' })
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
||||
else return toast.error('No file selected.')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
setMetadataFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void parseMetadata()
|
||||
if (!metadataFile)
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
}, [metadataFile])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: BadgeDetailsDataProps = {
|
||||
@ -155,13 +238,25 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (attributesState.values.length === 0)
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
const retrieveFeeRate = async () => {
|
||||
try {
|
||||
if (wallet.client) {
|
||||
const feeRateRaw = await wallet.client.queryContractRaw(
|
||||
BADGE_HUB_ADDRESS,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
setMetadataFeeRate(Number(feeRate.metadata))
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error retrieving metadata fee rate.')
|
||||
setMetadataFeeRate(0)
|
||||
console.log('Error retrieving fee rate: ', error)
|
||||
}
|
||||
}
|
||||
void retrieveFeeRate()
|
||||
}, [wallet.client])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -172,10 +267,12 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<NumberInput className="mt-2" {...maxSupplyState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
|
||||
<FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
<div className="mt-2 form-control">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="mt-2 w-1/3 form-control">
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Transferrable</span>
|
||||
<input
|
||||
@ -186,6 +283,20 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Conditional test={managerState.value !== ''}>
|
||||
<Tooltip
|
||||
backgroundColor="bg-stargaze"
|
||||
className="bg-yellow-600"
|
||||
label="This is only an estimate. Be sure to check the final amount before signing the transaction."
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="grid grid-cols-2 ml-12 w-full">
|
||||
<div className="mt-4 font-bold">Fee Estimate:</div>
|
||||
<span className="mt-4">{(metadataSize * Number(metadataFeeRate)) / 1000000} stars</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('ml-10')}>
|
||||
<div>
|
||||
@ -197,6 +308,40 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
||||
title="Traits"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label="A metadata file can be selected to automatically fill in the related fields."
|
||||
placement="bottom"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Metadata File Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="application/json"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="metadataFile"
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,7 +172,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-8">
|
||||
<div className="p-3 py-5 pb-4">
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
@ -187,8 +187,17 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
||||
</Anchor>{' '}
|
||||
and upload your image manually to get an image URL for your badge.
|
||||
</p>
|
||||
<div>
|
||||
<TextInput {...imageUrlState} className="mt-2 ml-4 w-1/2" />
|
||||
<div className="flex flex-row w-full">
|
||||
<TextInput {...imageUrlState} className="mt-2 ml-6 w-full max-w-2xl" />
|
||||
<Conditional test={imageUrlState.value !== ''}>
|
||||
<div className="mt-2 ml-4 w-1/4 border-2 border-dashed">
|
||||
<img
|
||||
alt="badge-preview"
|
||||
className="w-full"
|
||||
src={imageUrlState.value.replace('IPFS://', 'ipfs://').replace(/,/g, '').replace(/"/g, '').trim()}
|
||||
/>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
@ -252,6 +261,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<label
|
||||
@ -280,6 +290,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={assetFile !== undefined}>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFile}
|
||||
|
@ -124,8 +124,8 @@ export const CollectionActions = ({
|
||||
const priceState = useNumberInputState({
|
||||
id: 'update-mint-price',
|
||||
name: 'updateMintPrice',
|
||||
title: 'Update Mint Price',
|
||||
subtitle: 'New minting price in STARS',
|
||||
title: type === 'update_discount_price' ? 'Discount Price' : 'Update Mint Price',
|
||||
subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'New minting price in STARS',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
@ -161,7 +161,7 @@ export const CollectionActions = ({
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '8%',
|
||||
placeholder: '5%',
|
||||
})
|
||||
|
||||
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
||||
@ -185,7 +185,7 @@ export const CollectionActions = ({
|
||||
'batch_mint_for',
|
||||
])
|
||||
const showAirdropFileField = type === 'airdrop'
|
||||
const showPriceField = type === 'update_mint_price'
|
||||
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
|
||||
const showDescriptionField = type === 'update_collection_info'
|
||||
const showImageField = type === 'update_collection_info'
|
||||
const showExternalLinkField = type === 'update_collection_info'
|
||||
@ -379,8 +379,8 @@ export const CollectionActions = ({
|
||||
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
|
||||
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
|
||||
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
|
||||
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
|
||||
{showPriceField && <NumberInput {...priceState} />}
|
||||
{showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />}
|
||||
{showPriceField && <NumberInput className="mt-2" {...priceState} />}
|
||||
{showDescriptionField && <TextInput className="my-2" {...descriptionState} />}
|
||||
{showImageField && <TextInput className="mb-2" {...imageState} />}
|
||||
{showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />}
|
||||
|
@ -13,6 +13,8 @@ export const ACTION_TYPES = [
|
||||
'mint_token_uri',
|
||||
'purge',
|
||||
'update_mint_price',
|
||||
'update_discount_price',
|
||||
'remove_discount_price',
|
||||
'mint_to',
|
||||
'mint_for',
|
||||
'batch_mint',
|
||||
@ -90,16 +92,21 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Mint',
|
||||
description: `Mint a token`,
|
||||
},
|
||||
{
|
||||
id: 'purge',
|
||||
name: 'Purge',
|
||||
description: `Purge`,
|
||||
},
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update mint price`,
|
||||
},
|
||||
{
|
||||
id: 'update_discount_price',
|
||||
name: 'Update Discount Price',
|
||||
description: `Update discount price`,
|
||||
},
|
||||
{
|
||||
id: 'remove_discount_price',
|
||||
name: 'Remove Discount Price',
|
||||
description: `Remove discount price`,
|
||||
},
|
||||
{
|
||||
id: 'mint_to',
|
||||
name: 'Mint To',
|
||||
@ -185,6 +192,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Burn Remaining Tokens',
|
||||
description: 'Burn remaining tokens',
|
||||
},
|
||||
{
|
||||
id: 'purge',
|
||||
name: 'Purge',
|
||||
description: `Purge`,
|
||||
},
|
||||
]
|
||||
|
||||
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
|
||||
@ -226,6 +238,8 @@ export type DispatchExecuteArgs = {
|
||||
| { type: Select<'mint_token_uri'>; tokenUri: string }
|
||||
| { type: Select<'purge'> }
|
||||
| { type: Select<'update_mint_price'>; price: string }
|
||||
| { type: Select<'update_discount_price'>; price: string }
|
||||
| { type: Select<'remove_discount_price'> }
|
||||
| { type: Select<'mint_to'>; recipient: string }
|
||||
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
|
||||
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
|
||||
@ -266,6 +280,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'update_mint_price': {
|
||||
return vendingMinterMessages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return vendingMinterMessages.updateDiscountPrice(txSigner, args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return vendingMinterMessages.removeDiscountPrice(txSigner)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return vendingMinterMessages.mintTo(txSigner, args.recipient)
|
||||
}
|
||||
@ -353,6 +373,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'update_mint_price': {
|
||||
return vendingMinterMessages(minterContract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return vendingMinterMessages(minterContract)?.updateDiscountPrice(args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return vendingMinterMessages(minterContract)?.removeDiscountPrice()
|
||||
}
|
||||
case 'mint_to': {
|
||||
return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '8%',
|
||||
placeholder: '5%',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -73,9 +73,10 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
|
||||
}, [traitTypeState.value, traitValueState.value, id])
|
||||
|
||||
return (
|
||||
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2">
|
||||
<div className="grid relative 2xl:grid-cols-[1fr_1fr_auto] 2xl:space-x-2">
|
||||
<TraitTypeInput {...traitTypeState} />
|
||||
<TraitValueInput {...traitValueState} />
|
||||
|
||||
<div className="flex justify-end items-end pb-2 w-8">
|
||||
<button
|
||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
||||
|
@ -2,7 +2,7 @@
|
||||
"path": "/assets/",
|
||||
"appName": "StargazeStudio",
|
||||
"appShortName": "StargazeStudio",
|
||||
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.",
|
||||
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT collection in no time.",
|
||||
"developerName": "StargazeStudio",
|
||||
"developerURL": "https://",
|
||||
"background": "#FFC27D",
|
||||
|
@ -28,7 +28,7 @@ export interface MigrateResponse {
|
||||
export interface Rule {
|
||||
by_key?: string
|
||||
by_minter?: string
|
||||
by_keys?: string[]
|
||||
by_keys?: string
|
||||
}
|
||||
|
||||
export interface Trait {
|
||||
@ -53,7 +53,7 @@ export interface Badge {
|
||||
manager: string
|
||||
metadata: Metadata
|
||||
transferrable: boolean
|
||||
rule: Rule
|
||||
rule: Rule | string
|
||||
expiry?: number
|
||||
max_supply?: number
|
||||
}
|
||||
@ -102,7 +102,7 @@ export interface CreateBadgeMessage {
|
||||
manager: string
|
||||
metadata: Metadata
|
||||
transferrable: boolean
|
||||
rule: Rule
|
||||
rule: Rule | string
|
||||
expiry?: number
|
||||
max_supply?: number
|
||||
}
|
||||
@ -349,6 +349,16 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
|
||||
}
|
||||
|
||||
const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => {
|
||||
const feeRateRaw = await client.queryContractRaw(
|
||||
contractAddress,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
console.log('Fee Rate:', feeRate)
|
||||
|
||||
console.log('keys size: ', sizeof(keys))
|
||||
console.log(keys)
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
@ -360,6 +370,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
|
@ -32,36 +32,36 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: ` Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
// {
|
||||
// id: 'add_keys',
|
||||
// name: 'Add Keys',
|
||||
// description: `Add keys to the badge with the specified ID`,
|
||||
// },
|
||||
// {
|
||||
// id: 'purge_keys',
|
||||
// name: 'Purge Keys',
|
||||
// description: `Purge keys from the badge with the specified ID`,
|
||||
// },
|
||||
{
|
||||
id: 'add_keys',
|
||||
name: 'Add Keys',
|
||||
description: `Add keys to the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_keys',
|
||||
name: 'Purge Keys',
|
||||
description: `Purge keys from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
// {
|
||||
// id: 'mint_by_minter',
|
||||
// name: 'Mint by Minter',
|
||||
// description: `Mint a new token by the minter with the specified ID`,
|
||||
// },
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new token by the minter with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_key',
|
||||
name: 'Mint by Key',
|
||||
description: `Mint a new token by the key with the specified ID`,
|
||||
},
|
||||
// {
|
||||
// id: 'mint_by_keys',
|
||||
// name: 'Mint by Keys',
|
||||
// description: `Mint a new token by the keys with the specified ID`,
|
||||
// },
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new token by the keys with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'set_nft',
|
||||
name: 'Set NFT',
|
||||
|
@ -14,8 +14,8 @@ export const QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
||||
// { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' },
|
||||
// { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||
{ id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' },
|
||||
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||
]
|
||||
|
||||
export interface DispatchQueryProps {
|
||||
|
@ -48,6 +48,8 @@ export interface VendingMinterInstance {
|
||||
withdraw: (senderAddress: string) => Promise<string>
|
||||
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
||||
burnRemaining: (senderAddress: string) => Promise<string>
|
||||
updateDiscountPrice: (senderAddress: string, price: string) => Promise<string>
|
||||
removeDiscountPrice: (senderAddress: string) => Promise<string>
|
||||
}
|
||||
|
||||
export interface VendingMinterMessages {
|
||||
@ -66,6 +68,8 @@ export interface VendingMinterMessages {
|
||||
withdraw: () => WithdrawMessage
|
||||
airdrop: (recipients: string[]) => CustomMessage
|
||||
burnRemaining: () => BurnRemainingMessage
|
||||
updateDiscountPrice: (price: string) => UpdateDiscountPriceMessage
|
||||
removeDiscountPrice: () => RemoveDiscountPriceMessage
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
@ -97,6 +101,26 @@ export interface UpdateMintPriceMessage {
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateDiscountPriceMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_discount_price: {
|
||||
price: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface RemoveDiscountPriceMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
remove_discount_price: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface SetWhitelistMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
@ -326,6 +350,36 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateDiscountPrice = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_discount_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const removeDiscountPrice = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
remove_discount_price: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const setWhitelist = async (senderAddress: string, whitelist: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
@ -552,6 +606,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
mint,
|
||||
purge,
|
||||
updateMintPrice,
|
||||
updateDiscountPrice,
|
||||
removeDiscountPrice,
|
||||
setWhitelist,
|
||||
updateStartTime,
|
||||
updateStartTradingTime,
|
||||
@ -633,6 +689,30 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
}
|
||||
}
|
||||
|
||||
const updateDiscountPrice = (price: string): UpdateDiscountPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_discount_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const removeDiscountPrice = (): RemoveDiscountPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
remove_discount_price: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const setWhitelist = (whitelist: string): SetWhitelistMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
@ -795,6 +875,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
||||
mint,
|
||||
purge,
|
||||
updateMintPrice,
|
||||
updateDiscountPrice,
|
||||
removeDiscountPrice,
|
||||
setWhitelist,
|
||||
updateStartTime,
|
||||
updateStartTradingTime,
|
||||
|
@ -7,6 +7,8 @@ export const EXECUTE_TYPES = [
|
||||
'mint',
|
||||
'purge',
|
||||
'update_mint_price',
|
||||
'update_discount_price',
|
||||
'remove_discount_price',
|
||||
'set_whitelist',
|
||||
'update_start_time',
|
||||
'update_start_trading_time',
|
||||
@ -39,6 +41,16 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
name: 'Update Mint Price',
|
||||
description: `Update mint price`,
|
||||
},
|
||||
{
|
||||
id: 'update_discount_price',
|
||||
name: 'Update Discount Price',
|
||||
description: `Update discount price`,
|
||||
},
|
||||
{
|
||||
id: 'remove_discount_price',
|
||||
name: 'Remove Discount Price',
|
||||
description: `Remove discount price`,
|
||||
},
|
||||
{
|
||||
id: 'set_whitelist',
|
||||
name: 'Set Whitelist',
|
||||
@ -98,6 +110,8 @@ export type DispatchExecuteArgs = {
|
||||
| { type: Select<'mint'> }
|
||||
| { type: Select<'purge'> }
|
||||
| { type: Select<'update_mint_price'>; price: string }
|
||||
| { type: Select<'update_discount_price'>; price: string }
|
||||
| { type: Select<'remove_discount_price'> }
|
||||
| { type: Select<'set_whitelist'>; whitelist: string }
|
||||
| { type: Select<'update_start_time'>; startTime: string }
|
||||
| { type: Select<'update_start_trading_time'>; startTime?: string }
|
||||
@ -123,6 +137,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'update_mint_price': {
|
||||
return messages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return messages.updateDiscountPrice(txSigner, args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return messages.removeDiscountPrice(txSigner)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return messages.setWhitelist(txSigner, args.whitelist)
|
||||
}
|
||||
@ -167,6 +187,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'update_mint_price': {
|
||||
return messages(contract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'update_discount_price': {
|
||||
return messages(contract)?.updateDiscountPrice(args.price)
|
||||
}
|
||||
case 'remove_discount_price': {
|
||||
return messages(contract)?.removeDiscountPrice()
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return messages(contract)?.setWhitelist(args.whitelist)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stargaze-studio",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.8",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
@ -1,5 +1,8 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
@ -20,12 +23,14 @@ import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Badge } from 'contracts/badgeHub'
|
||||
import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute'
|
||||
import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute'
|
||||
import * as crypto from 'crypto'
|
||||
import { toPng } from 'html-to-image'
|
||||
import type { NextPage } from 'next'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import sizeof from 'object-sizeof'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
@ -36,8 +41,11 @@ import { copy } from 'utils/clipboard'
|
||||
import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
import { truncateMiddle } from 'utils/text'
|
||||
|
||||
import { generateKeyPairs } from '../../utils/hash'
|
||||
|
||||
const BadgeCreationPage: NextPage = () => {
|
||||
const wallet = useWallet()
|
||||
const { badgeHub: badgeHubContract } = useContracts()
|
||||
@ -50,13 +58,18 @@ const BadgeCreationPage: NextPage = () => {
|
||||
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [creatingBadge, setCreatingBadge] = useState(false)
|
||||
const [isAddingKeysComplete, setIsAddingKeysComplete] = useState(false)
|
||||
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
|
||||
const [mintRule, setMintRule] = useState<MintRule>('by_key')
|
||||
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
|
||||
const [tempBadge, setTempBadge] = useState<Badge>()
|
||||
|
||||
const [badgeId, setBadgeId] = useState<string | null>(null)
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
||||
const [transactionHash, setTransactionHash] = useState<string | null>(null)
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(1)
|
||||
const qrRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const keyState = useInputState({
|
||||
@ -66,6 +79,14 @@ const BadgeCreationPage: NextPage = () => {
|
||||
subtitle: 'Part of the key pair to be utilized for post-creation access control',
|
||||
})
|
||||
|
||||
const designatedMinterState = useInputState({
|
||||
id: 'designatedMinter',
|
||||
name: 'designatedMinter',
|
||||
title: 'Minter Address',
|
||||
subtitle: 'The address of the designated minter for this badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const performBadgeCreationChecks = () => {
|
||||
try {
|
||||
setReadyToCreateBadge(false)
|
||||
@ -112,6 +133,46 @@ const BadgeCreationPage: NextPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveMinterAddress = async () => {
|
||||
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedMinterAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveMinterAddress()
|
||||
}, [designatedMinterState.value])
|
||||
|
||||
useEffect(() => {
|
||||
const badge = {
|
||||
manager: badgeDetails?.manager as string,
|
||||
metadata: {
|
||||
name: badgeDetails?.name || undefined,
|
||||
description: badgeDetails?.description || undefined,
|
||||
image: imageUrl || undefined,
|
||||
image_data: badgeDetails?.image_data || undefined,
|
||||
external_url: badgeDetails?.external_url || undefined,
|
||||
attributes: badgeDetails?.attributes || undefined,
|
||||
background_color: badgeDetails?.background_color || undefined,
|
||||
animation_url: badgeDetails?.animation_url || undefined,
|
||||
youtube_url: badgeDetails?.youtube_url || undefined,
|
||||
},
|
||||
transferrable: badgeDetails?.transferrable as boolean,
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: badgeDetails?.expiry || undefined,
|
||||
max_supply: badgeDetails?.max_supply || undefined,
|
||||
}
|
||||
setTempBadge(badge)
|
||||
}, [badgeDetails, keyState.value, mintRule, resolvedMinterAddress, imageUrl])
|
||||
|
||||
const createNewBadge = async () => {
|
||||
try {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected')
|
||||
@ -133,9 +194,16 @@ const BadgeCreationPage: NextPage = () => {
|
||||
youtube_url: badgeDetails?.youtube_url || undefined,
|
||||
},
|
||||
transferrable: badgeDetails?.transferrable as boolean,
|
||||
rule: {
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: badgeDetails?.expiry || undefined,
|
||||
max_supply: badgeDetails?.max_supply || undefined,
|
||||
}
|
||||
@ -147,13 +215,50 @@ const BadgeCreationPage: NextPage = () => {
|
||||
badge,
|
||||
type: 'create_badge',
|
||||
}
|
||||
if (mintRule !== 'by_keys') {
|
||||
setBadgeId(null)
|
||||
setIsAddingKeysComplete(false)
|
||||
const data = await badgeHubDispatchExecute(payload)
|
||||
console.log(data)
|
||||
setCreatingBadge(false)
|
||||
setTransactionHash(data.split(':')[0])
|
||||
setBadgeId(data.split(':')[1])
|
||||
} catch (error: any) {
|
||||
} else {
|
||||
setBadgeId(null)
|
||||
setIsAddingKeysComplete(false)
|
||||
setKeyPairs([])
|
||||
const generatedKeyPairs = generateKeyPairs(numberOfKeys)
|
||||
setKeyPairs(generatedKeyPairs)
|
||||
await badgeHubDispatchExecute(payload)
|
||||
.then(async (data) => {
|
||||
setCreatingBadge(false)
|
||||
setTransactionHash(data.split(':')[0])
|
||||
setBadgeId(data.split(':')[1])
|
||||
const res = await toast.promise(
|
||||
badgeHubContract.use(BADGE_HUB_ADDRESS)?.addKeys(
|
||||
wallet.address,
|
||||
Number(data.split(':')[1]),
|
||||
generatedKeyPairs.map((key) => key.publicKey),
|
||||
) as Promise<string>,
|
||||
{
|
||||
loading: 'Adding keys...',
|
||||
success: (result) => {
|
||||
setIsAddingKeysComplete(true)
|
||||
return `Keys added successfully! Tx Hash: ${result}`
|
||||
},
|
||||
error: (error: { message: any }) => `Failed to add keys: ${error.message}`,
|
||||
},
|
||||
)
|
||||
})
|
||||
.catch((error: { message: any }) => {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
setUploading(false)
|
||||
setIsAddingKeysComplete(false)
|
||||
setCreatingBadge(false)
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message, { style: { maxWidth: 'none' } })
|
||||
setCreatingBadge(false)
|
||||
setUploading(false)
|
||||
}
|
||||
@ -184,7 +289,8 @@ const BadgeCreationPage: NextPage = () => {
|
||||
|
||||
const checkBadgeDetails = () => {
|
||||
if (!badgeDetails) throw new Error('Please fill out the required fields')
|
||||
if (keyState.value === '' || !createdBadgeKey) throw new Error('Please generate a public key')
|
||||
if (mintRule === 'by_key' && (keyState.value === '' || !createdBadgeKey))
|
||||
throw new Error('Please generate a public key')
|
||||
if (badgeDetails.external_url) {
|
||||
try {
|
||||
const url = new URL(badgeDetails.external_url)
|
||||
@ -219,7 +325,15 @@ const BadgeCreationPage: NextPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// copy claim url to clipboard
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeId as string}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const copyClaimURL = async () => {
|
||||
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||
const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}`
|
||||
@ -244,6 +358,12 @@ const BadgeCreationPage: NextPage = () => {
|
||||
setReadyToCreateBadge(false)
|
||||
}, [imageUploadDetails?.uploadMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (keyPairs.length > 0) {
|
||||
toast.success('Key pairs generated successfully.')
|
||||
}
|
||||
}, [keyPairs])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NextSeo title="Create Badge" />
|
||||
@ -265,6 +385,7 @@ const BadgeCreationPage: NextPage = () => {
|
||||
</div>
|
||||
<div className="mx-10" ref={scrollRef}>
|
||||
<Conditional test={badgeId !== null}>
|
||||
<Conditional test={mintRule === 'by_key'}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
@ -349,13 +470,121 @@ const BadgeCreationPage: NextPage = () => {
|
||||
</span>
|
||||
</div>
|
||||
<br />
|
||||
<span className="mt-4">You may download the QR code or copy the claim URL to share with others.</span>
|
||||
<span className="mt-4">
|
||||
You may download the QR code or copy the claim URL to share with others.
|
||||
</span>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_keys'}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge ID:{` ${badgeId as string}`}
|
||||
<br />
|
||||
Transaction Hash: {' '}
|
||||
<Conditional test={NETWORK === 'testnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<br />
|
||||
<Conditional test={isAddingKeysComplete}>
|
||||
<div className="pt-2 mt-4 border-t-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted keys added during badge creation.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Keys
|
||||
</Button>
|
||||
</div>
|
||||
</Conditional>
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may click{' '}
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
|
||||
badgeId as string
|
||||
}`}
|
||||
>
|
||||
here
|
||||
</Anchor>{' '}
|
||||
and select Actions {'>'} Add Keys to add (additional) whitelisted keys or select Actions {'>'}{' '}
|
||||
Mint by Keys to use one of the keys to mint a badge.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_minter'}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge ID:{` ${badgeId as string}`}
|
||||
<br />
|
||||
Designated Minter Address: {` ${resolvedMinterAddress}`}
|
||||
<br />
|
||||
Transaction Hash: {' '}
|
||||
<Conditional test={NETWORK === 'testnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<br />
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may click{' '}
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
|
||||
badgeId as string
|
||||
}`}
|
||||
>
|
||||
here
|
||||
</Anchor>{' '}
|
||||
and select Actions {'>'} Mint By Minter to mint a badge.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -373,82 +602,137 @@ const BadgeCreationPage: NextPage = () => {
|
||||
mintRule === 'by_key' ? 'border-stargaze' : 'border-transparent',
|
||||
mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="m-0 w-1/3"
|
||||
label="The same single private key can be utilized by multiple users to share badge minting authority. Ideal for projects with multiple administrators."
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
onClick={() => {
|
||||
setMintRule('by_key')
|
||||
setReadyToCreateBadge(false)
|
||||
setBadgeId(null)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Mint Rule: By Key</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
Badges can be minted more than once with a badge specific message signed by a designated private key.
|
||||
<span className="text-sm text-white/80 line-clamp-4">
|
||||
Multiple badges can be minted to different addresses by the owner of a single designated key.
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent',
|
||||
mintRule !== 'by_keys' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5',
|
||||
mintRule !== 'by_keys' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="m-0 w-1/3"
|
||||
label="The key pairs are intended to be saved and shared with others. Each user can claim a badge separately using the key pair that they received."
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
disabled
|
||||
onClick={() => {
|
||||
setMintRule('by_keys')
|
||||
setReadyToCreateBadge(false)
|
||||
setBadgeId(null)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Mint Rule: By Keys</h4>
|
||||
<span className="text-sm text-slate-500 line-clamp-2">
|
||||
Similar to the By Key rule, however each designated private key can only be used once to mint a badge.
|
||||
<span className="text-sm text-white/80 line-clamp-4">
|
||||
Multiple key pairs are generated and designated to be only used once to mint a single badge.
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent',
|
||||
mintRule !== 'by_minter' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5',
|
||||
mintRule !== 'by_minter' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
className="m-0 w-1/3"
|
||||
label="The most basic approach. However, having just one authorized address for minting badges might limit your ability to delegate that responsibility."
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
disabled
|
||||
onClick={() => {
|
||||
setMintRule('by_minter')
|
||||
setReadyToCreateBadge(false)
|
||||
setBadgeId(null)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Mint Rule: By Minter</h4>
|
||||
<span className="text-sm line-clamp-2 text-slate/500">
|
||||
Badges can be minted by a designated minter account.
|
||||
<span className="text-sm text-white/80 line-clamp-4">
|
||||
No key designation. Multiple badges can be minted to different addresses by a pre-determined minter
|
||||
address.
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-10">
|
||||
<ImageUploadDetails mintRule={mintRule} onChange={setImageUploadDetails} />
|
||||
|
||||
<Conditional test={mintRule === 'by_key'}>
|
||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||
<TextInput className="ml-4 w-full max-w-2xl" {...keyState} disabled required />
|
||||
<Button className="mt-14 ml-4" isDisabled={creatingBadge} onClick={handleGenerateKey}>
|
||||
Generate Key
|
||||
</Button>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of key pairs to be whitelisted for post-creation access control
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 w-1/4 max-w-2xl bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_minter'}>
|
||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||
<TextInput className="ml-4 w-full max-w-lg" {...designatedMinterState} required />
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
|
||||
<BadgeDetails
|
||||
metadataSize={
|
||||
tempBadge?.metadata.image === undefined
|
||||
? Number(sizeof(tempBadge)) + Number(sizeof(tempBadge?.metadata.attributes)) + 150
|
||||
: Number(sizeof(tempBadge)) + Number(sizeof(tempBadge.metadata.attributes))
|
||||
}
|
||||
mintRule={mintRule}
|
||||
onChange={setBadgeDetails}
|
||||
uploadMethod={imageUploadDetails?.uploadMethod ? imageUploadDetails.uploadMethod : 'new'}
|
||||
|
@ -49,12 +49,12 @@ const BadgeList: NextPage = () => {
|
||||
{myBadges.map((badge: any, index: any) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="w-[55%] bg-black">
|
||||
<td className="w-[35%] bg-black">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="avatar">
|
||||
<div className="w-28 h-28 mask mask-squircle">
|
||||
<img
|
||||
alt="Cover"
|
||||
alt="badge-preview"
|
||||
src={
|
||||
(badge?.image as string).startsWith('ipfs')
|
||||
? `https://ipfs-gw.stargaze-apis.com/ipfs/${(badge?.image as string).substring(7)}`
|
||||
@ -64,13 +64,15 @@ const BadgeList: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<p className="overflow-auto max-w-xs font-bold no-scrollbar ">{badge.name}</p>
|
||||
<p className="overflow-auto max-w-xs font-bold no-scrollbar ">
|
||||
{badge.name ? badge.name : 'No name provided.'}
|
||||
</p>
|
||||
<p className="max-w-xs text-sm truncate opacity-50">Badge ID: {badge.tokenId}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="overflow-auto w-[35%] max-w-xl bg-black no-scrollbar">
|
||||
{badge.description}
|
||||
<td className="overflow-auto w-[55%] max-w-xl bg-black no-scrollbar">
|
||||
{badge.description ? badge.description : 'No description provided.'}
|
||||
{/* <br /> */}
|
||||
{/* <span className="badge badge-ghost badge-sm"></span> */}
|
||||
</td>
|
||||
|
@ -754,12 +754,17 @@ const CollectionCreationPage: NextPage = () => {
|
||||
mintingDetails.perAddressLimit > mintingDetails.numTokens
|
||||
)
|
||||
throw new Error('Invalid limit for tokens per address')
|
||||
if (
|
||||
mintingDetails.numTokens > 100 &&
|
||||
mintingDetails.numTokens < 100 * mintingDetails.perAddressLimit &&
|
||||
mintingDetails.perAddressLimit > mintingDetails.numTokens / 100
|
||||
if (mintingDetails.numTokens < 100 && mintingDetails.perAddressLimit > 3)
|
||||
throw new Error(
|
||||
'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.',
|
||||
)
|
||||
if (
|
||||
mintingDetails.numTokens >= 100 &&
|
||||
mintingDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
|
||||
)
|
||||
throw new Error(
|
||||
'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.',
|
||||
)
|
||||
throw new Error('Invalid limit for tokens per address. The limit cannot exceed 1% of the total number of tokens.')
|
||||
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
||||
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
|
||||
}
|
||||
@ -776,15 +781,25 @@ const CollectionCreationPage: NextPage = () => {
|
||||
const whitelistStartDate = new Date(Number(config?.start_time) / 1000000)
|
||||
throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`)
|
||||
}
|
||||
if (
|
||||
mintingDetails?.numTokens &&
|
||||
config?.per_address_limit &&
|
||||
mintingDetails.numTokens > 100 &&
|
||||
Number(config.per_address_limit) > mintingDetails.numTokens / 100
|
||||
)
|
||||
|
||||
if (mintingDetails?.numTokens && config?.per_address_limit) {
|
||||
if (mintingDetails.numTokens >= 100 && Number(config.per_address_limit) > 50) {
|
||||
throw Error(
|
||||
`Invalid limit for tokens per address (${config.per_address_limit} tokens). The limit cannot exceed 1% of the total number of tokens.`,
|
||||
`Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
|
||||
)
|
||||
} else if (
|
||||
mintingDetails.numTokens >= 100 &&
|
||||
Number(config.per_address_limit) > Math.ceil((mintingDetails.numTokens / 100) * 3)
|
||||
) {
|
||||
throw Error(
|
||||
`Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`,
|
||||
)
|
||||
} else if (mintingDetails.numTokens < 100 && Number(config.per_address_limit) > 3) {
|
||||
throw Error(
|
||||
`Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (whitelistDetails.whitelistType === 'new') {
|
||||
if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty')
|
||||
@ -801,15 +816,24 @@ const CollectionCreationPage: NextPage = () => {
|
||||
throw new Error('Whitelist start time cannot be later than whitelist end time')
|
||||
if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime))
|
||||
throw new Error('Whitelist start time must be the same as the minting start time')
|
||||
if (
|
||||
mintingDetails?.numTokens &&
|
||||
whitelistDetails.perAddressLimit &&
|
||||
mintingDetails.numTokens > 100 &&
|
||||
whitelistDetails.perAddressLimit > mintingDetails.numTokens / 100
|
||||
)
|
||||
if (whitelistDetails.perAddressLimit && mintingDetails?.numTokens) {
|
||||
if (mintingDetails.numTokens >= 100 && whitelistDetails.perAddressLimit > 50) {
|
||||
throw Error(
|
||||
`Invalid limit for tokens per address (${whitelistDetails.perAddressLimit} tokens). The limit cannot exceed 1% of the total number of tokens.`,
|
||||
`Invalid limit for tokens per address. Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
|
||||
)
|
||||
} else if (
|
||||
mintingDetails.numTokens >= 100 &&
|
||||
whitelistDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
|
||||
) {
|
||||
throw Error(
|
||||
`Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`,
|
||||
)
|
||||
} else if (mintingDetails.numTokens < 100 && whitelistDetails.perAddressLimit > 3) {
|
||||
throw Error(
|
||||
`Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,22 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
import type { MintRule } from 'components/badges/creation/ImageUploadDetails'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox'
|
||||
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressList } from 'components/forms/AddressList'
|
||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
@ -20,6 +25,7 @@ import { LinkTabs } from 'components/LinkTabs'
|
||||
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Badge } from 'contracts/badgeHub'
|
||||
@ -40,7 +46,8 @@ import { useMutation } from 'react-query'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
import { copy } from 'utils/clipboard'
|
||||
import { NETWORK } from 'utils/constants'
|
||||
import { sha256 } from 'utils/hash'
|
||||
import { generateKeyPairs, sha256 } from 'utils/hash'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
@ -62,10 +69,15 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null)
|
||||
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
|
||||
const [signature, setSignature] = useState<string>('')
|
||||
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
||||
const qrRef = useRef<HTMLDivElement>(null)
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [mintRule, setMintRule] = useState<MintRule>('by_key')
|
||||
|
||||
const comboboxState = useExecuteComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
@ -173,20 +185,26 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
name: 'owner',
|
||||
title: 'Owner',
|
||||
subtitle: 'The owner of the badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const ownerListState = useAddressListState()
|
||||
|
||||
const pubkeyState = useInputState({
|
||||
id: 'pubkey',
|
||||
name: 'pubkey',
|
||||
title: 'Pubkey',
|
||||
subtitle: 'The public key for the badge',
|
||||
subtitle: 'The whitelisted public key authorized to mint a badge',
|
||||
})
|
||||
|
||||
const privateKeyState = useInputState({
|
||||
id: 'privateKey',
|
||||
name: 'privateKey',
|
||||
title: 'Private Key',
|
||||
subtitle: 'The private key generated during badge creation',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The corresponding private key for the whitelisted public key'
|
||||
: 'The private key that was generated during badge creation',
|
||||
})
|
||||
|
||||
const nftState = useInputState({
|
||||
@ -200,15 +218,34 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
id: 'limit',
|
||||
name: 'limit',
|
||||
title: 'Limit',
|
||||
subtitle: 'Number of keys/owners to execute the action for',
|
||||
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
|
||||
})
|
||||
|
||||
const designatedMinterState = useInputState({
|
||||
id: 'designatedMinter',
|
||||
name: 'designatedMinter',
|
||||
title: 'Minter Address',
|
||||
subtitle: 'The address of the designated minter for this badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const showBadgeField = type === 'create_badge'
|
||||
const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge'])
|
||||
const showIdField = isEitherType(type, ['edit_badge', 'mint_by_key'])
|
||||
const showIdField = isEitherType(type, [
|
||||
'edit_badge',
|
||||
'add_keys',
|
||||
'purge_keys',
|
||||
'purge_owners',
|
||||
'mint_by_key',
|
||||
'mint_by_keys',
|
||||
'mint_by_minter',
|
||||
])
|
||||
const showLimitField = isEitherType(type, ['purge_keys', 'purge_owners'])
|
||||
const showNFTField = type === 'set_nft'
|
||||
const showOwnerField = type === 'mint_by_key'
|
||||
const showPrivateKeyField = type === 'mint_by_key'
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||
const showOwnerListField = isEitherType(type, ['mint_by_minter'])
|
||||
const showPubkeyField = isEitherType(type, ['mint_by_keys'])
|
||||
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||
|
||||
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||
const payload: DispatchExecuteArgs = {
|
||||
@ -234,9 +271,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable,
|
||||
rule: {
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
},
|
||||
@ -260,12 +304,19 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
id: badgeIdState.value,
|
||||
owner: ownerState.value,
|
||||
owner: resolvedOwnerAddress,
|
||||
pubkey: pubkeyState.value,
|
||||
signature,
|
||||
keys: [],
|
||||
limit: limitState.value,
|
||||
owners: [],
|
||||
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||
limit: limitState.value || undefined,
|
||||
owners: [
|
||||
...new Set(
|
||||
ownerListState.values
|
||||
.map((a) => a.address.trim())
|
||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||
.concat(ownerList),
|
||||
),
|
||||
],
|
||||
nft: nftState.value,
|
||||
editFee,
|
||||
contract: contractState.value,
|
||||
@ -337,6 +388,7 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
if (txHash) {
|
||||
setLastTx(txHash.split(':')[0])
|
||||
setCreatedBadgeId(txHash.split(':')[1])
|
||||
badgeIdState.onChange(!isNaN(Number(txHash.split(':')[1])) ? Number(txHash.split(':')[1]) : 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -388,6 +440,14 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
link.click()
|
||||
})
|
||||
}
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeIdState.value}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const copyClaimURL = async () => {
|
||||
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||
@ -409,6 +469,12 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfKeys > 0) {
|
||||
setKeyPairs(generateKeyPairs(numberOfKeys))
|
||||
}
|
||||
}, [numberOfKeys])
|
||||
|
||||
useEffect(() => {
|
||||
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
|
||||
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
|
||||
@ -448,6 +514,15 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
void resolveOwnerAddress()
|
||||
}, [ownerState.value])
|
||||
|
||||
const resolveMinterAddress = async () => {
|
||||
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedMinterAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveMinterAddress()
|
||||
}, [designatedMinterState.value])
|
||||
|
||||
const resolveManagerAddress = async () => {
|
||||
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setBadge({
|
||||
@ -472,9 +547,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable,
|
||||
rule: {
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
})
|
||||
@ -508,9 +590,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable,
|
||||
rule: {
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
})
|
||||
@ -541,7 +630,7 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
/>
|
||||
<LinkTabs activeIndex={2} data={badgeHubLinkTabs} />
|
||||
|
||||
{showBadgeField && createdBadgeId && createdBadgeKey && (
|
||||
{showBadgeField && createdBadgeId && createdBadgeKey && mintRule === 'by_key' && (
|
||||
<div className="flex flex-row">
|
||||
<div className="ml-4">
|
||||
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
||||
@ -598,14 +687,116 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBadgeField && createdBadgeId && mintRule === 'by_keys' && (
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge ID:{` ${createdBadgeId as string}`}
|
||||
<br />
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may select Message Type {'>'} Add Keys to add whitelisted keys authorized to mint a badge.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{showBadgeField && createdBadgeId && mintRule === 'by_minter' && (
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge successfully created with ID:{` ${createdBadgeId as string}`}
|
||||
<br />
|
||||
Designated Minter Address: {` ${resolvedMinterAddress}`}
|
||||
<br />
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may select Message Type {'>'} Mint by Minter to mint badges using the designated minter wallet.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<ExecuteCombobox {...comboboxState} />
|
||||
<Conditional test={type === 'create_badge'}>
|
||||
<div className={clsx('flex flex-col space-y-2')}>
|
||||
<div>
|
||||
<div className="flex">
|
||||
<span className="mt-1 text-base font-bold first-letter:capitalize">Mint Rule: </span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={mintRule === 'by_key'}
|
||||
className="peer sr-only"
|
||||
id="ruleRadio1"
|
||||
name="ruletRadio1"
|
||||
onClick={() => {
|
||||
setMintRule('by_key')
|
||||
setCreatedBadgeId(null)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="ruleRadio1"
|
||||
>
|
||||
By Key
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={mintRule === 'by_keys'}
|
||||
className="peer sr-only"
|
||||
id="ruleRadio2"
|
||||
name="ruletRadio2"
|
||||
onClick={() => {
|
||||
setMintRule('by_keys')
|
||||
setCreatedBadgeId(null)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="ruleRadio2"
|
||||
>
|
||||
By Keys
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={mintRule === 'by_minter'}
|
||||
className="peer sr-only"
|
||||
id="ruleRadio3"
|
||||
name="ruletRadio3"
|
||||
onClick={() => {
|
||||
setMintRule('by_minter')
|
||||
setCreatedBadgeId(null)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="ruleRadio3"
|
||||
>
|
||||
By Minter
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
{showIdField && <NumberInput {...badgeIdState} />}
|
||||
{showBadgeField && <AddressInput {...managerState} />}
|
||||
{showBadgeField && <TextInput {...keyState} />}
|
||||
{showBadgeField && <Button onClick={handleGenerateKey}>Generate Key</Button>}
|
||||
{showBadgeField && mintRule === 'by_key' && <TextInput {...keyState} />}
|
||||
{showBadgeField && mintRule === 'by_key' && <Button onClick={handleGenerateKey}>Generate Key</Button>}
|
||||
{showBadgeField && mintRule === 'by_minter' && <TextInput {...designatedMinterState} />}
|
||||
{showMetadataField && (
|
||||
<div className="p-4 rounded-md border-2 border-gray-800">
|
||||
<span className="text-gray-400">Metadata</span>
|
||||
@ -636,6 +827,57 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
title="Owner"
|
||||
/>
|
||||
)}
|
||||
<Conditional test={showOwnerListField}>
|
||||
<div className="mt-4">
|
||||
<AddressList
|
||||
entries={ownerListState.entries}
|
||||
isRequired
|
||||
onAdd={ownerListState.add}
|
||||
onChange={ownerListState.update}
|
||||
onRemove={ownerListState.remove}
|
||||
subtitle="Enter the owner addresses"
|
||||
title="Addresses"
|
||||
/>
|
||||
<Alert className="mt-8" type="info">
|
||||
You may optionally choose a text file of additional owner addresses.
|
||||
</Alert>
|
||||
<WhitelistUpload onChange={setOwnerList} />
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={type === 'add_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of public keys to be whitelisted for minting badges
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
|
||||
<Alert type="info">
|
||||
<div className="pt-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted public keys together with their private key counterparts.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Key Pairs
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
{showLimitField && <NumberInput {...limitState} />}
|
||||
{showPubkeyField && <TextInput className="mt-2" {...pubkeyState} />}
|
||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
||||
{showNFTField && <AddressInput {...nftState} />}
|
||||
</div>
|
||||
|
@ -154,7 +154,7 @@ const BadgeHubQueryPage: NextPage = () => {
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
<Conditional test={type === 'getBadge' || type === 'getKey'}>
|
||||
<Conditional test={type === 'getBadge' || type === 'getKey' || type === 'getKeys'}>
|
||||
<NumberInput {...idState} />
|
||||
</Conditional>
|
||||
<Conditional test={type === 'getKey'}>
|
||||
|
@ -105,7 +105,7 @@ const BaseMinterInstantiatePage: NextPage = () => {
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '8%',
|
||||
placeholder: '5%',
|
||||
})
|
||||
|
||||
const { data, isLoading, mutate } = useMutation(
|
||||
|
@ -55,8 +55,8 @@ const VendingMinterExecutePage: NextPage = () => {
|
||||
const priceState = useNumberInputState({
|
||||
id: 'price',
|
||||
name: 'price',
|
||||
title: 'Price',
|
||||
subtitle: 'Enter the token price',
|
||||
title: type === 'update_discount_price' ? 'Discount Price' : 'Price',
|
||||
subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'Enter the token price',
|
||||
})
|
||||
|
||||
const contractState = useInputState({
|
||||
@ -86,7 +86,7 @@ const VendingMinterExecutePage: NextPage = () => {
|
||||
const showLimitField = type === 'update_per_address_limit'
|
||||
const showTokenIdField = type === 'mint_for'
|
||||
const showRecipientField = isEitherType(type, ['mint_to', 'mint_for'])
|
||||
const showPriceField = type === 'update_mint_price'
|
||||
const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
|
||||
|
||||
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||
const payload: DispatchExecuteArgs = {
|
||||
|
@ -103,7 +103,7 @@ const VendingMinterInstantiatePage: NextPage = () => {
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '8%',
|
||||
placeholder: '5%',
|
||||
})
|
||||
|
||||
const unitPriceState = useNumberInputState({
|
||||
|
@ -15,8 +15,8 @@ const HomePage: NextPage = () => {
|
||||
Looking for a fast and efficient way to build an NFT collection? Stargaze Studio is the solution.
|
||||
<br />
|
||||
<br />
|
||||
Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own
|
||||
NFT collections in no time.
|
||||
Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT
|
||||
collections in no time.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import * as crypto from 'crypto'
|
||||
import { Word32Array } from 'jscrypto'
|
||||
import { SHA256 } from 'jscrypto/SHA256'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
@ -23,3 +24,21 @@ export function generateSignature(id: number, owner: string, privateKey: string)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function generateKeyPairs(amount: number) {
|
||||
const keyPairs: { publicKey: string; privateKey: string }[] = []
|
||||
for (let i = 0; i < amount; i++) {
|
||||
let privKey: Buffer
|
||||
do {
|
||||
privKey = crypto.randomBytes(32)
|
||||
} while (!secp256k1.privateKeyVerify(privKey))
|
||||
|
||||
const privateKey = privKey.toString('hex')
|
||||
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
|
||||
keyPairs.push({
|
||||
publicKey,
|
||||
privateKey,
|
||||
})
|
||||
}
|
||||
return keyPairs
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user