UNPKG

@dojima-wallet/connection

Version:

Initialise and connection for layer 1&2 blockchain

375 lines (334 loc) 9.88 kB
import { proto } from "@cosmos-client/core"; import { Balance, BaseChainClient, ChainClient, ChainClientParams, FeeType, Fees, Network, Tx, TxHash, TxHistoryParams, TxParams, TxsPage, singleFee, } from "../client"; import { Address, Asset, AssetAtom, BaseAmount, CosmosChain, assetToString, baseAmount, eqAsset, } from "@dojima-wallet/utils"; import BigNumber from "bignumber.js"; import { COSMOS_DECIMAL, DEFAULT_FEE, DEFAULT_GAS_LIMIT } from "./const"; import { CosmosSDKClient } from "./cosmos"; import { TxOfflineParams } from "./cosmos"; import { ChainIds, ClientUrls, CosmosClientParams } from "./types"; import { getAsset, getDefaultChainIds, getDefaultClientUrls, getDefaultRootDerivationPaths, getDenom, getTxsFromHistory, protoFee, } from "./util"; /** * Interface for custom Cosmos client */ export interface CosmosClient { getSDKClient(): CosmosSDKClient; } /** * Custom Cosmos client */ class Client extends BaseChainClient implements CosmosClient, ChainClient { private sdkClient: CosmosSDKClient; private clientUrls: ClientUrls; private chainIds: ChainIds; /** * Constructor * * Client has to be initialised with network type and phrase. * It will throw an error if an invalid phrase has been passed. * * @param {ChainClientParams} params * * @throws {"Invalid phrase"} Thrown if the given phase is invalid. */ constructor({ network = Network.Mainnet, phrase, clientUrls = getDefaultClientUrls(), chainIds = getDefaultChainIds(), rootDerivationPaths = getDefaultRootDerivationPaths(), }: ChainClientParams & CosmosClientParams) { super(CosmosChain, { network, rootDerivationPaths, phrase }); this.clientUrls = clientUrls; this.chainIds = chainIds; 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], }); } /** * Get the explorer url. * * @returns {string} The explorer url. */ getExplorerUrl(): string { switch (this.network) { case Network.Mainnet: case Network.Stagenet: return "https://cosmos.bigdipper.live"; case Network.Testnet: return "https://explorer.theta-testnet.polypore.xyz"; } } /** * Get the explorer url for the given address. * * @param {Address} address * @returns {string} The explorer url for the given address. */ getExplorerAddressUrl(address: Address): string { return `${this.getExplorerUrl()}/account/${address}`; } /** * Get the explorer url for the given transaction id. * * @param {string} txID * @returns {string} The explorer url for the given transaction id. */ getExplorerTxUrl(txID: string): string { return `${this.getExplorerUrl()}/transactions/${txID}`; } /** * @private * Get private key. * * @returns {PrivKey} The private key generated from the given phrase * * @throws {"Phrase not set"} * Throws an error if phrase has not been set before * */ private getPrivateKey(index = 0): proto.cosmos.crypto.secp256k1.PrivKey { if (!this.phrase) throw new Error("Phrase not set"); return this.getSDKClient().getPrivKeyFromMnemonic( this.phrase, this.getFullDerivationPath(index) ); } getSDKClient(): CosmosSDKClient { return this.sdkClient; } /** * Get the current address. * * @returns {Address} The current address. * * @throws {Error} Thrown if phrase has not been set before. A phrase is needed to create a wallet and to derive an address from it. */ getAddress(index = 0): string { if (!this.phrase) throw new Error("Phrase not set"); return this.getSDKClient().getAddressFromMnemonic( this.phrase, this.getFullDerivationPath(index) ); } /** * Validate the given address. * * @param {Address} address * @returns {boolean} `true` or `false` */ validateAddress(address: Address): boolean { return this.getSDKClient().checkAddress(address); } /** * Get the balance of a given address. * * @param {Address} address By default, it will return the balance of the current wallet. (optional) * @param {Asset} asset If not set, it will return all assets available. (optional) * @returns {Balance[]} The balance of the address. */ async getBalance(address: Address, assets?: Asset[]): Promise<Balance[]> { 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", COSMOS_DECIMAL) }, ] : acc; }, []) .filter( ({ asset: balanceAsset }) => !assets || assets.filter((asset) => eqAsset(balanceAsset, asset)).length ); return balances; } /** * 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) * @returns {TxsPage} The transaction history. */ async getTransactions(params?: TxHistoryParams): Promise<TxsPage> { const messageAction: any = undefined; const page = (params && params.offset) || undefined; const limit = (params && params.limit) || undefined; const txMinHeight: any = undefined; const txMaxHeight: any = undefined; const asset = getAsset(params?.asset ?? "") || AssetAtom; const messageSender = params?.address ?? this.getAddress(); const txHistory = await this.getSDKClient().searchTx({ messageAction, messageSender, page, limit, txMinHeight, txMaxHeight, }); return { total: parseInt(txHistory.pagination?.total || "0"), txs: getTxsFromHistory(txHistory.tx_responses || [], asset), }; } /** * 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. */ async getTransactionData(txId: string): Promise<Tx> { const txResult = await this.getSDKClient().txsHashGet(txId); if (!txResult || txResult.txhash === "") { throw new Error("transaction not found"); } const txs = getTxsFromHistory([txResult], AssetAtom); if (txs.length === 0) throw new Error("transaction not found"); return txs[0]; } /** * Transfer balances. * * @param {TxParams} params The transfer options. * @returns {TxHash} The transaction hash. */ async transfer({ walletIndex, asset = AssetAtom, amount, recipient, memo, gasLimit = new BigNumber(DEFAULT_GAS_LIMIT), feeAmount = DEFAULT_FEE, }: TxParams & { gasLimit?: BigNumber; feeAmount?: BaseAmount; }): Promise<TxHash> { const fromAddressIndex = walletIndex || 0; const denom = getDenom(asset); if (!denom) throw Error( `Invalid asset ${assetToString( asset )} - Only ATOM asset is currently supported to transfer` ); const fee = protoFee({ denom, amount: feeAmount, gasLimit }); return this.getSDKClient().transfer({ privkey: this.getPrivateKey(fromAddressIndex), from: this.getAddress(fromAddressIndex), to: recipient, amount, denom, memo, fee, }); } /** * Transfer offline balances. * * @param {TxOfflineParams} params The transfer offline options. * @returns {string} The signed transaction bytes. */ async transferOffline({ walletIndex, asset = AssetAtom, amount, recipient, memo, from_account_number, from_sequence, gasLimit = new BigNumber(DEFAULT_GAS_LIMIT), feeAmount = DEFAULT_FEE, }: TxOfflineParams): Promise<string> { const fromAddressIndex = walletIndex || 0; const denom = getDenom(asset); if (!denom) throw Error( `Invalid asset ${assetToString( asset )} - Only ATOM asset is currently supported to transfer` ); const fee = protoFee({ denom, amount: feeAmount, gasLimit }); return await this.getSDKClient().transferSignedOffline({ privkey: this.getPrivateKey(fromAddressIndex), from: this.getAddress(fromAddressIndex), from_account_number, from_sequence, to: recipient, amount, denom, memo, fee, }); } /** * Returns fees. * It tries to get chain fees from HermesChain `inbound_addresses` first * If it fails, it returns DEFAULT fees. * * @returns {Fees} Current fees */ async getFees(): Promise<Fees> { try { const feeRate = await this.getFeeRateFromHermeschain(); // convert decimal: 1e8 (HermesChain) to 1e6 (COSMOS) // Similar to `fromCosmosToHermeschain` in HermesNode const decimalDiff = COSMOS_DECIMAL - 8; /* HERMESCHAIN_DECIMAL */ const feeRate1e6 = feeRate * 10 ** decimalDiff; const fee = baseAmount(feeRate1e6, COSMOS_DECIMAL); return singleFee(FeeType.FlatFee, fee); } catch (error) { return singleFee(FeeType.FlatFee, DEFAULT_FEE); } } } export { Client };