From eb82d10140080eb335c3ea608a0c5c2e306002c7 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 13:40:29 +0300 Subject: [PATCH 1/9] Update contract helpers for sg721 --- contracts/sg721/contract.ts | 29 +++++++++++++++++++++++++++-- contracts/sg721/useContract.ts | 17 ++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index ba108f1..f15d8c2 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -1,6 +1,6 @@ import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { toBase64, toUtf8 } from '@cosmjs/encoding' -import type { Coin } from '@cosmjs/stargate' +import type { Coin, logs } from '@cosmjs/stargate' import { coin } from '@cosmjs/stargate' import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' @@ -9,6 +9,11 @@ export interface InstantiateResponse { readonly transactionHash: string } +export interface MigrateResponse { + readonly transactionHash: string + readonly logs: readonly logs.Log[] +} + export type Expiration = { at_height: number } | { at_time: string } | { never: Record } export interface SG721Instance { @@ -206,6 +211,13 @@ export interface SG721Contract { admin?: string, ) => Promise + migrate: ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ) => Promise + use: (contractAddress: string) => SG721Instance messages: (contractAddress: string) => Sg721Messages @@ -540,6 +552,19 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } + const migrate = async ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ): Promise => { + const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto') + return { + transactionHash: result.transactionHash, + logs: result.logs, + } + } + const instantiate = async ( senderAddress: string, codeId: number, @@ -734,5 +759,5 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } - return { use, instantiate, messages } + return { use, instantiate, migrate, messages } } diff --git a/contracts/sg721/useContract.ts b/contracts/sg721/useContract.ts index d5fee82..4eae403 100644 --- a/contracts/sg721/useContract.ts +++ b/contracts/sg721/useContract.ts @@ -2,7 +2,7 @@ import type { Coin } from '@cosmjs/proto-signing' import { useWallet } from 'contexts/wallet' import { useCallback, useEffect, useState } from 'react' -import type { SG721Contract, SG721Instance, Sg721Messages } from './contract' +import type { MigrateResponse, SG721Contract, SG721Instance, Sg721Messages } from './contract' import { SG721 as initContract } from './contract' interface InstantiateResponse { @@ -18,6 +18,7 @@ export interface UseSG721ContractProps { admin?: string, funds?: Coin[], ) => Promise + migrate: (contractAddress: string, codeId: number, migrateMsg: Record) => Promise use: (customAddress: string) => SG721Instance | undefined updateContractAddress: (contractAddress: string) => void messages: (contractAddress: string) => Sg721Messages | undefined @@ -55,6 +56,19 @@ export function useSG721Contract(): UseSG721ContractProps { [SG721, wallet], ) + const migrate = useCallback( + (contractAddress: string, codeId: number, migrateMsg: Record): Promise => { + return new Promise((resolve, reject) => { + if (!SG721) { + reject(new Error('Contract is not initialized.')) + return + } + SG721.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject) + }) + }, + [SG721, wallet], + ) + const use = useCallback( (customAddress = ''): SG721Instance | undefined => { return SG721?.use(address || customAddress) @@ -74,5 +88,6 @@ export function useSG721Contract(): UseSG721ContractProps { use, updateContractAddress, messages, + migrate, } } From 07a08ca35a82aadca3a17b6952150bb7f0e2151e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 13:42:27 +0300 Subject: [PATCH 2/9] Update sg721 dashboard link tabs --- components/LinkTabs.data.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index cd0afa1..f8f3a45 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -16,6 +16,11 @@ export const sg721LinkTabs: LinkTabProps[] = [ description: `Execute SG721 contract actions`, href: '/contracts/sg721/execute', }, + { + title: 'Migrate', + description: `Migrate SG721 contract`, + href: '/contracts/sg721/migrate', + }, ] export const minterLinkTabs: LinkTabProps[] = [ From f4f11dbe6a99ffe15220e7b0eaa5854477315a3a Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 13:57:44 +0300 Subject: [PATCH 3/9] Implement SG721 dashboard > Migrate tab --- pages/contracts/sg721/migrate.tsx | 132 ++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 pages/contracts/sg721/migrate.tsx diff --git a/pages/contracts/sg721/migrate.tsx b/pages/contracts/sg721/migrate.tsx new file mode 100644 index 0000000..ce37207 --- /dev/null +++ b/pages/contracts/sg721/migrate.tsx @@ -0,0 +1,132 @@ +import { Button } from 'components/Button' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { useExecuteComboboxState } from 'components/contracts/sg721/ExecuteCombobox.hooks' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { sg721LinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { MigrateResponse } from 'contracts/sg721' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +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 { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const Sg721MigratePage: NextPage = () => { + const { sg721: contract } = useContracts() + const wallet = useWallet() + + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + const codeIdState = useNumberInputState({ + id: 'code-id', + name: 'code-id', + title: 'Code ID', + subtitle: 'Code ID of the New Sg721 contract', + placeholder: '1', + }) + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Sg721 Address', + subtitle: 'Address of the Sg721 contract', + }) + const contractAddress = contractState.value + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + if (!wallet.initialized) { + throw new Error('Please connect your wallet.') + } + + const migrateMsg = {} + return toast.promise(contract.migrate(contractAddress, codeIdState.value, migrateMsg), { + error: `Migration failed!`, + loading: 'Executing message...', + success: (tx) => { + if (tx) { + setLastTx(tx.transactionHash) + } + return `Transaction success!` + }, + }) + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
+ + + + +
+
+ + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(Sg721MigratePage, { center: false }) From 6f36284fbd6acb4fb1adae97a3b3b01d0c8f5f91 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 15:57:37 +0300 Subject: [PATCH 4/9] Add update_royalty_info() among sg721 contract helpers --- contracts/sg721/contract.ts | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index f15d8c2..ba342d8 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -14,6 +14,11 @@ export interface MigrateResponse { readonly logs: readonly logs.Log[] } +export interface RoyaltyInfo { + payment_address: string + share_bps: number +} + export type Expiration = { at_height: number } | { at_time: string } | { never: Record } export interface SG721Instance { @@ -70,6 +75,7 @@ export interface SG721Instance { revokeAll: (operator: string) => Promise /// Mint a new NFT, can only be called by the contract minter mint: (tokenId: string, owner: string, tokenURI?: string) => Promise //MintMsg + updateRoyaltyInfo: (royaltyInfo: RoyaltyInfo) => Promise /// Burn an NFT the sender has access to burn: (tokenId: string) => Promise @@ -90,6 +96,18 @@ export interface Sg721Messages { batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage } +export interface UpdateRoyaltyInfoMessage { + sender: string + contract: string + msg: { + update_royalty_info: { + payment_address: string + share_bps: string + } + } + funds: Coin[] +} + export interface TransferNFTMessage { sender: string contract: string @@ -425,6 +443,22 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con return res.transactionHash } + const updateRoyaltyInfo = async (royaltyInfo: RoyaltyInfo): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_royalty_info: { + payment_address: royaltyInfo.payment_address, + share_bps: royaltyInfo.share_bps, + }, + }, + 'auto', + '', + ) + return res.transactionHash + } + const burn = async (tokenId: string): Promise => { const res = await client.execute( txSigner, @@ -546,6 +580,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con approveAll, revokeAll, mint, + updateRoyaltyInfo, burn, batchBurn, batchTransfer, @@ -684,6 +719,20 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } + const updateRoyaltyInfo = (royaltyInfo: RoyaltyInfo) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_royalty_info: { + payment_address: royaltyInfo.payment_address, + share_bps: royaltyInfo.share_bps, + }, + }, + funds: [], + } + } + const burn = (tokenId: string) => { return { sender: txSigner, @@ -753,6 +802,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con approveAll, revokeAll, mint, + updateRoyaltyInfo, burn, batchBurn, batchTransfer, From 129953f7b3f3b887ce3c1a338fc0a927f7bfdc90 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 16:33:33 +0300 Subject: [PATCH 5/9] Include Update Royalty Info on Collection Actions --- components/collections/actions/Action.tsx | 58 ++++++++++++++++++++++- components/collections/actions/actions.ts | 15 +++++- contracts/sg721/contract.ts | 3 +- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index 19779a9..1633fbc 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -1,3 +1,4 @@ +import { toUtf8 } from '@cosmjs/encoding' import { AirdropUpload } from 'components/AirdropUpload' import { Button } from 'components/Button' import type { DispatchExecuteArgs } from 'components/collections/actions/actions' @@ -90,6 +91,22 @@ export const CollectionActions = ({ subtitle: 'Address of the whitelist contract', }) + const royaltyPaymentAddressState = useInputState({ + id: 'royalty-payment-address', + name: 'royaltyPaymentAddress', + title: 'Royalty Payment Address', + subtitle: 'Address to receive royalties.', + placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', + }) + + const royaltyShareState = useInputState({ + id: 'royalty-share', + name: 'royaltyShare', + title: 'Share Percentage', + subtitle: 'Percentage of royalties to be paid', + placeholder: '8%', + }) + const showWhitelistField = type === 'set_whitelist' const showDateField = type === 'update_start_time' const showLimitField = type === 'update_per_address_limit' @@ -98,6 +115,7 @@ export const CollectionActions = ({ const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer']) const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer']) const showAirdropFileField = type === 'airdrop' + const showRoyaltyInfoFields = type === 'update_royalty_info' const payload: DispatchExecuteArgs = { whitelist: whitelistState.value, @@ -113,6 +131,10 @@ export const CollectionActions = ({ recipient: recipientState.value, recipients: airdropArray, txSigner: wallet.address, + royaltyInfo: { + payment_address: royaltyPaymentAddressState.value, + share_bps: Number(royaltyShareState.value), + }, type, } @@ -140,6 +162,38 @@ export const CollectionActions = ({ if (minterContractAddress === '' && sg721ContractAddress === '') { throw new Error('Please enter minter and sg721 contract addresses!') } + + if ( + type === 'update_royalty_info' && + (royaltyShareState.value ? !royaltyPaymentAddressState.value : royaltyPaymentAddressState.value) + ) { + throw new Error('Royalty payment address and share percentage are both required') + } + + if ( + type === 'update_royalty_info' && + royaltyPaymentAddressState.value && + !royaltyPaymentAddressState.value.trim().endsWith('.stars') + ) { + const contractInfoResponse = await wallet.client + ?.queryContractRaw( + royaltyPaymentAddressState.value.trim(), + toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), + ) + .catch((e) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (e.message.includes('bech32')) throw new Error('Invalid royalty payment address.') + console.log(e.message) + }) + if (contractInfoResponse !== undefined) { + const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array)) + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (contractInfo && !contractInfo.contract.includes('splits')) + throw new Error('The provided royalty payment address does not belong to a splits contract.') + else console.log(contractInfo) + } + } + const txHash = await toast.promise(dispatchExecute(payload), { error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, loading: 'Executing message...', @@ -151,7 +205,7 @@ export const CollectionActions = ({ }, { onError: (error) => { - toast.error(String(error)) + toast.error(String(error), { style: { maxWidth: 'none' } }) }, }, ) @@ -172,6 +226,8 @@ export const CollectionActions = ({ {showTokenIdField && } {showTokenIdListField && } {showNumberOfTokensField && } + {showRoyaltyInfoFields && } + {showRoyaltyInfoFields && } {showAirdropFileField && ( ; recipient: string; tokenIds: string } | { type: Select<'burn'>; tokenId: number } | { type: Select<'batch_burn'>; tokenIds: string } + | { type: Select<'update_royalty_info'>; royaltyInfo: RoyaltyInfo } | { type: Select<'airdrop'>; recipients: string[] } ) @@ -165,6 +172,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'burn': { return sg721Messages.burn(args.tokenId.toString()) } + case 'update_royalty_info': { + return sg721Messages.updateRoyaltyInfo(args.royaltyInfo) + } case 'batch_burn': { return sg721Messages.batchBurn(args.tokenIds) } @@ -217,6 +227,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'burn': { return sg721Messages(sg721Contract)?.burn(args.tokenId.toString()) } + case 'update_royalty_info': { + return sg721Messages(sg721Contract)?.updateRoyaltyInfo(args.royaltyInfo) + } case 'batch_burn': { return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds) } diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index ba342d8..fb6b286 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -91,6 +91,7 @@ export interface Sg721Messages { approveAll: (operator: string, expires?: Expiration) => ApproveAllMessage revokeAll: (operator: string) => RevokeAllMessage mint: (tokenId: string, owner: string, tokenURI?: string) => MintMessage + updateRoyaltyInfo: (royaltyInfo: RoyaltyInfo) => UpdateRoyaltyInfoMessage burn: (tokenId: string) => BurnMessage batchBurn: (tokenIds: string) => BatchBurnMessage batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage @@ -102,7 +103,7 @@ export interface UpdateRoyaltyInfoMessage { msg: { update_royalty_info: { payment_address: string - share_bps: string + share_bps: number } } funds: Coin[] From 019a0c4d736694ab6cbb2441ec0b8e3e1f05880d Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 16:52:30 +0300 Subject: [PATCH 6/9] Add update/batch_update/freeze_token_metadata() among sg721 contract helpers --- contracts/sg721/contract.ts | 167 ++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index fb6b286..eed7a70 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -81,6 +81,9 @@ export interface SG721Instance { burn: (tokenId: string) => Promise batchBurn: (tokenIds: string) => Promise batchTransfer: (recipient: string, tokenIds: string) => Promise + updateTokenMetadata: (tokenId: string, tokenURI?: string) => Promise + batchUpdateTokenMetadata: (tokenIds: string, tokenURI?: string) => Promise + freezeTokenMetadata: () => Promise } export interface Sg721Messages { @@ -95,6 +98,9 @@ export interface Sg721Messages { burn: (tokenId: string) => BurnMessage batchBurn: (tokenIds: string) => BatchBurnMessage batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage + updateTokenMetadata: (tokenId: string, tokenURI?: string) => UpdateTokenMetadataMessage + batchUpdateTokenMetadata: (tokenIds: string, tokenURI?: string) => BatchUpdateTokenMetadataMessage + freezeTokenMetadata: () => FreezeTokenMetadataMessage } export interface UpdateRoyaltyInfoMessage { @@ -109,6 +115,32 @@ export interface UpdateRoyaltyInfoMessage { funds: Coin[] } +export interface UpdateTokenMetadataMessage { + sender: string + contract: string + msg: { + update_token_metadata: { + token_id: string + token_uri: string + } + } + funds: Coin[] +} + +export interface BatchUpdateTokenMetadataMessage { + sender: string + contract: string + msg: Record[] + funds: Coin[] +} + +export interface FreezeTokenMetadataMessage { + sender: string + contract: string + msg: { freeze_token_metadata: Record } + funds: Coin[] +} + export interface TransferNFTMessage { sender: string contract: string @@ -560,6 +592,81 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con return res.transactionHash } + const batchUpdateTokenMetadata = async (tokenIds: string, baseURI?: string): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + if (tokenIds.includes(':')) { + const [start, end] = tokenIds.split(':').map(Number) + for (let i = start; i <= end; i++) { + const msg = { + update_token_metadata: { token_id: i.toString(), token_uri: baseURI ? `${baseURI}/${i}` : undefined }, + } + 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 = { + update_token_metadata: { + token_id: tokenNumbers[i].toString(), + token_uri: baseURI ? `${baseURI}/${tokenNumbers[i]}` : undefined, + }, + } + 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 update metadata') + + return res.transactionHash + } + + const updateTokenMetadata = async (tokenId: string, tokenURI?: string): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_token_metadata: { + token_id: tokenId, + token_uri: tokenURI ? tokenURI : undefined, + }, + }, + 'auto', + '', + ) + return res.transactionHash + } + + const freezeTokenMetadata = async (): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + freeze_token_metadata: {}, + }, + 'auto', + '', + ) + return res.transactionHash + } + return { contractAddress, ownerOf, @@ -568,6 +675,9 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con allOperators, numTokens, contractInfo, + updateTokenMetadata, + freezeTokenMetadata, + batchUpdateTokenMetadata, nftInfo, allNftInfo, tokens, @@ -795,6 +905,60 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } + const batchUpdateTokenMetadata = (tokenIds: string, baseURI?: string): BatchUpdateTokenMetadataMessage => { + const msg: Record[] = [] + if (tokenIds.includes(':')) { + const [start, end] = tokenIds.split(':').map(Number) + for (let i = start; i <= end; i++) { + msg.push({ + update_token_metadata: { token_id: i.toString(), token_uri: baseURI ? `${baseURI}/${i}` : '' }, + }) + } + } else { + const tokenNumbers = tokenIds.split(',').map(Number) + for (let i = 0; i < tokenNumbers.length; i++) { + msg.push({ + update_token_metadata: { + token_id: tokenNumbers[i].toString(), + token_uri: baseURI ? `${baseURI}/${tokenNumbers[i]}` : '', + }, + }) + } + } + + return { + sender: txSigner, + contract: contractAddress, + msg, + funds: [], + } + } + + const updateTokenMetadata = (tokenId: string, tokenURI?: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_token_metadata: { + token_id: tokenId, + token_uri: tokenURI ? tokenURI : '', + }, + }, + funds: [], + } + } + + const freezeTokenMetadata = () => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + freeze_token_metadata: {}, + }, + funds: [], + } + } + return { transferNft, sendNft, @@ -807,6 +971,9 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con burn, batchBurn, batchTransfer, + batchUpdateTokenMetadata, + updateTokenMetadata, + freezeTokenMetadata, } } From e64073499d79a81117398c35bd85219ca9831782 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 17:29:23 +0300 Subject: [PATCH 7/9] Add update/batch_update/freeze_token_metadata() to Collection Actions --- components/collections/actions/Action.tsx | 30 +++++++++++++++-- components/collections/actions/actions.ts | 39 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index 1633fbc..f8b5f49 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -84,6 +84,22 @@ export const CollectionActions = ({ subtitle: 'Address of the recipient', }) + const tokenURIState = useInputState({ + id: 'token-uri', + name: 'tokenURI', + title: 'Token URI', + subtitle: 'URI for the token', + placeholder: 'ipfs://', + }) + + const baseURIState = useInputState({ + id: 'base-uri', + name: 'baseURI', + title: 'Base URI', + subtitle: 'Base URI to batch update token metadata with', + placeholder: 'ipfs://', + }) + const whitelistState = useInputState({ id: 'whitelist-address', name: 'whitelistAddress', @@ -110,12 +126,14 @@ export const CollectionActions = ({ 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', 'burn']) + const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata']) const showNumberOfTokensField = type === 'batch_mint' - const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer']) + const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer', 'batch_update_token_metadata']) const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer']) const showAirdropFileField = type === 'airdrop' const showRoyaltyInfoFields = type === 'update_royalty_info' + const showTokenUriField = type === 'update_token_metadata' + const showBaseUriField = type === 'batch_update_token_metadata' const payload: DispatchExecuteArgs = { whitelist: whitelistState.value, @@ -135,6 +153,12 @@ export const CollectionActions = ({ payment_address: royaltyPaymentAddressState.value, share_bps: Number(royaltyShareState.value), }, + baseUri: baseURIState.value.trim().endsWith('/') + ? baseURIState.value.trim().slice(0, -1) + : baseURIState.value.trim(), + tokenUri: tokenURIState.value.trim().endsWith('/') + ? tokenURIState.value.trim().slice(0, -1) + : tokenURIState.value.trim(), type, } @@ -224,7 +248,9 @@ export const CollectionActions = ({ {showWhitelistField && } {showLimitField && } {showTokenIdField && } + {showTokenUriField && } {showTokenIdListField && } + {showBaseUriField && } {showNumberOfTokensField && } {showRoyaltyInfoFields && } {showRoyaltyInfoFields && } diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts index 06fa4a3..f321c24 100644 --- a/components/collections/actions/actions.ts +++ b/components/collections/actions/actions.ts @@ -20,6 +20,9 @@ export const ACTION_TYPES = [ 'batch_burn', 'shuffle', 'airdrop', + 'update_token_metadata', + 'batch_update_token_metadata', + 'freeze_token_metadata', ] as const export interface ActionListItem { @@ -64,6 +67,21 @@ export const ACTION_LIST: ActionListItem[] = [ name: 'Update Tokens Per Address Limit', description: `Update token per address limit`, }, + { + id: 'update_token_metadata', + name: 'Update Token Metadata', + description: `Update the metadata URI for a token`, + }, + { + id: 'batch_update_token_metadata', + name: 'Batch Update Token Metadata', + description: `Update the metadata URI for a range of tokens`, + }, + { + id: 'freeze_token_metadata', + name: 'Freeze Token Metadata', + description: `Render the metadata for tokens no longer updatable`, + }, { id: 'withdraw', name: 'Withdraw Tokens', @@ -131,6 +149,9 @@ export type DispatchExecuteArgs = { | { type: Select<'batch_burn'>; tokenIds: string } | { type: Select<'update_royalty_info'>; royaltyInfo: RoyaltyInfo } | { type: Select<'airdrop'>; recipients: string[] } + | { type: Select<'update_token_metadata'>; tokenId: number; tokenUri: string } + | { type: Select<'batch_update_token_metadata'>; tokenIds: string; baseUri: string } + | { type: Select<'freeze_token_metadata'> } ) export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -181,6 +202,15 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'airdrop': { return minterMessages.airdrop(txSigner, args.recipients) } + case 'update_token_metadata': { + return sg721Messages.updateTokenMetadata(args.tokenId.toString(), args.tokenUri) + } + case 'batch_update_token_metadata': { + return sg721Messages.batchUpdateTokenMetadata(args.tokenIds, args.baseUri) + } + case 'freeze_token_metadata': { + return sg721Messages.freezeTokenMetadata() + } default: { throw new Error('Unknown action') } @@ -218,6 +248,15 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'withdraw': { return minterMessages(minterContract)?.withdraw() } + case 'update_token_metadata': { + return sg721Messages(sg721Contract)?.updateTokenMetadata(args.tokenId.toString(), args.tokenUri) + } + case 'batch_update_token_metadata': { + return sg721Messages(sg721Contract)?.batchUpdateTokenMetadata(args.tokenIds, args.baseUri) + } + case 'freeze_token_metadata': { + return sg721Messages(sg721Contract)?.freezeTokenMetadata() + } case 'transfer': { return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString()) } From 35e736062a5f753e2bddec14111de44829b48c8e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 18:06:39 +0300 Subject: [PATCH 8/9] Update bps calc --- contracts/sg721/contract.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index eed7a70..b27ae05 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -483,7 +483,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con { update_royalty_info: { payment_address: royaltyInfo.payment_address, - share_bps: royaltyInfo.share_bps, + share_bps: royaltyInfo.share_bps * 100, }, }, 'auto', @@ -837,7 +837,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con msg: { update_royalty_info: { payment_address: royaltyInfo.payment_address, - share_bps: royaltyInfo.share_bps, + share_bps: royaltyInfo.share_bps * 100, }, }, funds: [], From dc902313a6d22382fe2d7ddfc7ba40eeefbbbb6d Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 11 Apr 2023 18:09:20 +0300 Subject: [PATCH 9/9] Update codeowners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b480a52..b27b987 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @findolor @MightOfOaks @name-user1 +* @MightOfOaks @Ninjatosba