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_SG721_CODE_ID=793
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=275
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1s48apjumprma0d64ge88dmu7lr8exu02tlw90xxupgds0s25gfasx447dn"
NEXT_PUBLIC_SG721_CODE_ID=1702
NEXT_PUBLIC_VENDING_MINTER_CODE_ID=1701
NEXT_PUBLIC_VENDING_FACTORY_ADDRESS="stars1xz4d6wzxqn3udgsm5qnr78y032xng4r2ycv7aw6mjtsuw59s2n9s93ec0v"
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34tfq3zk87l78sdxngzv"
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
NEXT_PUBLIC_BASE_MINTER_CODE_ID=613

View File

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

View File

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

View File

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

View File

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

View File

@ -172,7 +172,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
</div>
</div>
<div className="p-3 py-5 pb-8">
<div className="p-3 py-5 pb-4">
<Conditional test={uploadMethod === 'existing'}>
<div className="ml-3 flex-column">
<p className="mb-5 ml-5">
@ -187,8 +187,17 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
</Anchor>{' '}
and upload your image manually to get an image URL for your badge.
</p>
<div>
<TextInput {...imageUrlState} className="mt-2 ml-4 w-1/2" />
<div className="flex flex-row w-full">
<TextInput {...imageUrlState} className="mt-2 ml-6 w-full max-w-2xl" />
<Conditional test={imageUrlState.value !== ''}>
<div className="mt-2 ml-4 w-1/4 border-2 border-dashed">
<img
alt="badge-preview"
className="w-full"
src={imageUrlState.value.replace('IPFS://', 'ipfs://').replace(/,/g, '').replace(/"/g, '').trim()}
/>
</div>
</Conditional>
</div>
</div>
</Conditional>
@ -252,31 +261,33 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro
<div className="mt-6">
<div className="grid grid-cols-2">
<div className="w-full">
<div>
<label
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>
<div
className={clsx(
'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/*"
<div>
<div className="w-full">
<div>
<label
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>
<div
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',
'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',
)}
id="assetFile"
onChange={selectAsset}
ref={assetFileRef}
type="file"
/>
>
<input
accept="image/*"
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
<br />
<br />
Stargaze Studio is built to provide useful smart contract interfaces that helps you build and deploy your own
NFT collections in no time.
Stargaze Studio is built to provide useful smart contract interfaces that help you build and deploy your own NFT
collections in no time.
</p>
<br />

View File

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