UNPKG

@tedcryptoorg/cosmos-signer

Version:

Cosmos Signer - A library for signing transactions for Cosmos SDK chains

160 lines (159 loc) 6.64 kB
import axios from 'axios'; import { assertIsDeliverTxSuccess, GasPrice } from '@cosmjs/stargate'; import { toBase64 } from '@cosmjs/encoding'; import { Fee, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; import _ from "lodash"; import BigNumber from 'bignumber.js'; import { DefaultAdapter } from "./adapter/DefaultAdapter"; import { isEmpty } from "../util/TypeUtils"; import { sleep } from '../util/Sleep'; import { coin } from "../util/Coin"; import { parseTxResult } from '../util/TransactionHelper'; export class SigningClient { network; wallet; defaultGasPrice; defaultGasModifier; selectedAdapter; adapter; constructor(network, wallet, defaultGasPrice, defaultGasModifier = 1.5, selectedAdapter) { this.network = network; this.wallet = wallet; this.defaultGasPrice = defaultGasPrice; this.defaultGasModifier = defaultGasModifier; this.selectedAdapter = selectedAdapter; this.adapter = selectedAdapter ?? new DefaultAdapter(network, wallet); } async getAccount(address) { return await axios .get(this.network.restUrl + '/cosmos/auth/v1beta1/accounts/' + address) .then((res) => res.data.account) .then((value) => { if (!value) throw new Error('Failed to fetch account, please try again'); const baseAccount = 'BaseAccount' in value ? value.BaseAccount : ('baseAccount' in value ? value.baseAccount : value.base_account); if (!isEmpty(baseAccount)) { value = baseAccount; } const baseVestingAccount = 'BaseVestingAccount' in value ? value.BaseVestingAccount : ('baseVestingAccount' in value ? value.baseVestingAccount : value.base_vesting_account); if (!isEmpty(baseVestingAccount)) { value = baseVestingAccount; const baseAccount = 'BaseAccount' in value ? value.BaseAccount : ('baseAccount' in value ? value.baseAccount : value.base_account); if (!isEmpty(baseAccount)) { value = baseAccount; } } const nestedAccount = value.account; if (!isEmpty(nestedAccount)) { value = nestedAccount; } return value; }) .catch((error) => { if (error.response?.status === 404) { throw new Error('Account does not exist on chain'); } else { throw new Error('Unknown error when getting account'); } }); } calculateFee(gasLimit, gasPrice) { const processedGasPrice = typeof gasPrice === 'string' ? GasPrice.fromString(gasPrice) : gasPrice; const { denom, amount: gasPriceAmount } = processedGasPrice; const amount = new BigNumber(gasPriceAmount.toString()) .multipliedBy(new BigNumber(gasLimit.toString())) .integerValue(BigNumber.ROUND_CEIL); return { amount: [coin(amount, denom)], gas: BigInt(gasLimit) }; } getFee(gas, gasPrice) { if (gas === undefined || gas === 0) { gas = 200000; } const fee = this.calculateFee(gas, gasPrice ?? this.defaultGasPrice); return Fee.fromPartial({ amount: fee.amount, gasLimit: fee.gas }); } async signAndBroadcastWithoutBalanceCheck(address, msgs, gas, memo, gasPrice) { const defaultOptions = _.cloneDeep(this.wallet.getOptions()); this.wallet.setOptions({ sign: { disableBalanceCheck: true } }); try { return await this.signAndBroadcast(address, msgs, gas, memo, gasPrice); } finally { this.wallet.setOptions(defaultOptions); } } async signAndBroadcast(address, messages, gas, memo, gasPrice) { if (!gas) { gas = await this.simulate(address, messages, memo); } const fee = this.getFee(gas, gasPrice); const txBody = await this.sign(address, messages, fee, memo); return await this.broadcast(txBody); } async sign(address, messages, fee, memo) { const account = await this.getAccount(address); return await this.adapter.sign(account, messages, fee, memo); } async simulate(address, messages, memo, modifier) { const account = await this.getAccount(address); const fee = this.getFee(100_000); const txBody = await this.adapter.simulate(account, messages, fee, memo); try { const estimate = await axios.post(this.network.restUrl + '/cosmos/tx/v1beta1/simulate', { tx_bytes: toBase64(TxRaw.encode(txBody).finish()), }).then(el => el.data.gas_info.gas_used); return (parseInt(estimate * (modifier ?? this.defaultGasModifier))); } catch (error) { throw new Error(error.response?.data?.message || error.message); } } async broadcast(txBody) { const timeoutMs = this.network.txTimeout !== 0 ? this.network.txTimeout : 60_000; const pollIntervalMs = 3_000; let timedOut = false; const txPollTimeout = setTimeout(() => { timedOut = true; }, timeoutMs); const pollForTx = async (txId) => { if (timedOut) { throw new Error(`Transaction with ID ${txId} was submitted but was not yet found on the chain. You might want to check later. There was a wait of ${timeoutMs / 1000} seconds.`); } await sleep(pollIntervalMs); try { const response = await axios.get(this.network.restUrl + '/cosmos/tx/v1beta1/txs/' + txId); return parseTxResult(response.data.tx_response); } catch { return await pollForTx(txId); } }; const response = await axios.post(this.network.restUrl + '/cosmos/tx/v1beta1/txs', { tx_bytes: toBase64(TxRaw.encode(txBody).finish()), mode: "BROADCAST_MODE_SYNC" }); const result = parseTxResult(response.data.tx_response); assertIsDeliverTxSuccess(result); return await pollForTx(result.transactionHash).then((value) => { clearTimeout(txPollTimeout); assertIsDeliverTxSuccess(value); return value; }, (error) => { clearTimeout(txPollTimeout); return error; }); } }