/* eslint-disable eslint-comments/disable-enable-pair */ /* 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 sizeof from 'object-sizeof' import type { FormEvent } from 'react' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' import { generateKeyPairs, sha256 } from 'utils/hash' import { isValidAddress } from 'utils/isValidAddress' import { resolveAddress } from 'utils/resolveAddress' import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload' import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput' import type { MintRule } from '../creation/ImageUploadDetails' interface BadgeActionsProps { badgeHubContractAddress: string badgeId: number badgeHubMessages: BadgeHubInstance | undefined mintRule: MintRule } type TransferrableType = true | false | undefined export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeActionsProps) => { const wallet = useWallet() const [lastTx, setLastTx] = useState('') const [timestamp, setTimestamp] = useState(undefined) const [airdropAllocationArray, setAirdropAllocationArray] = useState([]) const [badge, setBadge] = useState() const [transferrable, setTransferrable] = useState(undefined) const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') const [editFee, setEditFee] = useState(undefined) const [triggerDispatch, setTriggerDispatch] = useState(false) const [keyPairs, setKeyPairs] = useState<{ publicKey: string; privateKey: string }[]>([]) const [signature, setSignature] = useState('') const [ownerList, setOwnerList] = useState([]) const [numberOfKeys, setNumberOfKeys] = useState(0) const actionComboboxState = useActionsComboboxState() const type = actionComboboxState.value?.id const maxSupplyState = useNumberInputState({ id: 'max-supply', name: 'max-supply', title: 'Max Supply', subtitle: 'Maximum number of badges that can be minted', }) // Metadata related fields const managerState = useInputState({ id: 'manager-address', name: 'manager', title: 'Manager', subtitle: 'Badge Hub Manager', defaultValue: wallet.address, }) const nameState = useInputState({ id: 'metadata-name', name: 'metadata-name', title: 'Name', subtitle: 'Name of the badge', }) const descriptionState = useInputState({ id: 'metadata-description', name: 'metadata-description', title: 'Description', subtitle: 'Description of the badge', }) const imageState = useInputState({ id: 'metadata-image', name: 'metadata-image', title: 'Image', subtitle: 'Badge Image URL', }) const imageDataState = useInputState({ id: 'metadata-image-data', name: 'metadata-image-data', title: 'Image Data', subtitle: 'Raw SVG image data', }) const externalUrlState = useInputState({ id: 'metadata-external-url', name: 'metadata-external-url', title: 'External URL', subtitle: 'External URL for the badge', }) const attributesState = useMetadataAttributesState() const backgroundColorState = useInputState({ id: 'metadata-background-color', name: 'metadata-background-color', title: 'Background Color', subtitle: 'Background color of the badge', }) const animationUrlState = useInputState({ id: 'metadata-animation-url', name: 'metadata-animation-url', title: 'Animation URL', subtitle: 'Animation URL for the badge', }) const youtubeUrlState = useInputState({ id: 'metadata-youtube-url', name: 'metadata-youtube-url', title: 'YouTube URL', subtitle: 'YouTube URL for the badge', }) // Rules related fields const keyState = useInputState({ id: 'key', name: 'key', title: 'Key', subtitle: 'The key generated for the badge', }) const ownerState = useInputState({ id: 'owner-address', name: 'owner', title: 'Owner', subtitle: 'The owner of the badge', defaultValue: wallet.address, }) 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: 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({ id: 'nft-address', name: 'nft-address', title: 'NFT Contract Address', subtitle: 'The NFT Contract Address for the badge', }) const limitState = useNumberInputState({ id: 'limit', name: 'limit', title: 'Limit', 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', '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: { manager: badge?.manager || managerState.value, metadata: { name: nameState.value || undefined, description: descriptionState.value || undefined, image: imageState.value || undefined, image_data: imageDataState.value || undefined, external_url: externalUrlState.value || undefined, attributes: attributesState.values[0]?.trait_type && attributesState.values[0]?.value ? attributesState.values .map((attr) => ({ trait_type: attr.trait_type, value: attr.value, })) .filter((attr) => attr.trait_type && attr.value) : undefined, background_color: backgroundColorState.value || undefined, animation_url: animationUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined, }, transferrable: transferrable === true, rule: { by_key: keyState.value, }, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, max_supply: maxSupplyState.value || undefined, }, metadata: { name: nameState.value || undefined, description: descriptionState.value || undefined, image: imageState.value || undefined, image_data: imageDataState.value || undefined, external_url: externalUrlState.value || undefined, attributes: attributesState.values[0]?.trait_type && attributesState.values[0]?.value ? attributesState.values .map((attr) => ({ trait_type: attr.trait_type, value: attr.value, })) .filter((attr) => attr.trait_type && attr.value) : undefined, background_color: backgroundColorState.value || undefined, animation_url: animationUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined, }, id: badgeId, editFee, owner: resolvedOwnerAddress, pubkey: pubKeyState.value, signature, 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, badgeHubMessages, badgeHubContract: badgeHubContractAddress, txSigner: wallet.address, type, } const resolveOwnerAddress = async () => { await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => { setResolvedOwnerAddress(resolvedAddress) }) } useEffect(() => { void resolveOwnerAddress() }, [ownerState.value]) const resolveManagerAddress = async () => { await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => { setBadge({ manager: resolvedAddress, metadata: { name: nameState.value || undefined, description: descriptionState.value || undefined, image: imageState.value || undefined, image_data: imageDataState.value || undefined, external_url: externalUrlState.value || undefined, attributes: attributesState.values[0]?.trait_type && attributesState.values[0]?.value ? attributesState.values .map((attr) => ({ trait_type: attr.trait_type, value: attr.value, })) .filter((attr) => attr.trait_type && attr.value) : undefined, background_color: backgroundColorState.value || undefined, animation_url: animationUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined, }, transferrable: transferrable === true, rule: { by_key: keyState.value, }, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, max_supply: maxSupplyState.value || undefined, }) }) } useEffect(() => { void resolveManagerAddress() }, [managerState.value]) useEffect(() => { setBadge({ manager: managerState.value, metadata: { name: nameState.value || undefined, description: descriptionState.value || undefined, image: imageState.value || undefined, image_data: imageDataState.value || undefined, external_url: externalUrlState.value || undefined, attributes: attributesState.values[0]?.trait_type && attributesState.values[0]?.value ? attributesState.values .map((attr) => ({ trait_type: attr.trait_type, value: attr.value, })) .filter((attr) => attr.trait_type && attr.value) : undefined, background_color: backgroundColorState.value || undefined, animation_url: animationUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined, }, transferrable: transferrable === true, rule: { by_key: keyState.value, }, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, max_supply: maxSupplyState.value || undefined, }) }, [ nameState.value, descriptionState.value, imageState.value, imageDataState.value, externalUrlState.value, attributesState.values, backgroundColorState.value, animationUrlState.value, youtubeUrlState.value, transferrable, keyState.value, timestamp, maxSupplyState.value, ]) useEffect(() => { if (attributesState.values.length === 0) attributesState.add({ trait_type: '', value: '', }) }, []) useEffect(() => { void dispatchEditBadgeMessage().catch((err) => { toast.error(String(err), { style: { maxWidth: 'none' } }) }) }, [triggerDispatch]) useEffect(() => { if (privateKeyState.value.length === 64 && resolvedOwnerAddress) 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) { throw new Error('Please connect your wallet.') } event.preventDefault() if (!type) { throw new Error('Please select an action.') } if (badgeHubContractAddress === '') { throw new Error('Please enter the Badge Hub contract addresses.') } if (type === 'mint_by_key' && privateKeyState.value.length !== 64) { throw new Error('Please enter a valid private key.') } if (wallet.client && type === 'edit_badge') { const feeRateRaw = await wallet.client.queryContractRaw( badgeHubContractAddress, toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()), ) const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array)) await toast .promise( wallet.client.queryContractSmart(badgeHubContractAddress, { badge: { id: badgeId }, }), { error: `Edit Fee calculation failed!`, loading: 'Calculating Edit Fee...', success: (currentBadge) => { console.log('Current badge: ', currentBadge) return `Current metadata is ${ Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes)) } bytes in size.` }, }, ) .then((currentBadge) => { // TODO - Go over the calculation const currentBadgeMetadataSize = Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2) console.log('Current badge metadata size: ', currentBadgeMetadataSize) const newBadgeMetadataSize = Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2 console.log('New badge metadata size: ', newBadgeMetadataSize) if (newBadgeMetadataSize > currentBadgeMetadataSize) { const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2 setEditFee(calculatedFee) setTriggerDispatch(!triggerDispatch) } else { setEditFee(undefined) setTriggerDispatch(!triggerDispatch) } }) .catch((error) => { throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7)) }) } else { const txHash = await toast.promise(dispatchExecute(payload), { error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, loading: 'Executing message...', success: (tx) => `Transaction ${tx} success!`, }) if (txHash) { setLastTx(txHash) } } }, { onError: (error) => { toast.error(String(error), { style: { maxWidth: 'none' } }) }, }, ) const dispatchEditBadgeMessage = async () => { if (type) { const txHash = await toast.promise(dispatchExecute(payload), { error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, loading: 'Executing message...', success: (tx) => `Transaction ${tx} success!`, }) if (txHash) { setLastTx(txHash) } } } const airdropFileOnChange = (data: string[]) => { console.log(data) setAirdropAllocationArray(data) } const handleGenerateSignature = (id: number, owner: string, privateKey: string) => { try { const message = `claim badge ${id} for user ${owner}` const privKey = Buffer.from(privateKey, 'hex') // const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true)) const msgBytes = Buffer.from(message, 'utf8') const msgHashBytes = sha256(msgBytes) const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey) setSignature(Buffer.from(signedMessage.signature).toString('hex')) } catch (error) { console.log(error) toast.error('Error generating signature.') } } return (
{showMetadataField && (
Metadata
)} {showOwnerField && ( )} {showPubKeyField && } {showPrivateKeyField && } {showLimitState && } This action is only available if the badge with the specified id is either minted out or expired.
Number of Keys The number of public keys to be whitelisted for minting badges
setNumberOfKeys(Number(e.target.value))} required type="number" value={numberOfKeys} />
0 && type === 'add_keys'}>
Make sure to download the whitelisted public keys together with their private key counterparts.
You may optionally choose a text file of additional owner addresses.
{showAirdropFileField && ( )}
) }