Merge branch 'develop' into sg721-updatable-integration

This commit is contained in:
Serkan Reis 2023-03-07 19:07:59 +03:00 committed by GitHub
commit 3dc1843d06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1261 additions and 298 deletions

View File

@ -1,9 +1,9 @@
APP_VERSION=0.4.5 APP_VERSION=0.4.8
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=793 NEXT_PUBLIC_SG721_CODE_ID=1702
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=275 NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1701
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1s48apjumprma0d64ge88dmu7lr8exu02tlw90xxupgds0s25gfasx447dn" NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1xz4d6wzxqn3udgsm5qnr78y032xng4r2ycv7aw6mjtsuw59s2n9s93ec0v"
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34tfq3zk87l78sdxngzv" NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34tfq3zk87l78sdxngzv"
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr" NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
NEXT_PUBLIC_BASE_MINTER_CODE_ID=613 NEXT_PUBLIC_BASE_MINTER_CODE_ID=613

View File

@ -7,6 +7,7 @@ export interface TooltipProps extends ComponentProps<'div'> {
label: ReactNode label: ReactNode
children: ReactElement children: ReactElement
placement?: 'top' | 'bottom' | 'left' | 'right' placement?: 'top' | 'bottom' | 'left' | 'right'
backgroundColor?: string
} }
export const Tooltip = ({ label, children, ...props }: TooltipProps) => { export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
@ -33,7 +34,11 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => {
<div <div
{...props} {...props}
{...attributes.popper} {...attributes.popper}
className={clsx('py-1 px-2 m-1 text-sm bg-black/80 rounded shadow-md', props.className)} className={clsx(
'py-1 px-2 m-1 text-sm rounded shadow-md',
props.backgroundColor ? props.backgroundColor : 'bg-slate-900',
props.className,
)}
ref={setPopperElement} ref={setPopperElement}
style={{ ...styles.popper, ...props.style }} style={{ ...styles.popper, ...props.style }}
> >

View File

@ -3,21 +3,25 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
// import { AirdropUpload } from 'components/AirdropUpload' // import { AirdropUpload } from 'components/AirdropUpload'
import { toUtf8 } from '@cosmjs/encoding' import { toUtf8 } from '@cosmjs/encoding'
import { Alert } from 'components/Alert'
import type { DispatchExecuteArgs } from 'components/badges/actions/actions' import type { DispatchExecuteArgs } from 'components/badges/actions/actions'
import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions' import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions'
import { ActionsCombobox } from 'components/badges/actions/Combobox' import { ActionsCombobox } from 'components/badges/actions/Combobox'
import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks' import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks'
import { Button } from 'components/Button' import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { MetadataAttributes } from 'components/forms/MetadataAttributes' import { MetadataAttributes } from 'components/forms/MetadataAttributes'
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash' import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { Badge, BadgeHubInstance } from 'contracts/badgeHub' import type { Badge, BadgeHubInstance } from 'contracts/badgeHub'
import * as crypto from 'crypto'
import sizeof from 'object-sizeof' import sizeof from 'object-sizeof'
import type { FormEvent } from 'react' import type { FormEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -25,11 +29,12 @@ import { toast } from 'react-hot-toast'
import { FaArrowRight } from 'react-icons/fa' import { FaArrowRight } from 'react-icons/fa'
import { useMutation } from 'react-query' import { useMutation } from 'react-query'
import * as secp256k1 from 'secp256k1' import * as secp256k1 from 'secp256k1'
import { sha256 } from 'utils/hash' import { generateKeyPairs, sha256 } from 'utils/hash'
import { isValidAddress } from 'utils/isValidAddress'
import { resolveAddress } from 'utils/resolveAddress' import { resolveAddress } from 'utils/resolveAddress'
import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload' import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload'
import { AddressInput, TextInput } from '../../forms/FormInput' import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
import type { MintRule } from '../creation/ImageUploadDetails' import type { MintRule } from '../creation/ImageUploadDetails'
interface BadgeActionsProps { interface BadgeActionsProps {
@ -52,8 +57,10 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('') const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
const [editFee, setEditFee] = useState<number | undefined>(undefined) const [editFee, setEditFee] = useState<number | undefined>(undefined)
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false) const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
const [keyPairs, setKeyPairs] = useState<string[]>([]) const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
const [signature, setSignature] = useState<string>('') const [signature, setSignature] = useState<string>('')
const [ownerList, setOwnerList] = useState<string[]>([])
const [numberOfKeys, setNumberOfKeys] = useState(0)
const actionComboboxState = useActionsComboboxState() const actionComboboxState = useActionsComboboxState()
const type = actionComboboxState.value?.id const type = actionComboboxState.value?.id
@ -147,18 +154,26 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
defaultValue: wallet.address, defaultValue: wallet.address,
}) })
const pubkeyState = useInputState({ const ownerListState = useAddressListState()
id: 'pubkey',
name: 'pubkey', const pubKeyState = useInputState({
title: 'Pubkey', id: 'pubKey',
subtitle: 'The public key to check whether it can be used to mint a badge', name: 'pubKey',
title: 'Public Key',
subtitle:
type === 'mint_by_keys'
? 'The whitelisted public key authorized to mint a badge'
: 'The public key to check whether it can be used to mint a badge',
}) })
const privateKeyState = useInputState({ const privateKeyState = useInputState({
id: 'privateKey', id: 'privateKey',
name: 'privateKey', name: 'privateKey',
title: 'Private Key', title: 'Private Key',
subtitle: 'The private key that was generated during badge creation', subtitle:
type === 'mint_by_keys'
? 'The corresponding private key for the whitelisted public key'
: 'The private key that was generated during badge creation',
}) })
const nftState = useInputState({ const nftState = useInputState({
@ -172,13 +187,16 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
id: 'limit', id: 'limit',
name: 'limit', name: 'limit',
title: 'Limit', title: 'Limit',
subtitle: 'Number of keys/owners to execute the action for', subtitle: 'Number of keys/owners to execute the action for (0 for all)',
}) })
const showMetadataField = isEitherType(type, ['edit_badge']) const showMetadataField = isEitherType(type, ['edit_badge'])
const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys']) const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'mint_by_minter'])
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key']) const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key'])
const showAirdropFileField = isEitherType(type, ['airdrop_by_key']) const showAirdropFileField = isEitherType(type, ['airdrop_by_key'])
const showOwnerList = isEitherType(type, ['mint_by_minter'])
const showPubKeyField = isEitherType(type, ['mint_by_keys'])
const showLimitState = isEitherType(type, ['purge_keys', 'purge_owners'])
const payload: DispatchExecuteArgs = { const payload: DispatchExecuteArgs = {
badge: { badge: {
@ -231,11 +249,18 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
id: badgeId, id: badgeId,
editFee, editFee,
owner: resolvedOwnerAddress, owner: resolvedOwnerAddress,
pubkey: pubkeyState.value, pubkey: pubKeyState.value,
signature, signature,
keys: [], keys: keyPairs.map((keyPair) => keyPair.publicKey),
limit: limitState.value, limit: limitState.value || undefined,
owners: [], owners: [
...new Set(
ownerListState.values
.map((a) => a.address.trim())
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
.concat(ownerList),
),
],
recipients: airdropAllocationArray, recipients: airdropAllocationArray,
privateKey: privateKeyState.value, privateKey: privateKeyState.value,
nft: nftState.value, nft: nftState.value,
@ -355,6 +380,21 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value) handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value)
}, [privateKeyState.value, resolvedOwnerAddress]) }, [privateKeyState.value, resolvedOwnerAddress])
useEffect(() => {
if (numberOfKeys > 0) {
setKeyPairs(generateKeyPairs(numberOfKeys))
}
}, [numberOfKeys])
const handleDownloadKeys = () => {
const element = document.createElement('a')
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
element.href = URL.createObjectURL(file)
element.download = `badge-${badgeId.toString()}-keys.json`
document.body.appendChild(element)
element.click()
}
const { isLoading, mutate } = useMutation( const { isLoading, mutate } = useMutation(
async (event: FormEvent) => { async (event: FormEvent) => {
if (!wallet.client) { if (!wallet.client) {
@ -467,19 +507,6 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
} }
} }
const handleGenerateKeys = (amount: number) => {
for (let i = 0; i < amount; i++) {
let privKey: Buffer
do {
privKey = crypto.randomBytes(32)
} while (!secp256k1.privateKeyVerify(privKey))
const privateKey = privKey.toString('hex')
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
keyPairs.push(publicKey.concat(',', privateKey))
}
}
return ( return (
<form> <form>
<div className="grid grid-cols-2 mt-4"> <div className="grid grid-cols-2 mt-4">
@ -515,7 +542,65 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
title="Owner" title="Owner"
/> />
)} )}
{showPubKeyField && <TextInput className="mt-2" {...pubKeyState} />}
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />} {showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
{showLimitState && <NumberInput className="mt-2" {...limitState} />}
<Conditional test={isEitherType(type, ['purge_owners', 'purge_keys'])}>
<Alert className="mt-4" type="info">
This action is only available if the badge with the specified id is either minted out or expired.
</Alert>
</Conditional>
<Conditional test={type === 'add_keys'}>
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
<div className="grid grid-cols-2 gap-24">
<div className="flex flex-col ml-4">
<span className="font-bold">Number of Keys</span>
<span className="text-sm text-white/80">
The number of public keys to be whitelisted for minting badges
</span>
</div>
<input
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
required
type="number"
value={numberOfKeys}
/>
</div>
</div>
</Conditional>
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
<Alert type="info">
<div className="pt-2">
<span className="mt-2">
Make sure to download the whitelisted public keys together with their private key counterparts.
</span>
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
Download Key Pairs
</Button>
</div>
</Alert>
</Conditional>
<Conditional test={showOwnerList}>
<div className="mt-4">
<AddressList
entries={ownerListState.entries}
isRequired
onAdd={ownerListState.add}
onChange={ownerListState.update}
onRemove={ownerListState.remove}
subtitle="Enter the owner addresses"
title="Addresses"
/>
<Alert className="mt-8" type="info">
You may optionally choose a text file of additional owner addresses.
</Alert>
<WhitelistUpload onChange={setOwnerList} />
</div>
</Conditional>
{showAirdropFileField && ( {showAirdropFileField && (
<FormGroup <FormGroup

View File

@ -28,11 +28,6 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
name: 'Edit Badge', name: 'Edit Badge',
description: `Edit badge metadata for the badge with the specified ID`, description: `Edit badge metadata for the badge with the specified ID`,
}, },
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
{ {
id: 'mint_by_key', id: 'mint_by_key',
name: 'Mint by Key', name: 'Mint by Key',
@ -43,6 +38,11 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [
name: 'Airdrop by Key', name: 'Airdrop by Key',
description: `Airdrop badges to a list of specified addresses`, description: `Airdrop badges to a list of specified addresses`,
}, },
{
id: 'purge_owners',
name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`,
},
] ]
export const BY_KEYS_ACTION_LIST: ActionListItem[] = [ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
@ -51,6 +51,11 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
name: 'Edit Badge', name: 'Edit Badge',
description: `Edit badge metadata for the badge with the specified ID`, description: `Edit badge metadata for the badge with the specified ID`,
}, },
{
id: 'mint_by_keys',
name: 'Mint by Keys',
description: `Mint a new badge with a whitelisted private key`,
},
{ {
id: 'add_keys', id: 'add_keys',
name: 'Add Keys', name: 'Add Keys',
@ -66,11 +71,6 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [
name: 'Purge Owners', name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`, description: `Purge owners from the badge with the specified ID`,
}, },
{
id: 'mint_by_keys',
name: 'Mint by Keys',
description: `Mint a new badge with a whitelisted private key`,
},
] ]
export const BY_MINTER_ACTION_LIST: ActionListItem[] = [ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
@ -79,16 +79,16 @@ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [
name: 'Edit Badge', name: 'Edit Badge',
description: `Edit badge metadata for the badge with the specified ID`, description: `Edit badge metadata for the badge with the specified ID`,
}, },
{
id: 'mint_by_minter',
name: 'Mint by Minter',
description: `Mint a new badge to specified owner addresses`,
},
{ {
id: 'purge_owners', id: 'purge_owners',
name: 'Purge Owners', name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`, description: `Purge owners from the badge with the specified ID`,
}, },
{
id: 'mint_by_minter',
name: 'Mint by Minter',
description: `Mint a new badge to the specified addresses`,
},
] ]
export interface DispatchExecuteProps { export interface DispatchExecuteProps {

View File

@ -3,24 +3,30 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx' import clsx from 'clsx'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { Trait } from 'contracts/badgeHub' import type { Trait } from 'contracts/badgeHub'
import { useEffect, useState } from 'react' import type { ChangeEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { BADGE_HUB_ADDRESS } from 'utils/constants'
import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput' import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput'
import { MetadataAttributes } from '../../forms/MetadataAttributes' import { MetadataAttributes } from '../../forms/MetadataAttributes'
import { Tooltip } from '../../Tooltip'
import type { MintRule, UploadMethod } from './ImageUploadDetails' import type { MintRule, UploadMethod } from './ImageUploadDetails'
interface BadgeDetailsProps { interface BadgeDetailsProps {
onChange: (data: BadgeDetailsDataProps) => void onChange: (data: BadgeDetailsDataProps) => void
uploadMethod: UploadMethod | undefined uploadMethod: UploadMethod | undefined
mintRule: MintRule mintRule: MintRule
metadataSize: number
} }
export interface BadgeDetailsDataProps { export interface BadgeDetailsDataProps {
@ -38,10 +44,14 @@ export interface BadgeDetailsDataProps {
youtube_url?: string youtube_url?: string
} }
export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => { export const BadgeDetails = ({ metadataSize, onChange }: BadgeDetailsProps) => {
const wallet = useWallet() const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined) const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [transferrable, setTransferrable] = useState<boolean>(false) const [transferrable, setTransferrable] = useState<boolean>(false)
const [metadataFile, setMetadataFile] = useState<File>()
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
const metadataFileRef = useRef<HTMLInputElement | null>(null)
const managerState = useInputState({ const managerState = useInputState({
id: 'manager-address', id: 'manager-address',
@ -109,6 +119,79 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
subtitle: 'YouTube URL for the badge', subtitle: 'YouTube URL for the badge',
}) })
const parseMetadata = async () => {
try {
let parsedMetadata: any
if (metadataFile) {
attributesState.reset()
parsedMetadata = JSON.parse(await metadataFile.text())
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
attributesState.add({
trait_type: '',
value: '',
})
} else {
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
attributesState.add({
trait_type: parsedMetadata.attributes[i].trait_type,
value: parsedMetadata.attributes[i].value,
})
}
}
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
backgroundColorState.onChange(parsedMetadata.background_color ? parsedMetadata.background_color : '')
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
} else {
attributesState.reset()
nameState.onChange('')
descriptionState.onChange('')
externalUrlState.onChange('')
youtubeUrlState.onChange('')
animationUrlState.onChange('')
backgroundColorState.onChange('')
imageDataState.onChange('')
}
} catch (error) {
toast.error('Error parsing metadata file: Invalid JSON format.')
if (metadataFileRef.current) metadataFileRef.current.value = ''
setMetadataFile(undefined)
}
}
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFile(undefined)
if (event.target.files === null) return
let selectedFile: File
const reader = new FileReader()
reader.onload = (e) => {
if (!event.target.files) return toast.error('No file selected.')
if (!e.target?.result) return toast.error('Error parsing file.')
selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'application/json' })
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
else return toast.error('No file selected.')
reader.onloadend = () => {
if (!event.target.files) return toast.error('No file selected.')
setMetadataFile(selectedFile)
}
}
useEffect(() => {
void parseMetadata()
if (!metadataFile)
attributesState.add({
trait_type: '',
value: '',
})
}, [metadataFile])
useEffect(() => { useEffect(() => {
try { try {
const data: BadgeDetailsDataProps = { const data: BadgeDetailsDataProps = {
@ -155,13 +238,25 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
]) ])
useEffect(() => { useEffect(() => {
if (attributesState.values.length === 0) const retrieveFeeRate = async () => {
attributesState.add({ try {
trait_type: '', if (wallet.client) {
value: '', const feeRateRaw = await wallet.client.queryContractRaw(
}) BADGE_HUB_ADDRESS,
// eslint-disable-next-line react-hooks/exhaustive-deps toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
}, []) )
console.log('Fee Rate Raw: ', feeRateRaw)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
setMetadataFeeRate(Number(feeRate.metadata))
}
} catch (error) {
toast.error('Error retrieving metadata fee rate.')
setMetadataFeeRate(0)
console.log('Error retrieving fee rate: ', error)
}
}
void retrieveFeeRate()
}, [wallet.client])
return ( return (
<div> <div>
@ -172,19 +267,35 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
<TextInput className="mt-2" {...descriptionState} /> <TextInput className="mt-2" {...descriptionState} />
<NumberInput className="mt-2" {...maxSupplyState} /> <NumberInput className="mt-2" {...maxSupplyState} />
<TextInput className="mt-2" {...externalUrlState} /> <TextInput className="mt-2" {...externalUrlState} />
<FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date"> <FormControl className="mt-2" htmlId="expiry-date" subtitle="Badge minting expiry date" title="Expiry Date">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} /> <InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl> </FormControl>
<div className="mt-2 form-control"> <div className="grid grid-cols-2">
<label className="justify-start cursor-pointer label"> <div className="mt-2 w-1/3 form-control">
<span className="mr-4 font-bold">Transferrable</span> <label className="justify-start cursor-pointer label">
<input <span className="mr-4 font-bold">Transferrable</span>
checked={transferrable} <input
className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`} checked={transferrable}
onClick={() => setTransferrable(!transferrable)} className={`toggle ${transferrable ? `bg-stargaze` : `bg-gray-600`}`}
type="checkbox" onClick={() => setTransferrable(!transferrable)}
/> type="checkbox"
</label> />
</label>
</div>
<Conditional test={managerState.value !== ''}>
<Tooltip
backgroundColor="bg-stargaze"
className="bg-yellow-600"
label="This is only an estimate. Be sure to check the final amount before signing the transaction."
placement="bottom"
>
<div className="grid grid-cols-2 ml-12 w-full">
<div className="mt-4 font-bold">Fee Estimate:</div>
<span className="mt-4">{(metadataSize * Number(metadataFeeRate)) / 1000000} stars</span>
</div>
</Tooltip>
</Conditional>
</div> </div>
</div> </div>
<div className={clsx('ml-10')}> <div className={clsx('ml-10')}>
@ -197,6 +308,40 @@ export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => {
title="Traits" title="Traits"
/> />
</div> </div>
<div className="w-full">
<Tooltip
backgroundColor="bg-blue-500"
label="A metadata file can be selected to automatically fill in the related fields."
placement="bottom"
>
<div>
<label
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
htmlFor="assetFile"
>
Metadata File Selection (optional)
</label>
<div
className={clsx(
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="application/json"
className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="metadataFile"
onChange={selectMetadata}
ref={metadataFileRef}
type="file"
/>
</div>
</div>
</Tooltip>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -172,7 +172,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
</div> </div>
</div> </div>
<div className="p-3 py-5 pb-8"> <div className="p-3 py-5 pb-4">
<Conditional test={uploadMethod === 'existing'}> <Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column"> <div className="ml-3 flex-column">
<p className="mb-5 ml-5"> <p className="mb-5 ml-5">
@ -187,8 +187,17 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
</Anchor>{' '} </Anchor>{' '}
and upload your image manually to get an image URL for your badge. and upload your image manually to get an image URL for your badge.
</p> </p>
<div> <div className="flex flex-row w-full">
<TextInput {...imageUrlState} className="mt-2 ml-4 w-1/2" /> <TextInput {...imageUrlState} className="mt-2 ml-6 w-full max-w-2xl" />
<Conditional test={imageUrlState.value !== ''}>
<div className="mt-2 ml-4 w-1/4 border-2 border-dashed">
<img
alt="badge-preview"
className="w-full"
src={imageUrlState.value.replace('IPFS://', 'ipfs://').replace(/,/g, '').replace(/"/g, '').trim()}
/>
</div>
</Conditional>
</div> </div>
</div> </div>
</Conditional> </Conditional>
@ -252,31 +261,33 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
<div className="mt-6"> <div className="mt-6">
<div className="grid grid-cols-2"> <div className="grid grid-cols-2">
<div className="w-full"> <div>
<div> <div className="w-full">
<label <div>
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300" <label
htmlFor="assetFile" className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
> htmlFor="assetFile"
Image Selection >
</label> Image Selection
<div </label>
className={clsx( <div
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'rounded border-2 border-white/20 border-dashed',
)}
>
<input
accept="image/*"
className={clsx( className={clsx(
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer', 'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', 'rounded border-2 border-white/20 border-dashed',
)} )}
id="assetFile" >
onChange={selectAsset} <input
ref={assetFileRef} accept="image/*"
type="file" className={clsx(
/> 'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)}
id="assetFile"
onChange={selectAsset}
ref={assetFileRef}
type="file"
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -124,8 +124,8 @@ export const CollectionActions = ({
const priceState = useNumberInputState({ const priceState = useNumberInputState({
id: 'update-mint-price', id: 'update-mint-price',
name: 'updateMintPrice', name: 'updateMintPrice',
title: 'Update Mint Price', title: type === 'update_discount_price' ? 'Discount Price' : 'Update Mint Price',
subtitle: 'New minting price in STARS', subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'New minting price in STARS',
}) })
const descriptionState = useInputState({ const descriptionState = useInputState({
@ -161,7 +161,7 @@ export const CollectionActions = ({
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '8%', placeholder: '5%',
}) })
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata']) const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
@ -185,7 +185,7 @@ export const CollectionActions = ({
'batch_mint_for', 'batch_mint_for',
]) ])
const showAirdropFileField = type === 'airdrop' const showAirdropFileField = type === 'airdrop'
const showPriceField = type === 'update_mint_price' const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const showDescriptionField = type === 'update_collection_info' const showDescriptionField = type === 'update_collection_info'
const showImageField = type === 'update_collection_info' const showImageField = type === 'update_collection_info'
const showExternalLinkField = type === 'update_collection_info' const showExternalLinkField = type === 'update_collection_info'
@ -379,8 +379,8 @@ export const CollectionActions = ({
{showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />} {showTokenIdField && <NumberInput className="mt-2" {...tokenIdState} />}
{showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />} {showTokenIdListField && <TextInput className="mt-2" {...tokenIdListState} />}
{showBaseUriField && <TextInput className="mt-2" {...baseURIState} />} {showBaseUriField && <TextInput className="mt-2" {...baseURIState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />} {showNumberOfTokensField && <NumberInput className="mt-2" {...batchNumberState} />}
{showPriceField && <NumberInput {...priceState} />} {showPriceField && <NumberInput className="mt-2" {...priceState} />}
{showDescriptionField && <TextInput className="my-2" {...descriptionState} />} {showDescriptionField && <TextInput className="my-2" {...descriptionState} />}
{showImageField && <TextInput className="mb-2" {...imageState} />} {showImageField && <TextInput className="mb-2" {...imageState} />}
{showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />} {showExternalLinkField && <TextInput className="mb-2" {...externalLinkState} />}

View File

@ -13,6 +13,8 @@ export const ACTION_TYPES = [
'mint_token_uri', 'mint_token_uri',
'purge', 'purge',
'update_mint_price', 'update_mint_price',
'update_discount_price',
'remove_discount_price',
'mint_to', 'mint_to',
'mint_for', 'mint_for',
'batch_mint', 'batch_mint',
@ -90,16 +92,21 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Mint', name: 'Mint',
description: `Mint a token`, description: `Mint a token`,
}, },
{
id: 'purge',
name: 'Purge',
description: `Purge`,
},
{ {
id: 'update_mint_price', id: 'update_mint_price',
name: 'Update Mint Price', name: 'Update Mint Price',
description: `Update mint price`, description: `Update mint price`,
}, },
{
id: 'update_discount_price',
name: 'Update Discount Price',
description: `Update discount price`,
},
{
id: 'remove_discount_price',
name: 'Remove Discount Price',
description: `Remove discount price`,
},
{ {
id: 'mint_to', id: 'mint_to',
name: 'Mint To', name: 'Mint To',
@ -185,6 +192,11 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
name: 'Burn Remaining Tokens', name: 'Burn Remaining Tokens',
description: 'Burn remaining tokens', description: 'Burn remaining tokens',
}, },
{
id: 'purge',
name: 'Purge',
description: `Purge`,
},
] ]
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [ export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
@ -226,6 +238,8 @@ export type DispatchExecuteArgs = {
| { type: Select<'mint_token_uri'>; tokenUri: string } | { type: Select<'mint_token_uri'>; tokenUri: string }
| { type: Select<'purge'> } | { type: Select<'purge'> }
| { type: Select<'update_mint_price'>; price: string } | { type: Select<'update_mint_price'>; price: string }
| { type: Select<'update_discount_price'>; price: string }
| { type: Select<'remove_discount_price'> }
| { type: Select<'mint_to'>; recipient: string } | { type: Select<'mint_to'>; recipient: string }
| { type: Select<'mint_for'>; recipient: string; tokenId: number } | { type: Select<'mint_for'>; recipient: string; tokenId: number }
| { type: Select<'batch_mint'>; recipient: string; batchNumber: number } | { type: Select<'batch_mint'>; recipient: string; batchNumber: number }
@ -266,6 +280,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'update_mint_price': { case 'update_mint_price': {
return vendingMinterMessages.updateMintPrice(txSigner, args.price) return vendingMinterMessages.updateMintPrice(txSigner, args.price)
} }
case 'update_discount_price': {
return vendingMinterMessages.updateDiscountPrice(txSigner, args.price)
}
case 'remove_discount_price': {
return vendingMinterMessages.removeDiscountPrice(txSigner)
}
case 'mint_to': { case 'mint_to': {
return vendingMinterMessages.mintTo(txSigner, args.recipient) return vendingMinterMessages.mintTo(txSigner, args.recipient)
} }
@ -353,6 +373,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'update_mint_price': { case 'update_mint_price': {
return vendingMinterMessages(minterContract)?.updateMintPrice(args.price) return vendingMinterMessages(minterContract)?.updateMintPrice(args.price)
} }
case 'update_discount_price': {
return vendingMinterMessages(minterContract)?.updateDiscountPrice(args.price)
}
case 'remove_discount_price': {
return vendingMinterMessages(minterContract)?.removeDiscountPrice()
}
case 'mint_to': { case 'mint_to': {
return vendingMinterMessages(minterContract)?.mintTo(args.recipient) return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
} }

View File

@ -36,7 +36,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '8%', placeholder: '5%',
}) })
useEffect(() => { useEffect(() => {

View File

@ -73,9 +73,10 @@ export function MetadataAttribute({ id, isLast, onAdd, onChange, onRemove, defau
}, [traitTypeState.value, traitValueState.value, id]) }, [traitTypeState.value, traitValueState.value, id])
return ( return (
<div className="grid relative grid-cols-[1fr_1fr_auto] space-x-2"> <div className="grid relative 2xl:grid-cols-[1fr_1fr_auto] 2xl:space-x-2">
<TraitTypeInput {...traitTypeState} /> <TraitTypeInput {...traitTypeState} />
<TraitValueInput {...traitValueState} /> <TraitValueInput {...traitValueState} />
<div className="flex justify-end items-end pb-2 w-8"> <div className="flex justify-end items-end pb-2 w-8">
<button <button
className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full" className="flex justify-center items-center p-2 bg-stargaze-80 hover:bg-plumbus-60 rounded-full"

View File

@ -2,7 +2,7 @@
"path": "/assets/", "path": "/assets/",
"appName": "StargazeStudio", "appName": "StargazeStudio",
"appShortName": "StargazeStudio", "appShortName": "StargazeStudio",
"appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own NFT collection in no time.", "appDescription": "Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT collection in no time.",
"developerName": "StargazeStudio", "developerName": "StargazeStudio",
"developerURL": "https://", "developerURL": "https://",
"background": "#FFC27D", "background": "#FFC27D",

View File

@ -28,7 +28,7 @@ export interface MigrateResponse {
export interface Rule { export interface Rule {
by_key?: string by_key?: string
by_minter?: string by_minter?: string
by_keys?: string[] by_keys?: string
} }
export interface Trait { export interface Trait {
@ -53,7 +53,7 @@ export interface Badge {
manager: string manager: string
metadata: Metadata metadata: Metadata
transferrable: boolean transferrable: boolean
rule: Rule rule: Rule | string
expiry?: number expiry?: number
max_supply?: number max_supply?: number
} }
@ -102,7 +102,7 @@ export interface CreateBadgeMessage {
manager: string manager: string
metadata: Metadata metadata: Metadata
transferrable: boolean transferrable: boolean
rule: Rule rule: Rule | string
expiry?: number expiry?: number
max_supply?: number max_supply?: number
} }
@ -349,6 +349,16 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
} }
const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => { const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise<string> => {
const feeRateRaw = await client.queryContractRaw(
contractAddress,
toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()),
)
console.log('Fee Rate Raw: ', feeRateRaw)
const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array))
console.log('Fee Rate:', feeRate)
console.log('keys size: ', sizeof(keys))
console.log(keys)
const res = await client.execute( const res = await client.execute(
senderAddress, senderAddress,
contractAddress, contractAddress,
@ -360,6 +370,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge
}, },
'auto', 'auto',
'', '',
[coin(Math.ceil((Number(sizeof(keys)) * 1.1 * Number(feeRate.key)) / 2), 'ustars')],
) )
return res.transactionHash return res.transactionHash

View File

@ -32,36 +32,36 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
name: 'Edit Badge', name: 'Edit Badge',
description: ` Edit badge metadata for the badge with the specified ID`, description: ` Edit badge metadata for the badge with the specified ID`,
}, },
// { {
// id: 'add_keys', id: 'add_keys',
// name: 'Add Keys', name: 'Add Keys',
// description: `Add keys to the badge with the specified ID`, description: `Add keys to the badge with the specified ID`,
// }, },
// { {
// id: 'purge_keys', id: 'purge_keys',
// name: 'Purge Keys', name: 'Purge Keys',
// description: `Purge keys from the badge with the specified ID`, description: `Purge keys from the badge with the specified ID`,
// }, },
{ {
id: 'purge_owners', id: 'purge_owners',
name: 'Purge Owners', name: 'Purge Owners',
description: `Purge owners from the badge with the specified ID`, description: `Purge owners from the badge with the specified ID`,
}, },
// { {
// id: 'mint_by_minter', id: 'mint_by_minter',
// name: 'Mint by Minter', name: 'Mint by Minter',
// description: `Mint a new token by the minter with the specified ID`, description: `Mint a new token by the minter with the specified ID`,
// }, },
{ {
id: 'mint_by_key', id: 'mint_by_key',
name: 'Mint by Key', name: 'Mint by Key',
description: `Mint a new token by the key with the specified ID`, description: `Mint a new token by the key with the specified ID`,
}, },
// { {
// id: 'mint_by_keys', id: 'mint_by_keys',
// name: 'Mint by Keys', name: 'Mint by Keys',
// description: `Mint a new token by the keys with the specified ID`, description: `Mint a new token by the keys with the specified ID`,
// }, },
{ {
id: 'set_nft', id: 'set_nft',
name: 'Set NFT', name: 'Set NFT',

View File

@ -14,8 +14,8 @@ export const QUERY_LIST: QueryListItem[] = [
{ id: 'config', name: 'Config', description: 'View current config' }, { id: 'config', name: 'Config', description: 'View current config' },
{ id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' },
{ id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' },
// { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it&apos;s whitelisted' }, { 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: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' },
] ]
export interface DispatchQueryProps { export interface DispatchQueryProps {

View File

@ -48,6 +48,8 @@ export interface VendingMinterInstance {
withdraw: (senderAddress: string) => Promise<string> withdraw: (senderAddress: string) => Promise<string>
airdrop: (senderAddress: string, recipients: string[]) => Promise<string> airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
burnRemaining: (senderAddress: string) => Promise<string> burnRemaining: (senderAddress: string) => Promise<string>
updateDiscountPrice: (senderAddress: string, price: string) => Promise<string>
removeDiscountPrice: (senderAddress: string) => Promise<string>
} }
export interface VendingMinterMessages { export interface VendingMinterMessages {
@ -66,6 +68,8 @@ export interface VendingMinterMessages {
withdraw: () => WithdrawMessage withdraw: () => WithdrawMessage
airdrop: (recipients: string[]) => CustomMessage airdrop: (recipients: string[]) => CustomMessage
burnRemaining: () => BurnRemainingMessage burnRemaining: () => BurnRemainingMessage
updateDiscountPrice: (price: string) => UpdateDiscountPriceMessage
removeDiscountPrice: () => RemoveDiscountPriceMessage
} }
export interface MintMessage { export interface MintMessage {
@ -97,6 +101,26 @@ export interface UpdateMintPriceMessage {
funds: Coin[] funds: Coin[]
} }
export interface UpdateDiscountPriceMessage {
sender: string
contract: string
msg: {
update_discount_price: {
price: string
}
}
funds: Coin[]
}
export interface RemoveDiscountPriceMessage {
sender: string
contract: string
msg: {
remove_discount_price: Record<string, never>
}
funds: Coin[]
}
export interface SetWhitelistMessage { export interface SetWhitelistMessage {
sender: string sender: string
contract: string contract: string
@ -326,6 +350,36 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
return res.transactionHash return res.transactionHash
} }
const updateDiscountPrice = async (senderAddress: string, price: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
update_discount_price: {
price: (Number(price) * 1000000).toString(),
},
},
'auto',
'',
)
return res.transactionHash
}
const removeDiscountPrice = async (senderAddress: string): Promise<string> => {
const res = await client.execute(
senderAddress,
contractAddress,
{
remove_discount_price: {},
},
'auto',
'',
)
return res.transactionHash
}
const setWhitelist = async (senderAddress: string, whitelist: string): Promise<string> => { const setWhitelist = async (senderAddress: string, whitelist: string): Promise<string> => {
const res = await client.execute( const res = await client.execute(
senderAddress, senderAddress,
@ -552,6 +606,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
mint, mint,
purge, purge,
updateMintPrice, updateMintPrice,
updateDiscountPrice,
removeDiscountPrice,
setWhitelist, setWhitelist,
updateStartTime, updateStartTime,
updateStartTradingTime, updateStartTradingTime,
@ -633,6 +689,30 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
} }
} }
const updateDiscountPrice = (price: string): UpdateDiscountPriceMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
update_discount_price: {
price: (Number(price) * 1000000).toString(),
},
},
funds: [],
}
}
const removeDiscountPrice = (): RemoveDiscountPriceMessage => {
return {
sender: txSigner,
contract: contractAddress,
msg: {
remove_discount_price: {},
},
funds: [],
}
}
const setWhitelist = (whitelist: string): SetWhitelistMessage => { const setWhitelist = (whitelist: string): SetWhitelistMessage => {
return { return {
sender: txSigner, sender: txSigner,
@ -795,6 +875,8 @@ export const vendingMinter = (client: SigningCosmWasmClient, txSigner: string):
mint, mint,
purge, purge,
updateMintPrice, updateMintPrice,
updateDiscountPrice,
removeDiscountPrice,
setWhitelist, setWhitelist,
updateStartTime, updateStartTime,
updateStartTradingTime, updateStartTradingTime,

View File

@ -7,6 +7,8 @@ export const EXECUTE_TYPES = [
'mint', 'mint',
'purge', 'purge',
'update_mint_price', 'update_mint_price',
'update_discount_price',
'remove_discount_price',
'set_whitelist', 'set_whitelist',
'update_start_time', 'update_start_time',
'update_start_trading_time', 'update_start_trading_time',
@ -39,6 +41,16 @@ export const EXECUTE_LIST: ExecuteListItem[] = [
name: 'Update Mint Price', name: 'Update Mint Price',
description: `Update mint price`, description: `Update mint price`,
}, },
{
id: 'update_discount_price',
name: 'Update Discount Price',
description: `Update discount price`,
},
{
id: 'remove_discount_price',
name: 'Remove Discount Price',
description: `Remove discount price`,
},
{ {
id: 'set_whitelist', id: 'set_whitelist',
name: 'Set Whitelist', name: 'Set Whitelist',
@ -98,6 +110,8 @@ export type DispatchExecuteArgs = {
| { type: Select<'mint'> } | { type: Select<'mint'> }
| { type: Select<'purge'> } | { type: Select<'purge'> }
| { type: Select<'update_mint_price'>; price: string } | { type: Select<'update_mint_price'>; price: string }
| { type: Select<'update_discount_price'>; price: string }
| { type: Select<'remove_discount_price'> }
| { type: Select<'set_whitelist'>; whitelist: string } | { type: Select<'set_whitelist'>; whitelist: string }
| { type: Select<'update_start_time'>; startTime: string } | { type: Select<'update_start_time'>; startTime: string }
| { type: Select<'update_start_trading_time'>; startTime?: string } | { type: Select<'update_start_trading_time'>; startTime?: string }
@ -123,6 +137,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'update_mint_price': { case 'update_mint_price': {
return messages.updateMintPrice(txSigner, args.price) return messages.updateMintPrice(txSigner, args.price)
} }
case 'update_discount_price': {
return messages.updateDiscountPrice(txSigner, args.price)
}
case 'remove_discount_price': {
return messages.removeDiscountPrice(txSigner)
}
case 'set_whitelist': { case 'set_whitelist': {
return messages.setWhitelist(txSigner, args.whitelist) return messages.setWhitelist(txSigner, args.whitelist)
} }
@ -167,6 +187,12 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'update_mint_price': { case 'update_mint_price': {
return messages(contract)?.updateMintPrice(args.price) return messages(contract)?.updateMintPrice(args.price)
} }
case 'update_discount_price': {
return messages(contract)?.updateDiscountPrice(args.price)
}
case 'remove_discount_price': {
return messages(contract)?.removeDiscountPrice()
}
case 'set_whitelist': { case 'set_whitelist': {
return messages(contract)?.setWhitelist(args.whitelist) return messages(contract)?.setWhitelist(args.whitelist)
} }

View File

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

View File

@ -1,5 +1,8 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
@ -20,12 +23,14 @@ import { useInputState } from 'components/forms/FormInput.hooks'
import { Tooltip } from 'components/Tooltip' import { Tooltip } from 'components/Tooltip'
import { useContracts } from 'contexts/contracts' import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { Badge } from 'contracts/badgeHub'
import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute'
import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute' import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute'
import * as crypto from 'crypto' import * as crypto from 'crypto'
import { toPng } from 'html-to-image' import { toPng } from 'html-to-image'
import type { NextPage } from 'next' import type { NextPage } from 'next'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import sizeof from 'object-sizeof'
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
@ -36,8 +41,11 @@ import { copy } from 'utils/clipboard'
import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants' import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants'
import { withMetadata } from 'utils/layout' import { withMetadata } from 'utils/layout'
import { links } from 'utils/links' import { links } from 'utils/links'
import { resolveAddress } from 'utils/resolveAddress'
import { truncateMiddle } from 'utils/text' import { truncateMiddle } from 'utils/text'
import { generateKeyPairs } from '../../utils/hash'
const BadgeCreationPage: NextPage = () => { const BadgeCreationPage: NextPage = () => {
const wallet = useWallet() const wallet = useWallet()
const { badgeHub: badgeHubContract } = useContracts() const { badgeHub: badgeHubContract } = useContracts()
@ -50,13 +58,18 @@ const BadgeCreationPage: NextPage = () => {
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [creatingBadge, setCreatingBadge] = useState(false) const [creatingBadge, setCreatingBadge] = useState(false)
const [isAddingKeysComplete, setIsAddingKeysComplete] = useState(false)
const [readyToCreateBadge, setReadyToCreateBadge] = useState(false) const [readyToCreateBadge, setReadyToCreateBadge] = useState(false)
const [mintRule, setMintRule] = useState<MintRule>('by_key') const [mintRule, setMintRule] = useState<MintRule>('by_key')
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
const [tempBadge, setTempBadge] = useState<Badge>()
const [badgeId, setBadgeId] = useState<string | null>(null) const [badgeId, setBadgeId] = useState<string | null>(null)
const [imageUrl, setImageUrl] = useState<string | null>(null) const [imageUrl, setImageUrl] = useState<string | null>(null)
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined) const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
const [transactionHash, setTransactionHash] = useState<string | null>(null) const [transactionHash, setTransactionHash] = useState<string | null>(null)
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
const [numberOfKeys, setNumberOfKeys] = useState(1)
const qrRef = useRef<HTMLDivElement>(null) const qrRef = useRef<HTMLDivElement>(null)
const keyState = useInputState({ const keyState = useInputState({
@ -66,6 +79,14 @@ const BadgeCreationPage: NextPage = () => {
subtitle: 'Part of the key pair to be utilized for post-creation access control', subtitle: 'Part of the key pair to be utilized for post-creation access control',
}) })
const designatedMinterState = useInputState({
id: 'designatedMinter',
name: 'designatedMinter',
title: 'Minter Address',
subtitle: 'The address of the designated minter for this badge',
defaultValue: wallet.address,
})
const performBadgeCreationChecks = () => { const performBadgeCreationChecks = () => {
try { try {
setReadyToCreateBadge(false) setReadyToCreateBadge(false)
@ -112,6 +133,46 @@ const BadgeCreationPage: NextPage = () => {
} }
} }
const resolveMinterAddress = async () => {
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
setResolvedMinterAddress(resolvedAddress)
})
}
useEffect(() => {
void resolveMinterAddress()
}, [designatedMinterState.value])
useEffect(() => {
const badge = {
manager: badgeDetails?.manager as string,
metadata: {
name: badgeDetails?.name || undefined,
description: badgeDetails?.description || undefined,
image: imageUrl || undefined,
image_data: badgeDetails?.image_data || undefined,
external_url: badgeDetails?.external_url || undefined,
attributes: badgeDetails?.attributes || undefined,
background_color: badgeDetails?.background_color || undefined,
animation_url: badgeDetails?.animation_url || undefined,
youtube_url: badgeDetails?.youtube_url || undefined,
},
transferrable: badgeDetails?.transferrable as boolean,
rule:
mintRule === 'by_key'
? {
by_key: keyState.value,
}
: mintRule === 'by_minter'
? {
by_minter: resolvedMinterAddress,
}
: 'by_keys',
expiry: badgeDetails?.expiry || undefined,
max_supply: badgeDetails?.max_supply || undefined,
}
setTempBadge(badge)
}, [badgeDetails, keyState.value, mintRule, resolvedMinterAddress, imageUrl])
const createNewBadge = async () => { const createNewBadge = async () => {
try { try {
if (!wallet.initialized) throw new Error('Wallet not connected') if (!wallet.initialized) throw new Error('Wallet not connected')
@ -133,9 +194,16 @@ const BadgeCreationPage: NextPage = () => {
youtube_url: badgeDetails?.youtube_url || undefined, youtube_url: badgeDetails?.youtube_url || undefined,
}, },
transferrable: badgeDetails?.transferrable as boolean, transferrable: badgeDetails?.transferrable as boolean,
rule: { rule:
by_key: keyState.value, mintRule === 'by_key'
}, ? {
by_key: keyState.value,
}
: mintRule === 'by_minter'
? {
by_minter: resolvedMinterAddress,
}
: 'by_keys',
expiry: badgeDetails?.expiry || undefined, expiry: badgeDetails?.expiry || undefined,
max_supply: badgeDetails?.max_supply || undefined, max_supply: badgeDetails?.max_supply || undefined,
} }
@ -147,13 +215,50 @@ const BadgeCreationPage: NextPage = () => {
badge, badge,
type: 'create_badge', type: 'create_badge',
} }
const data = await badgeHubDispatchExecute(payload) if (mintRule !== 'by_keys') {
console.log(data) setBadgeId(null)
setCreatingBadge(false) setIsAddingKeysComplete(false)
setTransactionHash(data.split(':')[0]) const data = await badgeHubDispatchExecute(payload)
setBadgeId(data.split(':')[1]) console.log(data)
} catch (error: any) { setCreatingBadge(false)
toast.error(error.message, { style: { maxWidth: 'none' } }) setTransactionHash(data.split(':')[0])
setBadgeId(data.split(':')[1])
} else {
setBadgeId(null)
setIsAddingKeysComplete(false)
setKeyPairs([])
const generatedKeyPairs = generateKeyPairs(numberOfKeys)
setKeyPairs(generatedKeyPairs)
await badgeHubDispatchExecute(payload)
.then(async (data) => {
setCreatingBadge(false)
setTransactionHash(data.split(':')[0])
setBadgeId(data.split(':')[1])
const res = await toast.promise(
badgeHubContract.use(BADGE_HUB_ADDRESS)?.addKeys(
wallet.address,
Number(data.split(':')[1]),
generatedKeyPairs.map((key) => key.publicKey),
) as Promise<string>,
{
loading: 'Adding keys...',
success: (result) => {
setIsAddingKeysComplete(true)
return `Keys added successfully! Tx Hash: ${result}`
},
error: (error: { message: any }) => `Failed to add keys: ${error.message}`,
},
)
})
.catch((error: { message: any }) => {
toast.error(error.message, { style: { maxWidth: 'none' } })
setUploading(false)
setIsAddingKeysComplete(false)
setCreatingBadge(false)
})
}
} catch (err: any) {
toast.error(err.message, { style: { maxWidth: 'none' } })
setCreatingBadge(false) setCreatingBadge(false)
setUploading(false) setUploading(false)
} }
@ -184,7 +289,8 @@ const BadgeCreationPage: NextPage = () => {
const checkBadgeDetails = () => { const checkBadgeDetails = () => {
if (!badgeDetails) throw new Error('Please fill out the required fields') if (!badgeDetails) throw new Error('Please fill out the required fields')
if (keyState.value === '' || !createdBadgeKey) throw new Error('Please generate a public key') if (mintRule === 'by_key' && (keyState.value === '' || !createdBadgeKey))
throw new Error('Please generate a public key')
if (badgeDetails.external_url) { if (badgeDetails.external_url) {
try { try {
const url = new URL(badgeDetails.external_url) const url = new URL(badgeDetails.external_url)
@ -219,7 +325,15 @@ const BadgeCreationPage: NextPage = () => {
}) })
} }
// copy claim url to clipboard const handleDownloadKeys = () => {
const element = document.createElement('a')
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
element.href = URL.createObjectURL(file)
element.download = `badge-${badgeId as string}-keys.json`
document.body.appendChild(element)
element.click()
}
const copyClaimURL = async () => { const copyClaimURL = async () => {
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}` const claimURL = `${baseURL}/?id=${badgeId as string}&key=${createdBadgeKey as string}`
@ -244,6 +358,12 @@ const BadgeCreationPage: NextPage = () => {
setReadyToCreateBadge(false) setReadyToCreateBadge(false)
}, [imageUploadDetails?.uploadMethod]) }, [imageUploadDetails?.uploadMethod])
useEffect(() => {
if (keyPairs.length > 0) {
toast.success('Key pairs generated successfully.')
}
}, [keyPairs])
return ( return (
<div> <div>
<NextSeo title="Create Badge" /> <NextSeo title="Create Badge" />
@ -265,52 +385,164 @@ const BadgeCreationPage: NextPage = () => {
</div> </div>
<div className="mx-10" ref={scrollRef}> <div className="mx-10" ref={scrollRef}>
<Conditional test={badgeId !== null}> <Conditional test={badgeId !== null}>
<Alert className="mt-5" type="info"> <Conditional test={mintRule === 'by_key'}>
<div className="flex flex-row"> <Alert className="mt-5" type="info">
<div> <div className="flex flex-row">
<div className="w-[384px] h-[384px]" ref={qrRef}> <div>
<QRCodeSVG <div className="w-[384px] h-[384px]" ref={qrRef}>
className="mx-auto" <QRCodeSVG
level="H" className="mx-auto"
size={384} level="H"
value={`${ size={384}
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' value={`${
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`} NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
/> }/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
/>
</div>
<div className="grid grid-cols-2 gap-2 mt-2 w-[384px]">
<Button
className="items-center w-full text-sm text-center rounded"
leftIcon={<FaSave />}
onClick={() => void handleDownloadQr()}
>
Download QR Code
</Button>
<Button
className="w-full text-sm text-center rounded"
isWide
leftIcon={<FaCopy />}
onClick={() => void copyClaimURL()}
variant="solid"
>
Copy Claim URL
</Button>
</div>
</div> </div>
<div className="grid grid-cols-2 gap-2 mt-2 w-[384px]"> <div className="ml-4 text-lg">
<Button Badge ID:{` ${badgeId as string}`}
className="items-center w-full text-sm text-center rounded" <br />
leftIcon={<FaSave />} Private Key:
onClick={() => void handleDownloadQr()} <Tooltip label="Click to copy the private key">
> <button
Download QR Code className="group flex space-x-2 font-mono text-base text-white/50 hover:underline"
</Button> onClick={() => void copy(createdBadgeKey as string)}
<Button type="button"
className="w-full text-sm text-center rounded" >
isWide <span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span>
leftIcon={<FaCopy />} <FaCopy className="opacity-50 group-hover:opacity-100" />
onClick={() => void copyClaimURL()} </button>
variant="solid" </Tooltip>
> <br />
Copy Claim URL Transaction Hash: {' '}
</Button> <Conditional test={NETWORK === 'testnet'}>
<Anchor
className="text-stargaze hover:underline"
external
href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
>
{transactionHash}
</Anchor>
</Conditional>
<Conditional test={NETWORK === 'mainnet'}>
<Anchor
className="text-stargaze hover:underline"
external
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
>
{transactionHash}
</Anchor>
</Conditional>
<br />
<div className="text-base">
<div className="flex-row pt-4 mt-4 border-t-2">
<span>
You may click{' '}
<Anchor
className="text-stargaze hover:underline"
external
href={`${
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`}
>
here
</Anchor>{' '}
or scan the QR code to claim a badge.
</span>
</div>
<br />
<span className="mt-4">
You may download the QR code or copy the claim URL to share with others.
</span>
</div>
<br />
</div> </div>
</div> </div>
</Alert>
</Conditional>
<Conditional test={mintRule === 'by_keys'}>
<Alert className="mt-5" type="info">
<div className="ml-4 text-lg"> <div className="ml-4 text-lg">
Badge ID:{` ${badgeId as string}`} Badge ID:{` ${badgeId as string}`}
<br /> <br />
Private Key: Transaction Hash: {' '}
<Tooltip label="Click to copy the private key"> <Conditional test={NETWORK === 'testnet'}>
<button <Anchor
className="group flex space-x-2 font-mono text-base text-white/50 hover:underline" className="text-stargaze hover:underline"
onClick={() => void copy(createdBadgeKey as string)} external
type="button" href={`${BLOCK_EXPLORER_URL}/tx/${transactionHash as string}`}
> >
<span>{truncateMiddle(createdBadgeKey ? createdBadgeKey : '', 32)}</span> {transactionHash}
<FaCopy className="opacity-50 group-hover:opacity-100" /> </Anchor>
</button> </Conditional>
</Tooltip> <Conditional test={NETWORK === 'mainnet'}>
<Anchor
className="text-stargaze hover:underline"
external
href={`${BLOCK_EXPLORER_URL}/txs/${transactionHash as string}`}
>
{transactionHash}
</Anchor>
</Conditional>
<br />
<Conditional test={isAddingKeysComplete}>
<div className="pt-2 mt-4 border-t-2">
<span className="mt-2">
Make sure to download the whitelisted keys added during badge creation.
</span>
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
Download Keys
</Button>
</div>
</Conditional>
<div className="text-base">
<div className="flex-row pt-4 mt-4 border-t-2">
<span>
You may click{' '}
<Anchor
className="text-stargaze hover:underline"
external
href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
badgeId as string
}`}
>
here
</Anchor>{' '}
and select Actions {'>'} Add Keys to add (additional) whitelisted keys or select Actions {'>'}{' '}
Mint by Keys to use one of the keys to mint a badge.
</span>
</div>
</div>
</div>
</Alert>
</Conditional>
<Conditional test={mintRule === 'by_minter'}>
<Alert className="mt-5" type="info">
<div className="ml-4 text-lg">
Badge ID:{` ${badgeId as string}`}
<br />
Designated Minter Address: {` ${resolvedMinterAddress}`}
<br /> <br />
Transaction Hash: {' '} Transaction Hash: {' '}
<Conditional test={NETWORK === 'testnet'}> <Conditional test={NETWORK === 'testnet'}>
@ -339,22 +571,19 @@ const BadgeCreationPage: NextPage = () => {
<Anchor <Anchor
className="text-stargaze hover:underline" className="text-stargaze hover:underline"
external external
href={`${ href={`/badges/actions/?badgeHubContractAddress=${BADGE_HUB_ADDRESS}&badgeId=${
NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' badgeId as string
}/?id=${badgeId as string}&key=${createdBadgeKey as string}`} }`}
> >
here here
</Anchor>{' '} </Anchor>{' '}
or scan the QR code to claim a badge. and select Actions {'>'} Mint By Minter to mint a badge.
</span> </span>
</div> </div>
<br />
<span className="mt-4">You may download the QR code or copy the claim URL to share with others.</span>
</div> </div>
<br />
</div> </div>
</div> </Alert>
</Alert> </Conditional>
</Conditional> </Conditional>
</div> </div>
@ -374,81 +603,136 @@ const BadgeCreationPage: NextPage = () => {
mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5', mintRule !== 'by_key' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)} )}
> >
<button <Tooltip
className="p-4 w-full h-full text-left bg-transparent" backgroundColor="bg-blue-500"
onClick={() => { className="m-0 w-1/3"
setMintRule('by_key') label="The same single private key can be utilized by multiple users to share badge minting authority. Ideal for projects with multiple administrators."
setReadyToCreateBadge(false) placement="bottom"
}}
type="button"
> >
<h4 className="font-bold">Mint Rule: By Key</h4> <button
<span className="text-sm text-white/80 line-clamp-2"> className="p-4 w-full h-full text-left bg-transparent"
Badges can be minted more than once with a badge specific message signed by a designated private key. onClick={() => {
</span> setMintRule('by_key')
</button> setReadyToCreateBadge(false)
setBadgeId(null)
}}
type="button"
>
<h4 className="font-bold">Mint Rule: By Key</h4>
<span className="text-sm text-white/80 line-clamp-4">
Multiple badges can be minted to different addresses by the owner of a single designated key.
</span>
</button>
</Tooltip>
</div> </div>
<div <div
className={clsx( className={clsx(
'isolate space-y-1 border-2', 'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md', 'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent', mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent',
mintRule !== 'by_keys' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5', mintRule !== 'by_keys' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)} )}
> >
<button <Tooltip
className="p-4 w-full h-full text-left bg-transparent" backgroundColor="bg-blue-500"
disabled className="m-0 w-1/3"
onClick={() => { label="The key pairs are intended to be saved and shared with others. Each user can claim a badge separately using the key pair that they received."
setMintRule('by_keys') placement="bottom"
setReadyToCreateBadge(false)
}}
type="button"
> >
<h4 className="font-bold">Mint Rule: By Keys</h4> <button
<span className="text-sm text-slate-500 line-clamp-2"> className="p-4 w-full h-full text-left bg-transparent"
Similar to the By Key rule, however each designated private key can only be used once to mint a badge. onClick={() => {
</span> setMintRule('by_keys')
</button> setReadyToCreateBadge(false)
setBadgeId(null)
}}
type="button"
>
<h4 className="font-bold">Mint Rule: By Keys</h4>
<span className="text-sm text-white/80 line-clamp-4">
Multiple key pairs are generated and designated to be only used once to mint a single badge.
</span>
</button>
</Tooltip>
</div> </div>
<div <div
className={clsx( className={clsx(
'isolate space-y-1 border-2', 'isolate space-y-1 border-2',
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md', 'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent', mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent',
mintRule !== 'by_minter' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5', mintRule !== 'by_minter' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
)} )}
> >
<button <Tooltip
className="p-4 w-full h-full text-left bg-transparent" backgroundColor="bg-blue-500"
disabled className="m-0 w-1/3"
onClick={() => { label="The most basic approach. However, having just one authorized address for minting badges might limit your ability to delegate that responsibility."
setMintRule('by_minter') placement="bottom"
setReadyToCreateBadge(false)
}}
type="button"
> >
<h4 className="font-bold">Mint Rule: By Minter</h4> <button
<span className="text-sm line-clamp-2 text-slate/500"> className="p-4 w-full h-full text-left bg-transparent"
Badges can be minted by a designated minter account. onClick={() => {
</span> setMintRule('by_minter')
</button> setReadyToCreateBadge(false)
setBadgeId(null)
}}
type="button"
>
<h4 className="font-bold">Mint Rule: By Minter</h4>
<span className="text-sm text-white/80 line-clamp-4">
No key designation. Multiple badges can be minted to different addresses by a pre-determined minter
address.
</span>
</button>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
<div className="mx-10"> <div className="mx-10">
<ImageUploadDetails mintRule={mintRule} onChange={setImageUploadDetails} /> <ImageUploadDetails mintRule={mintRule} onChange={setImageUploadDetails} />
<Conditional test={mintRule === 'by_key'}>
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
<TextInput className="ml-4 w-full max-w-2xl" {...keyState} disabled required />
<Button className="mt-14 ml-4" isDisabled={creatingBadge} onClick={handleGenerateKey}>
Generate Key
</Button>
</div>
</Conditional>
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20"> <Conditional test={mintRule === 'by_keys'}>
<TextInput className="ml-4 w-full max-w-2xl" {...keyState} disabled required /> <div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
<Button className="mt-14 ml-4" isDisabled={creatingBadge} onClick={handleGenerateKey}> <div className="grid grid-cols-2 gap-24">
Generate Key <div className="flex flex-col ml-4">
</Button> <span className="font-bold">Number of Keys</span>
</div> <span className="text-sm text-white/80">
The number of key pairs to be whitelisted for post-creation access control
</span>
</div>
<input
className="p-2 w-1/4 max-w-2xl bg-white/10 rounded border-2 border-white/20"
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
required
type="number"
value={numberOfKeys}
/>
</div>
</div>
</Conditional>
<Conditional test={mintRule === 'by_minter'}>
<div className="flex flex-row justify-start py-3 px-8 mb-3 w-full rounded border-2 border-white/20">
<TextInput className="ml-4 w-full max-w-lg" {...designatedMinterState} required />
</div>
</Conditional>
<div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2"> <div className="flex justify-between py-3 px-8 rounded border-2 border-white/20 grid-col-2">
<BadgeDetails <BadgeDetails
metadataSize={
tempBadge?.metadata.image === undefined
? Number(sizeof(tempBadge)) + Number(sizeof(tempBadge?.metadata.attributes)) + 150
: Number(sizeof(tempBadge)) + Number(sizeof(tempBadge.metadata.attributes))
}
mintRule={mintRule} mintRule={mintRule}
onChange={setBadgeDetails} onChange={setBadgeDetails}
uploadMethod={imageUploadDetails?.uploadMethod ? imageUploadDetails.uploadMethod : 'new'} uploadMethod={imageUploadDetails?.uploadMethod ? imageUploadDetails.uploadMethod : 'new'}

View File

@ -49,12 +49,12 @@ const BadgeList: NextPage = () => {
{myBadges.map((badge: any, index: any) => { {myBadges.map((badge: any, index: any) => {
return ( return (
<tr key={index}> <tr key={index}>
<td className="w-[55%] bg-black"> <td className="w-[35%] bg-black">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="avatar"> <div className="avatar">
<div className="w-28 h-28 mask mask-squircle"> <div className="w-28 h-28 mask mask-squircle">
<img <img
alt="Cover" alt="badge-preview"
src={ src={
(badge?.image as string).startsWith('ipfs') (badge?.image as string).startsWith('ipfs')
? `https://ipfs-gw.stargaze-apis.com/ipfs/${(badge?.image as string).substring(7)}` ? `https://ipfs-gw.stargaze-apis.com/ipfs/${(badge?.image as string).substring(7)}`
@ -64,13 +64,15 @@ const BadgeList: NextPage = () => {
</div> </div>
</div> </div>
<div className="pl-2"> <div className="pl-2">
<p className="overflow-auto max-w-xs font-bold no-scrollbar ">{badge.name}</p> <p className="overflow-auto max-w-xs font-bold no-scrollbar ">
{badge.name ? badge.name : 'No name provided.'}
</p>
<p className="max-w-xs text-sm truncate opacity-50">Badge ID: {badge.tokenId}</p> <p className="max-w-xs text-sm truncate opacity-50">Badge ID: {badge.tokenId}</p>
</div> </div>
</div> </div>
</td> </td>
<td className="overflow-auto w-[35%] max-w-xl bg-black no-scrollbar"> <td className="overflow-auto w-[55%] max-w-xl bg-black no-scrollbar">
{badge.description} {badge.description ? badge.description : 'No description provided.'}
{/* <br /> */} {/* <br /> */}
{/* <span className="badge badge-ghost badge-sm"></span> */} {/* <span className="badge badge-ghost badge-sm"></span> */}
</td> </td>

View File

@ -754,12 +754,17 @@ const CollectionCreationPage: NextPage = () => {
mintingDetails.perAddressLimit > mintingDetails.numTokens mintingDetails.perAddressLimit > mintingDetails.numTokens
) )
throw new Error('Invalid limit for tokens per address') throw new Error('Invalid limit for tokens per address')
if (mintingDetails.numTokens < 100 && mintingDetails.perAddressLimit > 3)
throw new Error(
'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.',
)
if ( if (
mintingDetails.numTokens > 100 && mintingDetails.numTokens >= 100 &&
mintingDetails.numTokens < 100 * mintingDetails.perAddressLimit && mintingDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
mintingDetails.perAddressLimit > mintingDetails.numTokens / 100
) )
throw new Error('Invalid limit for tokens per address. The limit cannot exceed 1% of the total number of tokens.') throw new Error(
'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.',
)
if (mintingDetails.startTime === '') throw new Error('Start time is required') if (mintingDetails.startTime === '') throw new Error('Start time is required')
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time') if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
} }
@ -776,15 +781,25 @@ const CollectionCreationPage: NextPage = () => {
const whitelistStartDate = new Date(Number(config?.start_time) / 1000000) const whitelistStartDate = new Date(Number(config?.start_time) / 1000000)
throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`) throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`)
} }
if (
mintingDetails?.numTokens && if (mintingDetails?.numTokens && config?.per_address_limit) {
config?.per_address_limit && if (mintingDetails.numTokens >= 100 && Number(config.per_address_limit) > 50) {
mintingDetails.numTokens > 100 && throw Error(
Number(config.per_address_limit) > mintingDetails.numTokens / 100 `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
) )
throw Error( } else if (
`Invalid limit for tokens per address (${config.per_address_limit} tokens). The limit cannot exceed 1% of the total number of tokens.`, mintingDetails.numTokens >= 100 &&
) Number(config.per_address_limit) > Math.ceil((mintingDetails.numTokens / 100) * 3)
) {
throw Error(
`Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`,
)
} else if (mintingDetails.numTokens < 100 && Number(config.per_address_limit) > 3) {
throw Error(
`Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`,
)
}
}
} }
} else if (whitelistDetails.whitelistType === 'new') { } else if (whitelistDetails.whitelistType === 'new') {
if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty') if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty')
@ -801,15 +816,24 @@ const CollectionCreationPage: NextPage = () => {
throw new Error('Whitelist start time cannot be later than whitelist end time') throw new Error('Whitelist start time cannot be later than whitelist end time')
if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime)) if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime))
throw new Error('Whitelist start time must be the same as the minting start time') throw new Error('Whitelist start time must be the same as the minting start time')
if ( if (whitelistDetails.perAddressLimit && mintingDetails?.numTokens) {
mintingDetails?.numTokens && if (mintingDetails.numTokens >= 100 && whitelistDetails.perAddressLimit > 50) {
whitelistDetails.perAddressLimit && throw Error(
mintingDetails.numTokens > 100 && `Invalid limit for tokens per address. Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`,
whitelistDetails.perAddressLimit > mintingDetails.numTokens / 100 )
) } else if (
throw Error( mintingDetails.numTokens >= 100 &&
`Invalid limit for tokens per address (${whitelistDetails.perAddressLimit} tokens). The limit cannot exceed 1% of the total number of tokens.`, whitelistDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3)
) ) {
throw Error(
`Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`,
)
} else if (mintingDetails.numTokens < 100 && whitelistDetails.perAddressLimit > 3) {
throw Error(
`Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`,
)
}
}
} }
} }

View File

@ -1,17 +1,22 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { toUtf8 } from '@cosmjs/encoding' import { toUtf8 } from '@cosmjs/encoding'
import clsx from 'clsx'
import { Alert } from 'components/Alert' import { Alert } from 'components/Alert'
import type { MintRule } from 'components/badges/creation/ImageUploadDetails'
import { Button } from 'components/Button' import { Button } from 'components/Button'
import { Conditional } from 'components/Conditional' import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader' import { ContractPageHeader } from 'components/ContractPageHeader'
import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox' import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox'
import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks' import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { AddressList } from 'components/forms/AddressList'
import { useAddressListState } from 'components/forms/AddressList.hooks'
import { AddressInput, NumberInput } from 'components/forms/FormInput' import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
@ -20,6 +25,7 @@ import { LinkTabs } from 'components/LinkTabs'
import { badgeHubLinkTabs } from 'components/LinkTabs.data' import { badgeHubLinkTabs } from 'components/LinkTabs.data'
import { Tooltip } from 'components/Tooltip' import { Tooltip } from 'components/Tooltip'
import { TransactionHash } from 'components/TransactionHash' import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import { useContracts } from 'contexts/contracts' import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { Badge } from 'contracts/badgeHub' import type { Badge } from 'contracts/badgeHub'
@ -40,7 +46,8 @@ import { useMutation } from 'react-query'
import * as secp256k1 from 'secp256k1' import * as secp256k1 from 'secp256k1'
import { copy } from 'utils/clipboard' import { copy } from 'utils/clipboard'
import { NETWORK } from 'utils/constants' import { NETWORK } from 'utils/constants'
import { sha256 } from 'utils/hash' import { generateKeyPairs, sha256 } from 'utils/hash'
import { isValidAddress } from 'utils/isValidAddress'
import { withMetadata } from 'utils/layout' import { withMetadata } from 'utils/layout'
import { links } from 'utils/links' import { links } from 'utils/links'
import { resolveAddress } from 'utils/resolveAddress' import { resolveAddress } from 'utils/resolveAddress'
@ -62,10 +69,15 @@ const BadgeHubExecutePage: NextPage = () => {
const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null) const [createdBadgeId, setCreatedBadgeId] = useState<string | null>(null)
const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined) const [createdBadgeKey, setCreatedBadgeKey] = useState<string | undefined>(undefined)
const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('') const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState<string>('')
const [resolvedMinterAddress, setResolvedMinterAddress] = useState<string>('')
const [signature, setSignature] = useState<string>('') const [signature, setSignature] = useState<string>('')
const [ownerList, setOwnerList] = useState<string[]>([])
const [editFee, setEditFee] = useState<number | undefined>(undefined) const [editFee, setEditFee] = useState<number | undefined>(undefined)
const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false) const [triggerDispatch, setTriggerDispatch] = useState<boolean>(false)
const qrRef = useRef<HTMLDivElement>(null) const qrRef = useRef<HTMLDivElement>(null)
const [numberOfKeys, setNumberOfKeys] = useState(0)
const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([])
const [mintRule, setMintRule] = useState<MintRule>('by_key')
const comboboxState = useExecuteComboboxState() const comboboxState = useExecuteComboboxState()
const type = comboboxState.value?.id const type = comboboxState.value?.id
@ -173,20 +185,26 @@ const BadgeHubExecutePage: NextPage = () => {
name: 'owner', name: 'owner',
title: 'Owner', title: 'Owner',
subtitle: 'The owner of the badge', subtitle: 'The owner of the badge',
defaultValue: wallet.address,
}) })
const ownerListState = useAddressListState()
const pubkeyState = useInputState({ const pubkeyState = useInputState({
id: 'pubkey', id: 'pubkey',
name: 'pubkey', name: 'pubkey',
title: 'Pubkey', title: 'Pubkey',
subtitle: 'The public key for the badge', subtitle: 'The whitelisted public key authorized to mint a badge',
}) })
const privateKeyState = useInputState({ const privateKeyState = useInputState({
id: 'privateKey', id: 'privateKey',
name: 'privateKey', name: 'privateKey',
title: 'Private Key', title: 'Private Key',
subtitle: 'The private key generated during badge creation', subtitle:
type === 'mint_by_keys'
? 'The corresponding private key for the whitelisted public key'
: 'The private key that was generated during badge creation',
}) })
const nftState = useInputState({ const nftState = useInputState({
@ -200,15 +218,34 @@ const BadgeHubExecutePage: NextPage = () => {
id: 'limit', id: 'limit',
name: 'limit', name: 'limit',
title: 'Limit', title: 'Limit',
subtitle: 'Number of keys/owners to execute the action for', subtitle: 'Number of keys/owners to execute the action for (0 for all)',
})
const designatedMinterState = useInputState({
id: 'designatedMinter',
name: 'designatedMinter',
title: 'Minter Address',
subtitle: 'The address of the designated minter for this badge',
defaultValue: wallet.address,
}) })
const showBadgeField = type === 'create_badge' const showBadgeField = type === 'create_badge'
const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge']) const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge'])
const showIdField = isEitherType(type, ['edit_badge', 'mint_by_key']) const showIdField = isEitherType(type, [
'edit_badge',
'add_keys',
'purge_keys',
'purge_owners',
'mint_by_key',
'mint_by_keys',
'mint_by_minter',
])
const showLimitField = isEitherType(type, ['purge_keys', 'purge_owners'])
const showNFTField = type === 'set_nft' const showNFTField = type === 'set_nft'
const showOwnerField = type === 'mint_by_key' const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
const showPrivateKeyField = type === 'mint_by_key' const showOwnerListField = isEitherType(type, ['mint_by_minter'])
const showPubkeyField = isEitherType(type, ['mint_by_keys'])
const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys'])
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
const payload: DispatchExecuteArgs = { const payload: DispatchExecuteArgs = {
@ -234,9 +271,16 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined,
}, },
transferrable, transferrable,
rule: { rule:
by_key: keyState.value, mintRule === 'by_key'
}, ? {
by_key: keyState.value,
}
: mintRule === 'by_minter'
? {
by_minter: resolvedMinterAddress,
}
: 'by_keys',
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined, max_supply: maxSupplyState.value || undefined,
}, },
@ -260,12 +304,19 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined,
}, },
id: badgeIdState.value, id: badgeIdState.value,
owner: ownerState.value, owner: resolvedOwnerAddress,
pubkey: pubkeyState.value, pubkey: pubkeyState.value,
signature, signature,
keys: [], keys: keyPairs.map((keyPair) => keyPair.publicKey),
limit: limitState.value, limit: limitState.value || undefined,
owners: [], owners: [
...new Set(
ownerListState.values
.map((a) => a.address.trim())
.filter((address) => address !== '' && isValidAddress(address.trim()) && address.startsWith('stars'))
.concat(ownerList),
),
],
nft: nftState.value, nft: nftState.value,
editFee, editFee,
contract: contractState.value, contract: contractState.value,
@ -337,6 +388,7 @@ const BadgeHubExecutePage: NextPage = () => {
if (txHash) { if (txHash) {
setLastTx(txHash.split(':')[0]) setLastTx(txHash.split(':')[0])
setCreatedBadgeId(txHash.split(':')[1]) setCreatedBadgeId(txHash.split(':')[1])
badgeIdState.onChange(!isNaN(Number(txHash.split(':')[1])) ? Number(txHash.split(':')[1]) : 1)
} }
} }
}, },
@ -388,6 +440,14 @@ const BadgeHubExecutePage: NextPage = () => {
link.click() link.click()
}) })
} }
const handleDownloadKeys = () => {
const element = document.createElement('a')
const file = new Blob([JSON.stringify(keyPairs)], { type: 'text/plain' })
element.href = URL.createObjectURL(file)
element.download = `badge-${badgeIdState.value}-keys.json`
document.body.appendChild(element)
element.click()
}
const copyClaimURL = async () => { const copyClaimURL = async () => {
const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone'
@ -409,6 +469,12 @@ const BadgeHubExecutePage: NextPage = () => {
} }
} }
useEffect(() => {
if (numberOfKeys > 0) {
setKeyPairs(generateKeyPairs(numberOfKeys))
}
}, [numberOfKeys])
useEffect(() => { useEffect(() => {
if (privateKeyState.value.length === 64 && resolvedOwnerAddress) if (privateKeyState.value.length === 64 && resolvedOwnerAddress)
handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value) handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value)
@ -448,6 +514,15 @@ const BadgeHubExecutePage: NextPage = () => {
void resolveOwnerAddress() void resolveOwnerAddress()
}, [ownerState.value]) }, [ownerState.value])
const resolveMinterAddress = async () => {
await resolveAddress(designatedMinterState.value.trim(), wallet).then((resolvedAddress) => {
setResolvedMinterAddress(resolvedAddress)
})
}
useEffect(() => {
void resolveMinterAddress()
}, [designatedMinterState.value])
const resolveManagerAddress = async () => { const resolveManagerAddress = async () => {
await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => { await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => {
setBadge({ setBadge({
@ -472,9 +547,16 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined,
}, },
transferrable, transferrable,
rule: { rule:
by_key: keyState.value, mintRule === 'by_key'
}, ? {
by_key: keyState.value,
}
: mintRule === 'by_minter'
? {
by_minter: resolvedMinterAddress,
}
: 'by_keys',
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined, max_supply: maxSupplyState.value || undefined,
}) })
@ -508,9 +590,16 @@ const BadgeHubExecutePage: NextPage = () => {
youtube_url: youtubeUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined,
}, },
transferrable, transferrable,
rule: { rule:
by_key: keyState.value, mintRule === 'by_key'
}, ? {
by_key: keyState.value,
}
: mintRule === 'by_minter'
? {
by_minter: resolvedMinterAddress,
}
: 'by_keys',
expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined,
max_supply: maxSupplyState.value || undefined, max_supply: maxSupplyState.value || undefined,
}) })
@ -541,7 +630,7 @@ const BadgeHubExecutePage: NextPage = () => {
/> />
<LinkTabs activeIndex={2} data={badgeHubLinkTabs} /> <LinkTabs activeIndex={2} data={badgeHubLinkTabs} />
{showBadgeField && createdBadgeId && createdBadgeKey && ( {showBadgeField && createdBadgeId && createdBadgeKey && mintRule === 'by_key' && (
<div className="flex flex-row"> <div className="flex flex-row">
<div className="ml-4"> <div className="ml-4">
<div className="w-[384px] h-[384px]" ref={qrRef}> <div className="w-[384px] h-[384px]" ref={qrRef}>
@ -598,14 +687,116 @@ const BadgeHubExecutePage: NextPage = () => {
</div> </div>
</div> </div>
)} )}
{showBadgeField && createdBadgeId && mintRule === 'by_keys' && (
<Alert className="mt-5" type="info">
<div className="ml-4 text-lg">
Badge ID:{` ${createdBadgeId as string}`}
<br />
<div className="text-base">
<div className="flex-row pt-4 mt-4 border-t-2">
<span>
You may select Message Type {'>'} Add Keys to add whitelisted keys authorized to mint a badge.
</span>
</div>
</div>
</div>
</Alert>
)}
{showBadgeField && createdBadgeId && mintRule === 'by_minter' && (
<Alert className="mt-5" type="info">
<div className="ml-4 text-lg">
Badge successfully created with ID:{` ${createdBadgeId as string}`}
<br />
Designated Minter Address: {` ${resolvedMinterAddress}`}
<br />
<div className="text-base">
<div className="flex-row pt-4 mt-4 border-t-2">
<span>
You may select Message Type {'>'} Mint by Minter to mint badges using the designated minter wallet.
</span>
</div>
</div>
</div>
</Alert>
)}
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}> <form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
<div className="space-y-8"> <div className="space-y-8">
<AddressInput {...contractState} /> <AddressInput {...contractState} />
<ExecuteCombobox {...comboboxState} /> <ExecuteCombobox {...comboboxState} />
<Conditional test={type === 'create_badge'}>
<div className={clsx('flex flex-col space-y-2')}>
<div>
<div className="flex">
<span className="mt-1 text-base font-bold first-letter:capitalize">Mint Rule: </span>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={mintRule === 'by_key'}
className="peer sr-only"
id="ruleRadio1"
name="ruletRadio1"
onClick={() => {
setMintRule('by_key')
setCreatedBadgeId(null)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="ruleRadio1"
>
By Key
</label>
</div>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={mintRule === 'by_keys'}
className="peer sr-only"
id="ruleRadio2"
name="ruletRadio2"
onClick={() => {
setMintRule('by_keys')
setCreatedBadgeId(null)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="ruleRadio2"
>
By Keys
</label>
</div>
<div className="ml-2 font-bold form-check form-check-inline">
<input
checked={mintRule === 'by_minter'}
className="peer sr-only"
id="ruleRadio3"
name="ruletRadio3"
onClick={() => {
setMintRule('by_minter')
setCreatedBadgeId(null)
}}
type="radio"
/>
<label
className="inline-block py-1 px-2 text-base text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="ruleRadio3"
>
By Minter
</label>
</div>
</div>
</div>
</div>
</Conditional>
{showIdField && <NumberInput {...badgeIdState} />} {showIdField && <NumberInput {...badgeIdState} />}
{showBadgeField && <AddressInput {...managerState} />} {showBadgeField && <AddressInput {...managerState} />}
{showBadgeField && <TextInput {...keyState} />} {showBadgeField && mintRule === 'by_key' && <TextInput {...keyState} />}
{showBadgeField && <Button onClick={handleGenerateKey}>Generate Key</Button>} {showBadgeField && mintRule === 'by_key' && <Button onClick={handleGenerateKey}>Generate Key</Button>}
{showBadgeField && mintRule === 'by_minter' && <TextInput {...designatedMinterState} />}
{showMetadataField && ( {showMetadataField && (
<div className="p-4 rounded-md border-2 border-gray-800"> <div className="p-4 rounded-md border-2 border-gray-800">
<span className="text-gray-400">Metadata</span> <span className="text-gray-400">Metadata</span>
@ -636,6 +827,57 @@ const BadgeHubExecutePage: NextPage = () => {
title="Owner" title="Owner"
/> />
)} )}
<Conditional test={showOwnerListField}>
<div className="mt-4">
<AddressList
entries={ownerListState.entries}
isRequired
onAdd={ownerListState.add}
onChange={ownerListState.update}
onRemove={ownerListState.remove}
subtitle="Enter the owner addresses"
title="Addresses"
/>
<Alert className="mt-8" type="info">
You may optionally choose a text file of additional owner addresses.
</Alert>
<WhitelistUpload onChange={setOwnerList} />
</div>
</Conditional>
<Conditional test={type === 'add_keys'}>
<div className="flex flex-row justify-start py-3 mt-4 mb-3 w-full rounded border-2 border-white/20">
<div className="grid grid-cols-2 gap-24">
<div className="flex flex-col ml-4">
<span className="font-bold">Number of Keys</span>
<span className="text-sm text-white/80">
The number of public keys to be whitelisted for minting badges
</span>
</div>
<input
className="p-2 mt-4 w-1/2 max-w-2xl h-1/2 bg-white/10 rounded border-2 border-white/20"
onChange={(e) => setNumberOfKeys(Number(e.target.value))}
required
type="number"
value={numberOfKeys}
/>
</div>
</div>
</Conditional>
<Conditional test={numberOfKeys > 0 && type === 'add_keys'}>
<Alert type="info">
<div className="pt-2">
<span className="mt-2">
Make sure to download the whitelisted public keys together with their private key counterparts.
</span>
<Button className="mt-2" onClick={() => handleDownloadKeys()}>
Download Key Pairs
</Button>
</div>
</Alert>
</Conditional>
{showLimitField && <NumberInput {...limitState} />}
{showPubkeyField && <TextInput className="mt-2" {...pubkeyState} />}
{showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />} {showPrivateKeyField && <TextInput className="mt-2" {...privateKeyState} />}
{showNFTField && <AddressInput {...nftState} />} {showNFTField && <AddressInput {...nftState} />}
</div> </div>

View File

@ -154,7 +154,7 @@ const BadgeHubQueryPage: NextPage = () => {
))} ))}
</select> </select>
</FormControl> </FormControl>
<Conditional test={type === 'getBadge' || type === 'getKey'}> <Conditional test={type === 'getBadge' || type === 'getKey' || type === 'getKeys'}>
<NumberInput {...idState} /> <NumberInput {...idState} />
</Conditional> </Conditional>
<Conditional test={type === 'getKey'}> <Conditional test={type === 'getKey'}>

View File

@ -105,7 +105,7 @@ const BaseMinterInstantiatePage: NextPage = () => {
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '8%', placeholder: '5%',
}) })
const { data, isLoading, mutate } = useMutation( const { data, isLoading, mutate } = useMutation(

View File

@ -55,8 +55,8 @@ const VendingMinterExecutePage: NextPage = () => {
const priceState = useNumberInputState({ const priceState = useNumberInputState({
id: 'price', id: 'price',
name: 'price', name: 'price',
title: 'Price', title: type === 'update_discount_price' ? 'Discount Price' : 'Price',
subtitle: 'Enter the token price', subtitle: type === 'update_discount_price' ? 'New discount price in STARS' : 'Enter the token price',
}) })
const contractState = useInputState({ const contractState = useInputState({
@ -86,7 +86,7 @@ const VendingMinterExecutePage: NextPage = () => {
const showLimitField = type === 'update_per_address_limit' const showLimitField = type === 'update_per_address_limit'
const showTokenIdField = type === 'mint_for' const showTokenIdField = type === 'mint_for'
const showRecipientField = isEitherType(type, ['mint_to', 'mint_for']) const showRecipientField = isEitherType(type, ['mint_to', 'mint_for'])
const showPriceField = type === 'update_mint_price' const showPriceField = isEitherType(type, ['update_mint_price', 'update_discount_price'])
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
const payload: DispatchExecuteArgs = { const payload: DispatchExecuteArgs = {

View File

@ -103,7 +103,7 @@ const VendingMinterInstantiatePage: NextPage = () => {
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '8%', placeholder: '5%',
}) })
const unitPriceState = useNumberInputState({ const unitPriceState = useNumberInputState({

View File

@ -15,8 +15,8 @@ const HomePage: NextPage = () => {
Looking for a fast and efficient way to build an NFT collection? Stargaze Studio is the solution. Looking for a fast and efficient way to build an NFT collection? Stargaze Studio is the solution.
<br /> <br />
<br /> <br />
Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT
NFT collections in no time. collections in no time.
</p> </p>
<br /> <br />

View File

@ -1,5 +1,6 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
import * as crypto from 'crypto'
import { Word32Array } from 'jscrypto' import { Word32Array } from 'jscrypto'
import { SHA256 } from 'jscrypto/SHA256' import { SHA256 } from 'jscrypto/SHA256'
import * as secp256k1 from 'secp256k1' import * as secp256k1 from 'secp256k1'
@ -23,3 +24,21 @@ export function generateSignature(id: number, owner: string, privateKey: string)
return '' return ''
} }
} }
export function generateKeyPairs(amount: number) {
const keyPairs: { publicKey: string; privateKey: string }[] = []
for (let i = 0; i < amount; i++) {
let privKey: Buffer
do {
privKey = crypto.randomBytes(32)
} while (!secp256k1.privateKeyVerify(privKey))
const privateKey = privKey.toString('hex')
const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex')
keyPairs.push({
publicKey,
privateKey,
})
}
return keyPairs
}