diff --git a/packages/xchain-cosmos/CHANGELOG.md b/packages/xchain-cosmos/CHANGELOG.md index ef226ecbd..8cf20e0ac 100644 --- a/packages/xchain-cosmos/CHANGELOG.md +++ b/packages/xchain-cosmos/CHANGELOG.md @@ -1,3 +1,32 @@ +# v.0.18.0-alpha.1 (2022-06-20) + +## Add + +- const `DEFAULT_GAS_LIMIT` +- const `DEFAULT_FEE` +- Optional parameter `gasLimit` in `transfer` and `transferOffline` +- Params `clientUrls`, `chainIds` for constructor +- Helper `getDefaultClientUrls` +- Helper `getDefaultChainIds` +- Setter `setNetwork` + +## Fix + +- `getFees` returns values based on `DEFAULT_FEE` +- Initial one instance of `CosmosSDKClient` only depending on network +- Support IBC assets in `getBalances` (#596) +- Get IBC assets from denom in `getAsset` + +## Update + +- Result of `getTxsFromHistory` is filtered by given asset +- Move misc. constants into `const.ts` + +## Breaking change + +- Remove deprecated `AssetMuon` +- Remove deprecated `Client.getMainAsset` + # v.0.17.0 (2022-03-23) ## Update diff --git a/packages/xchain-cosmos/__tests__/client.test.ts b/packages/xchain-cosmos/__tests__/client.test.ts index aa38a865b..96aa304a6 100644 --- a/packages/xchain-cosmos/__tests__/client.test.ts +++ b/packages/xchain-cosmos/__tests__/client.test.ts @@ -4,8 +4,8 @@ import { BaseAmount, baseAmount } from '@xchainjs/xchain-util' import nock from 'nock' import { Client } from '../src/client' +import { AssetAtom } from '../src/const' import { GetTxByHashResponse, TxHistoryResponse } from '../src/cosmos/types' -import { AssetMuon } from '../src/types' const getClientUrl = (client: Client): string => { return client.getNetwork() === Network.Testnet @@ -78,7 +78,7 @@ describe('Client Test', () => { const address1_testnet = 'cosmos1924f27fujxqnkt74u4d3ke3sfygugv9qp29hmk' beforeEach(() => { - cosmosClient = new Client({ phrase, network: 'testnet' as Network }) + cosmosClient = new Client({ phrase, network: Network.Testnet }) }) afterEach(() => { @@ -86,22 +86,22 @@ describe('Client Test', () => { }) it('should start with empty wallet', async () => { - const cosmosClientEmptyMain = new Client({ phrase, network: 'mainnet' as Network }) + const cosmosClientEmptyMain = new Client({ phrase, network: Network.Mainnet }) expect(cosmosClientEmptyMain.getAddress()).toEqual(address0_mainnet) expect(cosmosClientEmptyMain.getAddress(1)).toEqual(address1_mainnet) - const cosmosClientEmptyTest = new Client({ phrase, network: 'testnet' as Network }) + const cosmosClientEmptyTest = new Client({ phrase, network: Network.Testnet }) expect(cosmosClientEmptyTest.getAddress()).toEqual(address0_testnet) expect(cosmosClientEmptyTest.getAddress(1)).toEqual(address1_testnet) }) it('throws an error passing an invalid phrase', async () => { expect(() => { - new Client({ phrase: 'invalid phrase', network: 'mainnet' as Network }) + new Client({ phrase: 'invalid phrase', network: Network.Mainnet }) }).toThrow() expect(() => { - new Client({ phrase: 'invalid phrase', network: 'testnet' as Network }) + new Client({ phrase: 'invalid phrase', network: Network.Testnet }) }).toThrow() }) @@ -118,8 +118,8 @@ describe('Client Test', () => { }) it('should update net', async () => { - const client = new Client({ phrase, network: 'mainnet' as Network }) - client.setNetwork('testnet' as Network) + const client = new Client({ phrase, network: Network.Mainnet }) + client.setNetwork(Network.Testnet) expect(client.getNetwork()).toEqual('testnet') const address = client.getAddress() @@ -129,12 +129,12 @@ describe('Client Test', () => { it('should init, should have right prefix', async () => { expect(cosmosClient.validateAddress(cosmosClient.getAddress())).toEqual(true) - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) expect(cosmosClient.validateAddress(cosmosClient.getAddress())).toEqual(true) }) it('has no balances', async () => { - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) mockAccountsBalance(getClientUrl(cosmosClient), address0_mainnet, { balances: [], @@ -148,19 +148,20 @@ describe('Client Test', () => { mockAccountsBalance(getClientUrl(cosmosClient), 'cosmos1gehrq0pr5d79q8nxnaenvqh09g56jafm82thjv', { balances: [ new proto.cosmos.base.v1beta1.Coin({ - denom: 'muon', + denom: 'uatom', amount: '75000000', }), ], }) const balances = await cosmosClient.getBalance('cosmos1gehrq0pr5d79q8nxnaenvqh09g56jafm82thjv') + const expected = balances[0].amount.amount().isEqualTo(baseAmount(75000000, 6).amount()) expect(expected).toBeTruthy() - expect(balances[0].asset).toEqual(AssetMuon) + expect(balances[0].asset).toEqual(AssetAtom) }) it('has an empty tx history', async () => { - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) const expected: TxsPage = { total: 0, @@ -186,7 +187,7 @@ describe('Client Test', () => { to_address: 'cosmos155svs6sgxe55rnvs6ghprtqu0mh69kehrn0dqr', amount: [ { - denom: 'umuon', + denom: 'uatom', amount: '4318994970', }, ], @@ -222,7 +223,7 @@ describe('Client Test', () => { let transactions = await cosmosClient.getTransactions({ address: 'cosmos1xvt4e7xd0j9dwv2w83g50tpcltsl90h52003e2' }) expect(transactions.total).toBeGreaterThan(0) - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) const msgSend2 = new proto.cosmos.bank.v1beta1.MsgSend({ from_address: 'cosmos1pjkpqxmvz47a5aw40l98fyktlg7k6hd9heq95z', to_address: 'cosmos155svs6sgxe55rnvs6ghprtqu0mh69kehrn0dqr', @@ -291,7 +292,7 @@ describe('Client Test', () => { assertTxsPost(getClientUrl(cosmosClient), expected_txsPost_result) const result = await cosmosClient.transfer({ - asset: AssetMuon, + asset: AssetAtom, recipient: to_address, amount: send_amount, memo, @@ -301,7 +302,7 @@ describe('Client Test', () => { }) it('get transaction data', async () => { - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) const msgSend = new proto.cosmos.bank.v1beta1.MsgSend({ from_address: 'cosmos1pjkpqxmvz47a5aw40l98fyktlg7k6hd9heq95z', @@ -346,7 +347,7 @@ describe('Client Test', () => { // Client created with network === 'testnet' expect(cosmosClient.getExplorerUrl()).toEqual('https://explorer.theta-testnet.polypore.xyz') - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) expect(cosmosClient.getExplorerUrl()).toEqual('https://cosmos.bigdipper.live') }) @@ -355,7 +356,7 @@ describe('Client Test', () => { 'https://explorer.theta-testnet.polypore.xyz/account/anotherTestAddressHere', ) - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) expect(cosmosClient.getExplorerAddressUrl('testAddressHere')).toEqual( 'https://cosmos.bigdipper.live/account/testAddressHere', ) @@ -366,7 +367,7 @@ describe('Client Test', () => { 'https://explorer.theta-testnet.polypore.xyz/transactions/anotherTestTxHere', ) - cosmosClient.setNetwork('mainnet' as Network) + cosmosClient.setNetwork(Network.Mainnet) expect(cosmosClient.getExplorerTxUrl('testTxHere')).toEqual('https://cosmos.bigdipper.live/transactions/testTxHere') }) }) diff --git a/packages/xchain-cosmos/__tests__/util.test.ts b/packages/xchain-cosmos/__tests__/util.test.ts index 854d18c49..3a864e97c 100644 --- a/packages/xchain-cosmos/__tests__/util.test.ts +++ b/packages/xchain-cosmos/__tests__/util.test.ts @@ -1,8 +1,8 @@ import { cosmosclient, proto } from '@cosmos-client/core' -import { baseAmount } from '@xchainjs/xchain-util' +import { baseAmount, eqAsset } from '@xchainjs/xchain-util' +import { AssetAtom } from '../src/const' import { APIQueryParam, RawTxResponse, TxResponse } from '../src/cosmos/types' -import { AssetAtom, AssetMuon } from '../src/types' import { getAsset, getDenom, getQueryString, getTxsFromHistory, isMsgMultiSend, isMsgSend } from '../src/util' describe('cosmos/util', () => { @@ -76,23 +76,21 @@ describe('cosmos/util', () => { it('get denom for AssetAtom', () => { expect(getDenom(AssetAtom)).toEqual('uatom') }) - - it('get denom for AssetMuon', () => { - expect(getDenom(AssetMuon)).toEqual('umuon') - }) }) describe('getAsset', () => { - it('get asset for umuon', () => { - expect(getAsset('umuon')).toEqual(AssetMuon) - }) - it('get asset for uatom', () => { - expect(getAsset('uatom')).toEqual(AssetAtom) + const asset = getAsset('uatom') + const result = asset !== null && eqAsset(asset, AssetAtom) + expect(result).toBeTruthy() }) - it('get asset for unknown', () => { - expect(getAsset('unknown')).toBeNull() + it('get asset for ibc asset (BTSG - Bitsong)', () => { + // see https://github.com/bitsongofficial/docs.bitsong.io/blob/main/relayer.md#official-bitsong-ibc-channels + const denom = 'ibc/E7D5E9D0E9BF8B7354929A817DD28D4D017E745F638954764AA88522A7A409EC' + const asset = getAsset(denom) + const expected = { ...AssetAtom, symbol: denom, ticker: '' } + expect(asset !== null && eqAsset(asset, expected)).toBeTruthy() }) }) }) diff --git a/packages/xchain-cosmos/package.json b/packages/xchain-cosmos/package.json index eff8e89ad..ffdb65807 100644 --- a/packages/xchain-cosmos/package.json +++ b/packages/xchain-cosmos/package.json @@ -1,6 +1,6 @@ { "name": "@xchainjs/xchain-cosmos", - "version": "0.17.0", + "version": "0.18.0-alpha.1", "description": "Custom Cosmos client and utilities used by XChainJS clients", "keywords": [ "XChain", @@ -32,9 +32,9 @@ "prepublishOnly": "yarn build" }, "devDependencies": { - "@xchainjs/xchain-client": "^0.11.1", - "@xchainjs/xchain-crypto": "^0.2.4", - "@xchainjs/xchain-util": "^0.5.1", + "@xchainjs/xchain-client": "^0.11.3", + "@xchainjs/xchain-crypto": "^0.2.6", + "@xchainjs/xchain-util": "^0.7.1", "@cosmos-client/core": "^0.45.1", "axios": "^0.25.0", "nock": "^13.0.5" @@ -43,9 +43,9 @@ "access": "public" }, "peerDependencies": { - "@xchainjs/xchain-client": "^0.11.1", - "@xchainjs/xchain-crypto": "^0.2.4", - "@xchainjs/xchain-util": "^0.5.1", + "@xchainjs/xchain-client": "^0.11.3", + "@xchainjs/xchain-crypto": "^0.2.6", + "@xchainjs/xchain-util": "^0.7.1", "@cosmos-client/core": "^0.45.1", "axios": "^0.25.0" } diff --git a/packages/xchain-cosmos/src/client.ts b/packages/xchain-cosmos/src/client.ts index a468dd254..573746cb1 100644 --- a/packages/xchain-cosmos/src/client.ts +++ b/packages/xchain-cosmos/src/client.ts @@ -1,4 +1,4 @@ -import { proto } from '@cosmos-client/core' +import { cosmosclient, proto } from '@cosmos-client/core' import { Address, Balance, @@ -13,36 +13,31 @@ import { TxsPage, XChainClient, XChainClientParams, + singleFee, } from '@xchainjs/xchain-client' -import { Asset, Chain, assetToString, baseAmount } from '@xchainjs/xchain-util' +import { Asset, Chain, baseAmount, eqAsset } from '@xchainjs/xchain-util' +import BigNumber from 'bignumber.js' +import { AssetAtom, DECIMAL, DEFAULT_FEE, DEFAULT_GAS_LIMIT } from './const' import { CosmosSDKClient } from './cosmos/sdk-client' import { TxOfflineParams } from './cosmos/types' -import { AssetAtom, AssetMuon } from './types' -import { DECIMAL, getAsset, getDenom, getTxsFromHistory } from './util' +import { ChainIds, ClientUrls, CosmosClientParams } from './types' +import { getAsset, getDefaultChainIds, getDefaultClientUrls, getDenom, getTxsFromHistory } from './util' /** * Interface for custom Cosmos client */ export interface CosmosClient { - getMainAsset(): Asset getSDKClient(): CosmosSDKClient } -const MAINNET_SDK = new CosmosSDKClient({ - server: 'https://api.cosmos.network', - chainId: 'cosmoshub-4', -}) -const TESTNET_SDK = new CosmosSDKClient({ - server: 'https://rest.sentry-02.theta-testnet.polypore.xyz', - chainId: 'theta-testnet-001', -}) - /** * Custom Cosmos client */ class Client extends BaseXChainClient implements CosmosClient, XChainClient { - private sdkClients: Map = new Map() + private sdkClient: CosmosSDKClient + private clientUrls: ClientUrls + private chainIds: ChainIds /** * Constructor @@ -57,15 +52,40 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { constructor({ network = Network.Testnet, phrase, + clientUrls, + chainIds, rootDerivationPaths = { [Network.Mainnet]: `44'/118'/0'/0/`, [Network.Testnet]: `44'/118'/0'/0/`, [Network.Stagenet]: `44'/118'/0'/0/`, }, - }: XChainClientParams) { + }: XChainClientParams & CosmosClientParams) { super(Chain.Cosmos, { network, rootDerivationPaths, phrase }) - this.sdkClients.set(Network.Testnet, TESTNET_SDK) - this.sdkClients.set(Network.Mainnet, MAINNET_SDK) + + this.clientUrls = clientUrls || getDefaultClientUrls() + this.chainIds = chainIds || getDefaultChainIds() + + this.sdkClient = new CosmosSDKClient({ + server: this.clientUrls[network], + chainId: this.chainIds[network], + }) + } + + /** + * Updates current network. + * + * @param {Network} network + * @returns {void} + */ + setNetwork(network: Network): void { + // dirty check to avoid using and re-creation of same data + if (network === this.network) return + + super.setNetwork(network) + this.sdkClient = new CosmosSDKClient({ + server: this.clientUrls[network], + chainId: this.chainIds[network], + }) } /** @@ -119,7 +139,7 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { } getSDKClient(): CosmosSDKClient { - return this.sdkClients.get(this.network) || TESTNET_SDK + return this.sdkClient } /** @@ -145,21 +165,6 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { return this.getSDKClient().checkAddress(address) } - /** - * Get the main asset based on the network. - * - * @returns {string} The main asset based on the network. - */ - getMainAsset(): Asset { - switch (this.network) { - case Network.Mainnet: - case Network.Stagenet: - return AssetAtom - case Network.Testnet: - return AssetMuon - } - } - /** * Get the balance of a given address. * @@ -168,23 +173,21 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { * @returns {Balance[]} The balance of the address. */ async getBalance(address: Address, assets?: Asset[]): Promise { - const balances = await this.getSDKClient().getBalance(address) - const mainAsset = this.getMainAsset() + const coins = await this.getSDKClient().getBalance(address) + + const balances = coins + .reduce((acc: Balance[], { denom, amount }) => { + const asset = getAsset(denom) + return asset ? [...acc, { asset, amount: baseAmount(amount || '0', DECIMAL) }] : acc + }, []) + .filter(({ asset: balanceAsset }) => !assets || assets.filter((asset) => eqAsset(balanceAsset, asset)).length) return balances - .map((balance) => { - return { - asset: (balance.denom && getAsset(balance.denom)) || mainAsset, - amount: baseAmount(balance.amount, DECIMAL), - } - }) - .filter( - (balance) => !assets || assets.filter((asset) => assetToString(balance.asset) === assetToString(asset)).length, - ) } /** - * Get transaction history of a given address with pagination options. + * Get transaction history of a given address and asset with pagination options. + * If `asset` is not set, history will include `ATOM` txs only * By default it will return the transaction history of the current wallet. * * @param {TxHistoryParams} params The options to get transaction history. (optional) @@ -196,11 +199,12 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { const limit = (params && params.limit) || undefined const txMinHeight = undefined const txMaxHeight = undefined + const asset = getAsset(params?.asset ?? '') || AssetAtom + const messageSender = params?.address ?? this.getAddress() - const mainAsset = this.getMainAsset() const txHistory = await this.getSDKClient().searchTx({ messageAction, - messageSender: (params && params.address) || this.getAddress(), + messageSender, page, limit, txMinHeight, @@ -209,12 +213,12 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { return { total: parseInt(txHistory.pagination?.total || '0'), - txs: getTxsFromHistory(txHistory.tx_responses || [], mainAsset), + txs: getTxsFromHistory(txHistory.tx_responses || [], asset), } } /** - * Get the transaction details of a given transaction id. + * Get the transaction details of a given transaction id. Supports `ATOM` txs only. * * @param {string} txId The transaction id. * @returns {Tx} The transaction details of the given transaction id. @@ -226,7 +230,7 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { throw new Error('transaction not found') } - const txs = getTxsFromHistory([txResult], this.getMainAsset()) + const txs = getTxsFromHistory([txResult], AssetAtom) if (txs.length === 0) throw new Error('transaction not found') return txs[0] @@ -238,17 +242,29 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { * @param {TxParams} params The transfer options. * @returns {TxHash} The transaction hash. */ - async transfer({ walletIndex, asset, amount, recipient, memo }: TxParams): Promise { + async transfer({ + walletIndex, + asset = AssetAtom, + amount, + recipient, + memo, + gasLimit = new BigNumber(DEFAULT_GAS_LIMIT), + }: TxParams & { gasLimit?: BigNumber }): Promise { const fromAddressIndex = walletIndex || 0 - const mainAsset = this.getMainAsset() + const fee = new proto.cosmos.tx.v1beta1.Fee({ + amount: [], + gas_limit: cosmosclient.Long.fromString(gasLimit.toFixed(0)), + }) + return this.getSDKClient().transfer({ privkey: this.getPrivateKey(fromAddressIndex), from: this.getAddress(fromAddressIndex), to: recipient, amount: amount.amount().toString(), - asset: getDenom(asset || mainAsset), + asset: getDenom(asset), memo, + fee, }) } @@ -260,16 +276,21 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { */ async transferOffline({ walletIndex, - asset, + asset = AssetAtom, amount, recipient, memo, from_account_number, from_sequence, + gasLimit = new BigNumber(DEFAULT_GAS_LIMIT), }: TxOfflineParams): Promise { const fromAddressIndex = walletIndex || 0 - const mainAsset = this.getMainAsset() + const fee = new proto.cosmos.tx.v1beta1.Fee({ + amount: [], + gas_limit: cosmosclient.Long.fromString(gasLimit.toFixed(0)), + }) + return await this.getSDKClient().transferSignedOffline({ privkey: this.getPrivateKey(fromAddressIndex), from: this.getAddress(fromAddressIndex), @@ -277,24 +298,19 @@ class Client extends BaseXChainClient implements CosmosClient, XChainClient { from_sequence, to: recipient, amount: amount.amount().toString(), - asset: getDenom(asset || mainAsset), + asset: getDenom(asset), memo, + fee, }) } /** - * Get the current fee. + * Returns (default) fees. * - * @returns {Fees} The current fee. + * @returns {Fees} Current fees */ async getFees(): Promise { - // there is no fixed fee, we set fee amount when creating a transaction. - return { - type: FeeType.FlatFee, - fast: baseAmount(750, DECIMAL), - fastest: baseAmount(2500, DECIMAL), - average: baseAmount(0, DECIMAL), - } + return singleFee(FeeType.FlatFee, DEFAULT_FEE) } } diff --git a/packages/xchain-cosmos/src/const.ts b/packages/xchain-cosmos/src/const.ts new file mode 100644 index 000000000..9a9c9c942 --- /dev/null +++ b/packages/xchain-cosmos/src/const.ts @@ -0,0 +1,22 @@ +import { Asset, Chain, baseAmount } from '@xchainjs/xchain-util' + +/** + * The decimal for cosmos chain. + */ +export const DECIMAL = 6 + +export const AssetAtom: Asset = { chain: Chain.Cosmos, symbol: 'ATOM', ticker: 'ATOM', synth: false } + +/** + * Default gas limit + * As same as definition in Cosmosstation's web wallet + * @see https://github.com/cosmostation/web-wallet-ts-react/blob/4d78718b613defbd6c92079b33aa8ce9f86d597c/src/constants/chain.ts#L76 + */ +export const DEFAULT_GAS_LIMIT = '200000' + +/** + * Default fee + * As same as definition in Cosmosstation's web wallet + * @see https://github.com/cosmostation/web-wallet-ts-react/blob/4d78718b613defbd6c92079b33aa8ce9f86d597c/src/constants/chain.ts#L66 + */ +export const DEFAULT_FEE = baseAmount(5000, DECIMAL) diff --git a/packages/xchain-cosmos/src/cosmos/sdk-client.ts b/packages/xchain-cosmos/src/cosmos/sdk-client.ts index 65ec42907..e4bfcf94c 100644 --- a/packages/xchain-cosmos/src/cosmos/sdk-client.ts +++ b/packages/xchain-cosmos/src/cosmos/sdk-client.ts @@ -4,6 +4,7 @@ import * as xchainCrypto from '@xchainjs/xchain-crypto' import axios from 'axios' import * as BIP32 from 'bip32' +import { DEFAULT_GAS_LIMIT } from '../const' import { getQueryString } from '../util' import { @@ -22,8 +23,9 @@ import { const DEFAULT_FEE = new proto.cosmos.tx.v1beta1.Fee({ amount: [], - gas_limit: cosmosclient.Long.fromString('300000'), + gas_limit: cosmosclient.Long.fromString(DEFAULT_GAS_LIMIT), }) + export class CosmosSDKClient { sdk: cosmosclient.CosmosSDK @@ -116,7 +118,13 @@ export class CosmosSDKClient { const accAddress = cosmosclient.AccAddress.fromString(address) const response = await rest.bank.allBalances(this.sdk, accAddress) - return response.data.balances as proto.cosmos.base.v1beta1.Coin[] + const balances: proto.cosmos.base.v1beta1.Coin[] = + response.data.balances?.reduce( + (acc: proto.cosmos.base.v1beta1.Coin[], { amount, denom }) => + !!amount && !!denom ? [...acc, new proto.cosmos.base.v1beta1.Coin({ amount, denom })] : acc, + [], + ) || [] + return balances } async getAccount(address: cosmosclient.AccAddress): Promise { diff --git a/packages/xchain-cosmos/src/cosmos/types.ts b/packages/xchain-cosmos/src/cosmos/types.ts index 03bcdd9b5..a84640dc9 100644 --- a/packages/xchain-cosmos/src/cosmos/types.ts +++ b/packages/xchain-cosmos/src/cosmos/types.ts @@ -1,5 +1,6 @@ import { proto } from '@cosmos-client/core' import { TxParams } from '@xchainjs/xchain-client' +import BigNumber from 'bignumber.js' export type CosmosSDKClientParams = { server: string @@ -44,6 +45,7 @@ export type TransferOfflineParams = TransferParams & { export type TxOfflineParams = TxParams & { from_account_number: string from_sequence: string + gasLimit?: BigNumber } export type BaseAccountResponse = { diff --git a/packages/xchain-cosmos/src/index.ts b/packages/xchain-cosmos/src/index.ts index 3871b9cd6..0f0532cc3 100644 --- a/packages/xchain-cosmos/src/index.ts +++ b/packages/xchain-cosmos/src/index.ts @@ -1,4 +1,5 @@ -export * from './cosmos' -export * from './client' export * from './types' +export * from './const' export * from './util' +export * from './cosmos' +export * from './client' diff --git a/packages/xchain-cosmos/src/types/client-types.ts b/packages/xchain-cosmos/src/types/client-types.ts index 608d66804..bf4940101 100644 --- a/packages/xchain-cosmos/src/types/client-types.ts +++ b/packages/xchain-cosmos/src/types/client-types.ts @@ -1,4 +1,19 @@ -import { Asset, Chain } from '@xchainjs/xchain-util' +import { Network } from '@xchainjs/xchain-client' -export const AssetAtom: Asset = { chain: Chain.Cosmos, symbol: 'ATOM', ticker: 'ATOM', synth: false } -export const AssetMuon: Asset = { chain: Chain.Cosmos, symbol: 'MUON', ticker: 'MUON', synth: false } +export type ClientUrls = Record + +export type ExplorerUrl = Record + +export type ExplorerUrls = { + root: ExplorerUrl + tx: ExplorerUrl + address: ExplorerUrl +} + +export type ChainId = string +export type ChainIds = Record + +export type CosmosClientParams = { + clientUrls?: ClientUrls + chainIds?: ChainIds +} diff --git a/packages/xchain-cosmos/src/util.ts b/packages/xchain-cosmos/src/util.ts index ca7e5d2ac..4542a4e97 100644 --- a/packages/xchain-cosmos/src/util.ts +++ b/packages/xchain-cosmos/src/util.ts @@ -1,14 +1,10 @@ import { cosmosclient, proto } from '@cosmos-client/core' -import { FeeType, Fees, Tx, TxFrom, TxTo, TxType } from '@xchainjs/xchain-client' -import { Asset, assetToString, baseAmount } from '@xchainjs/xchain-util' +import { FeeType, Fees, Network, Tx, TxFrom, TxTo, TxType } from '@xchainjs/xchain-client' +import { Asset, BaseAmount, CosmosChain, baseAmount, eqAsset } from '@xchainjs/xchain-util' -import { APIQueryParam, RawTxResponse, TxResponse } from './cosmos/types' -import { AssetAtom, AssetMuon } from './types' - -/** - * The decimal for cosmos chain. - */ -export const DECIMAL = 6 +import { AssetAtom, DECIMAL } from './const' +import { APIQueryParam, TxResponse } from './cosmos/types' +import { ChainIds, ClientUrls as ClientUrls } from './types' /** * Type guard for MsgSend @@ -38,8 +34,7 @@ export const isMsgMultiSend = (msg: unknown): msg is proto.cosmos.bank.v1beta1.M * @returns {string} The denomination of the given asset. */ export const getDenom = (asset: Asset): string => { - if (assetToString(asset) === assetToString(AssetAtom)) return 'uatom' - if (assetToString(asset) === assetToString(AssetMuon)) return 'umuon' + if (eqAsset(asset, AssetAtom)) return 'uatom' return asset.symbol } @@ -51,32 +46,64 @@ export const getDenom = (asset: Asset): string => { */ export const getAsset = (denom: string): Asset | null => { if (denom === getDenom(AssetAtom)) return AssetAtom - if (denom === getDenom(AssetMuon)) return AssetMuon + // IBC assets + if (denom.startsWith('ibc/')) + // Note: Don't use `assetFromString` here, it will interpret `/` as synth + return { + chain: CosmosChain, + symbol: denom, + // TODO (xchain-contributors) + // Get readable ticker for IBC assets from denom #600 https://github.com/xchainjs/xchainjs-lib/issues/600 + // At the meantime ticker will be empty + ticker: '', + synth: false, + } return null } -const getCoinAmount = (coins?: proto.cosmos.base.v1beta1.ICoin[]) => { - return coins - ? coins - .map((coin) => baseAmount(coin.amount || 0, DECIMAL)) - .reduce((acc, cur) => baseAmount(acc.amount().plus(cur.amount()), DECIMAL), baseAmount(0, DECIMAL)) - : baseAmount(0, DECIMAL) -} /** - * Parse transaction type + * 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, DECIMAL)) + .reduce((acc, cur) => baseAmount(acc.amount().plus(cur.amount()), DECIMAL), baseAmount(0, 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} mainAsset Current main asset which depends on the network. - * @returns {Tx[]} The parsed transaction result. + * @param {Asset} asset Asset to get history of transactions from + * + * @returns {Tx[]} List of transactions */ -export const getTxsFromHistory = (txs: TxResponse[], mainAsset: Asset): Tx[] => { +export const getTxsFromHistory = (txs: TxResponse[], asset: Asset): Tx[] => { return txs.reduce((acc, tx) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let msgs: Array - if ((tx.tx as RawTxResponse).body === undefined) { - msgs = cosmosclient.codec.packAnyFromCosmosJSON(tx.tx).msg - } else { - msgs = cosmosclient.codec.packAnyFromCosmosJSON((tx.tx as RawTxResponse).body.messages) + let msgs: Array = [] + if (tx.tx) { + if (!tx.tx.body) { + msgs = cosmosclient.codec.packAnyFromCosmosJSON(tx.tx).msg + } else { + msgs = cosmosclient.codec.packAnyFromCosmosJSON(tx.tx.body.messages) + } } const from: TxFrom[] = [] @@ -84,7 +111,8 @@ export const getTxsFromHistory = (txs: TxResponse[], mainAsset: Asset): Tx[] => msgs.map((msg) => { if (isMsgSend(msg)) { const msgSend = msg - const amount = getCoinAmount(msgSend.amount) + const coins = getCoinsByAsset(msgSend.amount, asset) + const amount = getCoinAmount(coins) let from_index = -1 @@ -119,7 +147,8 @@ export const getTxsFromHistory = (txs: TxResponse[], mainAsset: Asset): Tx[] => const msgMultiSend = msg msgMultiSend.inputs.map((input) => { - const amount = getCoinAmount(input.coins || []) + const coins = getCoinsByAsset(input.coins || [], asset) + const amount = getCoinAmount(coins) let from_index = -1 @@ -138,7 +167,8 @@ export const getTxsFromHistory = (txs: TxResponse[], mainAsset: Asset): Tx[] => }) msgMultiSend.outputs.map((output) => { - const amount = getCoinAmount(output.coins || []) + const coins = getCoinsByAsset(output.coins || [], asset) + const amount = getCoinAmount(coins) let to_index = -1 @@ -161,7 +191,7 @@ export const getTxsFromHistory = (txs: TxResponse[], mainAsset: Asset): Tx[] => return [ ...acc, { - asset: mainAsset, + asset, from, to, date: new Date(tx.timestamp), @@ -206,3 +236,35 @@ export const getDefaultFees = (): Fees => { * **/ 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/ + // @see (Discord #xchainjs) https://discord.com/channels/838986635756044328/988096545926828082/988103739967688724 + return { + [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.Testnet]: 'theta-testnet-001', + [Network.Stagenet]: mainChainId, + [Network.Mainnet]: mainChainId, + } +}