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_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||||
NEXT_PUBLIC_SG721_CODE_ID=793
|
NEXT_PUBLIC_SG721_CODE_ID=1702
|
||||||
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=275
|
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1701
|
||||||
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1s48apjumprma0d64ge88dmu7lr8exu02tlw90xxupgds0s25gfasx447dn"
|
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1xz4d6wzxqn3udgsm5qnr78y032xng4r2ycv7aw6mjtsuw59s2n9s93ec0v"
|
||||||
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34tfq3zk87l78sdxngzv"
|
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34tfq3zk87l78sdxngzv"
|
||||||
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
||||||
NEXT_PUBLIC_BASE_MINTER_CODE_ID=613
|
NEXT_PUBLIC_BASE_MINTER_CODE_ID=613
|
||||||
|
@ -7,6 +7,7 @@ export interface TooltipProps extends ComponentProps<'div'> {
|
|||||||
label: ReactNode
|
label: ReactNode
|
||||||
children: ReactElement
|
children: ReactElement
|
||||||
placement?: 'top' | 'bottom' | 'left' | 'right'
|
placement?: 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
backgroundColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
||||||
@ -33,7 +34,11 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
|
|||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
{...attributes.popper}
|
{...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}
|
ref={setPopperElement}
|
||||||
style={{ ...styles.popper, ...props.style }}
|
style={{ ...styles.popper, ...props.style }}
|
||||||
>
|
>
|
||||||
|
@ -3,21 +3,25 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
// import { AirdropUpload } from 'components/AirdropUpload'
|
// import { AirdropUpload } from 'components/AirdropUpload'
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
import { toUtf8 } from '@cosmjs/encoding'
|
||||||
|
import { Alert } from 'components/Alert'
|
||||||
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
|
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
|
||||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
|
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
|
||||||
import { ActionsCombobox } from 'components/badges/actions/Combobox'
|
import { ActionsCombobox } from 'components/badges/actions/Combobox'
|
||||||
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
|
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
|
||||||
import { Button } from 'components/Button'
|
import { Button } from 'components/Button'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
import { FormControl } from 'components/FormControl'
|
import { FormControl } from 'components/FormControl'
|
||||||
import { FormGroup } from 'components/FormGroup'
|
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 { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||||
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
|
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
|
||||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||||
import { JsonPreview } from 'components/JsonPreview'
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
import { TransactionHash } from 'components/TransactionHash'
|
import { TransactionHash } from 'components/TransactionHash'
|
||||||
|
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||||
import { useWallet } from 'contexts/wallet'
|
import { useWallet } from 'contexts/wallet'
|
||||||
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
|
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
|
||||||
import * as crypto from 'crypto'
|
|
||||||
import sizeof from 'object-sizeof'
|
import sizeof from 'object-sizeof'
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -25,11 +29,12 @@ import { toast } from 'react-hot-toast'
|
|||||||
import { FaArrowRight } from 'react-icons/fa'
|
import { FaArrowRight } from 'react-icons/fa'
|
||||||
import { useMutation } from 'react-query'
|
import { useMutation } from 'react-query'
|
||||||
import * as secp256k1 from 'secp256k1'
|
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 { resolveAddress } from 'utils/resolveAddress'
|
||||||
|
|
||||||
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
|
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
|
||||||
import { AddressInput, TextInput } from '../../forms/FormInput'
|
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||||
|
|
||||||
interface BadgeActionsProps {
|
interface BadgeActionsProps {
|
||||||
@ -52,8 +57,10 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
|||||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
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 [signature, setSignature] = useState<string>('')
|
||||||
|
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||||
|
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
||||||
|
|
||||||
const actionComboboxState = useActionsComboboxState()
|
const actionComboboxState = useActionsComboboxState()
|
||||||
const type = actionComboboxState.value?.id
|
const type = actionComboboxState.value?.id
|
||||||
@ -147,18 +154,26 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
|||||||
defaultValue: wallet.address,
|
defaultValue: wallet.address,
|
||||||
})
|
})
|
||||||
|
|
||||||
const pubkeyState = useInputState({
|
const ownerListState = useAddressListState()
|
||||||
id: 'pubkey',
|
|
||||||
name: 'pubkey',
|
const pubKeyState = useInputState({
|
||||||
title: 'Pubkey',
|
id: 'pubKey',
|
||||||
subtitle: 'The public key to check whether it can be used to mint a badge',
|
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({
|
const privateKeyState = useInputState({
|
||||||
id: 'privateKey',
|
id: 'privateKey',
|
||||||
name: 'privateKey',
|
name: 'privateKey',
|
||||||
title: 'Private Key',
|
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({
|
const nftState = useInputState({
|
||||||
@ -172,13 +187,16 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
|||||||
id: 'limit',
|
id: 'limit',
|
||||||
name: 'limit',
|
name: 'limit',
|
||||||
title: '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 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 showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
|
||||||
const showAirdropFileField = isEitherType(type, ['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 = {
|
const payload: DispatchExecuteArgs = {
|
||||||
badge: {
|
badge: {
|
||||||
@ -231,11 +249,18 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
|||||||
id: badgeId,
|
id: badgeId,
|
||||||
editFee,
|
editFee,
|
||||||
owner: resolvedOwnerAddress,
|
owner: resolvedOwnerAddress,
|
||||||
pubkey: pubkeyState.value,
|
pubkey: pubKeyState.value,
|
||||||
signature,
|
signature,
|
||||||
keys: [],
|
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||||
limit: limitState.value,
|
limit: limitState.value || undefined,
|
||||||
owners: [],
|
owners: [
|
||||||
|
...new Set(
|
||||||
|
ownerListState.values
|
||||||
|
.map((a) => a.address.trim())
|
||||||
|
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||||
|
.concat(ownerList),
|
||||||
|
),
|
||||||
|
],
|
||||||
recipients: airdropAllocationArray,
|
recipients: airdropAllocationArray,
|
||||||
privateKey: privateKeyState.value,
|
privateKey: privateKeyState.value,
|
||||||
nft: nftState.value,
|
nft: nftState.value,
|
||||||
@ -355,6 +380,21 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
|||||||
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
|
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
|
||||||
}, [privateKeyState.value, resolvedOwnerAddress])
|
}, [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(
|
const { isLoading, mutate } = useMutation(
|
||||||
async (event: FormEvent) => {
|
async (event: FormEvent) => {
|
||||||
if (!wallet.client) {
|
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 (
|
return (
|
||||||
<form>
|
<form>
|
||||||
<div className="grid grid-cols-2 mt-4">
|
<div className="grid grid-cols-2 mt-4">
|
||||||
@ -515,7 +542,65 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
|||||||
title="Owner"
|
title="Owner"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
|
||||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
{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 && (
|
{showAirdropFileField && (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
@ -28,11 +28,6 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Edit Badge',
|
name: 'Edit Badge',
|
||||||
description: `Edit badge metadata for the badge with the specified ID`,
|
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',
|
id: 'mint_by_key',
|
||||||
name: 'Mint by Key',
|
name: 'Mint by Key',
|
||||||
@ -43,6 +38,11 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Airdrop by Key',
|
name: 'Airdrop by Key',
|
||||||
description: `Airdrop badges to a list of specified addresses`,
|
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[] = [
|
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||||
@ -51,6 +51,11 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Edit Badge',
|
name: 'Edit Badge',
|
||||||
description: `Edit badge metadata for the badge with the specified ID`,
|
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',
|
id: 'add_keys',
|
||||||
name: 'Add Keys',
|
name: 'Add Keys',
|
||||||
@ -66,11 +71,6 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Purge Owners',
|
name: 'Purge Owners',
|
||||||
description: `Purge owners from the badge with the specified ID`,
|
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[] = [
|
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
||||||
@ -79,16 +79,16 @@ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Edit Badge',
|
name: 'Edit Badge',
|
||||||
description: `Edit badge metadata for the badge with the specified ID`,
|
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',
|
id: 'purge_owners',
|
||||||
name: 'Purge Owners',
|
name: 'Purge Owners',
|
||||||
description: `Purge owners from the badge with the specified ID`,
|
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 {
|
export interface DispatchExecuteProps {
|
||||||
|
@ -3,24 +3,30 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
|
||||||
|
import { toUtf8 } from '@cosmjs/encoding'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { Conditional } from 'components/Conditional'
|
||||||
import { FormControl } from 'components/FormControl'
|
import { FormControl } from 'components/FormControl'
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||||
import { InputDateTime } from 'components/InputDateTime'
|
import { InputDateTime } from 'components/InputDateTime'
|
||||||
import { useWallet } from 'contexts/wallet'
|
import { useWallet } from 'contexts/wallet'
|
||||||
import type { Trait } from 'contracts/badgeHub'
|
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 { toast } from 'react-hot-toast'
|
||||||
|
import { BADGE_HUB_ADDRESS } from 'utils/constants'
|
||||||
|
|
||||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||||
import { MetadataAttributes } from '../../forms/MetadataAttributes'
|
import { MetadataAttributes } from '../../forms/MetadataAttributes'
|
||||||
|
import { Tooltip } from '../../Tooltip'
|
||||||
import type { MintRule, UploadMethod } from './ImageUploadDetails'
|
import type { MintRule, UploadMethod } from './ImageUploadDetails'
|
||||||
|
|
||||||
interface BadgeDetailsProps {
|
interface BadgeDetailsProps {
|
||||||
onChange: (data: BadgeDetailsDataProps) => void
|
onChange: (data: BadgeDetailsDataProps) => void
|
||||||
uploadMethod: UploadMethod | undefined
|
uploadMethod: UploadMethod | undefined
|
||||||
mintRule: MintRule
|
mintRule: MintRule
|
||||||
|
metadataSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BadgeDetailsDataProps {
|
export interface BadgeDetailsDataProps {
|
||||||
@ -38,10 +44,14 @@ export interface BadgeDetailsDataProps {
|
|||||||
youtube_url?: string
|
youtube_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
export const BadgeDetails = ({ metadataSize, onChange }: BadgeDetailsProps) => {
|
||||||
const wallet = useWallet()
|
const wallet = useWallet()
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||||
const [transferrable, setTransferrable] = useState<boolean>(false)
|
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({
|
const managerState = useInputState({
|
||||||
id: 'manager-address',
|
id: 'manager-address',
|
||||||
@ -109,6 +119,79 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
|||||||
subtitle: 'YouTube URL for the badge',
|
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(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const data: BadgeDetailsDataProps = {
|
const data: BadgeDetailsDataProps = {
|
||||||
@ -155,13 +238,25 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (attributesState.values.length === 0)
|
const retrieveFeeRate = async () => {
|
||||||
attributesState.add({
|
try {
|
||||||
trait_type: '',
|
if (wallet.client) {
|
||||||
value: '',
|
const feeRateRaw = await wallet.client.queryContractRaw(
|
||||||
})
|
BADGE_HUB_ADDRESS,
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -172,19 +267,35 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
|||||||
<TextInput className="mt-2" {...descriptionState} />
|
<TextInput className="mt-2" {...descriptionState} />
|
||||||
<NumberInput className="mt-2" {...maxSupplyState} />
|
<NumberInput className="mt-2" {...maxSupplyState} />
|
||||||
<TextInput className="mt-2" {...externalUrlState} />
|
<TextInput className="mt-2" {...externalUrlState} />
|
||||||
|
|
||||||
<FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
|
<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} />
|
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="mt-2 form-control">
|
<div className="grid grid-cols-2">
|
||||||
<label className="justify-start cursor-pointer label">
|
<div className="mt-2 w-1/3 form-control">
|
||||||
<span className="mr-4 font-bold">Transferrable</span>
|
<label className="justify-start cursor-pointer label">
|
||||||
<input
|
<span className="mr-4 font-bold">Transferrable</span>
|
||||||
checked={transferrable}
|
<input
|
||||||
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
|
checked={transferrable}
|
||||||
onClick={() => setTransferrable(!transferrable)}
|
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||||
type="checkbox"
|
onClick={() => setTransferrable(!transferrable)}
|
||||||
/>
|
type="checkbox"
|
||||||
</label>
|
/>
|
||||||
|
</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>
|
</div>
|
||||||
<div className={clsx('ml-10')}>
|
<div className={clsx('ml-10')}>
|
||||||
@ -197,6 +308,40 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
|
|||||||
title="Traits"
|
title="Traits"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,7 +172,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 py-5 pb-8">
|
<div className="p-3 py-5 pb-4">
|
||||||
<Conditional test={uploadMethod === 'existing'}>
|
<Conditional test={uploadMethod === 'existing'}>
|
||||||
<div className="ml-3 flex-column">
|
<div className="ml-3 flex-column">
|
||||||
<p className="mb-5 ml-5">
|
<p className="mb-5 ml-5">
|
||||||
@ -187,8 +187,17 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
|||||||
</Anchor>{' '}
|
</Anchor>{' '}
|
||||||
and upload your image manually to get an image URL for your badge.
|
and upload your image manually to get an image URL for your badge.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div className="flex flex-row w-full">
|
||||||
<TextInput {...imageUrlState} className="mt-2 ml-4 w-1/2" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Conditional>
|
</Conditional>
|
||||||
@ -252,31 +261,33 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
|
|||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="grid grid-cols-2">
|
<div className="grid grid-cols-2">
|
||||||
<div className="w-full">
|
<div>
|
||||||
<div>
|
<div className="w-full">
|
||||||
<label
|
<div>
|
||||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
<label
|
||||||
htmlFor="assetFile"
|
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||||
>
|
htmlFor="assetFile"
|
||||||
Image Selection
|
>
|
||||||
</label>
|
Image Selection
|
||||||
<div
|
</label>
|
||||||
className={clsx(
|
<div
|
||||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
|
||||||
'rounded border-2 border-white/20 border-dashed',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
accept="image/*"
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
'rounded border-2 border-white/20 border-dashed',
|
||||||
)}
|
)}
|
||||||
id="assetFile"
|
>
|
||||||
onChange={selectAsset}
|
<input
|
||||||
ref={assetFileRef}
|
accept="image/*"
|
||||||
type="file"
|
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="assetFile"
|
||||||
|
onChange={selectAsset}
|
||||||
|
ref={assetFileRef}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,8 +124,8 @@ export const CollectionActions = ({
|
|||||||
const priceState = useNumberInputState({
|
const priceState = useNumberInputState({
|
||||||
id: 'update-mint-price',
|
id: 'update-mint-price',
|
||||||
name: 'updateMintPrice',
|
name: 'updateMintPrice',
|
||||||
title: 'Update Mint Price',
|
title: type === 'update_discount_price' ? 'Discount Price' : 'Update Mint Price',
|
||||||
subtitle: 'New minting price in STARS',
|
subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'New minting price in STARS',
|
||||||
})
|
})
|
||||||
|
|
||||||
const descriptionState = useInputState({
|
const descriptionState = useInputState({
|
||||||
@ -161,7 +161,7 @@ export const CollectionActions = ({
|
|||||||
name: 'royaltyShare',
|
name: 'royaltyShare',
|
||||||
title: 'Share Percentage',
|
title: 'Share Percentage',
|
||||||
subtitle: 'Percentage of royalties to be paid',
|
subtitle: 'Percentage of royalties to be paid',
|
||||||
placeholder: '8%',
|
placeholder: '5%',
|
||||||
})
|
})
|
||||||
|
|
||||||
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
||||||
@ -185,7 +185,7 @@ export const CollectionActions = ({
|
|||||||
'batch_mint_for',
|
'batch_mint_for',
|
||||||
])
|
])
|
||||||
const showAirdropFileField = type === 'airdrop'
|
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 showDescriptionField = type === 'update_collection_info'
|
||||||
const showImageField = type === 'update_collection_info'
|
const showImageField = type === 'update_collection_info'
|
||||||
const showExternalLinkField = type === 'update_collection_info'
|
const showExternalLinkField = type === 'update_collection_info'
|
||||||
@ -379,8 +379,8 @@ export const CollectionActions = ({
|
|||||||
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
|
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
|
||||||
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
|
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
|
||||||
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
|
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
|
||||||
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
|
{showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />}
|
||||||
{showPriceField && <NumberInput {...priceState} />}
|
{showPriceField && <NumberInput className="mt-2" {...priceState} />}
|
||||||
{showDescriptionField && <TextInput className="my-2" {...descriptionState} />}
|
{showDescriptionField && <TextInput className="my-2" {...descriptionState} />}
|
||||||
{showImageField && <TextInput className="mb-2" {...imageState} />}
|
{showImageField && <TextInput className="mb-2" {...imageState} />}
|
||||||
{showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />}
|
{showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />}
|
||||||
|
@ -13,6 +13,8 @@ export const ACTION_TYPES = [
|
|||||||
'mint_token_uri',
|
'mint_token_uri',
|
||||||
'purge',
|
'purge',
|
||||||
'update_mint_price',
|
'update_mint_price',
|
||||||
|
'update_discount_price',
|
||||||
|
'remove_discount_price',
|
||||||
'mint_to',
|
'mint_to',
|
||||||
'mint_for',
|
'mint_for',
|
||||||
'batch_mint',
|
'batch_mint',
|
||||||
@ -90,16 +92,21 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Mint',
|
name: 'Mint',
|
||||||
description: `Mint a token`,
|
description: `Mint a token`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'purge',
|
|
||||||
name: 'Purge',
|
|
||||||
description: `Purge`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'update_mint_price',
|
id: 'update_mint_price',
|
||||||
name: 'Update Mint Price',
|
name: 'Update Mint Price',
|
||||||
description: `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',
|
id: 'mint_to',
|
||||||
name: 'Mint To',
|
name: 'Mint To',
|
||||||
@ -185,6 +192,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
|||||||
name: 'Burn Remaining Tokens',
|
name: 'Burn Remaining Tokens',
|
||||||
description: 'Burn remaining tokens',
|
description: 'Burn remaining tokens',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'purge',
|
||||||
|
name: 'Purge',
|
||||||
|
description: `Purge`,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
|
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
|
||||||
@ -226,6 +238,8 @@ export type DispatchExecuteArgs = {
|
|||||||
| { type: Select<'mint_token_uri'>; tokenUri: string }
|
| { type: Select<'mint_token_uri'>; tokenUri: string }
|
||||||
| { type: Select<'purge'> }
|
| { type: Select<'purge'> }
|
||||||
| { type: Select<'update_mint_price'>; price: string }
|
| { 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_to'>; recipient: string }
|
||||||
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
|
| { type: Select<'mint_for'>; recipient: string; tokenId: number }
|
||||||
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
|
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
|
||||||
@ -266,6 +280,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
|||||||
case 'update_mint_price': {
|
case 'update_mint_price': {
|
||||||
return vendingMinterMessages.updateMintPrice(txSigner, args.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': {
|
case 'mint_to': {
|
||||||
return vendingMinterMessages.mintTo(txSigner, args.recipient)
|
return vendingMinterMessages.mintTo(txSigner, args.recipient)
|
||||||
}
|
}
|
||||||
@ -353,6 +373,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
|||||||
case 'update_mint_price': {
|
case 'update_mint_price': {
|
||||||
return vendingMinterMessages(minterContract)?.updateMintPrice(args.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': {
|
case 'mint_to': {
|
||||||
return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
|
return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
|||||||
name: 'royaltyShare',
|
name: 'royaltyShare',
|
||||||
title: 'Share Percentage',
|
title: 'Share Percentage',
|
||||||
subtitle: 'Percentage of royalties to be paid',
|
subtitle: 'Percentage of royalties to be paid',
|
||||||
placeholder: '8%',
|
placeholder: '5%',
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -73,9 +73,10 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
|
|||||||
}, [traitTypeState.value, traitValueState.value, id])
|
}, [traitTypeState.value, traitValueState.value, id])
|
||||||
|
|
||||||
return (
|
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} />
|
<TraitTypeInput {...traitTypeState} />
|
||||||
<TraitValueInput {...traitValueState} />
|
<TraitValueInput {...traitValueState} />
|
||||||
|
|
||||||
<div className="flex justify-end items-end pb-2 w-8">
|
<div className="flex justify-end items-end pb-2 w-8">
|
||||||
<button
|
<button
|
||||||
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"path": "/assets/",
|
"path": "/assets/",
|
||||||
"appName": "StargazeStudio",
|
"appName": "StargazeStudio",
|
||||||
"appShortName": "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",
|
"developerName": "StargazeStudio",
|
||||||
"developerURL": "https://",
|
"developerURL": "https://",
|
||||||
"background": "#FFC27D",
|
"background": "#FFC27D",
|
||||||
|
@ -28,7 +28,7 @@ export interface MigrateResponse {
|
|||||||
export interface Rule {
|
export interface Rule {
|
||||||
by_key?: string
|
by_key?: string
|
||||||
by_minter?: string
|
by_minter?: string
|
||||||
by_keys?: string[]
|
by_keys?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trait {
|
export interface Trait {
|
||||||
@ -53,7 +53,7 @@ export interface Badge {
|
|||||||
manager: string
|
manager: string
|
||||||
metadata: Metadata
|
metadata: Metadata
|
||||||
transferrable: boolean
|
transferrable: boolean
|
||||||
rule: Rule
|
rule: Rule | string
|
||||||
expiry?: number
|
expiry?: number
|
||||||
max_supply?: number
|
max_supply?: number
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export interface CreateBadgeMessage {
|
|||||||
manager: string
|
manager: string
|
||||||
metadata: Metadata
|
metadata: Metadata
|
||||||
transferrable: boolean
|
transferrable: boolean
|
||||||
rule: Rule
|
rule: Rule | string
|
||||||
expiry?: number
|
expiry?: number
|
||||||
max_supply?: 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 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(
|
const res = await client.execute(
|
||||||
senderAddress,
|
senderAddress,
|
||||||
contractAddress,
|
contractAddress,
|
||||||
@ -360,6 +370,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
|
|||||||
},
|
},
|
||||||
'auto',
|
'auto',
|
||||||
'',
|
'',
|
||||||
|
[coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
|
||||||
)
|
)
|
||||||
|
|
||||||
return res.transactionHash
|
return res.transactionHash
|
||||||
|
@ -32,36 +32,36 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
|||||||
name: 'Edit Badge',
|
name: 'Edit Badge',
|
||||||
description: ` Edit badge metadata for the badge with the specified ID`,
|
description: ` Edit badge metadata for the badge with the specified ID`,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// id: 'add_keys',
|
id: 'add_keys',
|
||||||
// name: 'Add Keys',
|
name: 'Add Keys',
|
||||||
// description: `Add keys to the badge with the specified ID`,
|
description: `Add keys to the badge with the specified ID`,
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// id: 'purge_keys',
|
id: 'purge_keys',
|
||||||
// name: 'Purge Keys',
|
name: 'Purge Keys',
|
||||||
// description: `Purge keys from the badge with the specified ID`,
|
description: `Purge keys from the badge with the specified ID`,
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
id: 'purge_owners',
|
id: 'purge_owners',
|
||||||
name: 'Purge Owners',
|
name: 'Purge Owners',
|
||||||
description: `Purge owners from the badge with the specified ID`,
|
description: `Purge owners from the badge with the specified ID`,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// id: 'mint_by_minter',
|
id: 'mint_by_minter',
|
||||||
// name: 'Mint by Minter',
|
name: 'Mint by Minter',
|
||||||
// description: `Mint a new token by the minter with the specified ID`,
|
description: `Mint a new token by the minter with the specified ID`,
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
id: 'mint_by_key',
|
id: 'mint_by_key',
|
||||||
name: 'Mint by Key',
|
name: 'Mint by Key',
|
||||||
description: `Mint a new token by the key with the specified ID`,
|
description: `Mint a new token by the key with the specified ID`,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// id: 'mint_by_keys',
|
id: 'mint_by_keys',
|
||||||
// name: 'Mint by Keys',
|
name: 'Mint by Keys',
|
||||||
// description: `Mint a new token by the keys with the specified ID`,
|
description: `Mint a new token by the keys with the specified ID`,
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
id: 'set_nft',
|
id: 'set_nft',
|
||||||
name: 'Set NFT',
|
name: 'Set NFT',
|
||||||
|
@ -14,8 +14,8 @@ export const QUERY_LIST: QueryListItem[] = [
|
|||||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
{ 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: '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: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface DispatchQueryProps {
|
export interface DispatchQueryProps {
|
||||||
|
@ -48,6 +48,8 @@ export interface VendingMinterInstance {
|
|||||||
withdraw: (senderAddress: string) => Promise<string>
|
withdraw: (senderAddress: string) => Promise<string>
|
||||||
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
||||||
burnRemaining: (senderAddress: string) => Promise<string>
|
burnRemaining: (senderAddress: string) => Promise<string>
|
||||||
|
updateDiscountPrice: (senderAddress: string, price: string) => Promise<string>
|
||||||
|
removeDiscountPrice: (senderAddress: string) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VendingMinterMessages {
|
export interface VendingMinterMessages {
|
||||||
@ -66,6 +68,8 @@ export interface VendingMinterMessages {
|
|||||||
withdraw: () => WithdrawMessage
|
withdraw: () => WithdrawMessage
|
||||||
airdrop: (recipients: string[]) => CustomMessage
|
airdrop: (recipients: string[]) => CustomMessage
|
||||||
burnRemaining: () => BurnRemainingMessage
|
burnRemaining: () => BurnRemainingMessage
|
||||||
|
updateDiscountPrice: (price: string) => UpdateDiscountPriceMessage
|
||||||
|
removeDiscountPrice: () => RemoveDiscountPriceMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MintMessage {
|
export interface MintMessage {
|
||||||
@ -97,6 +101,26 @@ export interface UpdateMintPriceMessage {
|
|||||||
funds: Coin[]
|
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 {
|
export interface SetWhitelistMessage {
|
||||||
sender: string
|
sender: string
|
||||||
contract: string
|
contract: string
|
||||||
@ -326,6 +350,36 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
|||||||
return res.transactionHash
|
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 setWhitelist = async (senderAddress: string, whitelist: string): Promise<string> => {
|
||||||
const res = await client.execute(
|
const res = await client.execute(
|
||||||
senderAddress,
|
senderAddress,
|
||||||
@ -552,6 +606,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
|||||||
mint,
|
mint,
|
||||||
purge,
|
purge,
|
||||||
updateMintPrice,
|
updateMintPrice,
|
||||||
|
updateDiscountPrice,
|
||||||
|
removeDiscountPrice,
|
||||||
setWhitelist,
|
setWhitelist,
|
||||||
updateStartTime,
|
updateStartTime,
|
||||||
updateStartTradingTime,
|
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 => {
|
const setWhitelist = (whitelist: string): SetWhitelistMessage => {
|
||||||
return {
|
return {
|
||||||
sender: txSigner,
|
sender: txSigner,
|
||||||
@ -795,6 +875,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
|
|||||||
mint,
|
mint,
|
||||||
purge,
|
purge,
|
||||||
updateMintPrice,
|
updateMintPrice,
|
||||||
|
updateDiscountPrice,
|
||||||
|
removeDiscountPrice,
|
||||||
setWhitelist,
|
setWhitelist,
|
||||||
updateStartTime,
|
updateStartTime,
|
||||||
updateStartTradingTime,
|
updateStartTradingTime,
|
||||||
|
@ -7,6 +7,8 @@ export const EXECUTE_TYPES = [
|
|||||||
'mint',
|
'mint',
|
||||||
'purge',
|
'purge',
|
||||||
'update_mint_price',
|
'update_mint_price',
|
||||||
|
'update_discount_price',
|
||||||
|
'remove_discount_price',
|
||||||
'set_whitelist',
|
'set_whitelist',
|
||||||
'update_start_time',
|
'update_start_time',
|
||||||
'update_start_trading_time',
|
'update_start_trading_time',
|
||||||
@ -39,6 +41,16 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
|||||||
name: 'Update Mint Price',
|
name: 'Update Mint Price',
|
||||||
description: `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',
|
id: 'set_whitelist',
|
||||||
name: 'Set Whitelist',
|
name: 'Set Whitelist',
|
||||||
@ -98,6 +110,8 @@ export type DispatchExecuteArgs = {
|
|||||||
| { type: Select<'mint'> }
|
| { type: Select<'mint'> }
|
||||||
| { type: Select<'purge'> }
|
| { type: Select<'purge'> }
|
||||||
| { type: Select<'update_mint_price'>; price: string }
|
| { 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<'set_whitelist'>; whitelist: string }
|
||||||
| { type: Select<'update_start_time'>; startTime: string }
|
| { type: Select<'update_start_time'>; startTime: string }
|
||||||
| { type: Select<'update_start_trading_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': {
|
case 'update_mint_price': {
|
||||||
return messages.updateMintPrice(txSigner, args.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': {
|
case 'set_whitelist': {
|
||||||
return messages.setWhitelist(txSigner, args.whitelist)
|
return messages.setWhitelist(txSigner, args.whitelist)
|
||||||
}
|
}
|
||||||
@ -167,6 +187,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
|||||||
case 'update_mint_price': {
|
case 'update_mint_price': {
|
||||||
return messages(contract)?.updateMintPrice(args.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': {
|
case 'set_whitelist': {
|
||||||
return messages(contract)?.setWhitelist(args.whitelist)
|
return messages(contract)?.setWhitelist(args.whitelist)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stargaze-studio",
|
"name": "stargaze-studio",
|
||||||
"version": "0.4.5",
|
"version": "0.4.8",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
/* 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-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* 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 { Tooltip } from 'components/Tooltip'
|
||||||
import { useContracts } from 'contexts/contracts'
|
import { useContracts } from 'contexts/contracts'
|
||||||
import { useWallet } from 'contexts/wallet'
|
import { useWallet } from 'contexts/wallet'
|
||||||
|
import type { Badge } from 'contracts/badgeHub'
|
||||||
import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute'
|
import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute'
|
||||||
import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute'
|
import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import { toPng } from 'html-to-image'
|
import { toPng } from 'html-to-image'
|
||||||
import type { NextPage } from 'next'
|
import type { NextPage } from 'next'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
import sizeof from 'object-sizeof'
|
||||||
import { QRCodeSVG } from 'qrcode.react'
|
import { QRCodeSVG } from 'qrcode.react'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
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 { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants'
|
||||||
import { withMetadata } from 'utils/layout'
|
import { withMetadata } from 'utils/layout'
|
||||||
import { links } from 'utils/links'
|
import { links } from 'utils/links'
|
||||||
|
import { resolveAddress } from 'utils/resolveAddress'
|
||||||
import { truncateMiddle } from 'utils/text'
|
import { truncateMiddle } from 'utils/text'
|
||||||
|
|
||||||
|
import { generateKeyPairs } from '../../utils/hash'
|
||||||
|
|
||||||
const BadgeCreationPage: NextPage = () => {
|
const BadgeCreationPage: NextPage = () => {
|
||||||
const wallet = useWallet()
|
const wallet = useWallet()
|
||||||
const { badgeHub: badgeHubContract } = useContracts()
|
const { badgeHub: badgeHubContract } = useContracts()
|
||||||
@ -50,13 +58,18 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
|
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const [creatingBadge, setCreatingBadge] = useState(false)
|
const [creatingBadge, setCreatingBadge] = useState(false)
|
||||||
|
const [isAddingKeysComplete, setIsAddingKeysComplete] = useState(false)
|
||||||
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
|
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
|
||||||
const [mintRule, setMintRule] = useState<MintRule>('by_key')
|
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 [badgeId, setBadgeId] = useState<string | null>(null)
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||||
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
||||||
const [transactionHash, setTransactionHash] = useState<string | null>(null)
|
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 qrRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const keyState = useInputState({
|
const keyState = useInputState({
|
||||||
@ -66,6 +79,14 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
subtitle: 'Part of the key pair to be utilized for post-creation access control',
|
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 = () => {
|
const performBadgeCreationChecks = () => {
|
||||||
try {
|
try {
|
||||||
setReadyToCreateBadge(false)
|
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 () => {
|
const createNewBadge = async () => {
|
||||||
try {
|
try {
|
||||||
if (!wallet.initialized) throw new Error('Wallet not connected')
|
if (!wallet.initialized) throw new Error('Wallet not connected')
|
||||||
@ -133,9 +194,16 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
youtube_url: badgeDetails?.youtube_url || undefined,
|
youtube_url: badgeDetails?.youtube_url || undefined,
|
||||||
},
|
},
|
||||||
transferrable: badgeDetails?.transferrable as boolean,
|
transferrable: badgeDetails?.transferrable as boolean,
|
||||||
rule: {
|
rule:
|
||||||
by_key: keyState.value,
|
mintRule === 'by_key'
|
||||||
},
|
? {
|
||||||
|
by_key: keyState.value,
|
||||||
|
}
|
||||||
|
: mintRule === 'by_minter'
|
||||||
|
? {
|
||||||
|
by_minter: resolvedMinterAddress,
|
||||||
|
}
|
||||||
|
: 'by_keys',
|
||||||
expiry: badgeDetails?.expiry || undefined,
|
expiry: badgeDetails?.expiry || undefined,
|
||||||
max_supply: badgeDetails?.max_supply || undefined,
|
max_supply: badgeDetails?.max_supply || undefined,
|
||||||
}
|
}
|
||||||
@ -147,13 +215,50 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
badge,
|
badge,
|
||||||
type: 'create_badge',
|
type: 'create_badge',
|
||||||
}
|
}
|
||||||
const data = await badgeHubDispatchExecute(payload)
|
if (mintRule !== 'by_keys') {
|
||||||
console.log(data)
|
setBadgeId(null)
|
||||||
setCreatingBadge(false)
|
setIsAddingKeysComplete(false)
|
||||||
setTransactionHash(data.split(':')[0])
|
const data = await badgeHubDispatchExecute(payload)
|
||||||
setBadgeId(data.split(':')[1])
|
console.log(data)
|
||||||
} catch (error: any) {
|
setCreatingBadge(false)
|
||||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
setTransactionHash(data.split(':')[0])
|
||||||
|
setBadgeId(data.split(':')[1])
|
||||||
|
} 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)
|
setCreatingBadge(false)
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
@ -184,7 +289,8 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
|
|
||||||
const checkBadgeDetails = () => {
|
const checkBadgeDetails = () => {
|
||||||
if (!badgeDetails) throw new Error('Please fill out the required fields')
|
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) {
|
if (badgeDetails.external_url) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(badgeDetails.external_url)
|
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 copyClaimURL = async () => {
|
||||||
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||||
const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}`
|
const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}`
|
||||||
@ -244,6 +358,12 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
setReadyToCreateBadge(false)
|
setReadyToCreateBadge(false)
|
||||||
}, [imageUploadDetails?.uploadMethod])
|
}, [imageUploadDetails?.uploadMethod])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (keyPairs.length > 0) {
|
||||||
|
toast.success('Key pairs generated successfully.')
|
||||||
|
}
|
||||||
|
}, [keyPairs])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NextSeo title="Create Badge" />
|
<NextSeo title="Create Badge" />
|
||||||
@ -265,52 +385,164 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mx-10" ref={scrollRef}>
|
<div className="mx-10" ref={scrollRef}>
|
||||||
<Conditional test={badgeId !== null}>
|
<Conditional test={badgeId !== null}>
|
||||||
<Alert className="mt-5" type="info">
|
<Conditional test={mintRule === 'by_key'}>
|
||||||
<div className="flex flex-row">
|
<Alert className="mt-5" type="info">
|
||||||
<div>
|
<div className="flex flex-row">
|
||||||
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
<div>
|
||||||
<QRCodeSVG
|
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
||||||
className="mx-auto"
|
<QRCodeSVG
|
||||||
level="H"
|
className="mx-auto"
|
||||||
size={384}
|
level="H"
|
||||||
value={`${
|
size={384}
|
||||||
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
value={`${
|
||||||
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
|
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||||
/>
|
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 mt-2 w-[384px]">
|
||||||
|
<Button
|
||||||
|
className="items-center w-full text-sm text-center rounded"
|
||||||
|
leftIcon={<FaSave />}
|
||||||
|
onClick={() => void handleDownloadQr()}
|
||||||
|
>
|
||||||
|
Download QR Code
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full text-sm text-center rounded"
|
||||||
|
isWide
|
||||||
|
leftIcon={<FaCopy />}
|
||||||
|
onClick={() => void copyClaimURL()}
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
|
Copy Claim URL
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mt-2 w-[384px]">
|
<div className="ml-4 text-lg">
|
||||||
<Button
|
Badge ID:{` ${badgeId as string}`}
|
||||||
className="items-center w-full text-sm text-center rounded"
|
<br />
|
||||||
leftIcon={<FaSave />}
|
Private Key:
|
||||||
onClick={() => void handleDownloadQr()}
|
<Tooltip label="Click to copy the private key">
|
||||||
>
|
<button
|
||||||
Download QR Code
|
className="group flex space-x-2 font-mono text-base text-white/50 hover:underline"
|
||||||
</Button>
|
onClick={() => void copy(createdBadgeKey as string)}
|
||||||
<Button
|
type="button"
|
||||||
className="w-full text-sm text-center rounded"
|
>
|
||||||
isWide
|
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
|
||||||
leftIcon={<FaCopy />}
|
<FaCopy className="opacity-50 group-hover:opacity-100" />
|
||||||
onClick={() => void copyClaimURL()}
|
</button>
|
||||||
variant="solid"
|
</Tooltip>
|
||||||
>
|
<br />
|
||||||
Copy Claim URL
|
Transaction Hash: {' '}
|
||||||
</Button>
|
<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={`${
|
||||||
|
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||||
|
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</Anchor>{' '}
|
||||||
|
or scan the QR code to claim a badge.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<span className="mt-4">
|
||||||
|
You may download the QR code or copy the claim URL to share with others.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</Conditional>
|
||||||
|
|
||||||
|
<Conditional test={mintRule === 'by_keys'}>
|
||||||
|
<Alert className="mt-5" type="info">
|
||||||
<div className="ml-4 text-lg">
|
<div className="ml-4 text-lg">
|
||||||
Badge ID:{` ${badgeId as string}`}
|
Badge ID:{` ${badgeId as string}`}
|
||||||
<br />
|
<br />
|
||||||
Private Key:
|
Transaction Hash: {' '}
|
||||||
<Tooltip label="Click to copy the private key">
|
<Conditional test={NETWORK === 'testnet'}>
|
||||||
<button
|
<Anchor
|
||||||
className="group flex space-x-2 font-mono text-base text-white/50 hover:underline"
|
className="text-stargaze hover:underline"
|
||||||
onClick={() => void copy(createdBadgeKey as string)}
|
external
|
||||||
type="button"
|
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
|
||||||
>
|
>
|
||||||
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
|
{transactionHash}
|
||||||
<FaCopy className="opacity-50 group-hover:opacity-100" />
|
</Anchor>
|
||||||
</button>
|
</Conditional>
|
||||||
</Tooltip>
|
<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 />
|
<br />
|
||||||
Transaction Hash: {' '}
|
Transaction Hash: {' '}
|
||||||
<Conditional test={NETWORK === 'testnet'}>
|
<Conditional test={NETWORK === 'testnet'}>
|
||||||
@ -339,22 +571,19 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
<Anchor
|
<Anchor
|
||||||
className="text-stargaze hover:underline"
|
className="text-stargaze hover:underline"
|
||||||
external
|
external
|
||||||
href={`${
|
href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
|
||||||
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
badgeId as string
|
||||||
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
|
}`}
|
||||||
>
|
>
|
||||||
here
|
here
|
||||||
</Anchor>{' '}
|
</Anchor>{' '}
|
||||||
or scan the QR code to claim a badge.
|
and select Actions {'>'} Mint By Minter to mint a badge.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
<span className="mt-4">You may download the QR code or copy the claim URL to share with others.</span>
|
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Alert>
|
||||||
</Alert>
|
</Conditional>
|
||||||
</Conditional>
|
</Conditional>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -374,81 +603,136 @@ const BadgeCreationPage: NextPage = () => {
|
|||||||
mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<Tooltip
|
||||||
className="p-4 w-full h-full text-left bg-transparent"
|
backgroundColor="bg-blue-500"
|
||||||
onClick={() => {
|
className="m-0 w-1/3"
|
||||||
setMintRule('by_key')
|
label="The same single private key can be utilized by multiple users to share badge minting authority. Ideal for projects with multiple administrators."
|
||||||
setReadyToCreateBadge(false)
|
placement="bottom"
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<h4 className="font-bold">Mint Rule: By Key</h4>
|
<button
|
||||||
<span className="text-sm text-white/80 line-clamp-2">
|
className="p-4 w-full h-full text-left bg-transparent"
|
||||||
Badges can be minted more than once with a badge specific message signed by a designated private key.
|
onClick={() => {
|
||||||
</span>
|
setMintRule('by_key')
|
||||||
</button>
|
setReadyToCreateBadge(false)
|
||||||
|
setBadgeId(null)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<h4 className="font-bold">Mint Rule: By Key</h4>
|
||||||
|
<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>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'isolate space-y-1 border-2',
|
'isolate space-y-1 border-2',
|
||||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||||
mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent',
|
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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<Tooltip
|
||||||
className="p-4 w-full h-full text-left bg-transparent"
|
backgroundColor="bg-blue-500"
|
||||||
disabled
|
className="m-0 w-1/3"
|
||||||
onClick={() => {
|
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."
|
||||||
setMintRule('by_keys')
|
placement="bottom"
|
||||||
setReadyToCreateBadge(false)
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<h4 className="font-bold">Mint Rule: By Keys</h4>
|
<button
|
||||||
<span className="text-sm text-slate-500 line-clamp-2">
|
className="p-4 w-full h-full text-left bg-transparent"
|
||||||
Similar to the By Key rule, however each designated private key can only be used once to mint a badge.
|
onClick={() => {
|
||||||
</span>
|
setMintRule('by_keys')
|
||||||
</button>
|
setReadyToCreateBadge(false)
|
||||||
|
setBadgeId(null)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<h4 className="font-bold">Mint Rule: By Keys</h4>
|
||||||
|
<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>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'isolate space-y-1 border-2',
|
'isolate space-y-1 border-2',
|
||||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||||
mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent',
|
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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<Tooltip
|
||||||
className="p-4 w-full h-full text-left bg-transparent"
|
backgroundColor="bg-blue-500"
|
||||||
disabled
|
className="m-0 w-1/3"
|
||||||
onClick={() => {
|
label="The most basic approach. However, having just one authorized address for minting badges might limit your ability to delegate that responsibility."
|
||||||
setMintRule('by_minter')
|
placement="bottom"
|
||||||
setReadyToCreateBadge(false)
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<h4 className="font-bold">Mint Rule: By Minter</h4>
|
<button
|
||||||
<span className="text-sm line-clamp-2 text-slate/500">
|
className="p-4 w-full h-full text-left bg-transparent"
|
||||||
Badges can be minted by a designated minter account.
|
onClick={() => {
|
||||||
</span>
|
setMintRule('by_minter')
|
||||||
</button>
|
setReadyToCreateBadge(false)
|
||||||
|
setBadgeId(null)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<h4 className="font-bold">Mint Rule: By Minter</h4>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-10">
|
<div className="mx-10">
|
||||||
<ImageUploadDetails mintRule={mintRule} onChange={setImageUploadDetails} />
|
<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>
|
||||||
|
|
||||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
<Conditional test={mintRule === 'by_keys'}>
|
||||||
<TextInput className="ml-4 w-full max-w-2xl" {...keyState} disabled required />
|
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||||
<Button className="mt-14 ml-4" isDisabled={creatingBadge} onClick={handleGenerateKey}>
|
<div className="grid grid-cols-2 gap-24">
|
||||||
Generate Key
|
<div className="flex flex-col ml-4">
|
||||||
</Button>
|
<span className="font-bold">Number of Keys</span>
|
||||||
</div>
|
<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">
|
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
|
||||||
<BadgeDetails
|
<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}
|
mintRule={mintRule}
|
||||||
onChange={setBadgeDetails}
|
onChange={setBadgeDetails}
|
||||||
uploadMethod={imageUploadDetails?.uploadMethod ? imageUploadDetails.uploadMethod : 'new'}
|
uploadMethod={imageUploadDetails?.uploadMethod ? imageUploadDetails.uploadMethod : 'new'}
|
||||||
|
@ -49,12 +49,12 @@ const BadgeList: NextPage = () => {
|
|||||||
{myBadges.map((badge: any, index: any) => {
|
{myBadges.map((badge: any, index: any) => {
|
||||||
return (
|
return (
|
||||||
<tr key={index}>
|
<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="flex items-center space-x-3">
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<div className="w-28 h-28 mask mask-squircle">
|
<div className="w-28 h-28 mask mask-squircle">
|
||||||
<img
|
<img
|
||||||
alt="Cover"
|
alt="badge-preview"
|
||||||
src={
|
src={
|
||||||
(badge?.image as string).startsWith('ipfs')
|
(badge?.image as string).startsWith('ipfs')
|
||||||
? `https://ipfs-gw.stargaze-apis.com/ipfs/${(badge?.image as string).substring(7)}`
|
? `https://ipfs-gw.stargaze-apis.com/ipfs/${(badge?.image as string).substring(7)}`
|
||||||
@ -64,13 +64,15 @@ const BadgeList: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-2">
|
<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>
|
<p className="max-w-xs text-sm truncate opacity-50">Badge ID: {badge.tokenId}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="overflow-auto w-[35%] max-w-xl bg-black no-scrollbar">
|
<td className="overflow-auto w-[55%] max-w-xl bg-black no-scrollbar">
|
||||||
{badge.description}
|
{badge.description ? badge.description : 'No description provided.'}
|
||||||
{/* <br /> */}
|
{/* <br /> */}
|
||||||
{/* <span className="badge badge-ghost badge-sm"></span> */}
|
{/* <span className="badge badge-ghost badge-sm"></span> */}
|
||||||
</td>
|
</td>
|
||||||
|
@ -754,12 +754,17 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
mintingDetails.perAddressLimit > mintingDetails.numTokens
|
mintingDetails.perAddressLimit > mintingDetails.numTokens
|
||||||
)
|
)
|
||||||
throw new Error('Invalid limit for tokens per address')
|
throw new Error('Invalid limit for tokens per address')
|
||||||
|
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 (
|
if (
|
||||||
mintingDetails.numTokens > 100 &&
|
mintingDetails.numTokens >= 100 &&
|
||||||
mintingDetails.numTokens < 100 * mintingDetails.perAddressLimit &&
|
mintingDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
|
||||||
mintingDetails.perAddressLimit > mintingDetails.numTokens / 100
|
|
||||||
)
|
)
|
||||||
throw new Error('Invalid limit for tokens per address. The limit cannot exceed 1% of the total number of tokens.')
|
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.',
|
||||||
|
)
|
||||||
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
||||||
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
|
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)
|
const whitelistStartDate = new Date(Number(config?.start_time) / 1000000)
|
||||||
throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`)
|
throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`)
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
mintingDetails?.numTokens &&
|
if (mintingDetails?.numTokens && config?.per_address_limit) {
|
||||||
config?.per_address_limit &&
|
if (mintingDetails.numTokens >= 100 && Number(config.per_address_limit) > 50) {
|
||||||
mintingDetails.numTokens > 100 &&
|
throw Error(
|
||||||
Number(config.per_address_limit) > mintingDetails.numTokens / 100
|
`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.`,
|
||||||
)
|
)
|
||||||
throw Error(
|
} else if (
|
||||||
`Invalid limit for tokens per address (${config.per_address_limit} tokens). The limit cannot exceed 1% of the total number of tokens.`,
|
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') {
|
} else if (whitelistDetails.whitelistType === 'new') {
|
||||||
if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty')
|
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')
|
throw new Error('Whitelist start time cannot be later than whitelist end time')
|
||||||
if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime))
|
if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime))
|
||||||
throw new Error('Whitelist start time must be the same as the minting start time')
|
throw new Error('Whitelist start time must be the same as the minting start time')
|
||||||
if (
|
if (whitelistDetails.perAddressLimit && mintingDetails?.numTokens) {
|
||||||
mintingDetails?.numTokens &&
|
if (mintingDetails.numTokens >= 100 && whitelistDetails.perAddressLimit > 50) {
|
||||||
whitelistDetails.perAddressLimit &&
|
throw Error(
|
||||||
mintingDetails.numTokens > 100 &&
|
`Invalid limit for tokens per address. Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
|
||||||
whitelistDetails.perAddressLimit > mintingDetails.numTokens / 100
|
)
|
||||||
)
|
} else if (
|
||||||
throw Error(
|
mintingDetails.numTokens >= 100 &&
|
||||||
`Invalid limit for tokens per address (${whitelistDetails.perAddressLimit} tokens). The limit cannot exceed 1% of the total number of tokens.`,
|
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 eslint-comments/disable-enable-pair */
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { toUtf8 } from '@cosmjs/encoding'
|
import { toUtf8 } from '@cosmjs/encoding'
|
||||||
|
import clsx from 'clsx'
|
||||||
import { Alert } from 'components/Alert'
|
import { Alert } from 'components/Alert'
|
||||||
|
import type { MintRule } from 'components/badges/creation/ImageUploadDetails'
|
||||||
import { Button } from 'components/Button'
|
import { Button } from 'components/Button'
|
||||||
import { Conditional } from 'components/Conditional'
|
import { Conditional } from 'components/Conditional'
|
||||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||||
import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox'
|
import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox'
|
||||||
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
|
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
|
||||||
import { FormControl } from 'components/FormControl'
|
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 { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||||
import { InputDateTime } from 'components/InputDateTime'
|
import { InputDateTime } from 'components/InputDateTime'
|
||||||
@ -20,6 +25,7 @@ import { LinkTabs } from 'components/LinkTabs'
|
|||||||
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
|
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
|
||||||
import { Tooltip } from 'components/Tooltip'
|
import { Tooltip } from 'components/Tooltip'
|
||||||
import { TransactionHash } from 'components/TransactionHash'
|
import { TransactionHash } from 'components/TransactionHash'
|
||||||
|
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||||
import { useContracts } from 'contexts/contracts'
|
import { useContracts } from 'contexts/contracts'
|
||||||
import { useWallet } from 'contexts/wallet'
|
import { useWallet } from 'contexts/wallet'
|
||||||
import type { Badge } from 'contracts/badgeHub'
|
import type { Badge } from 'contracts/badgeHub'
|
||||||
@ -40,7 +46,8 @@ import { useMutation } from 'react-query'
|
|||||||
import * as secp256k1 from 'secp256k1'
|
import * as secp256k1 from 'secp256k1'
|
||||||
import { copy } from 'utils/clipboard'
|
import { copy } from 'utils/clipboard'
|
||||||
import { NETWORK } from 'utils/constants'
|
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 { withMetadata } from 'utils/layout'
|
||||||
import { links } from 'utils/links'
|
import { links } from 'utils/links'
|
||||||
import { resolveAddress } from 'utils/resolveAddress'
|
import { resolveAddress } from 'utils/resolveAddress'
|
||||||
@ -62,10 +69,15 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null)
|
const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null)
|
||||||
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
||||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||||
|
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
|
||||||
const [signature, setSignature] = useState<string>('')
|
const [signature, setSignature] = useState<string>('')
|
||||||
|
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
||||||
const qrRef = useRef<HTMLDivElement>(null)
|
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 comboboxState = useExecuteComboboxState()
|
||||||
const type = comboboxState.value?.id
|
const type = comboboxState.value?.id
|
||||||
@ -173,20 +185,26 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
name: 'owner',
|
name: 'owner',
|
||||||
title: 'Owner',
|
title: 'Owner',
|
||||||
subtitle: 'The owner of the badge',
|
subtitle: 'The owner of the badge',
|
||||||
|
defaultValue: wallet.address,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ownerListState = useAddressListState()
|
||||||
|
|
||||||
const pubkeyState = useInputState({
|
const pubkeyState = useInputState({
|
||||||
id: 'pubkey',
|
id: 'pubkey',
|
||||||
name: 'pubkey',
|
name: 'pubkey',
|
||||||
title: 'Pubkey',
|
title: 'Pubkey',
|
||||||
subtitle: 'The public key for the badge',
|
subtitle: 'The whitelisted public key authorized to mint a badge',
|
||||||
})
|
})
|
||||||
|
|
||||||
const privateKeyState = useInputState({
|
const privateKeyState = useInputState({
|
||||||
id: 'privateKey',
|
id: 'privateKey',
|
||||||
name: 'privateKey',
|
name: 'privateKey',
|
||||||
title: 'Private Key',
|
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({
|
const nftState = useInputState({
|
||||||
@ -200,15 +218,34 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
id: 'limit',
|
id: 'limit',
|
||||||
name: 'limit',
|
name: 'limit',
|
||||||
title: '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 showBadgeField = type === 'create_badge'
|
||||||
const showMetadataField = isEitherType(type, ['create_badge', 'edit_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 showNFTField = type === 'set_nft'
|
||||||
const showOwnerField = type === 'mint_by_key'
|
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||||
const showPrivateKeyField = type === 'mint_by_key'
|
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 messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||||
const payload: DispatchExecuteArgs = {
|
const payload: DispatchExecuteArgs = {
|
||||||
@ -234,9 +271,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
youtube_url: youtubeUrlState.value || undefined,
|
youtube_url: youtubeUrlState.value || undefined,
|
||||||
},
|
},
|
||||||
transferrable,
|
transferrable,
|
||||||
rule: {
|
rule:
|
||||||
by_key: keyState.value,
|
mintRule === 'by_key'
|
||||||
},
|
? {
|
||||||
|
by_key: keyState.value,
|
||||||
|
}
|
||||||
|
: mintRule === 'by_minter'
|
||||||
|
? {
|
||||||
|
by_minter: resolvedMinterAddress,
|
||||||
|
}
|
||||||
|
: 'by_keys',
|
||||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||||
max_supply: maxSupplyState.value || undefined,
|
max_supply: maxSupplyState.value || undefined,
|
||||||
},
|
},
|
||||||
@ -260,12 +304,19 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
youtube_url: youtubeUrlState.value || undefined,
|
youtube_url: youtubeUrlState.value || undefined,
|
||||||
},
|
},
|
||||||
id: badgeIdState.value,
|
id: badgeIdState.value,
|
||||||
owner: ownerState.value,
|
owner: resolvedOwnerAddress,
|
||||||
pubkey: pubkeyState.value,
|
pubkey: pubkeyState.value,
|
||||||
signature,
|
signature,
|
||||||
keys: [],
|
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||||
limit: limitState.value,
|
limit: limitState.value || undefined,
|
||||||
owners: [],
|
owners: [
|
||||||
|
...new Set(
|
||||||
|
ownerListState.values
|
||||||
|
.map((a) => a.address.trim())
|
||||||
|
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||||
|
.concat(ownerList),
|
||||||
|
),
|
||||||
|
],
|
||||||
nft: nftState.value,
|
nft: nftState.value,
|
||||||
editFee,
|
editFee,
|
||||||
contract: contractState.value,
|
contract: contractState.value,
|
||||||
@ -337,6 +388,7 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
if (txHash) {
|
if (txHash) {
|
||||||
setLastTx(txHash.split(':')[0])
|
setLastTx(txHash.split(':')[0])
|
||||||
setCreatedBadgeId(txHash.split(':')[1])
|
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()
|
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 copyClaimURL = async () => {
|
||||||
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
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(() => {
|
useEffect(() => {
|
||||||
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
|
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
|
||||||
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
|
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
|
||||||
@ -448,6 +514,15 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
void resolveOwnerAddress()
|
void resolveOwnerAddress()
|
||||||
}, [ownerState.value])
|
}, [ownerState.value])
|
||||||
|
|
||||||
|
const resolveMinterAddress = async () => {
|
||||||
|
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
|
||||||
|
setResolvedMinterAddress(resolvedAddress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
void resolveMinterAddress()
|
||||||
|
}, [designatedMinterState.value])
|
||||||
|
|
||||||
const resolveManagerAddress = async () => {
|
const resolveManagerAddress = async () => {
|
||||||
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
|
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
|
||||||
setBadge({
|
setBadge({
|
||||||
@ -472,9 +547,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
youtube_url: youtubeUrlState.value || undefined,
|
youtube_url: youtubeUrlState.value || undefined,
|
||||||
},
|
},
|
||||||
transferrable,
|
transferrable,
|
||||||
rule: {
|
rule:
|
||||||
by_key: keyState.value,
|
mintRule === 'by_key'
|
||||||
},
|
? {
|
||||||
|
by_key: keyState.value,
|
||||||
|
}
|
||||||
|
: mintRule === 'by_minter'
|
||||||
|
? {
|
||||||
|
by_minter: resolvedMinterAddress,
|
||||||
|
}
|
||||||
|
: 'by_keys',
|
||||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||||
max_supply: maxSupplyState.value || undefined,
|
max_supply: maxSupplyState.value || undefined,
|
||||||
})
|
})
|
||||||
@ -508,9 +590,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
youtube_url: youtubeUrlState.value || undefined,
|
youtube_url: youtubeUrlState.value || undefined,
|
||||||
},
|
},
|
||||||
transferrable,
|
transferrable,
|
||||||
rule: {
|
rule:
|
||||||
by_key: keyState.value,
|
mintRule === 'by_key'
|
||||||
},
|
? {
|
||||||
|
by_key: keyState.value,
|
||||||
|
}
|
||||||
|
: mintRule === 'by_minter'
|
||||||
|
? {
|
||||||
|
by_minter: resolvedMinterAddress,
|
||||||
|
}
|
||||||
|
: 'by_keys',
|
||||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||||
max_supply: maxSupplyState.value || undefined,
|
max_supply: maxSupplyState.value || undefined,
|
||||||
})
|
})
|
||||||
@ -541,7 +630,7 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
<LinkTabs activeIndex={2} data={badgeHubLinkTabs} />
|
<LinkTabs activeIndex={2} data={badgeHubLinkTabs} />
|
||||||
|
|
||||||
{showBadgeField && createdBadgeId && createdBadgeKey && (
|
{showBadgeField && createdBadgeId && createdBadgeKey && mintRule === 'by_key' && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
||||||
@ -598,14 +687,116 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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}>
|
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<AddressInput {...contractState} />
|
<AddressInput {...contractState} />
|
||||||
<ExecuteCombobox {...comboboxState} />
|
<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} />}
|
{showIdField && <NumberInput {...badgeIdState} />}
|
||||||
{showBadgeField && <AddressInput {...managerState} />}
|
{showBadgeField && <AddressInput {...managerState} />}
|
||||||
{showBadgeField && <TextInput {...keyState} />}
|
{showBadgeField && mintRule === 'by_key' && <TextInput {...keyState} />}
|
||||||
{showBadgeField && <Button onClick={handleGenerateKey}>Generate Key</Button>}
|
{showBadgeField && mintRule === 'by_key' && <Button onClick={handleGenerateKey}>Generate Key</Button>}
|
||||||
|
{showBadgeField && mintRule === 'by_minter' && <TextInput {...designatedMinterState} />}
|
||||||
{showMetadataField && (
|
{showMetadataField && (
|
||||||
<div className="p-4 rounded-md border-2 border-gray-800">
|
<div className="p-4 rounded-md border-2 border-gray-800">
|
||||||
<span className="text-gray-400">Metadata</span>
|
<span className="text-gray-400">Metadata</span>
|
||||||
@ -636,6 +827,57 @@ const BadgeHubExecutePage: NextPage = () => {
|
|||||||
title="Owner"
|
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} />}
|
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
||||||
{showNFTField && <AddressInput {...nftState} />}
|
{showNFTField && <AddressInput {...nftState} />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,7 +154,7 @@ const BadgeHubQueryPage: NextPage = () => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Conditional test={type === 'getBadge' || type === 'getKey'}>
|
<Conditional test={type === 'getBadge' || type === 'getKey' || type === 'getKeys'}>
|
||||||
<NumberInput {...idState} />
|
<NumberInput {...idState} />
|
||||||
</Conditional>
|
</Conditional>
|
||||||
<Conditional test={type === 'getKey'}>
|
<Conditional test={type === 'getKey'}>
|
||||||
|
@ -105,7 +105,7 @@ const BaseMinterInstantiatePage: NextPage = () => {
|
|||||||
name: 'royaltyShare',
|
name: 'royaltyShare',
|
||||||
title: 'Share Percentage',
|
title: 'Share Percentage',
|
||||||
subtitle: 'Percentage of royalties to be paid',
|
subtitle: 'Percentage of royalties to be paid',
|
||||||
placeholder: '8%',
|
placeholder: '5%',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data, isLoading, mutate } = useMutation(
|
const { data, isLoading, mutate } = useMutation(
|
||||||
|
@ -55,8 +55,8 @@ const VendingMinterExecutePage: NextPage = () => {
|
|||||||
const priceState = useNumberInputState({
|
const priceState = useNumberInputState({
|
||||||
id: 'price',
|
id: 'price',
|
||||||
name: 'price',
|
name: 'price',
|
||||||
title: 'Price',
|
title: type === 'update_discount_price' ? 'Discount Price' : 'Price',
|
||||||
subtitle: 'Enter the token price',
|
subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'Enter the token price',
|
||||||
})
|
})
|
||||||
|
|
||||||
const contractState = useInputState({
|
const contractState = useInputState({
|
||||||
@ -86,7 +86,7 @@ const VendingMinterExecutePage: NextPage = () => {
|
|||||||
const showLimitField = type === 'update_per_address_limit'
|
const showLimitField = type === 'update_per_address_limit'
|
||||||
const showTokenIdField = type === 'mint_for'
|
const showTokenIdField = type === 'mint_for'
|
||||||
const showRecipientField = isEitherType(type, ['mint_to', '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 messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||||
const payload: DispatchExecuteArgs = {
|
const payload: DispatchExecuteArgs = {
|
||||||
|
@ -103,7 +103,7 @@ const VendingMinterInstantiatePage: NextPage = () => {
|
|||||||
name: 'royaltyShare',
|
name: 'royaltyShare',
|
||||||
title: 'Share Percentage',
|
title: 'Share Percentage',
|
||||||
subtitle: 'Percentage of royalties to be paid',
|
subtitle: 'Percentage of royalties to be paid',
|
||||||
placeholder: '8%',
|
placeholder: '5%',
|
||||||
})
|
})
|
||||||
|
|
||||||
const unitPriceState = useNumberInputState({
|
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.
|
Looking for a fast and efficient way to build an NFT collection? Stargaze Studio is the solution.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own
|
Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT
|
||||||
NFT collections in no time.
|
collections in no time.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||||
|
|
||||||
|
import * as crypto from 'crypto'
|
||||||
import { Word32Array } from 'jscrypto'
|
import { Word32Array } from 'jscrypto'
|
||||||
import { SHA256 } from 'jscrypto/SHA256'
|
import { SHA256 } from 'jscrypto/SHA256'
|
||||||
import * as secp256k1 from 'secp256k1'
|
import * as secp256k1 from 'secp256k1'
|
||||||
@ -23,3 +24,21 @@ export function generateSignature(id: number, owner: string, privateKey: string)
|
|||||||
return ''
|
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