import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { toBase64, toUtf8 } from '@cosmjs/encoding' import type { Coin, logs } from '@cosmjs/stargate' import { coin } from '@cosmjs/stargate' import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' import type { AirdropAllocation } from 'utils/isValidAccountsFile' import type { RoyaltyInfo } from '../vendingMinter/contract' export interface InstantiateResponse { readonly contractAddress: string 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 CollectionInfo { description?: string image?: string external_link?: string explicit_content?: boolean royalty_info?: RoyaltyInfo | undefined } export interface SG721Instance { readonly contractAddress: string // queries ownerOf: (tokenId: string, includeExpired?: boolean | null) => Promise approval: (tokenId: string, spender: string, includeExpired?: boolean | null) => Promise approvals: (tokenId: string, includeExpired?: boolean | null) => Promise allOperators: ( owner: string, includeExpired?: boolean | null, startAfter?: string | null, limit?: number | null, ) => Promise numTokens: () => Promise contractInfo: () => Promise nftInfo: (tokenId: string) => Promise allNftInfo: (tokenId: string, includeExpired?: boolean | null) => Promise tokens: (owner: string, startAfter?: string | null, limit?: number | null) => Promise allTokens: (startAfter?: string | null, limit?: number | null) => Promise minter: () => Promise collectionInfo: () => Promise //Execute transferNft: (recipient: string, tokenId: string) => Promise /// Send is a base message to transfer a token to a contract and trigger an action /// on the receiving contract. sendNft: ( contract: string, tokenId: string, msg: Record, //Binary ) => Promise /// Allows operator to transfer / send the token from the owner's account. /// If expiration is set, then this allowance has a time/height limit approve: (spender: string, tokenId: string, expires?: Expiration) => Promise /// Remove previously granted Approval revoke: (spender: string, tokenId: string) => Promise /// Allows operator to transfer / send any token from the owner's account. /// If expiration is set, then this allowance has a time/height limit approveAll: (operator: string, expires?: Expiration) => Promise /// Remove previously granted ApproveAll permission 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 updateCollectionInfo: (collectionInfo: CollectionInfo) => Promise freezeCollectionInfo: () => Promise /// Burn an NFT the sender has access to burn: (tokenId: string) => Promise batchBurn: (tokenIds: string) => Promise batchTransfer: (recipient: string, tokenIds: string) => Promise batchTransferMultiAddress: (senderAddress: string, tokenRecipients: AirdropAllocation[]) => Promise updateTokenMetadata: (tokenId: string, tokenURI: string) => Promise batchUpdateTokenMetadata: (tokenIds: string, tokenURI: string, jsonExtensions: boolean) => Promise freezeTokenMetadata: () => Promise enableUpdatable: () => Promise } export interface Sg721Messages { transferNft: (recipient: string, tokenId: string) => TransferNFTMessage sendNft: (contract: string, tokenId: string, msg: Record) => SendNFTMessage approve: (recipient: string, tokenId: string, expires?: Expiration) => ApproveMessage revoke: (recipient: string, tokenId: string) => RevokeMessage approveAll: (operator: string, expires?: Expiration) => ApproveAllMessage revokeAll: (operator: string) => RevokeAllMessage mint: (tokenId: string, owner: string, tokenURI?: string) => MintMessage burn: (tokenId: string) => BurnMessage batchBurn: (tokenIds: string) => BatchBurnMessage batchTransfer: (recipient: string, tokenIds: string) => BatchTransferMessage batchTransferMultiAddress: (tokenRecipients: AirdropAllocation[]) => BatchTransferMultiAddressMessage updateCollectionInfo: (collectionInfo: CollectionInfo) => UpdateCollectionInfoMessage freezeCollectionInfo: () => FreezeCollectionInfoMessage updateTokenMetadata: (tokenId: string, tokenURI: string) => UpdateTokenMetadataMessage batchUpdateTokenMetadata: ( tokenIds: string, tokenURI: string, jsonExtensions: boolean, ) => BatchUpdateTokenMetadataMessage freezeTokenMetadata: () => FreezeTokenMetadataMessage enableUpdatable: () => EnableUpdatableMessage } export interface TransferNFTMessage { sender: string contract: string msg: { transfer_nft: { recipient: string token_id: string } } funds: Coin[] } export interface SendNFTMessage { sender: string contract: string msg: { send_nft: { contract: string token_id: string msg: Record } } funds: Coin[] } export interface ApproveMessage { sender: string contract: string msg: { approve: { spender: string token_id: string expires?: Expiration } } funds: Coin[] } export interface RevokeMessage { sender: string contract: string msg: { revoke: { spender: string token_id: string } } funds: Coin[] } export interface ApproveAllMessage { sender: string contract: string msg: { approve_all: { operator: string expires?: Expiration } } funds: Coin[] } export interface RevokeAllMessage { sender: string contract: string msg: { revoke_all: { operator: string } } funds: Coin[] } export interface MintMessage { sender: string contract: string msg: { mint: { token_id: string owner: string token_uri?: string } } funds: Coin[] } export interface BurnMessage { sender: string contract: string msg: { burn: { token_id: string } } funds: Coin[] } export interface BatchBurnMessage { sender: string contract: string msg: Record[] funds: Coin[] } export interface BatchTransferMessage { sender: string contract: string msg: Record[] funds: Coin[] } export interface BatchTransferMultiAddressMessage { sender: string contract: string msg: Record[] 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 EnableUpdatableMessage { sender: string contract: string msg: { enable_updatable: Record } funds: Coin[] } export interface UpdateCollectionInfoMessage { sender: string contract: string msg: { update_collection_info: { collection_info: CollectionInfo } } funds: Coin[] } export interface FreezeCollectionInfoMessage { sender: string contract: string msg: { freeze_collection_info: Record } funds: Coin[] } export interface SG721Contract { instantiate: ( senderAddress: string, codeId: number, initMsg: Record, label: string, admin?: string, ) => Promise migrate: ( senderAddress: string, contractAddress: string, codeId: number, migrateMsg: Record, ) => Promise use: (contractAddress: string) => SG721Instance messages: (contractAddress: string) => Sg721Messages } export const SG721 = (client: SigningCosmWasmClient, txSigner: string): SG721Contract => { const use = (contractAddress: string): SG721Instance => { const jsonToBinary = (json: Record): string => { return toBase64(toUtf8(JSON.stringify(json))) } const ownerOf = async (tokenId: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { owner_of: { token_id: tokenId, include_expired: includeExpired }, }) return res } const approval = async (tokenId: string, spender: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { approval: { token_id: tokenId, spender, include_expired: includeExpired }, }) return res } const approvals = async (tokenId: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { approvals: { token_id: tokenId, include_expired: includeExpired }, }) return res } const allOperators = async ( owner: string, includeExpired?: boolean | null, startAfter?: string | null, limit?: number | null, ): Promise => { const res = await client.queryContractSmart(contractAddress, { all_operators: { owner, include_expired: includeExpired, start_after: startAfter, limit }, }) return res } const numTokens = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { num_tokens: {}, }) return res } const contractInfo = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { contract_info: {}, }) return res } const nftInfo = async (tokenId: string): Promise => { const res = await client.queryContractSmart(contractAddress, { nft_info: { token_id: tokenId }, }) return res } const allNftInfo = async (tokenId: string, includeExpired?: boolean | null): Promise => { const res = await client.queryContractSmart(contractAddress, { all_nft_info: { token_id: tokenId, include_expired: includeExpired }, }) return res } const tokens = async (owner: string, startAfter?: string | null, limit?: number | null): Promise => { const res = await client.queryContractSmart(contractAddress, { tokens: { owner, start_after: startAfter, limit }, }) return res } const allTokens = async (startAfter?: string | null, limit?: number | null): Promise => { const res = await client.queryContractSmart(contractAddress, { all_tokens: { start_after: startAfter, limit }, }) return res } const minter = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { minter: {}, }) return res } const collectionInfo = async (): Promise => { const res = await client.queryContractSmart(contractAddress, { collection_info: {}, }) return res } //Execute const transferNft = async (recipient: string, tokenId: string): Promise => { const res = await client.execute( txSigner, contractAddress, { transfer_nft: { recipient, token_id: tokenId }, }, 'auto', '', ) return res.transactionHash } const sendNft = async ( contract: string, tokenId: string, msg: Record, //Binary ): Promise => { const res = await client.execute( txSigner, contractAddress, { send_nft: { contract, token_id: tokenId, msg: jsonToBinary(msg) }, }, 'auto', '', ) return res.transactionHash } const approve = async (spender: string, tokenId: string, expires?: Expiration): Promise => { const res = await client.execute( txSigner, contractAddress, { approve: { spender, token_id: tokenId, expires }, }, 'auto', '', ) return res.transactionHash } const revoke = async (spender: string, tokenId: string): Promise => { const res = await client.execute( txSigner, contractAddress, { revoke: { spender, token_id: tokenId }, }, 'auto', '', ) return res.transactionHash } const approveAll = async (operator: string, expires?: Expiration): Promise => { const res = await client.execute( txSigner, contractAddress, { approve_all: { operator, expires }, }, 'auto', '', ) return res.transactionHash } const revokeAll = async (operator: string): Promise => { const res = await client.execute( txSigner, contractAddress, { revoke_all: { operator }, }, 'auto', '', ) return res.transactionHash } const mint = async (tokenId: string, owner: string, tokenURI?: string): Promise => { const res = await client.execute( txSigner, contractAddress, { mint: { token_id: tokenId, owner, token_uri: tokenURI, }, }, 'auto', '', ) return res.transactionHash } const burn = async (tokenId: string): Promise => { const res = await client.execute( txSigner, contractAddress, { burn: { token_id: tokenId }, }, 'auto', '', ) 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 } const batchTransfer = async (recipient: string, 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 = { transfer_nft: { recipient, 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 = { transfer_nft: { recipient, 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 transfer') return res.transactionHash } const batchTransferMultiAddress = async ( senderAddress: string, recipients: AirdropAllocation[], ): Promise => { const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] for (let i = 0; i < recipients.length; i++) { const msg = { transfer_nft: { recipient: recipients[i].address, token_id: recipients[i].tokenId as string }, } const executeContractMsg: MsgExecuteContractEncodeObject = { typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', value: MsgExecuteContract.fromPartial({ sender: senderAddress, contract: contractAddress, msg: toUtf8(JSON.stringify(msg)), }), } executeContractMsgs.push(executeContractMsg) } const res = await client.signAndBroadcast( senderAddress, executeContractMsgs, 'auto', 'batch transfer to multiple recipients', ) return res.transactionHash } // eslint-disable-next-line @typescript-eslint/no-shadow const updateCollectionInfo = async (collectionInfo: CollectionInfo): Promise => { const res = await client.execute( txSigner, contractAddress, { update_collection_info: { collection_info: collectionInfo }, }, 'auto', '', ) return res.transactionHash } const batchUpdateTokenMetadata = async ( tokenIds: string, baseURI: string, jsonExtensions: boolean, ): 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}${jsonExtensions ? '.json' : ''}`, }, } 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]}${jsonExtensions ? '.json' : ''}`, }, } 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, contractAddress, { freeze_collection_info: {}, }, 'auto', '', ) return res.transactionHash } const freezeTokenMetadata = async (): Promise => { const res = await client.execute( txSigner, contractAddress, { freeze_token_metadata: {}, }, 'auto', '', ) return res.transactionHash } const enableUpdatable = async (): Promise => { const res = await client.execute( txSigner, contractAddress, { enable_updatable: {}, }, 'auto', '', [coin('2000000000', 'ustars')], ) return res.transactionHash } return { contractAddress, ownerOf, approval, approvals, allOperators, numTokens, contractInfo, nftInfo, allNftInfo, tokens, allTokens, minter, collectionInfo, transferNft, sendNft, approve, revoke, approveAll, revokeAll, mint, burn, batchBurn, batchTransfer, batchTransferMultiAddress, updateCollectionInfo, freezeCollectionInfo, updateTokenMetadata, batchUpdateTokenMetadata, freezeTokenMetadata, enableUpdatable, } } const instantiate = async ( senderAddress: string, codeId: number, initMsg: Record, label: string, admin?: string, ): Promise => { const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', { funds: [coin('3000000000', 'ustars')], memo: '', admin, }) return { contractAddress: result.contractAddress, transactionHash: result.transactionHash, } } 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 messages = (contractAddress: string) => { const transferNft = (recipient: string, tokenId: string) => { return { sender: txSigner, contract: contractAddress, msg: { transfer_nft: { recipient, token_id: tokenId, }, }, funds: [], } } const sendNft = (contract: string, tokenId: string, msg: Record) => { return { sender: txSigner, contract: contractAddress, msg: { send_nft: { contract, token_id: tokenId, msg, }, }, funds: [], } } const approve = (spender: string, tokenId: string, expires?: Expiration) => { return { sender: txSigner, contract: contractAddress, msg: { approve: { spender, token_id: tokenId, expires, }, }, funds: [], } } const revoke = (spender: string, tokenId: string) => { return { sender: txSigner, contract: contractAddress, msg: { revoke: { spender, token_id: tokenId, }, }, funds: [], } } const approveAll = (operator: string, expires?: Expiration) => { return { sender: txSigner, contract: contractAddress, msg: { approve_all: { operator, expires, }, }, funds: [], } } const revokeAll = (operator: string) => { return { sender: txSigner, contract: contractAddress, msg: { revoke_all: { operator, }, }, funds: [], } } const mint = (tokenId: string, owner: string, tokenURI?: string) => { return { sender: txSigner, contract: contractAddress, msg: { mint: { token_id: tokenId, owner, token_uri: tokenURI, }, }, funds: [], } } const burn = (tokenId: string) => { return { sender: txSigner, contract: contractAddress, msg: { burn: { token_id: tokenId, }, }, funds: [], } } const batchBurn = (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: contractAddress, msg, funds: [], } } const batchTransfer = (recipient: string, tokenIds: string): BatchTransferMessage => { const msg: Record[] = [] if (tokenIds.includes(':')) { const [start, end] = tokenIds.split(':').map(Number) for (let i = start; i <= end; i++) { msg.push({ trasnfer_nft: { recipient, token_id: i.toString() }, }) } } else { const tokenNumbers = tokenIds.split(',').map(Number) for (let i = 0; i < tokenNumbers.length; i++) { msg.push({ trasnfer_nft: { recipient, token_id: tokenNumbers[i].toString() } }) } } return { sender: txSigner, contract: contractAddress, msg, funds: [], } } const batchTransferMultiAddress = (recipients: AirdropAllocation[]): BatchTransferMultiAddressMessage => { const msg: Record[] = [] for (let i = 0; i < recipients.length; i++) { msg.push({ transfer_nft: { recipient: recipients[i].address, token_id: recipients[i].tokenId } }) } return { sender: txSigner, contract: contractAddress, msg, funds: [], } } const batchUpdateTokenMetadata = ( tokenIds: string, baseURI: string, jsonExtensions: boolean, ): 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}${jsonExtensions ? '.json' : ''}`, }, }) } } 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]}${jsonExtensions ? '.json' : ''}`, }, }) } } 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 freezeTokenMetadata = () => { return { sender: txSigner, contract: contractAddress, msg: { freeze_token_metadata: {}, }, funds: [], } } const enableUpdatable = () => { return { sender: txSigner, contract: contractAddress, msg: { enable_updatable: {}, }, funds: [coin('2000000000', 'ustars')], } } const updateCollectionInfo = (collectionInfo: CollectionInfo) => { return { sender: txSigner, contract: contractAddress, msg: { update_collection_info: { collection_info: collectionInfo }, }, funds: [], } } const freezeCollectionInfo = () => { return { sender: txSigner, contract: contractAddress, msg: { freeze_collection_info: {}, }, funds: [], } } return { transferNft, sendNft, approve, revoke, approveAll, revokeAll, mint, burn, batchBurn, batchTransfer, batchTransferMultiAddress, updateCollectionInfo, freezeCollectionInfo, updateTokenMetadata, batchUpdateTokenMetadata, freezeTokenMetadata, enableUpdatable, } } return { use, instantiate, migrate, messages } }