From e0cda70e7deb184f42a958bccb7f2fb358d201e4 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Mon, 10 Jun 2024 18:43:21 -0400 Subject: [PATCH] add skip and token queries --- packages/state/contracts/Cw20Base.ts | 600 ++++++++++++------ packages/state/query/queries/account.ts | 36 +- packages/state/query/queries/chain.ts | 67 ++ .../state/query/queries/contracts/Cw20Base.ts | 342 ++++++++++ .../state/query/queries/contracts/index.ts | 1 + packages/state/query/queries/index.ts | 2 + packages/state/query/queries/skip.ts | 82 +++ packages/state/query/queries/token.ts | 314 +++++++++ packages/types/contracts/Cw20Base.ts | 188 +++++- packages/utils/chain.ts | 11 +- 10 files changed, 1424 insertions(+), 219 deletions(-) create mode 100644 packages/state/query/queries/contracts/Cw20Base.ts create mode 100644 packages/state/query/queries/skip.ts create mode 100644 packages/state/query/queries/token.ts diff --git a/packages/state/contracts/Cw20Base.ts b/packages/state/contracts/Cw20Base.ts index c2b86e9ce..37f95b7e0 100644 --- a/packages/state/contracts/Cw20Base.ts +++ b/packages/state/contracts/Cw20Base.ts @@ -1,19 +1,30 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@1.10.0. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { Coin, StdFee } from '@cosmjs/amino' import { CosmWasmClient, ExecuteResult, SigningCosmWasmClient, } from '@cosmjs/cosmwasm-stargate' -import { Binary, Expiration, Uint128 } from '@dao-dao/types/contracts/common' import { AllAccountsResponse, AllAllowancesResponse, + AllSpenderAllowancesResponse, AllowanceResponse, BalanceResponse, + Binary, DownloadLogoResponse, + Expiration, + Logo, MarketingInfoResponse, MinterResponse, TokenInfoResponse, + Uint128, } from '@dao-dao/types/contracts/Cw20Base' import { CHAIN_GAS_MULTIPLIER } from '@dao-dao/utils' @@ -21,6 +32,7 @@ export interface Cw20BaseReadOnlyInterface { contractAddress: string balance: ({ address }: { address: string }) => Promise tokenInfo: () => Promise + minter: () => Promise allowance: ({ owner, spender, @@ -28,9 +40,6 @@ export interface Cw20BaseReadOnlyInterface { owner: string spender: string }) => Promise - minter: () => Promise - marketingInfo: () => Promise - downloadLogo: () => Promise allAllowances: ({ limit, owner, @@ -40,6 +49,15 @@ export interface Cw20BaseReadOnlyInterface { owner: string startAfter?: string }) => Promise + allSpenderAllowances: ({ + limit, + spender, + startAfter, + }: { + limit?: number + spender: string + startAfter?: string + }) => Promise allAccounts: ({ limit, startAfter, @@ -47,24 +65,25 @@ export interface Cw20BaseReadOnlyInterface { limit?: number startAfter?: string }) => Promise + marketingInfo: () => Promise + downloadLogo: () => Promise } export class Cw20BaseQueryClient implements Cw20BaseReadOnlyInterface { client: CosmWasmClient contractAddress: string - constructor(client: CosmWasmClient, contractAddress: string) { this.client = client this.contractAddress = contractAddress this.balance = this.balance.bind(this) this.tokenInfo = this.tokenInfo.bind(this) - this.allowance = this.allowance.bind(this) this.minter = this.minter.bind(this) - this.marketingInfo = this.marketingInfo.bind(this) - this.downloadLogo = this.downloadLogo.bind(this) + this.allowance = this.allowance.bind(this) this.allAllowances = this.allAllowances.bind(this) + this.allSpenderAllowances = this.allSpenderAllowances.bind(this) this.allAccounts = this.allAccounts.bind(this) + this.marketingInfo = this.marketingInfo.bind(this) + this.downloadLogo = this.downloadLogo.bind(this) } - balance = async ({ address, }: { @@ -81,6 +100,11 @@ export class Cw20BaseQueryClient implements Cw20BaseReadOnlyInterface { token_info: {}, }) } + minter = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + minter: {}, + }) + } allowance = async ({ owner, spender, @@ -95,21 +119,6 @@ export class Cw20BaseQueryClient implements Cw20BaseReadOnlyInterface { }, }) } - minter = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - minter: {}, - }) - } - marketingInfo = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - marketing_info: {}, - }) - } - downloadLogo = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - download_logo: {}, - }) - } allAllowances = async ({ limit, owner, @@ -127,6 +136,23 @@ export class Cw20BaseQueryClient implements Cw20BaseReadOnlyInterface { }, }) } + allSpenderAllowances = async ({ + limit, + spender, + startAfter, + }: { + limit?: number + spender: string + startAfter?: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + all_spender_allowances: { + limit, + spender, + start_after: startAfter, + }, + }) + } allAccounts = async ({ limit, startAfter, @@ -141,89 +167,168 @@ export class Cw20BaseQueryClient implements Cw20BaseReadOnlyInterface { }, }) } + marketingInfo = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + marketing_info: {}, + }) + } + downloadLogo = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + download_logo: {}, + }) + } } export interface Cw20BaseInterface extends Cw20BaseReadOnlyInterface { contractAddress: string sender: string - transfer: ({ - amount, - recipient, - }: { - amount: Uint128 - recipient: string - }) => Promise - burn: ({ amount }: { amount: Uint128 }) => Promise - send: ({ - amount, - contract, - msg, - }: { - amount: Uint128 - contract: string - msg: Binary - }) => Promise - increaseAllowance: ({ - amount, - expires, - spender, - }: { - amount: Uint128 - expires?: Expiration - spender: string - }) => Promise - decreaseAllowance: ({ - amount, - expires, - spender, - }: { - amount: Uint128 - expires?: Expiration - spender: string - }) => Promise - transferFrom: ({ - amount, - owner, - recipient, - }: { - amount: Uint128 - owner: string - recipient: string - }) => Promise - sendFrom: ({ - amount, - contract, - msg, - owner, - }: { - amount: Uint128 - contract: string - msg: Binary - owner: string - }) => Promise - burnFrom: ({ - amount, - owner, - }: { - amount: Uint128 - owner: string - }) => Promise - mint: ({ - amount, - recipient, - }: { - amount: Uint128 - recipient: string - }) => Promise - updateMarketing: ({ - description, - marketing, - project, - }: { - description?: string - marketing?: string - project?: string - }) => Promise - uploadLogo: () => Promise + transfer: ( + { + amount, + recipient, + }: { + amount: Uint128 + recipient: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + burn: ( + { + amount, + }: { + amount: Uint128 + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + send: ( + { + amount, + contract, + msg, + }: { + amount: Uint128 + contract: string + msg: Binary + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + increaseAllowance: ( + { + amount, + expires, + spender, + }: { + amount: Uint128 + expires?: Expiration + spender: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + decreaseAllowance: ( + { + amount, + expires, + spender, + }: { + amount: Uint128 + expires?: Expiration + spender: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + transferFrom: ( + { + amount, + owner, + recipient, + }: { + amount: Uint128 + owner: string + recipient: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + sendFrom: ( + { + amount, + contract, + msg, + owner, + }: { + amount: Uint128 + contract: string + msg: Binary + owner: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + burnFrom: ( + { + amount, + owner, + }: { + amount: Uint128 + owner: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + mint: ( + { + amount, + recipient, + }: { + amount: Uint128 + recipient: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + updateMinter: ( + { + newMinter, + }: { + newMinter?: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + updateMarketing: ( + { + description, + marketing, + project, + }: { + description?: string + marketing?: string + project?: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise + uploadLogo: ( + logo: Logo, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[] + ) => Promise } export class Cw20BaseClient extends Cw20BaseQueryClient @@ -232,7 +337,6 @@ export class Cw20BaseClient client: SigningCosmWasmClient sender: string contractAddress: string - constructor( client: SigningCosmWasmClient, sender: string, @@ -251,17 +355,22 @@ export class Cw20BaseClient this.sendFrom = this.sendFrom.bind(this) this.burnFrom = this.burnFrom.bind(this) this.mint = this.mint.bind(this) + this.updateMinter = this.updateMinter.bind(this) this.updateMarketing = this.updateMarketing.bind(this) this.uploadLogo = this.uploadLogo.bind(this) } - - transfer = async ({ - amount, - recipient, - }: { - amount: Uint128 - recipient: string - }): Promise => { + transfer = async ( + { + amount, + recipient, + }: { + amount: Uint128 + recipient: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -271,10 +380,21 @@ export class Cw20BaseClient recipient, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - burn = async ({ amount }: { amount: Uint128 }): Promise => { + burn = async ( + { + amount, + }: { + amount: Uint128 + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -283,18 +403,25 @@ export class Cw20BaseClient amount, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - send = async ({ - amount, - contract, - msg, - }: { - amount: Uint128 - contract: string - msg: Binary - }): Promise => { + send = async ( + { + amount, + contract, + msg, + }: { + amount: Uint128 + contract: string + msg: Binary + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -305,18 +432,25 @@ export class Cw20BaseClient msg, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - increaseAllowance = async ({ - amount, - expires, - spender, - }: { - amount: Uint128 - expires?: Expiration - spender: string - }): Promise => { + increaseAllowance = async ( + { + amount, + expires, + spender, + }: { + amount: Uint128 + expires?: Expiration + spender: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -327,18 +461,25 @@ export class Cw20BaseClient spender, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - decreaseAllowance = async ({ - amount, - expires, - spender, - }: { - amount: Uint128 - expires?: Expiration - spender: string - }): Promise => { + decreaseAllowance = async ( + { + amount, + expires, + spender, + }: { + amount: Uint128 + expires?: Expiration + spender: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -349,18 +490,25 @@ export class Cw20BaseClient spender, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - transferFrom = async ({ - amount, - owner, - recipient, - }: { - amount: Uint128 - owner: string - recipient: string - }): Promise => { + transferFrom = async ( + { + amount, + owner, + recipient, + }: { + amount: Uint128 + owner: string + recipient: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -371,20 +519,27 @@ export class Cw20BaseClient recipient, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - sendFrom = async ({ - amount, - contract, - msg, - owner, - }: { - amount: Uint128 - contract: string - msg: Binary - owner: string - }): Promise => { + sendFrom = async ( + { + amount, + contract, + msg, + owner, + }: { + amount: Uint128 + contract: string + msg: Binary + owner: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -396,16 +551,23 @@ export class Cw20BaseClient owner, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - burnFrom = async ({ - amount, - owner, - }: { - amount: Uint128 - owner: string - }): Promise => { + burnFrom = async ( + { + amount, + owner, + }: { + amount: Uint128 + owner: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -415,16 +577,23 @@ export class Cw20BaseClient owner, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - mint = async ({ - amount, - recipient, - }: { - amount: Uint128 - recipient: string - }): Promise => { + mint = async ( + { + amount, + recipient, + }: { + amount: Uint128 + recipient: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -434,18 +603,48 @@ export class Cw20BaseClient recipient, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - updateMarketing = async ({ - description, - marketing, - project, - }: { - description?: string - marketing?: string - project?: string - }): Promise => { + updateMinter = async ( + { + newMinter, + }: { + newMinter?: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_minter: { + new_minter: newMinter, + }, + }, + fee, + memo, + _funds + ) + } + updateMarketing = async ( + { + description, + marketing, + project, + }: { + description?: string + marketing?: string + project?: string + }, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, @@ -456,17 +655,26 @@ export class Cw20BaseClient project, }, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } - uploadLogo = async (): Promise => { + uploadLogo = async ( + logo: Logo, + fee: number | StdFee | 'auto' = CHAIN_GAS_MULTIPLIER, + memo?: string, + _funds?: Coin[] + ): Promise => { return await this.client.execute( this.sender, this.contractAddress, { - upload_logo: {}, + upload_logo: logo, }, - CHAIN_GAS_MULTIPLIER + fee, + memo, + _funds ) } } diff --git a/packages/state/query/queries/account.ts b/packages/state/query/queries/account.ts index 3bc0bafd1..d43ba29ec 100644 --- a/packages/state/query/queries/account.ts +++ b/packages/state/query/queries/account.ts @@ -7,7 +7,9 @@ import { CryptographicMultisigAccount, Cw3MultisigAccount, MultisigAccount, + PolytoneProxies, } from '@dao-dao/types' +import { ListItemsResponse } from '@dao-dao/types/contracts/DaoCore.v2' import { Threshold } from '@dao-dao/types/contracts/DaoProposalSingle.common' import { BaseAccount } from '@dao-dao/types/protobuf/codegen/cosmos/auth/v1beta1/auth' import { LegacyAminoPubKey } from '@dao-dao/types/protobuf/codegen/cosmos/crypto/multisig/keys' @@ -69,14 +71,22 @@ export const fetchAccountList = async ( ), ]) - // If this is a DAO, get its polytone proxies and registered ICAs (which is a - // chain the DAO has indicated it has an ICA on by storing an item in its KV). - const [polytoneProxies, registeredIcas] = isDao - ? await Promise.all([ - queryClient.fetchQuery( + const mainAccount: Account = { + chainId, + address, + type: isPolytoneProxy ? AccountType.Polytone : AccountType.Native, + } + + const [polytoneProxies, registeredIcas] = await Promise.all([ + mainAccount.type !== AccountType.Polytone + ? queryClient.fetchQuery( polytoneQueries.proxies(queryClient, { chainId, address }) - ), - queryClient.fetchQuery( + ) + : ({} as PolytoneProxies), + // If this is a DAO, get its registered ICAs (which is a chain the DAO has + // indicated it has an ICA on by storing an item in its KV). + isDao + ? queryClient.fetchQuery( daoDaoCoreQueries.listAllItems(queryClient, { chainId, contractAddress: address, @@ -84,15 +94,9 @@ export const fetchAccountList = async ( prefix: ICA_CHAINS_TX_PREFIX, }, }) - ), - ]) - : [] - - const mainAccount: Account = { - chainId, - address, - type: isPolytoneProxy ? AccountType.Polytone : AccountType.Native, - } + ) + : ([] as ListItemsResponse), + ]) const allAccounts: Account[] = [ // Main account. diff --git a/packages/state/query/queries/chain.ts b/packages/state/query/queries/chain.ts index 9394bec58..eb5618e92 100644 --- a/packages/state/query/queries/chain.ts +++ b/packages/state/query/queries/chain.ts @@ -3,6 +3,7 @@ import { QueryClient, queryOptions, skipToken } from '@tanstack/react-query' import { ChainId } from '@dao-dao/types' import { ModuleAccount } from '@dao-dao/types/protobuf/codegen/cosmos/auth/v1beta1/auth' +import { Metadata } from '@dao-dao/types/protobuf/codegen/cosmos/bank/v1beta1/bank' import { DecCoin } from '@dao-dao/types/protobuf/codegen/cosmos/base/v1beta1/coin' import { cosmWasmClientRouter, @@ -282,6 +283,64 @@ export const fetchWasmContractAdmin = async ({ ) } +/** + * Fetch the on-chain metadata for a denom if it exists. Returns null if denom + * not found. This likely exists for token factory denoms. + */ +export const fetchDenomMetadata = async ({ + chainId, + denom, +}: { + chainId: string + denom: string +}): Promise<{ + metadata: Metadata + preferredSymbol: string + preferredDecimals: number +} | null> => { + const client = await cosmosProtoRpcClientRouter.connect(chainId) + try { + const { metadata } = await client.bank.v1beta1.denomMetadata({ denom }) + + if (metadata) { + const { base, denomUnits, symbol, display } = metadata + + // If display is equal to the base, use the symbol denom unit if + // available. This fixes the case where display was not updated even + // though a nonzero exponent was created. + const searchDenom = display === base ? symbol : display + + const displayDenom = + denomUnits.find(({ denom }) => denom === searchDenom) ?? + denomUnits.find(({ denom }) => denom === display) ?? + denomUnits.find(({ exponent }) => exponent > 0) ?? + denomUnits[0] + + return { + metadata, + // If factory denom, extract symbol at the end. + preferredSymbol: + (displayDenom + ? displayDenom.denom.startsWith('factory/') + ? displayDenom.denom.split('/').pop()! + : displayDenom.denom + : metadata.symbol) || denom, + preferredDecimals: displayDenom?.exponent ?? 0, + } + } + } catch (err) { + // If denom not found, return null. + if (err instanceof Error && err.message.includes('key not found')) { + return null + } + + // Rethrow other errors. + throw err + } + + return null +} + export const chainQueries = { /** * Fetch the module address associated with the specified name. @@ -356,4 +415,12 @@ export const chainQueries = { queryKey: ['chain', 'wasmContractAdmin', options], queryFn: () => fetchWasmContractAdmin(options), }), + /** + * Fetch the on-chain metadata for a denom if it exists. + */ + denomMetadata: (options: Parameters[0]) => + queryOptions({ + queryKey: ['chain', 'denomMetadata', options], + queryFn: () => fetchDenomMetadata(options), + }), } diff --git a/packages/state/query/queries/contracts/Cw20Base.ts b/packages/state/query/queries/contracts/Cw20Base.ts new file mode 100644 index 000000000..b40268824 --- /dev/null +++ b/packages/state/query/queries/contracts/Cw20Base.ts @@ -0,0 +1,342 @@ +/** + * This file was automatically generated by @cosmwasm/ts-codegen@1.10.0. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { UseQueryOptions } from '@tanstack/react-query' + +import { + AllAccountsResponse, + AllAllowancesResponse, + AllSpenderAllowancesResponse, + AllowanceResponse, + BalanceResponse, + DownloadLogoResponse, + MarketingInfoResponse, + MinterResponse, + TokenInfoResponse, +} from '@dao-dao/types/contracts/Cw20Base' +import { cosmWasmClientRouter } from '@dao-dao/utils' + +import { Cw20BaseQueryClient } from '../../../contracts/Cw20Base' + +export const cw20BaseQueryKeys = { + contract: [ + { + contract: 'cw20Base', + }, + ] as const, + address: (contractAddress: string) => + [ + { + ...cw20BaseQueryKeys.contract[0], + address: contractAddress, + }, + ] as const, + balance: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'balance', + args, + }, + ] as const, + tokenInfo: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'token_info', + args, + }, + ] as const, + minter: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'minter', + args, + }, + ] as const, + allowance: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'allowance', + args, + }, + ] as const, + allAllowances: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'all_allowances', + args, + }, + ] as const, + allSpenderAllowances: ( + contractAddress: string, + args?: Record + ) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'all_spender_allowances', + args, + }, + ] as const, + allAccounts: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'all_accounts', + args, + }, + ] as const, + marketingInfo: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'marketing_info', + args, + }, + ] as const, + downloadLogo: (contractAddress: string, args?: Record) => + [ + { + ...cw20BaseQueryKeys.address(contractAddress)[0], + method: 'download_logo', + args, + }, + ] as const, +} +export const cw20BaseQueries = { + balance: ({ + chainId, + contractAddress, + args, + options, + }: Cw20BaseBalanceQuery): UseQueryOptions< + BalanceResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.balance(contractAddress, args), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).balance({ + address: args.address, + }), + ...options, + }), + tokenInfo: ({ + chainId, + contractAddress, + options, + }: Cw20BaseTokenInfoQuery): UseQueryOptions< + TokenInfoResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.tokenInfo(contractAddress), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).tokenInfo(), + ...options, + }), + minter: ({ + chainId, + contractAddress, + options, + }: Cw20BaseMinterQuery): UseQueryOptions< + MinterResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.minter(contractAddress), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).minter(), + ...options, + }), + allowance: ({ + chainId, + contractAddress, + args, + options, + }: Cw20BaseAllowanceQuery): UseQueryOptions< + AllowanceResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.allowance(contractAddress, args), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).allowance({ + owner: args.owner, + spender: args.spender, + }), + ...options, + }), + allAllowances: ({ + chainId, + contractAddress, + args, + options, + }: Cw20BaseAllAllowancesQuery): UseQueryOptions< + AllAllowancesResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.allAllowances(contractAddress, args), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).allAllowances({ + limit: args.limit, + owner: args.owner, + startAfter: args.startAfter, + }), + ...options, + }), + allSpenderAllowances: ({ + chainId, + contractAddress, + args, + options, + }: Cw20BaseAllSpenderAllowancesQuery): UseQueryOptions< + AllSpenderAllowancesResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.allSpenderAllowances(contractAddress, args), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).allSpenderAllowances({ + limit: args.limit, + spender: args.spender, + startAfter: args.startAfter, + }), + ...options, + }), + allAccounts: ({ + chainId, + contractAddress, + args, + options, + }: Cw20BaseAllAccountsQuery): UseQueryOptions< + AllAccountsResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.allAccounts(contractAddress, args), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).allAccounts({ + limit: args.limit, + startAfter: args.startAfter, + }), + ...options, + }), + marketingInfo: ({ + chainId, + contractAddress, + options, + }: Cw20BaseMarketingInfoQuery): UseQueryOptions< + MarketingInfoResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.marketingInfo(contractAddress), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).marketingInfo(), + ...options, + }), + downloadLogo: ({ + chainId, + contractAddress, + options, + }: Cw20BaseDownloadLogoQuery): UseQueryOptions< + DownloadLogoResponse, + Error, + TData + > => ({ + queryKey: cw20BaseQueryKeys.downloadLogo(contractAddress), + queryFn: async () => + new Cw20BaseQueryClient( + await cosmWasmClientRouter.connect(chainId), + contractAddress + ).downloadLogo(), + ...options, + }), +} +export interface Cw20BaseReactQuery { + chainId: string + contractAddress: string + options?: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' | 'initialData' + > & { + initialData?: undefined + } +} +export interface Cw20BaseDownloadLogoQuery + extends Cw20BaseReactQuery {} +export interface Cw20BaseMarketingInfoQuery + extends Cw20BaseReactQuery {} +export interface Cw20BaseAllAccountsQuery + extends Cw20BaseReactQuery { + args: { + limit?: number + startAfter?: string + } +} +export interface Cw20BaseAllSpenderAllowancesQuery + extends Cw20BaseReactQuery { + args: { + limit?: number + spender: string + startAfter?: string + } +} +export interface Cw20BaseAllAllowancesQuery + extends Cw20BaseReactQuery { + args: { + limit?: number + owner: string + startAfter?: string + } +} +export interface Cw20BaseAllowanceQuery + extends Cw20BaseReactQuery { + args: { + owner: string + spender: string + } +} +export interface Cw20BaseMinterQuery + extends Cw20BaseReactQuery {} +export interface Cw20BaseTokenInfoQuery + extends Cw20BaseReactQuery {} +export interface Cw20BaseBalanceQuery + extends Cw20BaseReactQuery { + args: { + address: string + } +} diff --git a/packages/state/query/queries/contracts/index.ts b/packages/state/query/queries/contracts/index.ts index a27e41ef0..fb2c0320d 100644 --- a/packages/state/query/queries/contracts/index.ts +++ b/packages/state/query/queries/contracts/index.ts @@ -1,4 +1,5 @@ export * from './Cw1Whitelist' +export * from './Cw20Base' export * from './Cw3FlexMultisig' export * from './DaoDaoCore' export * from './PolytoneNote' diff --git a/packages/state/query/queries/index.ts b/packages/state/query/queries/index.ts index cee7cc5a6..991727698 100644 --- a/packages/state/query/queries/index.ts +++ b/packages/state/query/queries/index.ts @@ -7,3 +7,5 @@ export * from './dao' export * from './indexer' export * from './polytone' export * from './profile' +export * from './skip' +export * from './token' diff --git a/packages/state/query/queries/skip.ts b/packages/state/query/queries/skip.ts new file mode 100644 index 000000000..49856e736 --- /dev/null +++ b/packages/state/query/queries/skip.ts @@ -0,0 +1,82 @@ +import { QueryClient, queryOptions } from '@tanstack/react-query' + +import { GenericTokenSource, SkipAsset, TokenType } from '@dao-dao/types' + +import { indexerQueries } from './indexer' + +/** + * Fetch Skip chain. + */ +export const fetchSkipChain = async ( + queryClient: QueryClient, + { + chainId, + }: { + chainId: string + } +): Promise => { + const chain = await queryClient.fetchQuery( + indexerQueries.snapper({ + query: 'skip-chain', + parameters: { + chainId, + }, + }) + ) + + if (!chain) { + throw new Error('No Skip chain found') + } + + return chain +} + +/** + * Fetch Skip asset. + */ +export const fetchSkipAsset = async ( + queryClient: QueryClient, + { type, chainId, denomOrAddress }: GenericTokenSource +): Promise => { + const asset = await queryClient.fetchQuery( + indexerQueries.snapper({ + query: 'skip-asset', + parameters: { + chainId, + denom: denomOrAddress, + cw20: (type === TokenType.Cw20).toString(), + }, + }) + ) + + if (!asset) { + throw new Error('No Skip asset found') + } + + return asset +} + +export const skipQueries = { + /** + * Fetch Skip chain. + */ + chain: ( + queryClient: QueryClient, + options: Parameters[1] + ) => + queryOptions({ + queryKey: ['skip', 'chain', options], + queryFn: () => fetchSkipChain(queryClient, options), + }), + /** + * Fetch Skip asset. + */ + asset: ( + queryClient: QueryClient, + options: Parameters[1] + ) => + queryOptions({ + queryKey: ['skip', 'asset', options], + queryFn: () => fetchSkipAsset(queryClient, options), + }), +} diff --git a/packages/state/query/queries/token.ts b/packages/state/query/queries/token.ts new file mode 100644 index 000000000..77e2f8b64 --- /dev/null +++ b/packages/state/query/queries/token.ts @@ -0,0 +1,314 @@ +import { QueryClient, queryOptions } from '@tanstack/react-query' + +import { GenericToken, GenericTokenSource, TokenType } from '@dao-dao/types' +import { + getChainForChainName, + getFallbackImage, + getIbcTransferInfoFromChannel, + getTokenForChainIdAndDenom, + ibcProtoRpcClientRouter, +} from '@dao-dao/utils' + +import { chainQueries } from './chain' +import { cw20BaseQueries } from './contracts' +import { indexerQueries } from './indexer' +import { skipQueries } from './skip' + +/** + * Fetch info for a token. + */ +export const fetchTokenInfo = async ( + queryClient: QueryClient, + { chainId, type, denomOrAddress }: GenericTokenSource +): Promise => { + const [source, asset] = await Promise.all([ + queryClient.fetchQuery( + tokenQueries.source(queryClient, { + chainId, + type, + denomOrAddress, + }) + ), + queryClient + .fetchQuery( + skipQueries.asset(queryClient, { + chainId, + type, + denomOrAddress, + }) + ) + .catch(() => undefined), + ]) + + if (asset) { + return { + chainId: asset.chain_id, + type: asset.is_cw20 ? TokenType.Cw20 : TokenType.Native, + denomOrAddress: (asset.is_cw20 && asset.token_contract) || asset.denom, + symbol: asset.recommended_symbol || asset.symbol || asset.denom, + decimals: asset.decimals || 0, + imageUrl: asset.logo_uri || getFallbackImage(denomOrAddress), + source, + } + } else if (source.chainId !== chainId) { + // If Skip API does not have the info, check if Skip API has the source + // if it's different. This has happened before when Skip does not have + // an IBC asset that we were able to reverse engineer the source for. + const sourceAsset = await queryClient.fetchQuery( + skipQueries.asset(queryClient, source) + ) + + if (sourceAsset) { + return { + chainId, + type, + denomOrAddress, + symbol: + sourceAsset.recommended_symbol || + sourceAsset.symbol || + sourceAsset.denom, + decimals: sourceAsset.decimals || 0, + imageUrl: sourceAsset.logo_uri || getFallbackImage(denomOrAddress), + source, + } + } + } + + if (type === TokenType.Cw20) { + const [tokenInfo, imageUrl] = await Promise.all([ + queryClient.fetchQuery( + cw20BaseQueries.tokenInfo({ + chainId, + contractAddress: denomOrAddress, + }) + ), + queryClient.fetchQuery( + tokenQueries.cw20LogoUrl(queryClient, { + chainId, + address: denomOrAddress, + }) + ), + ]) + + return { + chainId, + type, + denomOrAddress, + symbol: tokenInfo.symbol, + decimals: tokenInfo.decimals, + imageUrl: imageUrl || getFallbackImage(denomOrAddress), + source, + } + } + + // Attempt to fetch from local asset list, erroring if not found. + try { + return { + ...getTokenForChainIdAndDenom(chainId, denomOrAddress, false), + source, + } + } catch (err) { + console.error(err) + } + + // Attempt to fetch from chain. + try { + const chainMetadata = await queryClient.fetchQuery( + chainQueries.denomMetadata({ + chainId, + denom: denomOrAddress, + }) + ) + + if (chainMetadata) { + return { + chainId, + type, + denomOrAddress, + symbol: chainMetadata.preferredSymbol, + decimals: chainMetadata.preferredDecimals, + imageUrl: getFallbackImage(denomOrAddress), + source, + } + } + } catch (err) { + console.error(err) + } + + // If nothing found, just return empty token. + return { + chainId, + type: TokenType.Native, + denomOrAddress, + symbol: denomOrAddress, + decimals: 0, + imageUrl: getFallbackImage(denomOrAddress), + source, + } +} + +/** + * Resolve a denom on a chain to its source chain and base denom. If an IBC + * asset, reverse engineer IBC denom. Otherwise returns the inputs. + */ +export const fetchTokenSource = async ( + queryClient: QueryClient, + { chainId, type, denomOrAddress }: GenericTokenSource +): Promise => { + // Check if Skip API has the info. + const skipAsset = await queryClient.fetchQuery( + skipQueries.asset(queryClient, { + chainId, + type, + denomOrAddress, + }) + ) + + if (skipAsset) { + const sourceType = skipAsset.origin_denom.startsWith('cw20:') + ? TokenType.Cw20 + : TokenType.Native + return { + chainId: skipAsset.origin_chain_id, + type: sourceType, + denomOrAddress: + sourceType === TokenType.Cw20 + ? skipAsset.origin_denom.replace(/^cw20:/, '') + : skipAsset.origin_denom, + } + } + + let sourceChainId = chainId + let sourceDenom = (type === TokenType.Cw20 ? 'cw20:' : '') + denomOrAddress + + // Try to reverse engineer IBC denom. + if (denomOrAddress.startsWith('ibc/')) { + const ibc = await ibcProtoRpcClientRouter.connect(chainId) + + try { + const { denomTrace } = await ibc.applications.transfer.v1.denomTrace({ + hash: denomOrAddress, + }) + + // If trace exists, resolve IBC denom. + if (denomTrace) { + let channels = denomTrace.path.split('transfer/').slice(1) + // Trim trailing slash from all but last channel. + channels = channels.map((channel, index) => + index === channels.length - 1 ? channel : channel.slice(0, -1) + ) + if (channels.length) { + // Retrace channel paths to find source chain of denom. + sourceChainId = channels.reduce( + (currentChainId, channel) => + getChainForChainName( + getIbcTransferInfoFromChannel(currentChainId, channel) + .destinationChain.chain_name + ).chain_id, + chainId + ) + + sourceDenom = denomTrace.baseDenom + } + } + } catch (err) { + console.error(err) + // Ignore resolution error. + } + } + + const sourceType = sourceDenom.startsWith('cw20:') + ? TokenType.Cw20 + : TokenType.Native + + return { + chainId: sourceChainId, + type: sourceType, + denomOrAddress: + sourceType === TokenType.Cw20 + ? sourceDenom.replace(/^cw20:/, '') + : sourceDenom, + } +} + +/** + * Fetch the logo URL for a cw20 token if it exists. Returns null if not found. + */ +export const fetchCw20LogoUrl = async ( + queryClient: QueryClient, + { + chainId, + address, + }: { + chainId: string + address: string + } +): Promise => { + try { + const logoUrl = await queryClient.fetchQuery( + indexerQueries.queryContract(queryClient, { + chainId, + contractAddress: address, + formula: 'cw20/logoUrl', + }) + ) + return logoUrl ?? null + } catch (err) { + // Ignore error. + console.error(err) + } + + // If indexer query fails, fallback to contract query. + const logoInfo = ( + await queryClient + .fetchQuery( + cw20BaseQueries.marketingInfo({ + chainId, + contractAddress: address, + }) + ) + // Cw20 on some chains do not support marketing info. + .catch(() => undefined) + )?.logo + + return logoInfo && logoInfo !== 'embedded' && 'url' in logoInfo + ? logoInfo.url + : null +} + +export const tokenQueries = { + /** + * Fetch info for a token. + */ + info: ( + queryClient: QueryClient, + options: Parameters[1] + ) => + queryOptions({ + queryKey: ['token', 'info', options], + queryFn: () => fetchTokenInfo(queryClient, options), + }), + /** + * Resolve a denom on a chain to its source chain and base denom. If an IBC + * asset, reverse engineer IBC denom. Otherwise returns the inputs. + */ + source: ( + queryClient: QueryClient, + options: Parameters[1] + ) => + queryOptions({ + queryKey: ['token', 'source', options], + queryFn: () => fetchTokenSource(queryClient, options), + }), + /** + * Fetch the logo URL for a cw20 token if it exists. + */ + cw20LogoUrl: ( + queryClient: QueryClient, + options: Parameters[1] + ) => + queryOptions({ + queryKey: ['token', 'cw20LogoUrl', options], + queryFn: () => fetchCw20LogoUrl(queryClient, options), + }), +} diff --git a/packages/types/contracts/Cw20Base.ts b/packages/types/contracts/Cw20Base.ts index 4259a0b08..033a11e4f 100644 --- a/packages/types/contracts/Cw20Base.ts +++ b/packages/types/contracts/Cw20Base.ts @@ -1,5 +1,178 @@ -import { Addr, Binary, Expiration, Uint128 } from './common' +/** + * This file was automatically generated by @cosmwasm/ts-codegen@1.10.0. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ +export type Uint128 = string +export type Logo = + | { + url: string + } + | { + embedded: EmbeddedLogo + } +export type EmbeddedLogo = + | { + svg: Binary + } + | { + png: Binary + } +export type Binary = string +export interface InstantiateMsg { + decimals: number + initial_balances: Cw20Coin[] + marketing?: InstantiateMarketingInfo | null + mint?: MinterResponse | null + name: string + symbol: string +} +export interface Cw20Coin { + address: string + amount: Uint128 +} +export interface InstantiateMarketingInfo { + description?: string | null + logo?: Logo | null + marketing?: string | null + project?: string | null +} +export interface MinterResponse { + cap?: Uint128 | null + minter: string +} +export type ExecuteMsg = + | { + transfer: { + amount: Uint128 + recipient: string + } + } + | { + burn: { + amount: Uint128 + } + } + | { + send: { + amount: Uint128 + contract: string + msg: Binary + } + } + | { + increase_allowance: { + amount: Uint128 + expires?: Expiration | null + spender: string + } + } + | { + decrease_allowance: { + amount: Uint128 + expires?: Expiration | null + spender: string + } + } + | { + transfer_from: { + amount: Uint128 + owner: string + recipient: string + } + } + | { + send_from: { + amount: Uint128 + contract: string + msg: Binary + owner: string + } + } + | { + burn_from: { + amount: Uint128 + owner: string + } + } + | { + mint: { + amount: Uint128 + recipient: string + } + } + | { + update_minter: { + new_minter?: string | null + } + } + | { + update_marketing: { + description?: string | null + marketing?: string | null + project?: string | null + } + } + | { + upload_logo: Logo + } +export type Expiration = + | { + at_height: number + } + | { + at_time: Timestamp + } + | { + never: {} + } +export type Timestamp = Uint64 +export type Uint64 = string +export type QueryMsg = + | { + balance: { + address: string + } + } + | { + token_info: {} + } + | { + minter: {} + } + | { + allowance: { + owner: string + spender: string + } + } + | { + all_allowances: { + limit?: number | null + owner: string + start_after?: string | null + } + } + | { + all_spender_allowances: { + limit?: number | null + spender: string + start_after?: string | null + } + } + | { + all_accounts: { + limit?: number | null + start_after?: string | null + } + } + | { + marketing_info: {} + } + | { + download_logo: {} + } export interface AllAccountsResponse { accounts: string[] } @@ -11,6 +184,14 @@ export interface AllowanceInfo { expires: Expiration spender: string } +export interface AllSpenderAllowancesResponse { + allowances: SpenderAllowanceInfo[] +} +export interface SpenderAllowanceInfo { + allowance: Uint128 + expires: Expiration + owner: string +} export interface AllowanceResponse { allowance: Uint128 expires: Expiration @@ -27,16 +208,13 @@ export type LogoInfo = | { url: string } +export type Addr = string export interface MarketingInfoResponse { description?: string | null logo?: LogoInfo | null marketing?: Addr | null project?: string | null } -export interface MinterResponse { - cap?: Uint128 | null - minter: string -} export interface TokenInfoResponse { decimals: number name: string diff --git a/packages/utils/chain.ts b/packages/utils/chain.ts index 8af739e8a..2066c1984 100644 --- a/packages/utils/chain.ts +++ b/packages/utils/chain.ts @@ -261,11 +261,18 @@ export const maybeGetNativeTokenForChainId = ( } const cachedTokens: Record = {} +/** + * Find a token in the local asset list, if it exists. Depending on the value of + * the `placeholder` argument, it will either return an empty token placeholder + * or error. + */ export const getTokenForChainIdAndDenom = ( chainId: string, denom: string, - // If true, will return placeholder token if not found. If false, will throw - // error. + /** + * If true, will return placeholder token if not found. If false, will throw + * error. + */ placeholder = true ): GenericToken => { try {