Merge pull request #115 from public-awesome/develop
Sync development > main
This commit is contained in:
commit
eb86960ca6
@ -1,4 +1,4 @@
|
||||
APP_VERSION=0.4.5
|
||||
APP_VERSION=0.4.6
|
||||
|
||||
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
NEXT_PUBLIC_SG721_CODE_ID=793
|
||||
|
@ -3,21 +3,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
// import { AirdropUpload } from 'components/AirdropUpload'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { Alert } from 'components/Alert'
|
||||
import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
|
||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
|
||||
import { ActionsCombobox } from 'components/badges/actions/Combobox'
|
||||
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { AddressList } from 'components/forms/AddressList'
|
||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { MetadataAttributes } from 'components/forms/MetadataAttributes'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
|
||||
import * as crypto from 'crypto'
|
||||
import sizeof from 'object-sizeof'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -25,11 +29,12 @@ import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
import { sha256 } from 'utils/hash'
|
||||
import { generateKeyPairs, sha256 } from 'utils/hash'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
|
||||
import { AddressInput, TextInput } from '../../forms/FormInput'
|
||||
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
|
||||
import type { MintRule } from '../creation/ImageUploadDetails'
|
||||
|
||||
interface BadgeActionsProps {
|
||||
@ -52,8 +57,10 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
||||
const [keyPairs, setKeyPairs] = useState<string[]>([])
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [signature, setSignature] = useState<string>('')
|
||||
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
||||
|
||||
const actionComboboxState = useActionsComboboxState()
|
||||
const type = actionComboboxState.value?.id
|
||||
@ -147,18 +154,26 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const pubkeyState = useInputState({
|
||||
id: 'pubkey',
|
||||
name: 'pubkey',
|
||||
title: 'Pubkey',
|
||||
subtitle: 'The public key to check whether it can be used to mint a badge',
|
||||
const ownerListState = useAddressListState()
|
||||
|
||||
const pubKeyState = useInputState({
|
||||
id: 'pubKey',
|
||||
name: 'pubKey',
|
||||
title: 'Public Key',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The whitelisted public key authorized to mint a badge'
|
||||
: 'The public key to check whether it can be used to mint a badge',
|
||||
})
|
||||
|
||||
const privateKeyState = useInputState({
|
||||
id: 'privateKey',
|
||||
name: 'privateKey',
|
||||
title: 'Private Key',
|
||||
subtitle: 'The private key that was generated during badge creation',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The corresponding private key for the whitelisted public key'
|
||||
: 'The private key that was generated during badge creation',
|
||||
})
|
||||
|
||||
const nftState = useInputState({
|
||||
@ -172,13 +187,16 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
id: 'limit',
|
||||
name: 'limit',
|
||||
title: 'Limit',
|
||||
subtitle: 'Number of keys/owners to execute the action for',
|
||||
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
|
||||
})
|
||||
|
||||
const showMetadataField = isEitherType(type, ['edit_badge'])
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'mint_by_minter'])
|
||||
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
|
||||
const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
|
||||
const showOwnerList = isEitherType(type, ['mint_by_minter'])
|
||||
const showPubKeyField = isEitherType(type, ['mint_by_keys'])
|
||||
const showLimitState = isEitherType(type, ['purge_keys', 'purge_owners'])
|
||||
|
||||
const payload: DispatchExecuteArgs = {
|
||||
badge: {
|
||||
@ -231,11 +249,18 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
id: badgeId,
|
||||
editFee,
|
||||
owner: resolvedOwnerAddress,
|
||||
pubkey: pubkeyState.value,
|
||||
pubkey: pubKeyState.value,
|
||||
signature,
|
||||
keys: [],
|
||||
limit: limitState.value,
|
||||
owners: [],
|
||||
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||
limit: limitState.value || undefined,
|
||||
owners: [
|
||||
...new Set(
|
||||
ownerListState.values
|
||||
.map((a) => a.address.trim())
|
||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||
.concat(ownerList),
|
||||
),
|
||||
],
|
||||
recipients: airdropAllocationArray,
|
||||
privateKey: privateKeyState.value,
|
||||
nft: nftState.value,
|
||||
@ -355,6 +380,21 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
|
||||
}, [privateKeyState.value, resolvedOwnerAddress])
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfKeys > 0) {
|
||||
setKeyPairs(generateKeyPairs(numberOfKeys))
|
||||
}
|
||||
}, [numberOfKeys])
|
||||
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeId.toString()}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const { isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent) => {
|
||||
if (!wallet.client) {
|
||||
@ -467,19 +507,6 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerateKeys = (amount: number) => {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
let privKey: Buffer
|
||||
do {
|
||||
privKey = crypto.randomBytes(32)
|
||||
} while (!secp256k1.privateKeyVerify(privKey))
|
||||
|
||||
const privateKey = privKey.toString('hex')
|
||||
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
|
||||
keyPairs.push(publicKey.concat(',', privateKey))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="grid grid-cols-2 mt-4">
|
||||
@ -515,7 +542,65 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
|
||||
title="Owner"
|
||||
/>
|
||||
)}
|
||||
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
|
||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
||||
{showLimitState && <NumberInput className="mt-2" {...limitState} />}
|
||||
<Conditional test={isEitherType(type, ['purge_owners', 'purge_keys'])}>
|
||||
<Alert className="mt-4" type="info">
|
||||
This action is only available if the badge with the specified id is either minted out or expired.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={type === 'add_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of public keys to be whitelisted for minting badges
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
|
||||
<Alert type="info">
|
||||
<div className="pt-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted public keys together with their private key counterparts.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Key Pairs
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={showOwnerList}>
|
||||
<div className="mt-4">
|
||||
<AddressList
|
||||
entries={ownerListState.entries}
|
||||
isRequired
|
||||
onAdd={ownerListState.add}
|
||||
onChange={ownerListState.update}
|
||||
onRemove={ownerListState.remove}
|
||||
subtitle="Enter the owner addresses"
|
||||
title="Addresses"
|
||||
/>
|
||||
<Alert className="mt-8" type="info">
|
||||
You may optionally choose a text file of additional owner addresses.
|
||||
</Alert>
|
||||
<WhitelistUpload onChange={setOwnerList} />
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
{showAirdropFileField && (
|
||||
<FormGroup
|
||||
|
@ -28,11 +28,6 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_key',
|
||||
name: 'Mint by Key',
|
||||
@ -43,6 +38,11 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Airdrop by Key',
|
||||
description: `Airdrop badges to a list of specified addresses`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
@ -51,6 +51,11 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new badge with a whitelisted private key`,
|
||||
},
|
||||
{
|
||||
id: 'add_keys',
|
||||
name: 'Add Keys',
|
||||
@ -66,11 +71,6 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new badge with a whitelisted private key`,
|
||||
},
|
||||
]
|
||||
|
||||
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
||||
@ -79,16 +79,16 @@ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: `Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new badge to specified owner addresses`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new badge to the specified addresses`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
|
@ -28,7 +28,7 @@ export interface MigrateResponse {
|
||||
export interface Rule {
|
||||
by_key?: string
|
||||
by_minter?: string
|
||||
by_keys?: string[]
|
||||
by_keys?: string
|
||||
}
|
||||
|
||||
export interface Trait {
|
||||
@ -53,7 +53,7 @@ export interface Badge {
|
||||
manager: string
|
||||
metadata: Metadata
|
||||
transferrable: boolean
|
||||
rule: Rule
|
||||
rule: Rule | string
|
||||
expiry?: number
|
||||
max_supply?: number
|
||||
}
|
||||
@ -102,7 +102,7 @@ export interface CreateBadgeMessage {
|
||||
manager: string
|
||||
metadata: Metadata
|
||||
transferrable: boolean
|
||||
rule: Rule
|
||||
rule: Rule | string
|
||||
expiry?: number
|
||||
max_supply?: number
|
||||
}
|
||||
@ -349,6 +349,16 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
|
||||
}
|
||||
|
||||
const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => {
|
||||
const feeRateRaw = await client.queryContractRaw(
|
||||
contractAddress,
|
||||
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
console.log('Fee Rate Raw: ', feeRateRaw)
|
||||
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
|
||||
console.log('Fee Rate:', feeRate)
|
||||
|
||||
console.log('keys size: ', sizeof(keys))
|
||||
console.log(keys)
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
@ -360,6 +370,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
|
@ -32,36 +32,36 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
name: 'Edit Badge',
|
||||
description: ` Edit badge metadata for the badge with the specified ID`,
|
||||
},
|
||||
// {
|
||||
// id: 'add_keys',
|
||||
// name: 'Add Keys',
|
||||
// description: `Add keys to the badge with the specified ID`,
|
||||
// },
|
||||
// {
|
||||
// id: 'purge_keys',
|
||||
// name: 'Purge Keys',
|
||||
// description: `Purge keys from the badge with the specified ID`,
|
||||
// },
|
||||
{
|
||||
id: 'add_keys',
|
||||
name: 'Add Keys',
|
||||
description: `Add keys to the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_keys',
|
||||
name: 'Purge Keys',
|
||||
description: `Purge keys from the badge with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'purge_owners',
|
||||
name: 'Purge Owners',
|
||||
description: `Purge owners from the badge with the specified ID`,
|
||||
},
|
||||
// {
|
||||
// id: 'mint_by_minter',
|
||||
// name: 'Mint by Minter',
|
||||
// description: `Mint a new token by the minter with the specified ID`,
|
||||
// },
|
||||
{
|
||||
id: 'mint_by_minter',
|
||||
name: 'Mint by Minter',
|
||||
description: `Mint a new token by the minter with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'mint_by_key',
|
||||
name: 'Mint by Key',
|
||||
description: `Mint a new token by the key with the specified ID`,
|
||||
},
|
||||
// {
|
||||
// id: 'mint_by_keys',
|
||||
// name: 'Mint by Keys',
|
||||
// description: `Mint a new token by the keys with the specified ID`,
|
||||
// },
|
||||
{
|
||||
id: 'mint_by_keys',
|
||||
name: 'Mint by Keys',
|
||||
description: `Mint a new token by the keys with the specified ID`,
|
||||
},
|
||||
{
|
||||
id: 'set_nft',
|
||||
name: 'Set NFT',
|
||||
|
@ -14,8 +14,8 @@ export const QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
|
||||
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
|
||||
// { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' },
|
||||
// { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||
{ id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' },
|
||||
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
|
||||
]
|
||||
|
||||
export interface DispatchQueryProps {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stargaze-studio",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
@ -1,4 +1,6 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
@ -36,8 +38,11 @@ import { copy } from 'utils/clipboard'
|
||||
import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
import { truncateMiddle } from 'utils/text'
|
||||
|
||||
import { generateKeyPairs } from '../../utils/hash'
|
||||
|
||||
const BadgeCreationPage: NextPage = () => {
|
||||
const wallet = useWallet()
|
||||
const { badgeHub: badgeHubContract } = useContracts()
|
||||
@ -50,13 +55,17 @@ const BadgeCreationPage: NextPage = () => {
|
||||
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [creatingBadge, setCreatingBadge] = useState(false)
|
||||
const [isAddingKeysComplete, setIsAddingKeysComplete] = useState(false)
|
||||
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
|
||||
const [mintRule, setMintRule] = useState<MintRule>('by_key')
|
||||
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
|
||||
|
||||
const [badgeId, setBadgeId] = useState<string | null>(null)
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
||||
const [transactionHash, setTransactionHash] = useState<string | null>(null)
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(1)
|
||||
const qrRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const keyState = useInputState({
|
||||
@ -66,6 +75,14 @@ const BadgeCreationPage: NextPage = () => {
|
||||
subtitle: 'Part of the key pair to be utilized for post-creation access control',
|
||||
})
|
||||
|
||||
const designatedMinterState = useInputState({
|
||||
id: 'designatedMinter',
|
||||
name: 'designatedMinter',
|
||||
title: 'Minter Address',
|
||||
subtitle: 'The address of the designated minter for this badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const performBadgeCreationChecks = () => {
|
||||
try {
|
||||
setReadyToCreateBadge(false)
|
||||
@ -112,6 +129,15 @@ const BadgeCreationPage: NextPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveMinterAddress = async () => {
|
||||
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedMinterAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveMinterAddress()
|
||||
}, [designatedMinterState.value])
|
||||
|
||||
const createNewBadge = async () => {
|
||||
try {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected')
|
||||
@ -133,9 +159,16 @@ const BadgeCreationPage: NextPage = () => {
|
||||
youtube_url: badgeDetails?.youtube_url || undefined,
|
||||
},
|
||||
transferrable: badgeDetails?.transferrable as boolean,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: badgeDetails?.expiry || undefined,
|
||||
max_supply: badgeDetails?.max_supply || undefined,
|
||||
}
|
||||
@ -147,13 +180,50 @@ const BadgeCreationPage: NextPage = () => {
|
||||
badge,
|
||||
type: 'create_badge',
|
||||
}
|
||||
const data = await badgeHubDispatchExecute(payload)
|
||||
console.log(data)
|
||||
setCreatingBadge(false)
|
||||
setTransactionHash(data.split(':')[0])
|
||||
setBadgeId(data.split(':')[1])
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
if (mintRule !== 'by_keys') {
|
||||
setBadgeId(null)
|
||||
setIsAddingKeysComplete(false)
|
||||
const data = await badgeHubDispatchExecute(payload)
|
||||
console.log(data)
|
||||
setCreatingBadge(false)
|
||||
setTransactionHash(data.split(':')[0])
|
||||
setBadgeId(data.split(':')[1])
|
||||
} else {
|
||||
setBadgeId(null)
|
||||
setIsAddingKeysComplete(false)
|
||||
setKeyPairs([])
|
||||
const generatedKeyPairs = generateKeyPairs(numberOfKeys)
|
||||
setKeyPairs(generatedKeyPairs)
|
||||
await badgeHubDispatchExecute(payload)
|
||||
.then(async (data) => {
|
||||
setCreatingBadge(false)
|
||||
setTransactionHash(data.split(':')[0])
|
||||
setBadgeId(data.split(':')[1])
|
||||
const res = await toast.promise(
|
||||
badgeHubContract.use(BADGE_HUB_ADDRESS)?.addKeys(
|
||||
wallet.address,
|
||||
Number(data.split(':')[1]),
|
||||
generatedKeyPairs.map((key) => key.publicKey),
|
||||
) as Promise<string>,
|
||||
{
|
||||
loading: 'Adding keys...',
|
||||
success: (result) => {
|
||||
setIsAddingKeysComplete(true)
|
||||
return `Keys added successfully! Tx Hash: ${result}`
|
||||
},
|
||||
error: (error: { message: any }) => `Failed to add keys: ${error.message}`,
|
||||
},
|
||||
)
|
||||
})
|
||||
.catch((error: { message: any }) => {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
setUploading(false)
|
||||
setIsAddingKeysComplete(false)
|
||||
setCreatingBadge(false)
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message, { style: { maxWidth: 'none' } })
|
||||
setCreatingBadge(false)
|
||||
setUploading(false)
|
||||
}
|
||||
@ -184,7 +254,8 @@ const BadgeCreationPage: NextPage = () => {
|
||||
|
||||
const checkBadgeDetails = () => {
|
||||
if (!badgeDetails) throw new Error('Please fill out the required fields')
|
||||
if (keyState.value === '' || !createdBadgeKey) throw new Error('Please generate a public key')
|
||||
if (mintRule === 'by_key' && (keyState.value === '' || !createdBadgeKey))
|
||||
throw new Error('Please generate a public key')
|
||||
if (badgeDetails.external_url) {
|
||||
try {
|
||||
const url = new URL(badgeDetails.external_url)
|
||||
@ -219,7 +290,15 @@ const BadgeCreationPage: NextPage = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// copy claim url to clipboard
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeId as string}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const copyClaimURL = async () => {
|
||||
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||
const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}`
|
||||
@ -244,6 +323,12 @@ const BadgeCreationPage: NextPage = () => {
|
||||
setReadyToCreateBadge(false)
|
||||
}, [imageUploadDetails?.uploadMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (keyPairs.length > 0) {
|
||||
toast.success('Key pairs generated successfully.')
|
||||
}
|
||||
}, [keyPairs])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NextSeo title="Create Badge" />
|
||||
@ -265,52 +350,164 @@ const BadgeCreationPage: NextPage = () => {
|
||||
</div>
|
||||
<div className="mx-10" ref={scrollRef}>
|
||||
<Conditional test={badgeId !== null}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
||||
<QRCodeSVG
|
||||
className="mx-auto"
|
||||
level="H"
|
||||
size={384}
|
||||
value={`${
|
||||
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
|
||||
/>
|
||||
<Conditional test={mintRule === 'by_key'}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="flex flex-row">
|
||||
<div>
|
||||
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
||||
<QRCodeSVG
|
||||
className="mx-auto"
|
||||
level="H"
|
||||
size={384}
|
||||
value={`${
|
||||
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 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 className="ml-4 text-lg">
|
||||
Badge ID:{` ${badgeId as string}`}
|
||||
<br />
|
||||
Private Key:
|
||||
<Tooltip label="Click to copy the private key">
|
||||
<button
|
||||
className="group flex space-x-2 font-mono text-base text-white/50 hover:underline"
|
||||
onClick={() => void copy(createdBadgeKey as string)}
|
||||
type="button"
|
||||
>
|
||||
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
|
||||
<FaCopy className="opacity-50 group-hover:opacity-100" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<br />
|
||||
Transaction Hash: {' '}
|
||||
<Conditional test={NETWORK === 'testnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<br />
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may click{' '}
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${
|
||||
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>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_keys'}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge ID:{` ${badgeId as string}`}
|
||||
<br />
|
||||
Private Key:
|
||||
<Tooltip label="Click to copy the private key">
|
||||
<button
|
||||
className="group flex space-x-2 font-mono text-base text-white/50 hover:underline"
|
||||
onClick={() => void copy(createdBadgeKey as string)}
|
||||
type="button"
|
||||
Transaction Hash: {' '}
|
||||
<Conditional test={NETWORK === 'testnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
|
||||
>
|
||||
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
|
||||
<FaCopy className="opacity-50 group-hover:opacity-100" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
|
||||
>
|
||||
{transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<br />
|
||||
<Conditional test={isAddingKeysComplete}>
|
||||
<div className="pt-2 mt-4 border-t-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted keys added during badge creation.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Keys
|
||||
</Button>
|
||||
</div>
|
||||
</Conditional>
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may click{' '}
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
|
||||
badgeId as string
|
||||
}`}
|
||||
>
|
||||
here
|
||||
</Anchor>{' '}
|
||||
and select Actions {'>'} Add Keys to add (additional) whitelisted keys or select Actions {'>'}{' '}
|
||||
Mint by Keys to use one of the keys to mint a badge.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_minter'}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge ID:{` ${badgeId as string}`}
|
||||
<br />
|
||||
Designated Minter Address: {` ${resolvedMinterAddress}`}
|
||||
<br />
|
||||
Transaction Hash: {' '}
|
||||
<Conditional test={NETWORK === 'testnet'}>
|
||||
@ -339,22 +536,19 @@ const BadgeCreationPage: NextPage = () => {
|
||||
<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}`}
|
||||
href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
|
||||
badgeId as string
|
||||
}`}
|
||||
>
|
||||
here
|
||||
</Anchor>{' '}
|
||||
or scan the QR code to claim a badge.
|
||||
and select Actions {'>'} Mint By Minter to mint 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>
|
||||
</Alert>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
</Conditional>
|
||||
</div>
|
||||
|
||||
@ -379,6 +573,7 @@ const BadgeCreationPage: NextPage = () => {
|
||||
onClick={() => {
|
||||
setMintRule('by_key')
|
||||
setReadyToCreateBadge(false)
|
||||
setBadgeId(null)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
@ -393,20 +588,20 @@ const BadgeCreationPage: NextPage = () => {
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent',
|
||||
mintRule !== 'by_keys' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5',
|
||||
mintRule !== 'by_keys' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
disabled
|
||||
onClick={() => {
|
||||
setMintRule('by_keys')
|
||||
setReadyToCreateBadge(false)
|
||||
setBadgeId(null)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Mint Rule: By Keys</h4>
|
||||
<span className="text-sm text-slate-500 line-clamp-2">
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
Similar to the By Key rule, however each designated private key can only be used once to mint a badge.
|
||||
</span>
|
||||
</button>
|
||||
@ -416,20 +611,20 @@ const BadgeCreationPage: NextPage = () => {
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent',
|
||||
mintRule !== 'by_minter' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5',
|
||||
mintRule !== 'by_minter' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
disabled
|
||||
onClick={() => {
|
||||
setMintRule('by_minter')
|
||||
setReadyToCreateBadge(false)
|
||||
setBadgeId(null)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Mint Rule: By Minter</h4>
|
||||
<span className="text-sm line-clamp-2 text-slate/500">
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
Badges can be minted by a designated minter account.
|
||||
</span>
|
||||
</button>
|
||||
@ -439,13 +634,40 @@ const BadgeCreationPage: NextPage = () => {
|
||||
|
||||
<div className="mx-10">
|
||||
<ImageUploadDetails mintRule={mintRule} onChange={setImageUploadDetails} />
|
||||
<Conditional test={mintRule === 'by_key'}>
|
||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||
<TextInput className="ml-4 w-full max-w-2xl" {...keyState} disabled required />
|
||||
<Button className="mt-14 ml-4" isDisabled={creatingBadge} onClick={handleGenerateKey}>
|
||||
Generate Key
|
||||
</Button>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<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 test={mintRule === 'by_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of key pairs to be whitelisted for post-creation access control
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 w-1/4 max-w-2xl bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={mintRule === 'by_minter'}>
|
||||
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
|
||||
<TextInput className="ml-4 w-full max-w-lg" {...designatedMinterState} required />
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
|
||||
<BadgeDetails
|
||||
|
@ -1,17 +1,22 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
import type { MintRule } from 'components/badges/creation/ImageUploadDetails'
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox'
|
||||
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressList } from 'components/forms/AddressList'
|
||||
import { useAddressListState } from 'components/forms/AddressList.hooks'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
@ -20,6 +25,7 @@ import { LinkTabs } from 'components/LinkTabs'
|
||||
import { badgeHubLinkTabs } from 'components/LinkTabs.data'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Badge } from 'contracts/badgeHub'
|
||||
@ -40,7 +46,8 @@ import { useMutation } from 'react-query'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
import { copy } from 'utils/clipboard'
|
||||
import { NETWORK } from 'utils/constants'
|
||||
import { sha256 } from 'utils/hash'
|
||||
import { generateKeyPairs, sha256 } from 'utils/hash'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
@ -62,10 +69,15 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null)
|
||||
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
|
||||
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
|
||||
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
|
||||
const [signature, setSignature] = useState<string>('')
|
||||
const [ownerList, setOwnerList] = useState<string[]>([])
|
||||
const [editFee, setEditFee] = useState<number | undefined>(undefined)
|
||||
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
|
||||
const qrRef = useRef<HTMLDivElement>(null)
|
||||
const [numberOfKeys, setNumberOfKeys] = useState(0)
|
||||
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
|
||||
const [mintRule, setMintRule] = useState<MintRule>('by_key')
|
||||
|
||||
const comboboxState = useExecuteComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
@ -173,20 +185,26 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
name: 'owner',
|
||||
title: 'Owner',
|
||||
subtitle: 'The owner of the badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const ownerListState = useAddressListState()
|
||||
|
||||
const pubkeyState = useInputState({
|
||||
id: 'pubkey',
|
||||
name: 'pubkey',
|
||||
title: 'Pubkey',
|
||||
subtitle: 'The public key for the badge',
|
||||
subtitle: 'The whitelisted public key authorized to mint a badge',
|
||||
})
|
||||
|
||||
const privateKeyState = useInputState({
|
||||
id: 'privateKey',
|
||||
name: 'privateKey',
|
||||
title: 'Private Key',
|
||||
subtitle: 'The private key generated during badge creation',
|
||||
subtitle:
|
||||
type === 'mint_by_keys'
|
||||
? 'The corresponding private key for the whitelisted public key'
|
||||
: 'The private key that was generated during badge creation',
|
||||
})
|
||||
|
||||
const nftState = useInputState({
|
||||
@ -200,15 +218,34 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
id: 'limit',
|
||||
name: 'limit',
|
||||
title: 'Limit',
|
||||
subtitle: 'Number of keys/owners to execute the action for',
|
||||
subtitle: 'Number of keys/owners to execute the action for (0 for all)',
|
||||
})
|
||||
|
||||
const designatedMinterState = useInputState({
|
||||
id: 'designatedMinter',
|
||||
name: 'designatedMinter',
|
||||
title: 'Minter Address',
|
||||
subtitle: 'The address of the designated minter for this badge',
|
||||
defaultValue: wallet.address,
|
||||
})
|
||||
|
||||
const showBadgeField = type === 'create_badge'
|
||||
const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge'])
|
||||
const showIdField = isEitherType(type, ['edit_badge', 'mint_by_key'])
|
||||
const showIdField = isEitherType(type, [
|
||||
'edit_badge',
|
||||
'add_keys',
|
||||
'purge_keys',
|
||||
'purge_owners',
|
||||
'mint_by_key',
|
||||
'mint_by_keys',
|
||||
'mint_by_minter',
|
||||
])
|
||||
const showLimitField = isEitherType(type, ['purge_keys', 'purge_owners'])
|
||||
const showNFTField = type === 'set_nft'
|
||||
const showOwnerField = type === 'mint_by_key'
|
||||
const showPrivateKeyField = type === 'mint_by_key'
|
||||
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||
const showOwnerListField = isEitherType(type, ['mint_by_minter'])
|
||||
const showPubkeyField = isEitherType(type, ['mint_by_keys'])
|
||||
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
|
||||
|
||||
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||
const payload: DispatchExecuteArgs = {
|
||||
@ -234,9 +271,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
},
|
||||
@ -260,12 +304,19 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
id: badgeIdState.value,
|
||||
owner: ownerState.value,
|
||||
owner: resolvedOwnerAddress,
|
||||
pubkey: pubkeyState.value,
|
||||
signature,
|
||||
keys: [],
|
||||
limit: limitState.value,
|
||||
owners: [],
|
||||
keys: keyPairs.map((keyPair) => keyPair.publicKey),
|
||||
limit: limitState.value || undefined,
|
||||
owners: [
|
||||
...new Set(
|
||||
ownerListState.values
|
||||
.map((a) => a.address.trim())
|
||||
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
|
||||
.concat(ownerList),
|
||||
),
|
||||
],
|
||||
nft: nftState.value,
|
||||
editFee,
|
||||
contract: contractState.value,
|
||||
@ -337,6 +388,7 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
if (txHash) {
|
||||
setLastTx(txHash.split(':')[0])
|
||||
setCreatedBadgeId(txHash.split(':')[1])
|
||||
badgeIdState.onChange(!isNaN(Number(txHash.split(':')[1])) ? Number(txHash.split(':')[1]) : 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -388,6 +440,14 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
link.click()
|
||||
})
|
||||
}
|
||||
const handleDownloadKeys = () => {
|
||||
const element = document.createElement('a')
|
||||
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
|
||||
element.href = URL.createObjectURL(file)
|
||||
element.download = `badge-${badgeIdState.value}-keys.json`
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
}
|
||||
|
||||
const copyClaimURL = async () => {
|
||||
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
|
||||
@ -409,6 +469,12 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (numberOfKeys > 0) {
|
||||
setKeyPairs(generateKeyPairs(numberOfKeys))
|
||||
}
|
||||
}, [numberOfKeys])
|
||||
|
||||
useEffect(() => {
|
||||
if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
|
||||
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
|
||||
@ -448,6 +514,15 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
void resolveOwnerAddress()
|
||||
}, [ownerState.value])
|
||||
|
||||
const resolveMinterAddress = async () => {
|
||||
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedMinterAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveMinterAddress()
|
||||
}, [designatedMinterState.value])
|
||||
|
||||
const resolveManagerAddress = async () => {
|
||||
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setBadge({
|
||||
@ -472,9 +547,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
})
|
||||
@ -508,9 +590,16 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
},
|
||||
transferrable,
|
||||
rule: {
|
||||
by_key: keyState.value,
|
||||
},
|
||||
rule:
|
||||
mintRule === 'by_key'
|
||||
? {
|
||||
by_key: keyState.value,
|
||||
}
|
||||
: mintRule === 'by_minter'
|
||||
? {
|
||||
by_minter: resolvedMinterAddress,
|
||||
}
|
||||
: 'by_keys',
|
||||
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
|
||||
max_supply: maxSupplyState.value || undefined,
|
||||
})
|
||||
@ -541,7 +630,7 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
/>
|
||||
<LinkTabs activeIndex={2} data={badgeHubLinkTabs} />
|
||||
|
||||
{showBadgeField && createdBadgeId && createdBadgeKey && (
|
||||
{showBadgeField && createdBadgeId && createdBadgeKey && mintRule === 'by_key' && (
|
||||
<div className="flex flex-row">
|
||||
<div className="ml-4">
|
||||
<div className="w-[384px] h-[384px]" ref={qrRef}>
|
||||
@ -598,14 +687,116 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBadgeField && createdBadgeId && mintRule === 'by_keys' && (
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge ID:{` ${createdBadgeId as string}`}
|
||||
<br />
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may select Message Type {'>'} Add Keys to add whitelisted keys authorized to mint a badge.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{showBadgeField && createdBadgeId && mintRule === 'by_minter' && (
|
||||
<Alert className="mt-5" type="info">
|
||||
<div className="ml-4 text-lg">
|
||||
Badge successfully created with ID:{` ${createdBadgeId as string}`}
|
||||
<br />
|
||||
Designated Minter Address: {` ${resolvedMinterAddress}`}
|
||||
<br />
|
||||
<div className="text-base">
|
||||
<div className="flex-row pt-4 mt-4 border-t-2">
|
||||
<span>
|
||||
You may select Message Type {'>'} Mint by Minter to mint badges using the designated minter wallet.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<ExecuteCombobox {...comboboxState} />
|
||||
<Conditional test={type === 'create_badge'}>
|
||||
<div className={clsx('flex flex-col space-y-2')}>
|
||||
<div>
|
||||
<div className="flex">
|
||||
<span className="mt-1 text-base font-bold first-letter:capitalize">Mint Rule: </span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={mintRule === 'by_key'}
|
||||
className="peer sr-only"
|
||||
id="ruleRadio1"
|
||||
name="ruletRadio1"
|
||||
onClick={() => {
|
||||
setMintRule('by_key')
|
||||
setCreatedBadgeId(null)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="ruleRadio1"
|
||||
>
|
||||
By Key
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={mintRule === 'by_keys'}
|
||||
className="peer sr-only"
|
||||
id="ruleRadio2"
|
||||
name="ruletRadio2"
|
||||
onClick={() => {
|
||||
setMintRule('by_keys')
|
||||
setCreatedBadgeId(null)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="ruleRadio2"
|
||||
>
|
||||
By Keys
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={mintRule === 'by_minter'}
|
||||
className="peer sr-only"
|
||||
id="ruleRadio3"
|
||||
name="ruletRadio3"
|
||||
onClick={() => {
|
||||
setMintRule('by_minter')
|
||||
setCreatedBadgeId(null)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="ruleRadio3"
|
||||
>
|
||||
By Minter
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
{showIdField && <NumberInput {...badgeIdState} />}
|
||||
{showBadgeField && <AddressInput {...managerState} />}
|
||||
{showBadgeField && <TextInput {...keyState} />}
|
||||
{showBadgeField && <Button onClick={handleGenerateKey}>Generate Key</Button>}
|
||||
{showBadgeField && mintRule === 'by_key' && <TextInput {...keyState} />}
|
||||
{showBadgeField && mintRule === 'by_key' && <Button onClick={handleGenerateKey}>Generate Key</Button>}
|
||||
{showBadgeField && mintRule === 'by_minter' && <TextInput {...designatedMinterState} />}
|
||||
{showMetadataField && (
|
||||
<div className="p-4 rounded-md border-2 border-gray-800">
|
||||
<span className="text-gray-400">Metadata</span>
|
||||
@ -636,6 +827,57 @@ const BadgeHubExecutePage: NextPage = () => {
|
||||
title="Owner"
|
||||
/>
|
||||
)}
|
||||
<Conditional test={showOwnerListField}>
|
||||
<div className="mt-4">
|
||||
<AddressList
|
||||
entries={ownerListState.entries}
|
||||
isRequired
|
||||
onAdd={ownerListState.add}
|
||||
onChange={ownerListState.update}
|
||||
onRemove={ownerListState.remove}
|
||||
subtitle="Enter the owner addresses"
|
||||
title="Addresses"
|
||||
/>
|
||||
<Alert className="mt-8" type="info">
|
||||
You may optionally choose a text file of additional owner addresses.
|
||||
</Alert>
|
||||
<WhitelistUpload onChange={setOwnerList} />
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={type === 'add_keys'}>
|
||||
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
|
||||
<div className="grid grid-cols-2 gap-24">
|
||||
<div className="flex flex-col ml-4">
|
||||
<span className="font-bold">Number of Keys</span>
|
||||
<span className="text-sm text-white/80">
|
||||
The number of public keys to be whitelisted for minting badges
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
|
||||
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
|
||||
required
|
||||
type="number"
|
||||
value={numberOfKeys}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
|
||||
<Alert type="info">
|
||||
<div className="pt-2">
|
||||
<span className="mt-2">
|
||||
Make sure to download the whitelisted public keys together with their private key counterparts.
|
||||
</span>
|
||||
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
|
||||
Download Key Pairs
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
{showLimitField && <NumberInput {...limitState} />}
|
||||
{showPubkeyField && <TextInput className="mt-2" {...pubkeyState} />}
|
||||
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
|
||||
{showNFTField && <AddressInput {...nftState} />}
|
||||
</div>
|
||||
|
@ -154,7 +154,7 @@ const BadgeHubQueryPage: NextPage = () => {
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
<Conditional test={type === 'getBadge' || type === 'getKey'}>
|
||||
<Conditional test={type === 'getBadge' || type === 'getKey' || type === 'getKeys'}>
|
||||
<NumberInput {...idState} />
|
||||
</Conditional>
|
||||
<Conditional test={type === 'getKey'}>
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import * as crypto from 'crypto'
|
||||
import { Word32Array } from 'jscrypto'
|
||||
import { SHA256 } from 'jscrypto/SHA256'
|
||||
import * as secp256k1 from 'secp256k1'
|
||||
@ -23,3 +24,21 @@ export function generateSignature(id: number, owner: string, privateKey: string)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function generateKeyPairs(amount: number) {
|
||||
const keyPairs: { publicKey: string; privateKey: string }[] = []
|
||||
for (let i = 0; i < amount; i++) {
|
||||
let privKey: Buffer
|
||||
do {
|
||||
privKey = crypto.randomBytes(32)
|
||||
} while (!secp256k1.privateKeyVerify(privKey))
|
||||
|
||||
const privateKey = privKey.toString('hex')
|
||||
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
|
||||
keyPairs.push({
|
||||
publicKey,
|
||||
privateKey,
|
||||
})
|
||||
}
|
||||
return keyPairs
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user