Merge pull request #115 from public-awesome/develop

Sync development > main
This commit is contained in:
Serkan Reis 2023-02-28 18:49:47 +03:00 committed by GitHub
commit eb86960ca6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 746 additions and 167 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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',

View File

@ -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&apos;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&apos;s whitelisted' },
{ id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
]
export interface DispatchQueryProps {

View File

@ -1,6 +1,6 @@
{
"name": "stargaze-studio",
"version": "0.4.5",
"version": "0.4.6",
"workspaces": [
"packages/*"
],

View File

@ -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

View File

@ -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>

View File

@ -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'}>

View File

@ -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
}