From 88335ea733263ad6cae4aa6fd10ff1eaab0c2b22 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 25 Feb 2023 20:36:48 +0300 Subject: [PATCH 01/13] Retrieve SG721 type on Collection Actions --- components/collections/actions/Combobox.tsx | 1 + pages/collections/actions.tsx | 41 ++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/components/collections/actions/Combobox.tsx b/components/collections/actions/Combobox.tsx index 3aa0814..88a29b6 100644 --- a/components/collections/actions/Combobox.tsx +++ b/components/collections/actions/Combobox.tsx @@ -9,6 +9,7 @@ import type { ActionListItem } from './actions' import { BASE_ACTION_LIST, VENDING_ACTION_LIST } from './actions' export type MinterType = 'base' | 'vending' +export type Sg721Type = 'updatable' | 'base' export interface ActionsComboboxProps { value: ActionListItem | null diff --git a/pages/collections/actions.tsx b/pages/collections/actions.tsx index 5c50eda..128ef7b 100644 --- a/pages/collections/actions.tsx +++ b/pages/collections/actions.tsx @@ -15,7 +15,7 @@ import { useDebounce } from 'utils/debounce' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' -import type { MinterType } from '../../components/collections/actions/Combobox' +import type { MinterType, Sg721Type } from '../../components/collections/actions/Combobox' const CollectionActionsPage: NextPage = () => { const { baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, sg721: sg721Contract } = useContracts() @@ -23,6 +23,7 @@ const CollectionActionsPage: NextPage = () => { const [action, setAction] = useState(false) const [minterType, setMinterType] = useState('vending') + const [sg721Type, setSg721Type] = useState('base') const sg721ContractState = useInputState({ id: 'sg721-contract-address', @@ -39,6 +40,7 @@ const CollectionActionsPage: NextPage = () => { }) const debouncedMinterContractState = useDebounce(minterContractState.value, 300) + const debouncedSg721ContractState = useDebounce(sg721ContractState.value, 300) const vendingMinterMessages = useMemo( () => vendingMinterContract?.use(minterContractState.value), @@ -109,10 +111,45 @@ const CollectionActionsPage: NextPage = () => { .catch((err) => { console.log(err) setMinterType('vending') - console.log('Unable to retrieve contract version') + console.log('Unable to retrieve contract type. Defaulting to "vending".') }) }, [debouncedMinterContractState, wallet.client]) + useEffect(() => { + async function getSg721ContractType() { + if (wallet.client && debouncedSg721ContractState.length > 0) { + const client = wallet.client + const data = await toast.promise( + client.queryContractRaw( + debouncedSg721ContractState, + toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), + ), + { + loading: 'Retrieving SG721 type...', + error: 'SG721 type retrieval failed.', + success: 'SG721 type retrieved.', + }, + ) + const contract: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract + console.log(contract) + return contract + } + } + void getSg721ContractType() + .then((contract) => { + if (contract?.includes('sg721-updatable')) { + setSg721Type('updatable') + } else { + setSg721Type('base') + } + }) + .catch((err) => { + console.log(err) + setMinterType('base') + console.log('Unable to retrieve contract type. Defaulting to "base".') + }) + }, [debouncedSg721ContractState, wallet.client]) + return (
From a764b96727585f5b67de8ffff42d89327bcf3bbd Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 25 Feb 2023 21:09:43 +0300 Subject: [PATCH 02/13] Update action list wrt SG721 type on Collection Actions --- components/collections/actions/Action.tsx | 6 ++++-- components/collections/actions/Combobox.tsx | 17 +++++++++------ components/collections/actions/actions.ts | 24 +++++++++++++++++++++ pages/collections/actions.tsx | 3 ++- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index 68b0750..b1c88bd 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -26,7 +26,7 @@ import { resolveAddress } from 'utils/resolveAddress' import type { CollectionInfo } from '../../../contracts/sg721/contract' import { TextInput } from '../../forms/FormInput' -import type { MinterType } from './Combobox' +import type { MinterType, Sg721Type } from './Combobox' interface CollectionActionsProps { minterContractAddress: string @@ -35,6 +35,7 @@ interface CollectionActionsProps { vendingMinterMessages: VendingMinterInstance | undefined baseMinterMessages: BaseMinterInstance | undefined minterType: MinterType + sg721Type: Sg721Type } type ExplicitContentType = true | false | undefined @@ -46,6 +47,7 @@ export const CollectionActions = ({ vendingMinterMessages, baseMinterMessages, minterType, + sg721Type, }: CollectionActionsProps) => { const wallet = useWallet() const [lastTx, setLastTx] = useState('') @@ -350,7 +352,7 @@ export const CollectionActions = ({
- + {showRecipientField && } {showTokenUriField && } {showWhitelistField && } diff --git a/components/collections/actions/Combobox.tsx b/components/collections/actions/Combobox.tsx index 88a29b6..d520d6f 100644 --- a/components/collections/actions/Combobox.tsx +++ b/components/collections/actions/Combobox.tsx @@ -6,7 +6,7 @@ import { Fragment, useEffect, useState } from 'react' import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' import type { ActionListItem } from './actions' -import { BASE_ACTION_LIST, VENDING_ACTION_LIST } from './actions' +import { BASE_ACTION_LIST, SG721_UPDATABLE_ACTION_LIST, VENDING_ACTION_LIST } from './actions' export type MinterType = 'base' | 'vending' export type Sg721Type = 'updatable' | 'base' @@ -15,19 +15,22 @@ export interface ActionsComboboxProps { value: ActionListItem | null onChange: (item: ActionListItem) => void minterType?: MinterType + sg721Type?: Sg721Type } -export const ActionsCombobox = ({ value, onChange, minterType }: ActionsComboboxProps) => { +export const ActionsCombobox = ({ value, onChange, minterType, sg721Type }: ActionsComboboxProps) => { const [search, setSearch] = useState('') const [ACTION_LIST, SET_ACTION_LIST] = useState(VENDING_ACTION_LIST) useEffect(() => { if (minterType === 'base') { - SET_ACTION_LIST(BASE_ACTION_LIST) - } else { - SET_ACTION_LIST(VENDING_ACTION_LIST) - } - }, [minterType]) + if (sg721Type === 'updatable') SET_ACTION_LIST(BASE_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST)) + else SET_ACTION_LIST(BASE_ACTION_LIST) + } else if (minterType === 'vending') { + if (sg721Type === 'updatable') SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST)) + else SET_ACTION_LIST(VENDING_ACTION_LIST) + } else SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST)) + }, [minterType, sg721Type]) const filtered = search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] }) diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts index 98bff3f..e65561f 100644 --- a/components/collections/actions/actions.ts +++ b/components/collections/actions/actions.ts @@ -30,6 +30,9 @@ export const ACTION_TYPES = [ 'shuffle', 'airdrop', 'burn_remaining', + 'update_token_metadata', + 'batch_update_token_metadata', + 'freeze_metadata', ] as const export interface ActionListItem { @@ -184,6 +187,24 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [ }, ] +export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [ + { + 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_metadata', + name: 'Freeze Metadata', + description: `Render the metadata for the collection no longer updatable`, + }, +] + export interface DispatchExecuteProps { type: ActionType [k: string]: unknown @@ -222,6 +243,9 @@ export type DispatchExecuteArgs = { | { type: Select<'burn_remaining'> } | { type: Select<'update_collection_info'>; collectionInfo: CollectionInfo | undefined } | { type: Select<'freeze_collection_info'> } + | { type: Select<'update_token_metadata'> } + | { type: Select<'batch_update_token_metadata'> } + | { type: Select<'freeze_metadata'> } ) export const dispatchExecute = async (args: DispatchExecuteArgs) => { diff --git a/pages/collections/actions.tsx b/pages/collections/actions.tsx index 128ef7b..445a199 100644 --- a/pages/collections/actions.tsx +++ b/pages/collections/actions.tsx @@ -23,7 +23,7 @@ const CollectionActionsPage: NextPage = () => { const [action, setAction] = useState(false) const [minterType, setMinterType] = useState('vending') - const [sg721Type, setSg721Type] = useState('base') + const [sg721Type, setSg721Type] = useState('updatable') const sg721ContractState = useInputState({ id: 'sg721-contract-address', @@ -215,6 +215,7 @@ const CollectionActionsPage: NextPage = () => { minterType={minterType} sg721ContractAddress={sg721ContractState.value} sg721Messages={sg721Messages} + sg721Type={sg721Type} vendingMinterMessages={vendingMinterMessages} /> )) || ( From 4e646b9cf457d737855e9153e3e5ac11854d7981 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 10:13:26 +0300 Subject: [PATCH 03/13] Update sg721 helpers to include metadata update related functions --- contracts/sg721/contract.ts | 163 ++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index 270eb73..918cf9b 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -86,6 +86,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 + freezeMetadata: () => Promise } export interface Sg721Messages { @@ -101,6 +104,9 @@ export interface Sg721Messages { batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage updateCollectionInfo: (collectionInfo: CollectionInfo) => UpdateCollectionInfoMessage freezeCollectionInfo: () => FreezeCollectionInfoMessage + updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage + batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string) => BatchUpdateTokenMetadataMessage + freezeMetadata: () => FreezeMetadataMessage } export interface TransferNFTMessage { @@ -215,6 +221,32 @@ export interface BatchTransferMessage { 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 FreezeMetadataMessage { + sender: string + contract: string + msg: { freeze_metadata: Record } + funds: Coin[] +} + export interface UpdateCollectionInfoMessage { sender: string contract: string @@ -570,6 +602,65 @@ 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}/${i}` }, + } + 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}/${tokenNumbers[i]}` }, + } + 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, + }, + }, + 'auto', + '', + ) + return res.transactionHash + } + const freezeCollectionInfo = async (): Promise => { const res = await client.execute( txSigner, @@ -583,6 +674,19 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con return res.transactionHash } + const freezeMetadata = async (): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + freeze_metadata: {}, + }, + 'auto', + '', + ) + return res.transactionHash + } + return { contractAddress, ownerOf, @@ -609,6 +713,9 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con batchTransfer, updateCollectionInfo, freezeCollectionInfo, + updateTokenMetadata, + batchUpdateTokenMetadata, + freezeMetadata, } } @@ -804,6 +911,58 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con funds: [], } } + + 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}/${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}/${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, + }, + }, + funds: [], + } + } + + const freezeMetadata = () => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + freeze_metadata: {}, + }, + funds: [], + } + } + const updateCollectionInfo = (collectionInfo: CollectionInfo) => { return { sender: txSigner, @@ -814,6 +973,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con funds: [], } } + const freezeCollectionInfo = () => { return { sender: txSigner, @@ -838,6 +998,9 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con batchTransfer, updateCollectionInfo, freezeCollectionInfo, + updateTokenMetadata, + batchUpdateTokenMetadata, + freezeMetadata, } } From 16c859bbd8d30355820c4cadc10b242a855f66f6 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 10:24:46 +0300 Subject: [PATCH 04/13] Connect helper functions to update metadata related actions --- components/collections/actions/actions.ts | 32 ++++++++++++++++++----- contracts/sg721/contract.ts | 20 +++++++------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts index e65561f..5132da4 100644 --- a/components/collections/actions/actions.ts +++ b/components/collections/actions/actions.ts @@ -32,7 +32,7 @@ export const ACTION_TYPES = [ 'burn_remaining', 'update_token_metadata', 'batch_update_token_metadata', - 'freeze_metadata', + 'freeze_token_metadata', ] as const export interface ActionListItem { @@ -199,9 +199,9 @@ export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [ description: `Update the metadata URI for a range of tokens`, }, { - id: 'freeze_metadata', - name: 'Freeze Metadata', - description: `Render the metadata for the collection no longer updatable`, + id: 'freeze_token_metadata', + name: 'Freeze Token Metadata', + description: `Render the metadata for tokens no longer updatable`, }, ] @@ -243,9 +243,9 @@ export type DispatchExecuteArgs = { | { type: Select<'burn_remaining'> } | { type: Select<'update_collection_info'>; collectionInfo: CollectionInfo | undefined } | { type: Select<'freeze_collection_info'> } - | { type: Select<'update_token_metadata'> } - | { type: Select<'batch_update_token_metadata'> } - | { type: Select<'freeze_metadata'> } + | { 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) => { @@ -293,6 +293,15 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'freeze_collection_info': { return sg721Messages.freezeCollectionInfo() } + 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() + } case 'shuffle': { return vendingMinterMessages.shuffle(txSigner) } @@ -371,6 +380,15 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'freeze_collection_info': { return sg721Messages(sg721Contract)?.freezeCollectionInfo() } + 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 'shuffle': { return vendingMinterMessages(minterContract)?.shuffle() } diff --git a/contracts/sg721/contract.ts b/contracts/sg721/contract.ts index 918cf9b..bce6b13 100644 --- a/contracts/sg721/contract.ts +++ b/contracts/sg721/contract.ts @@ -88,7 +88,7 @@ export interface SG721Instance { batchTransfer: (recipient: string, tokenIds: string) => Promise updateTokenMetadata: (tokenId: string, tokenURI: string) => Promise batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string) => Promise - freezeMetadata: () => Promise + freezeTokenMetadata: () => Promise } export interface Sg721Messages { @@ -106,7 +106,7 @@ export interface Sg721Messages { freezeCollectionInfo: () => FreezeCollectionInfoMessage updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string) => BatchUpdateTokenMetadataMessage - freezeMetadata: () => FreezeMetadataMessage + freezeTokenMetadata: () => FreezeTokenMetadataMessage } export interface TransferNFTMessage { @@ -240,10 +240,10 @@ export interface BatchUpdateTokenMetadataMessage { funds: Coin[] } -export interface FreezeMetadataMessage { +export interface FreezeTokenMetadataMessage { sender: string contract: string - msg: { freeze_metadata: Record } + msg: { freeze_token_metadata: Record } funds: Coin[] } @@ -674,12 +674,12 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con return res.transactionHash } - const freezeMetadata = async (): Promise => { + const freezeTokenMetadata = async (): Promise => { const res = await client.execute( txSigner, contractAddress, { - freeze_metadata: {}, + freeze_token_metadata: {}, }, 'auto', '', @@ -715,7 +715,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con freezeCollectionInfo, updateTokenMetadata, batchUpdateTokenMetadata, - freezeMetadata, + freezeTokenMetadata, } } @@ -952,12 +952,12 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con } } - const freezeMetadata = () => { + const freezeTokenMetadata = () => { return { sender: txSigner, contract: contractAddress, msg: { - freeze_metadata: {}, + freeze_token_metadata: {}, }, funds: [], } @@ -1000,7 +1000,7 @@ export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Con freezeCollectionInfo, updateTokenMetadata, batchUpdateTokenMetadata, - freezeMetadata, + freezeTokenMetadata, } } From fbcd58a4d07c5cee76c120a99b7c33af18418316 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 10:52:58 +0300 Subject: [PATCH 05/13] Update input fields on Collection Actions --- components/collections/actions/Action.tsx | 32 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index b1c88bd..f25590a 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -102,7 +102,15 @@ export const CollectionActions = ({ id: 'token-uri', name: 'tokenURI', title: 'Token URI', - subtitle: 'URI for the token to be minted', + 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://', }) @@ -156,13 +164,18 @@ export const CollectionActions = ({ placeholder: '8%', }) - const showTokenUriField = type === 'mint_token_uri' + const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata']) const showWhitelistField = type === 'set_whitelist' const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_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', 'batch_mint_for']) + const showTokenIdListField = isEitherType(type, [ + 'batch_burn', + 'batch_transfer', + 'batch_mint_for', + 'batch_update_token_metadata', + ]) const showRecipientField = isEitherType(type, [ 'transfer', 'mint_to', @@ -178,6 +191,7 @@ export const CollectionActions = ({ const showExternalLinkField = type === 'update_collection_info' const showRoyaltyRelatedFields = type === 'update_collection_info' const showExplicitContentField = type === 'update_collection_info' + const showBaseUriField = type === 'batch_update_token_metadata' const payload: DispatchExecuteArgs = { whitelist: whitelistState.value, @@ -187,7 +201,9 @@ export const CollectionActions = ({ sg721Contract: sg721ContractAddress, tokenId: tokenIdState.value, tokenIds: tokenIdListState.value, - tokenUri: tokenURIState.value, + tokenUri: tokenURIState.value.trim().endsWith('/') + ? tokenURIState.value.trim().slice(0, -1) + : tokenURIState.value.trim(), batchNumber: batchNumberState.value, vendingMinterMessages, baseMinterMessages, @@ -197,6 +213,9 @@ export const CollectionActions = ({ txSigner: wallet.address, type, price: priceState.value.toString(), + baseUri: baseURIState.value.trim().endsWith('/') + ? baseURIState.value.trim().slice(0, -1) + : baseURIState.value.trim(), collectionInfo, } const resolveRecipientAddress = async () => { @@ -357,8 +376,9 @@ export const CollectionActions = ({ {showTokenUriField && } {showWhitelistField && } {showLimitField && } - {showTokenIdField && } + {showTokenIdField && } {showTokenIdListField && } + {showBaseUriField && } {showNumberOfTokensField && } {showPriceField && } {showDescriptionField && } From 4a8c199c536ca72eef863a2180d52b36b508eab4 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 12:36:34 +0300 Subject: [PATCH 06/13] Incorporate metadata updatability into the collection creation process --- components/Tooltip.tsx | 3 +- .../creation/CollectionDetails.tsx | 36 ++++++++++++++++++- env.d.ts | 3 ++ pages/collections/create.tsx | 15 ++++---- utils/constants.ts | 3 ++ 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/components/Tooltip.tsx b/components/Tooltip.tsx index fa43855..2b2e552 100644 --- a/components/Tooltip.tsx +++ b/components/Tooltip.tsx @@ -6,6 +6,7 @@ import { usePopper } from 'react-popper' export interface TooltipProps extends ComponentProps<'div'> { label: ReactNode children: ReactElement + placement?: 'top' | 'bottom' | 'left' | 'right' } export const Tooltip = ({ label, children, ...props }: TooltipProps) => { @@ -14,7 +15,7 @@ export const Tooltip = ({ label, children, ...props }: TooltipProps) => { const [show, setShow] = useState(false) const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: 'top', + placement: props.placement ? props.placement : 'top', }) return ( diff --git a/components/collections/creation/CollectionDetails.tsx b/components/collections/creation/CollectionDetails.tsx index bad2dec..8f0670f 100644 --- a/components/collections/creation/CollectionDetails.tsx +++ b/components/collections/creation/CollectionDetails.tsx @@ -8,6 +8,7 @@ import { FormControl } from 'components/FormControl' import { FormGroup } from 'components/FormGroup' import { useInputState } from 'components/forms/FormInput.hooks' import { InputDateTime } from 'components/InputDateTime' +import { Tooltip } from 'components/Tooltip' import type { ChangeEvent } from 'react' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' @@ -31,12 +32,14 @@ export interface CollectionDetailsDataProps { externalLink?: string startTradingTime?: string explicit: boolean + updatable: boolean } export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minterType }: CollectionDetailsProps) => { const [coverImage, setCoverImage] = useState(null) const [timestamp, setTimestamp] = useState() const [explicit, setExplicit] = useState(false) + const [updatable, setUpdatable] = useState(false) const nameState = useInputState({ id: 'name', @@ -76,6 +79,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte externalLink: externalLinkState.value || undefined, startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', explicit, + updatable, } onChange(data) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -91,6 +95,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte coverImage, timestamp, explicit, + updatable, ]) const selectCoverImage = (event: ChangeEvent) => { @@ -174,7 +179,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
- + Does the collection contain explicit content?
@@ -216,6 +221,35 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
+ + + ℹī¸ When enabled, the metadata for tokens can be updated after the collection is created until the + metadata is frozen by the creator. + +
+ } + placement="bottom" + > +
+ +
+
) diff --git a/env.d.ts b/env.d.ts index bb733fe..c4e8faf 100644 --- a/env.d.ts +++ b/env.d.ts @@ -15,10 +15,13 @@ declare namespace NodeJS { readonly APP_VERSION: string readonly NEXT_PUBLIC_SG721_CODE_ID: string + readonly NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID: string readonly NEXT_PUBLIC_WHITELIST_CODE_ID: string readonly NEXT_PUBLIC_VENDING_MINTER_CODE_ID: string readonly NEXT_PUBLIC_VENDING_FACTORY_ADDRESS: string + readonly NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS: string readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string + readonly NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS: string readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string readonly NEXT_PUBLIC_BASE_MINTER_CODE_ID: string readonly NEXT_PUBLIC_BADGE_HUB_CODE_ID: string diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 1116be5..d1b9fcf 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -39,11 +39,14 @@ import { upload } from 'services/upload' import { compareFileArrays } from 'utils/compareFileArrays' import { BASE_FACTORY_ADDRESS, + BASE_FACTORY_UPDATABLE_ADDRESS, BLOCK_EXPLORER_URL, NETWORK, SG721_CODE_ID, + SG721_UPDATABLE_CODE_ID, STARGAZE_URL, VENDING_FACTORY_ADDRESS, + VENDING_FACTORY_UPDATABLE_ADDRESS, WHITELIST_CODE_ID, } from 'utils/constants' import { withMetadata } from 'utils/layout' @@ -416,7 +419,7 @@ const CollectionCreationPage: NextPage = () => { whitelist, }, collection_params: { - code_id: SG721_CODE_ID, + code_id: collectionDetails?.updatable ? SG721_UPDATABLE_CODE_ID : SG721_CODE_ID, name: collectionDetails?.name, symbol: collectionDetails?.symbol, info: { @@ -437,11 +440,11 @@ const CollectionCreationPage: NextPage = () => { } const payload: VendingFactoryDispatchExecuteArgs = { - contract: VENDING_FACTORY_ADDRESS, + contract: collectionDetails?.updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, messages: vendingFactoryMessages, txSigner: wallet.address, msg, - funds: [coin('3000000000', 'ustars')], + funds: [coin(collectionDetails?.updatable ? '5000000000' : '3000000000', 'ustars')], } const data = await vendingFactoryDispatchExecute(payload) setTransactionHash(data.transactionHash) @@ -466,7 +469,7 @@ const CollectionCreationPage: NextPage = () => { create_minter: { init_msg: null, collection_params: { - code_id: SG721_CODE_ID, + code_id: collectionDetails?.updatable ? SG721_UPDATABLE_CODE_ID : SG721_CODE_ID, name: collectionDetails?.name, symbol: collectionDetails?.symbol, info: { @@ -487,11 +490,11 @@ const CollectionCreationPage: NextPage = () => { } const payload: BaseFactoryDispatchExecuteArgs = { - contract: BASE_FACTORY_ADDRESS, + contract: collectionDetails?.updatable ? BASE_FACTORY_UPDATABLE_ADDRESS : BASE_FACTORY_ADDRESS, messages: baseFactoryMessages, txSigner: wallet.address, msg, - funds: [coin('1000000000', 'ustars')], + funds: [coin(collectionDetails?.updatable ? '3000000000' : '1000000000', 'ustars')], } await baseFactoryDispatchExecute(payload) .then(async (data) => { diff --git a/utils/constants.ts b/utils/constants.ts index 9966325..f00df0b 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,8 +1,11 @@ export const SG721_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SG721_CODE_ID, 10) +export const SG721_UPDATABLE_CODE_ID = parseInt(process.env.NEXT_PUBLIC_SG721_UPDATABLE_CODE_ID, 10) export const WHITELIST_CODE_ID = parseInt(process.env.NEXT_PUBLIC_WHITELIST_CODE_ID, 10) export const VENDING_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_CODE_ID, 10) export const VENDING_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_ADDRESS +export const VENDING_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS export const BASE_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS +export const BASE_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS export const BASE_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_CODE_ID, 10) export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE_ID, 10) From 5f8dea9cc034759d417413c3a270b11a96e18d70 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 12:42:21 +0300 Subject: [PATCH 07/13] Update wallet balance checks --- pages/collections/create.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index d1b9fcf..50bea3c 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -1,4 +1,5 @@ /* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ @@ -828,11 +829,20 @@ const CollectionCreationPage: NextPage = () => { const checkwalletBalance = () => { if (!wallet.initialized) throw new Error('Wallet not connected.') if (minterType === 'vending' && whitelistDetails?.whitelistType === 'new' && whitelistDetails.memberLimit) { - const amountNeeded = Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + 3000000000 + const amountNeeded = + Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + + (collectionDetails?.updatable ? 5000000000 : 3000000000) if (amountNeeded >= Number(wallet.balance[0].amount)) throw new Error('Insufficient wallet balance to instantiate the required contracts.') } else { - const amountNeeded = minterType === 'vending' ? 3000000000 : 1000000000 + const amountNeeded = + minterType === 'vending' + ? collectionDetails?.updatable + ? 5000000000 + : 3000000000 + : collectionDetails?.updatable + ? 3000000000 + : 1000000000 if (amountNeeded >= Number(wallet.balance[0].amount)) throw new Error('Insufficient wallet balance to instantiate the required contracts.') } From d27d22367eb77f8ba07bdfd6fdb3300e5f2cabf5 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 13:08:03 +0300 Subject: [PATCH 08/13] Update factory contract helpers --- contracts/baseFactory/contract.ts | 19 +++++++++++++++---- contracts/baseFactory/messages/execute.ts | 5 +++-- contracts/vendingFactory/contract.ts | 19 +++++++++++++++---- contracts/vendingFactory/messages/execute.ts | 5 +++-- pages/collections/create.tsx | 2 ++ 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/contracts/baseFactory/contract.ts b/contracts/baseFactory/contract.ts index 20fd166..ef4e965 100644 --- a/contracts/baseFactory/contract.ts +++ b/contracts/baseFactory/contract.ts @@ -4,6 +4,8 @@ import { coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' import { BASE_FACTORY_ADDRESS } from 'utils/constants' +import { BASE_FACTORY_UPDATABLE_ADDRESS } from '../../utils/constants' + export interface CreateBaseMinterResponse { readonly baseMinterAddress: string readonly sg721Address: string @@ -21,11 +23,12 @@ export interface BaseFactoryInstance { senderAddress: string, msg: Record, funds: Coin[], + updatable?: boolean, ) => Promise } export interface BaseFactoryMessages { - createBaseMinter: (msg: Record) => CreateBaseMinterMessage + createBaseMinter: (msg: Record, updatable?: boolean) => CreateBaseMinterMessage } export interface CreateBaseMinterMessage { @@ -56,8 +59,16 @@ export const baseFactory = (client: SigningCosmWasmClient, txSigner: string): Ba senderAddress: string, msg: Record, funds: Coin[], + updatable?: boolean, ): Promise => { - const result = await client.execute(senderAddress, BASE_FACTORY_ADDRESS, msg, 'auto', '', funds) + const result = await client.execute( + senderAddress, + updatable ? BASE_FACTORY_UPDATABLE_ADDRESS : BASE_FACTORY_ADDRESS, + msg, + 'auto', + '', + funds, + ) return { baseMinterAddress: result.logs[0].events[5].attributes[0].value, @@ -75,12 +86,12 @@ export const baseFactory = (client: SigningCosmWasmClient, txSigner: string): Ba } const messages = (contractAddress: string) => { - const createBaseMinter = (msg: Record): CreateBaseMinterMessage => { + const createBaseMinter = (msg: Record, updatable?: boolean): CreateBaseMinterMessage => { return { sender: txSigner, contract: contractAddress, msg, - funds: [coin('1000000000', 'ustars')], + funds: [coin(updatable ? '3000000000' : '1000000000', 'ustars')], } } diff --git a/contracts/baseFactory/messages/execute.ts b/contracts/baseFactory/messages/execute.ts index 374aba9..1bb8612 100644 --- a/contracts/baseFactory/messages/execute.ts +++ b/contracts/baseFactory/messages/execute.ts @@ -10,6 +10,7 @@ export interface DispatchExecuteArgs { txSigner: string msg: Record funds: Coin[] + updatable?: boolean } export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -17,12 +18,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { if (!messages) { throw new Error('cannot dispatch execute, messages is not defined') } - return messages.createBaseMinter(txSigner, args.msg, args.funds) + return messages.createBaseMinter(txSigner, args.msg, args.funds, args.updatable) } export const previewExecutePayload = (args: DispatchExecuteArgs) => { // eslint-disable-next-line react-hooks/rules-of-hooks const { messages } = useBaseFactoryContract() const { contract } = args - return messages(contract)?.createBaseMinter(args.msg) + return messages(contract)?.createBaseMinter(args.msg, args.updatable) } diff --git a/contracts/vendingFactory/contract.ts b/contracts/vendingFactory/contract.ts index d8408a4..120907f 100644 --- a/contracts/vendingFactory/contract.ts +++ b/contracts/vendingFactory/contract.ts @@ -4,6 +4,8 @@ import { coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' import { VENDING_FACTORY_ADDRESS } from 'utils/constants' +import { VENDING_FACTORY_UPDATABLE_ADDRESS } from '../../utils/constants' + export interface CreateVendingMinterResponse { readonly vendingMinterAddress: string readonly sg721Address: string @@ -21,11 +23,12 @@ export interface VendingFactoryInstance { senderAddress: string, msg: Record, funds: Coin[], + updatable?: boolean, ) => Promise } export interface VendingFactoryMessages { - createVendingMinter: (msg: Record) => CreateVendingMinterMessage + createVendingMinter: (msg: Record, updatable?: boolean) => CreateVendingMinterMessage } export interface CreateVendingMinterMessage { @@ -50,8 +53,16 @@ export const vendingFactory = (client: SigningCosmWasmClient, txSigner: string): senderAddress: string, msg: Record, funds: Coin[], + updatable?: boolean, ): Promise => { - const result = await client.execute(senderAddress, VENDING_FACTORY_ADDRESS, msg, 'auto', '', funds) + const result = await client.execute( + senderAddress, + updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, + msg, + 'auto', + '', + funds, + ) return { vendingMinterAddress: result.logs[0].events[5].attributes[0].value, @@ -68,12 +79,12 @@ export const vendingFactory = (client: SigningCosmWasmClient, txSigner: string): } const messages = (contractAddress: string) => { - const createVendingMinter = (msg: Record): CreateVendingMinterMessage => { + const createVendingMinter = (msg: Record, updatable?: boolean): CreateVendingMinterMessage => { return { sender: txSigner, contract: contractAddress, msg, - funds: [coin('3000000000', 'ustars')], + funds: [coin(updatable ? '5000000000' : '3000000000', 'ustars')], } } diff --git a/contracts/vendingFactory/messages/execute.ts b/contracts/vendingFactory/messages/execute.ts index 7939a7f..05cbb8c 100644 --- a/contracts/vendingFactory/messages/execute.ts +++ b/contracts/vendingFactory/messages/execute.ts @@ -10,6 +10,7 @@ export interface DispatchExecuteArgs { txSigner: string msg: Record funds: Coin[] + updatable?: boolean } export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -17,12 +18,12 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { if (!messages) { throw new Error('cannot dispatch execute, messages is not defined') } - return messages.createVendingMinter(txSigner, args.msg, args.funds) + return messages.createVendingMinter(txSigner, args.msg, args.funds, args.updatable) } export const previewExecutePayload = (args: DispatchExecuteArgs) => { // eslint-disable-next-line react-hooks/rules-of-hooks const { messages } = useVendingFactoryContract() const { contract } = args - return messages(contract)?.createVendingMinter(args.msg) + return messages(contract)?.createVendingMinter(args.msg, args.updatable) } diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 50bea3c..d200120 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -446,6 +446,7 @@ const CollectionCreationPage: NextPage = () => { txSigner: wallet.address, msg, funds: [coin(collectionDetails?.updatable ? '5000000000' : '3000000000', 'ustars')], + updatable: collectionDetails?.updatable, } const data = await vendingFactoryDispatchExecute(payload) setTransactionHash(data.transactionHash) @@ -496,6 +497,7 @@ const CollectionCreationPage: NextPage = () => { txSigner: wallet.address, msg, funds: [coin(collectionDetails?.updatable ? '3000000000' : '1000000000', 'ustars')], + updatable: collectionDetails?.updatable, } await baseFactoryDispatchExecute(payload) .then(async (data) => { From 14983047cf13f82d6c53d14728d4b5268b7ef59c Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 26 Feb 2023 13:45:18 +0300 Subject: [PATCH 09/13] Update the list of actions for SG721 Dashboard > Execute --- components/ConfirmationModal.tsx | 2 +- contracts/sg721/messages/execute.ts | 26 ++++++++++++++++++++++++++ pages/contracts/sg721/execute.tsx | 14 +++++++++++--- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/components/ConfirmationModal.tsx b/components/ConfirmationModal.tsx index edaf792..8fc41fa 100644 --- a/components/ConfirmationModal.tsx +++ b/components/ConfirmationModal.tsx @@ -45,7 +45,7 @@ export const ConfirmationModal = (props: ConfirmationModalProps) => {