From aae2b14a017b470955a780d5198beffb752ef914 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 16 Aug 2022 10:18:35 +0300 Subject: [PATCH] Add burn & batch burn for sg721 (#39) * Add burn & batch burn for sg721 * Add batch burn range * Refactor contract messages logic Co-authored-by: findolor --- components/collections/actions/actions.ts | 42 ++++++++--- contracts/minter/contract.ts | 40 +++++------ contracts/minter/useContract.ts | 11 +-- contracts/sg721/contract.ts | 85 ++++++++++++++++++++++- contracts/sg721/messages/execute.ts | 2 +- contracts/sg721/useContract.ts | 9 ++- contracts/whitelist/useContract.ts | 9 ++- pages/collections/actions.tsx | 19 ++++- 8 files changed, 172 insertions(+), 45 deletions(-) diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts index c980910..ef53243 100644 --- a/components/collections/actions/actions.ts +++ b/components/collections/actions/actions.ts @@ -14,6 +14,8 @@ export const ACTION_TYPES = [ 'update_per_address_limit', 'withdraw', 'transfer', + 'burn', + 'batch_burn', 'shuffle', ] as const @@ -64,6 +66,16 @@ export const ACTION_LIST: ActionListItem[] = [ name: 'Transfer Tokens', description: `Transfer tokens from one address to another`, }, + { + id: 'burn', + name: 'Burn Token', + description: `Burn a specified token from the collection`, + }, + { + id: 'batch_burn', + name: 'Batch Burn Tokens', + description: `Burn a list of tokens from the collection`, + }, { id: 'shuffle', name: 'Shuffle Tokens', @@ -96,6 +108,8 @@ export type DispatchExecuteArgs = { | { type: Select<'shuffle'> } | { type: Select<'withdraw'> } | { type: Select<'transfer'>; recipient: string; tokenId: number } + | { type: Select<'burn'>; tokenId: number } + | { type: Select<'batch_burn'>; tokenIds: string } ) export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -131,6 +145,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'transfer': { return sg721Messages.transferNft(args.recipient, args.tokenId.toString()) } + case 'burn': { + return sg721Messages.burn(args.tokenId.toString()) + } + case 'batch_burn': { + return sg721Messages.batchBurn(args.tokenIds) + } default: { throw new Error('Unknown action') } @@ -145,32 +165,38 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { const { minterContract, sg721Contract } = args switch (args.type) { case 'mint_to': { - return minterMessages()?.mintTo(minterContract, args.recipient) + return minterMessages(minterContract)?.mintTo(args.recipient) } case 'mint_for': { - return minterMessages()?.mintFor(minterContract, args.recipient, args.tokenId) + return minterMessages(minterContract)?.mintFor(args.recipient, args.tokenId) } case 'batch_mint': { - return minterMessages()?.batchMint(minterContract, args.recipient, args.batchNumber) + return minterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber) } case 'set_whitelist': { - return minterMessages()?.setWhitelist(minterContract, args.whitelist) + return minterMessages(minterContract)?.setWhitelist(args.whitelist) } case 'update_start_time': { - return minterMessages()?.updateStartTime(minterContract, args.startTime) + return minterMessages(minterContract)?.updateStartTime(args.startTime) } case 'update_per_address_limit': { - return minterMessages()?.updatePerAddressLimit(minterContract, args.limit) + return minterMessages(minterContract)?.updatePerAddressLimit(args.limit) } case 'shuffle': { - return minterMessages()?.shuffle(minterContract) + return minterMessages(minterContract)?.shuffle() } case 'withdraw': { - return minterMessages()?.withdraw(minterContract) + return minterMessages(minterContract)?.withdraw() } case 'transfer': { return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString()) } + case 'burn': { + return sg721Messages(sg721Contract)?.burn(args.tokenId.toString()) + } + case 'batch_burn': { + return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds) + } default: { return {} } diff --git a/contracts/minter/contract.ts b/contracts/minter/contract.ts index ba7069d..e9a2ffe 100644 --- a/contracts/minter/contract.ts +++ b/contracts/minter/contract.ts @@ -40,15 +40,15 @@ export interface MinterInstance { } export interface MinterMessages { - mint: (contractAddress: string, price: string) => MintMessage - setWhitelist: (contractAddress: string, whitelist: string) => SetWhitelistMessage - updateStartTime: (contractAddress: string, time: Timestamp) => UpdateStarTimeMessage - updatePerAddressLimit: (contractAddress: string, perAddressLimit: number) => UpdatePerAddressLimitMessage - mintTo: (contractAddress: string, recipient: string) => MintToMessage - mintFor: (contractAddress: string, recipient: string, tokenId: number) => MintForMessage - batchMint: (contractAddress: string, recipient: string, batchNumber: number) => BatchMintMessage - shuffle: (contractAddress: string) => ShuffleMessage - withdraw: (contractAddress: string) => WithdrawMessage + mint: (price: string) => MintMessage + setWhitelist: (whitelist: string) => SetWhitelistMessage + updateStartTime: (time: Timestamp) => UpdateStarTimeMessage + updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage + mintTo: (recipient: string) => MintToMessage + mintFor: (recipient: string, tokenId: number) => MintForMessage + batchMint: (recipient: string, batchNumber: number) => BatchMintMessage + shuffle: () => ShuffleMessage + withdraw: () => WithdrawMessage } export interface MintMessage { @@ -151,7 +151,7 @@ export interface MinterContract { use: (contractAddress: string) => MinterInstance - messages: () => MinterMessages + messages: (contractAddress: string) => MinterMessages } export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterContract => { @@ -365,8 +365,8 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const messages = () => { - const mint = (contractAddress: string, price: string): MintMessage => { + const messages = (contractAddress: string) => { + const mint = (price: string): MintMessage => { return { sender: txSigner, contract: contractAddress, @@ -377,7 +377,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const setWhitelist = (contractAddress: string, whitelist: string): SetWhitelistMessage => { + const setWhitelist = (whitelist: string): SetWhitelistMessage => { return { sender: txSigner, contract: contractAddress, @@ -390,7 +390,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const updateStartTime = (contractAddress: string, startTime: string): UpdateStarTimeMessage => { + const updateStartTime = (startTime: string): UpdateStarTimeMessage => { return { sender: txSigner, contract: contractAddress, @@ -401,7 +401,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const updatePerAddressLimit = (contractAddress: string, limit: number): UpdatePerAddressLimitMessage => { + const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => { return { sender: txSigner, contract: contractAddress, @@ -414,7 +414,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const mintTo = (contractAddress: string, recipient: string): MintToMessage => { + const mintTo = (recipient: string): MintToMessage => { return { sender: txSigner, contract: contractAddress, @@ -427,7 +427,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const mintFor = (contractAddress: string, recipient: string, tokenId: number): MintForMessage => { + const mintFor = (recipient: string, tokenId: number): MintForMessage => { return { sender: txSigner, contract: contractAddress, @@ -441,7 +441,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const batchMint = (contractAddress: string, recipient: string, batchNumber: number): BatchMintMessage => { + const batchMint = (recipient: string, batchNumber: number): BatchMintMessage => { const msg: Record[] = [] for (let i = 0; i < batchNumber; i++) { msg.push({ mint_to: { recipient } }) @@ -454,7 +454,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const shuffle = (contractAddress: string): ShuffleMessage => { + const shuffle = (): ShuffleMessage => { return { sender: txSigner, contract: contractAddress, @@ -465,7 +465,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC } } - const withdraw = (contractAddress: string): WithdrawMessage => { + const withdraw = (): WithdrawMessage => { return { sender: txSigner, contract: contractAddress, diff --git a/contracts/minter/useContract.ts b/contracts/minter/useContract.ts index e8bdbbf..afca5c5 100644 --- a/contracts/minter/useContract.ts +++ b/contracts/minter/useContract.ts @@ -35,7 +35,7 @@ export interface UseMinterContractProps { use: (customAddress: string) => MinterInstance | undefined updateContractAddress: (contractAddress: string) => void getContractAddress: () => string | undefined - messages: () => MinterMessages | undefined + messages: (contractAddress: string) => MinterMessages | undefined } export function useMinterContract(): UseMinterContractProps { @@ -81,9 +81,12 @@ export function useMinterContract(): UseMinterContractProps { return address } - const messages = useCallback((): MinterMessages | undefined => { - return minter?.messages() - }, [minter]) + const messages = useCallback( + (customAddress = ''): MinterMessages | undefined => { + return minter?.messages(address || customAddress) + }, + [minter, address], + ) return { instantiate, diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index c12d24b..3df6102 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -1,7 +1,8 @@ -import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { toBase64, toUtf8 } from '@cosmjs/encoding' import type { Coin } from '@cosmjs/stargate' import { coin } from '@cosmjs/stargate' +import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' export interface InstantiateResponse { readonly contractAddress: string @@ -67,6 +68,7 @@ export interface SG721Instance { /// Burn an NFT the sender has access to burn: (tokenId: string) => Promise + batchBurn: (tokenIds: string) => Promise } export interface Sg721Messages { @@ -78,6 +80,7 @@ export interface Sg721Messages { revokeAll: (operator: string) => RevokeAllMessage mint: (tokenId: string, owner: string, tokenURI?: string) => MintMessage burn: (tokenId: string) => BurnMessage + batchBurn: (tokenIds: string) => BatchBurnMessage } export interface TransferNFTMessage { @@ -178,6 +181,13 @@ export interface BurnMessage { funds: Coin[] } +export interface BatchBurnMessage { + sender: string + contract: string + msg: Record[] + funds: Coin[] +} + export interface SG721Contract { instantiate: ( senderAddress: string, @@ -408,6 +418,49 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con return res.transactionHash } + const batchBurn = async (tokenIds: string): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + if (tokenIds.includes(':')) { + const [start, end] = tokenIds.split(':').map(Number) + for (let i = start; i <= end; i++) { + const msg = { + burn: { token_id: i.toString() }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: txSigner, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + } else { + const tokenNumbers = tokenIds.split(',').map(Number) + for (let i = 0; i < tokenNumbers.length; i++) { + const msg = { + burn: { token_id: tokenNumbers[i].toString() }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: txSigner, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + } + + const res = await client.signAndBroadcast(txSigner, executeContractMsgs, 'auto', 'batch burn') + + return res.transactionHash + } + return { contractAddress, ownerOf, @@ -430,6 +483,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con revokeAll, mint, burn, + batchBurn, } } @@ -552,10 +606,10 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } - const burn = (tokenId: string) => { + const burn = (contractAddr: string, tokenId: string) => { return { sender: txSigner, - contract: contractAddress, + contract: contractAddr, msg: { burn: { token_id: tokenId, @@ -565,6 +619,30 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } + const batchBurn = (contractAddr: string, tokenIds: string): BatchBurnMessage => { + const msg: Record[] = [] + if (tokenIds.includes(':')) { + const [start, end] = tokenIds.split(':').map(Number) + for (let i = start; i <= end; i++) { + msg.push({ + burn: { token_id: i.toString() }, + }) + } + } else { + const tokenNumbers = tokenIds.split(',').map(Number) + for (let i = 0; i < tokenNumbers.length; i++) { + msg.push({ burn: { token_id: tokenNumbers[i].toString() } }) + } + } + + return { + sender: txSigner, + contract: contractAddr, + msg, + funds: [], + } + } + return { transferNft, sendNft, @@ -574,6 +652,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con revokeAll, mint, burn, + batchBurn, } } diff --git a/contracts/sg721/messages/execute.ts b/contracts/sg721/messages/execute.ts index de628b5..1974805 100644 --- a/contracts/sg721/messages/execute.ts +++ b/contracts/sg721/messages/execute.ts @@ -149,7 +149,7 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { return messages(contract)?.mint(args.recipient, args.tokenId, args.tokenURI) } case 'burn': { - return messages(contract)?.burn(args.tokenId) + return messages(contract)?.burn(args.contract, args.tokenId) } default: { return {} diff --git a/contracts/sg721/useContract.ts b/contracts/sg721/useContract.ts index 6f1e199..d5fee82 100644 --- a/contracts/sg721/useContract.ts +++ b/contracts/sg721/useContract.ts @@ -62,9 +62,12 @@ export function useSG721Contract(): UseSG721ContractProps { [SG721, address], ) - const messages = useCallback((): Sg721Messages | undefined => { - return SG721?.messages(address) - }, [SG721, address]) + const messages = useCallback( + (customAddress = ''): Sg721Messages | undefined => { + return SG721?.messages(address || customAddress) + }, + [SG721, address], + ) return { instantiate, diff --git a/contracts/whitelist/useContract.ts b/contracts/whitelist/useContract.ts index 528b461..3144cce 100644 --- a/contracts/whitelist/useContract.ts +++ b/contracts/whitelist/useContract.ts @@ -58,9 +58,12 @@ export function useWhiteListContract(): UseWhiteListContractProps { [whiteList, address], ) - const messages = useCallback((): WhitelistMessages | undefined => { - return whiteList?.messages(address) - }, [whiteList, address]) + const messages = useCallback( + (customAddress = ''): WhitelistMessages | undefined => { + return whiteList?.messages(address || customAddress) + }, + [whiteList, address], + ) return { instantiate, diff --git a/pages/collections/actions.tsx b/pages/collections/actions.tsx index 324082f..6634a1a 100644 --- a/pages/collections/actions.tsx +++ b/pages/collections/actions.tsx @@ -23,6 +23,8 @@ import { useMutation } from 'react-query' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' +import { TextInput } from '../../components/forms/FormInput' + const CollectionActionsPage: NextPage = () => { const { minter: minterContract, sg721: sg721Contract } = useContracts() const wallet = useWallet() @@ -68,6 +70,14 @@ const CollectionActionsPage: NextPage = () => { subtitle: 'Enter the number of tokens to mint', }) + const tokenIdListState = useInputState({ + id: 'token-id-list', + name: 'tokenIdList', + title: 'The list of token IDs', + subtitle: + 'Specify individual token IDs separated by commas (e.g., 2, 4, 8) or a range of IDs separated by a colon (e.g, 8:13)', + }) + const recipientState = useInputState({ id: 'recipient-address', name: 'recipient', @@ -85,8 +95,9 @@ const CollectionActionsPage: NextPage = () => { const showWhitelistField = type === 'set_whitelist' const showDateField = type === 'update_start_time' const showLimitField = type === 'update_per_address_limit' - const showTokenIdField = isEitherType(type, ['transfer', 'mint_for']) - const showTokenIdListField = type === 'batch_mint' + const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn']) + const showNumberOfTokensField = type === 'batch_mint' + const showTokenIdListField = type === 'batch_burn' const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint']) const minterMessages = useMemo( @@ -104,6 +115,7 @@ const CollectionActionsPage: NextPage = () => { minterContract: minterContractState.value, sg721Contract: sg721ContractState.value, tokenId: tokenIdState.value, + tokenIds: tokenIdListState.value, batchNumber: batchNumberState.value, minterMessages, sg721Messages, @@ -154,7 +166,8 @@ const CollectionActionsPage: NextPage = () => { {showWhitelistField && } {showLimitField && } {showTokenIdField && } - {showTokenIdListField && } + {showTokenIdListField && } + {showNumberOfTokensField && } setTimestamp(date)} value={timestamp} />