@tedcryptoorg/cosmos-signer
Version:
Cosmos Signer - A library for signing transactions for Cosmos SDK chains
160 lines (159 loc) • 6.64 kB
JavaScript
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;
});
}
}