import { cosmosclient, proto } from '@cosmos-client/core' import { cosmos } from '@cosmos-client/core/cjs/proto' import { FeeType, Fees, Network, RootDerivationPaths, Tx, TxFrom, TxTo, TxType } from '@d11k-ts/client' import { Asset, AssetAtom, BaseAmount, CosmosChain, baseAmount, eqAsset } from '@d11k-ts/utils' import axios from 'axios' import BigNumber from 'bignumber.js' import Long from 'long' import { COSMOS_DECIMAL, DEFAULT_GAS_LIMIT } from './const' import { APIQueryParam, TxResponse, UnsignedTxParams } from './cosmos' import { ChainId, ChainIds, ClientUrls as ClientUrls } from './types' /** * Type guard for MsgSend * * @param {Msg} msg * @returns {boolean} `true` or `false`. */ export const isMsgSend = (msg: unknown): msg is proto.cosmos.bank.v1beta1.MsgSend => (msg as proto.cosmos.bank.v1beta1.MsgSend)?.amount !== undefined && (msg as proto.cosmos.bank.v1beta1.MsgSend)?.from_address !== undefined && (msg as proto.cosmos.bank.v1beta1.MsgSend)?.to_address !== undefined /** * Type guard for MsgMultiSend * * @param {Msg} msg * @returns {boolean} `true` or `false`. */ export const isMsgMultiSend = (msg: unknown): msg is proto.cosmos.bank.v1beta1.MsgMultiSend => (msg as proto.cosmos.bank.v1beta1.MsgMultiSend)?.inputs !== undefined && (msg as proto.cosmos.bank.v1beta1.MsgMultiSend)?.outputs !== undefined /** * Get denomination from Asset - currently `ATOM` supported only * * @param {Asset} asset * @returns {string} The denomination of the given asset. */ export const getDenom = (asset: Asset): string | null => { if (eqAsset(asset, AssetAtom)) return 'uatom' return null } /** * Get Asset from denomination * * @param {string} denom * @returns {Asset|null} The asset of the given denomination. */ export const getAsset = (denom: string): Asset | null => { if (denom === getDenom(AssetAtom)) return AssetAtom // IBC assets if (denom.startsWith('ibc/')) // Note: Don't use `assetFromString` here, it will interpret `/` as synth return { chain: CosmosChain, symbol: denom, // At the meantime ticker will be empty ticker: '', synth: false, } return null } /** * Parses amount from `ICoin[]` * * @param {ICoin[]} coinst List of coins * * @returns {BaseAmount} Coin amount */ const getCoinAmount = (coins: proto.cosmos.base.v1beta1.ICoin[]): BaseAmount => coins .map((coin) => baseAmount(coin.amount || 0, COSMOS_DECIMAL)) .reduce((acc, cur) => baseAmount(acc.amount().plus(cur.amount()), COSMOS_DECIMAL), baseAmount(0, COSMOS_DECIMAL)) /** * Filters `ICoin[]` by given `Asset` * * @param {ICoin[]} coinst List of coins * @param {Asset} asset Asset to filter coins * * @returns {ICoin[]} Filtered list */ const getCoinsByAsset = (coins: proto.cosmos.base.v1beta1.ICoin[], asset: Asset): proto.cosmos.base.v1beta1.ICoin[] => coins.filter(({ denom }) => { const coinAsset = !!denom ? getAsset(denom) : null return !!coinAsset ? eqAsset(coinAsset, asset) : false }) /** * Parses transaction history * * @param {TxResponse[]} txs The transaction response from the node. * @param {Asset} asset Asset to get history of transactions from * * @returns {Tx[]} List of transactions */ export const getTxsFromHistory = (txs: TxResponse[], asset: Asset): Tx[] => { return ( txs // order list to have latest txs first in list .sort((a, b) => { if (a.timestamp === b.timestamp) return 0 return a.timestamp > b.timestamp ? -1 : 1 }) .reduce((acc, tx) => { const msgs = tx.tx?.body.messages ?? [] const from: TxFrom[] = [] const to: TxTo[] = [] msgs.map((msg) => { if (isMsgSend(msg)) { const msgSend = msg const coins = getCoinsByAsset(msgSend.amount, asset) const amount = getCoinAmount(coins) let from_index = -1 from.forEach((value, index) => { if (value.from === msgSend.from_address) from_index = index }) if (from_index === -1) { from.push({ from: msgSend.from_address, amount, }) } else { from[from_index].amount = baseAmount( from[from_index].amount.amount().plus(amount.amount()), COSMOS_DECIMAL, ) } let to_index = -1 to.forEach((value, index) => { if (value.to === msgSend.to_address) to_index = index }) if (to_index === -1) { to.push({ to: msgSend.to_address, amount, }) } else { to[to_index].amount = baseAmount(to[to_index].amount.amount().plus(amount.amount()), COSMOS_DECIMAL) } } }) return [ ...acc, { asset, from, to, date: new Date(tx.timestamp), type: from.length > 0 || to.length > 0 ? TxType.Transfer : TxType.Unknown, hash: tx.txhash || '', }, ] }, [] as Tx[]) ) } /** * Get Query String * * @param {APIQueryParam} * @returns {string} The query string. */ export const getQueryString = (params: APIQueryParam): string => { return Object.keys(params) .filter((key) => key.length > 0) .map((key) => (params[key] == null ? key : `${key}=${encodeURIComponent(params[key].toString())}`)) .join('&') } /** * Get the default fee. * * @returns {Fees} The default fee. */ export const getDefaultFees = (): Fees => { return { type: FeeType.FlatFee, fast: baseAmount(750, COSMOS_DECIMAL), fastest: baseAmount(2500, COSMOS_DECIMAL), average: baseAmount(0, COSMOS_DECIMAL), } } /** * Get address prefix based on the network. * * @returns {string} The address prefix based on the network. * **/ export const getPrefix = () => 'cosmos' /** * Default client urls * * @returns {ClientUrls} The client urls for Cosmos. */ export const getDefaultClientUrls = (): ClientUrls => { const mainClientUrl = 'https://api.cosmos.network' // Note: In case anyone facing into CORS issue, try the following URLs // https://lcd-cosmos.cosmostation.io/ // https://lcd-cosmoshub.keplr.app/ return { [Network.DojTestnet]: 'https://rest.sentry-02.theta-testnet.polypore.xyz', [Network.Testnet]: 'https://rest.sentry-02.theta-testnet.polypore.xyz', [Network.Stagenet]: mainClientUrl, [Network.Mainnet]: mainClientUrl, } } /** * Default chain ids * * @returns {ChainIds} Chain ids for Cosmos. */ export const getDefaultChainIds = (): ChainIds => { const mainChainId = 'cosmoshub-4' return { [Network.DojTestnet]: 'theta-testnet-001', [Network.Testnet]: 'theta-testnet-001', [Network.Stagenet]: mainChainId, [Network.Mainnet]: mainChainId, } } export const getDefaultRootDerivationPaths = (): RootDerivationPaths => ({ [Network.Mainnet]: `44'/118'/0'/0/`, [Network.Testnet]: `44'/118'/0'/0/`, [Network.Stagenet]: `44'/118'/0'/0/`, [Network.DojTestnet]: `44'/118'/0'/0/`, }) export const protoFee = ({ denom, amount, gasLimit = new BigNumber(DEFAULT_GAS_LIMIT), }: { denom: string amount: BaseAmount gasLimit?: BigNumber }): proto.cosmos.tx.v1beta1.Fee => new proto.cosmos.tx.v1beta1.Fee({ amount: [ { denom, amount: amount.amount().toFixed(0), }, ], gas_limit: Long.fromString(gasLimit.toFixed(0)), }) export const protoMsgSend = ({ from, to, amount, denom, }: { from: string to: string amount: BaseAmount denom: string }): proto.cosmos.bank.v1beta1.MsgSend => new proto.cosmos.bank.v1beta1.MsgSend({ from_address: from, to_address: to, amount: [ { amount: amount.amount().toFixed(0), denom, }, ], }) export const protoTxBody = ({ from, to, amount, denom, memo }: UnsignedTxParams): proto.cosmos.tx.v1beta1.TxBody => { const msg = protoMsgSend({ from, to, amount, denom }) return new proto.cosmos.tx.v1beta1.TxBody({ messages: [cosmosclient.codec.instanceToProtoAny(msg)], memo, }) } export const protoAuthInfo = ({ pubKey, sequence, mode, fee, }: { pubKey: cosmosclient.PubKey sequence: Long.Long mode: proto.cosmos.tx.signing.v1beta1.SignMode fee?: cosmos.tx.v1beta1.IFee }): proto.cosmos.tx.v1beta1.AuthInfo => new proto.cosmos.tx.v1beta1.AuthInfo({ signer_infos: [ { public_key: cosmosclient.codec.instanceToProtoAny(pubKey), mode_info: { single: { mode, }, }, sequence, }, ], fee, }) /** * Helper to get Cosmos' chain id * @param {string} url API url */ export const getChainId = async (url: string): Promise => { const { data } = await axios.get<{ node_info: { network: string } }>(`${url}/node_info`) return data?.node_info?.network || Promise.reject('Could not parse chain id') } /** * Helper to get Cosmos' chain id for all networks * @param {ClientUrl} urls urls (use `getDefaultClientUrl()` if you don't need to use custom urls) */ export const getChainIds = async (urls: ClientUrls): Promise => { return Promise.all([ getChainId(urls[Network.DojTestnet]), getChainId(urls[Network.Testnet]), getChainId(urls[Network.Stagenet]), getChainId(urls[Network.Mainnet]), ]).then(([testnetId, stagenetId, mainnetId, dojtestnetId]) => ({ testnet: testnetId, stagenet: stagenetId, mainnet: mainnetId, dojtestnet: dojtestnetId, })) }