@xchainjs/xchain-cosmos
Version:
Custom Cosmos client and utilities used by XChainJS clients
607 lines (590 loc) • 25.5 kB
JavaScript
;
var encoding = require('@cosmjs/encoding');
var protoSigning = require('@cosmjs/proto-signing');
var stargate = require('@cosmjs/stargate');
var xchainCosmosSdk = require('@xchainjs/xchain-cosmos-sdk');
var xchainCrypto = require('@xchainjs/xchain-crypto');
var base = require('@scure/base');
var bip32 = require('@scure/bip32');
var crypto = require('crypto');
var secp = require('@bitcoin-js/tiny-secp256k1-asmjs');
var xchainClient = require('@xchainjs/xchain-client');
var xchainUtil = require('@xchainjs/xchain-util');
var BigNumber = require('bignumber.js');
var tx_js = require('cosmjs-types/cosmos/tx/v1beta1/tx.js');
var axios = require('axios');
var ledgerAmino = require('@cosmjs/ledger-amino');
var CosmosApp = require('@ledgerhq/hw-app-cosmos');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var secp__namespace = /*#__PURE__*/_interopNamespace(secp);
var BigNumber__default = /*#__PURE__*/_interopDefault(BigNumber);
var axios__default = /*#__PURE__*/_interopDefault(axios);
var CosmosApp__default = /*#__PURE__*/_interopDefault(CosmosApp);
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
// Explorer URLs for Mainnet and Testnet
const MAINNET_EXPLORER_URL = 'https://www.mintscan.io/cosmos';
const TESTNET_EXPLORER_URL = 'https://explorer.theta-testnet.polypore.xyz';
/**
* Function to retrieve default RPC client URLs for different networks.
* Returns a mapping of network to its corresponding RPC client URL.
*
* @returns {Record<Network, string>} Default RPC client URLs for different networks.
*/
const getDefaultClientUrls = () => {
return {
[xchainClient.Network.Testnet]: ['https://rpc.sentry-02.theta-testnet.polypore.xyz'],
[xchainClient.Network.Stagenet]: ['https://cosmos-rpc.publicnode.com', 'https://rpc.cosmos.directory/cosmoshub'],
[xchainClient.Network.Mainnet]: ['https://cosmos-rpc.publicnode.com', 'https://rpc.cosmos.directory/cosmoshub'],
};
};
/**
* Function to retrieve default root derivation paths for different networks.
* Returns a mapping of network to its corresponding root derivation path.
*
* @returns {RootDerivationPaths} Default root derivation paths for different networks.
*/
const getDefaultRootDerivationPaths = () => ({
[xchainClient.Network.Mainnet]: `m/44'/118'/0'/0/`,
[xchainClient.Network.Testnet]: `m/44'/118'/0'/0/`,
[xchainClient.Network.Stagenet]: `m/44'/118'/0'/0/`,
});
/**
* Function to retrieve default explorer URLs for different networks.
* Returns a mapping of network to its corresponding explorer URL.
*
* @returns {Record<Network, string>} Default explorer URLs for different networks.
*/
const getDefaultExplorers = () => ({
[xchainClient.Network.Mainnet]: MAINNET_EXPLORER_URL,
[xchainClient.Network.Testnet]: TESTNET_EXPLORER_URL,
[xchainClient.Network.Stagenet]: MAINNET_EXPLORER_URL,
});
/**
* Function to get the denomination of a given asset.
* Currently only supports 'ATOM'.
*
* @param {CompatibleAsset} asset The asset for which denomination is requested.
* @returns {string} The denomination of the given asset, or null if not supported.
*/
const getDenom = (asset) => {
if (xchainUtil.eqAsset(asset, AssetATOM))
return ATOM_DENOM;
return null;
};
/**
* Asynchronously fetches the chain ID from a node URL using an Axios HTTP GET request.
*
* @param {string} url The URL of the node.
* @returns {string} A Promise that resolves to the chain ID.
*/
const getChainId = (url) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const { data } = yield axios__default.default.get(`${url}/node_info`);
return ((_a = data === null || data === void 0 ? void 0 : data.node_info) === null || _a === void 0 ? void 0 : _a.network) || Promise.reject('Could not parse chain id');
});
/**
* Function to get the address prefix based on the network.
*
* @returns {string} The address prefix based on the network.
*/
const getPrefix = () => 'cosmos';
/**
* Number of decimals for the native asset of the Cosmos network.
*/
const COSMOS_DECIMAL = 6;
/**
* Default gas limit for Cosmos transactions.
* As defined in Cosmosstation's web wallet.
* @see https://github.com/cosmostation/web-wallet-ts-react/blob/4d78718b613defbd6c92079b33aa8ce9f86d597c/src/constants/chain.ts#L76
*/
const DEFAULT_GAS_LIMIT = '200000';
/**
* Default fee for Cosmos transactions.
* As defined in Cosmosstation's web wallet.
* @see https://github.com/cosmostation/web-wallet-ts-react/blob/4d78718b613defbd6c92079b33aa8ce9f86d597c/src/constants/chain.ts#L66
*/
const DEFAULT_FEE = xchainUtil.baseAmount(5000, COSMOS_DECIMAL);
/**
* Chain identifier for the Cosmos network.
*/
const GAIAChain = 'GAIA';
/**
* Base "chain" asset on the Cosmos mainnet.
* Based on the definition in Thorchain `common`.
* @see https://gitlab.com/thorchain/thornode/-/blob/master/common/asset.go#L12-24
*/
const AssetATOM = { chain: GAIAChain, symbol: 'ATOM', ticker: 'ATOM', type: xchainUtil.AssetType.NATIVE };
/**
* Denomination for the native Cosmos asset.
*/
const ATOM_DENOM = 'uatom';
/**
* Message type URL used to make transactions on the Cosmos network.
*/
const MSG_SEND_TYPE_URL = '/cosmos.bank.v1beta1.MsgSend';
/**
* Default parameters for Cosmos SDK client configuration.
*/
const defaultClientConfig = {
chain: GAIAChain,
network: xchainClient.Network.Mainnet,
clientUrls: getDefaultClientUrls(),
rootDerivationPaths: getDefaultRootDerivationPaths(),
prefix: 'cosmos',
defaultDecimals: 6,
defaultFee: DEFAULT_FEE,
baseDenom: 'uatom',
registryTypes: [],
};
/**
* Cosmos client class extending the Cosmos SDK client.
*/
class Client extends xchainCosmosSdk.Client {
/**
* Constructor for the Cosmos client.
* @param {CosmosClientParams} config Configuration parameters for the client.
*/
constructor(config = defaultClientConfig) {
super(Object.assign(Object.assign({}, defaultClientConfig), config));
}
/**
* Get information about the client's native asset.
* @returns {AssetInfo} Information about the native asset.
*/
getAssetInfo() {
return {
asset: AssetATOM,
decimal: COSMOS_DECIMAL,
};
}
/**
* Get the number of decimals for a given asset.
* @param {CompatibleAsset} asset The asset to get the decimals for.
* @returns {number} The number of decimals.
*/
getAssetDecimals(asset) {
if (xchainUtil.eqAsset(asset, AssetATOM))
return COSMOS_DECIMAL;
return this.defaultDecimals;
}
/**
* Get the explorer URL.
* @returns {string} The explorer URL.
*/
getExplorerUrl() {
return getDefaultExplorers()[this.network];
}
/**
* Get the explorer url for the given address.
*
* @param {Address} address
* @returns {string} The explorer url for the given address.
*/
getExplorerAddressUrl(address) {
return `${this.getExplorerUrl()}/accounts/${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) {
return `${this.getExplorerUrl()}/transactions/${txID}`;
}
/**
* Get the asset from a given denomination.
* @param {string} denom The denomination to convert.
* @returns {CompatibleAsset | null} The asset corresponding to the given denomination.
*/
assetFromDenom(denom) {
if (denom === this.getDenom(AssetATOM))
return AssetATOM;
// IBC assets
if (denom.startsWith('ibc/'))
// Note: Don't use `assetFromString` here, it will interpret `/` as synth
return {
chain: GAIAChain,
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: '',
type: xchainUtil.AssetType.TOKEN,
};
return null;
}
/**
* Get the denomination from a given asset.
* @param {Asset} asset The asset to get the denomination for.
* @returns {string | null} The denomination of the given asset.
*/
getDenom(asset) {
return getDenom(asset);
}
/**
* Get the current fees.
* If possible, fetches the fees from THORChain's `inbound_addresses`.
* Otherwise, returns default fees.
* @returns {Fees} The current fees.
*/
getFees() {
return __awaiter(this, void 0, void 0, function* () {
try {
const feeRate = yield this.getFeeRateFromThorchain();
// convert decimal: 1e8 (THORChain) to 1e6 (COSMOS)
// Similar to `fromCosmosToThorchain` in THORNode
// @see https://gitlab.com/thorchain/thornode/-/blob/e787022028f662b3a7c594e4a65aca618caa359c/bifrost/pkg/chainclients/gaia/util.go#L86
const decimalDiff = COSMOS_DECIMAL - 8; /* THORCHAIN_DECIMAL */
const feeRate1e6 = feeRate * Math.pow(10, decimalDiff);
const fee = xchainUtil.baseAmount(feeRate1e6, COSMOS_DECIMAL);
return xchainClient.singleFee(xchainClient.FeeType.FlatFee, fee);
}
catch (_error) {
return xchainClient.singleFee(xchainClient.FeeType.FlatFee, DEFAULT_FEE);
}
});
}
/**
* Prepare a transaction for signing.
* @param {TxParams & { sender: Address }} params Transaction parameters including sender address.
* @returns {PreparedTx} The prepared transaction.
* @throws {"Invalid sender address"} Thrown if the sender address is invalid.
* @throws {"Invalid recipient address"} Thrown if the recipient address is invalid.
* @throws {"Invalid asset"} Thrown if the asset is invalid or not supported.
*/
prepareTx(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, recipient, asset, amount, memo, }) {
if (!this.validateAddress(sender))
throw Error('Invalid sender address');
if (!this.validateAddress(recipient))
throw Error('Invalid recipient address');
const denom = this.getDenom(asset || this.getAssetInfo().asset);
if (!denom)
throw Error(`Invalid asset ${asset === null || asset === void 0 ? void 0 : asset.symbol} - Only ${this.baseDenom} asset is currently supported to transfer`);
const demonAmount = { amount: amount.amount().toString(), denom };
const txBody = {
typeUrl: '/cosmos.tx.v1beta1.TxBody',
value: {
messages: [
{
typeUrl: this.getMsgTypeUrlByType(xchainCosmosSdk.MsgTypes.TRANSFER),
value: {
fromAddress: sender,
toAddress: recipient,
amount: [demonAmount],
},
},
],
memo: memo,
},
};
const rawTx = tx_js.TxRaw.fromPartial({
bodyBytes: this.registry.encode(txBody),
});
return { rawUnsignedTx: encoding.toBase64(tx_js.TxRaw.encode(rawTx).finish()) };
});
}
/**
* Creates and signs a transaction without broadcasting it.
* @deprecated Use prepareTx instead.
*/
transferOffline(_a) {
return __awaiter(this, arguments, void 0, function* ({ walletIndex = 0, recipient, asset, amount, memo, gasLimit = new BigNumber__default.default(DEFAULT_GAS_LIMIT), }) {
const sender = yield this.getAddressAsync(walletIndex);
const { rawUnsignedTx } = yield this.prepareTx({
sender,
recipient: recipient,
asset: asset,
amount: amount,
memo: memo,
});
const unsignedTx = protoSigning.decodeTxRaw(encoding.fromBase64(rawUnsignedTx));
const signer = yield protoSigning.DirectSecp256k1HdWallet.fromMnemonic(this.phrase, {
prefix: this.prefix,
hdPaths: [xchainCosmosSdk.makeClientPath(this.getFullDerivationPath(walletIndex))],
});
const rawTx = yield this.roundRobinSign(sender, unsignedTx, signer, gasLimit);
return encoding.toBase64(tx_js.TxRaw.encode(rawTx).finish());
});
}
/**
* Get the address prefix based on the network.
* @param {Network} network The network of which return the prefix
* @returns the address prefix
*/
getPrefix() {
return getPrefix();
}
/**
* Get the message type URL by message type.
* @param {MsgTypes} msgType The message type.
* @returns {string} The message type URL.
*/
getMsgTypeUrlByType(msgType) {
const messageTypeUrls = {
[xchainCosmosSdk.MsgTypes.TRANSFER]: MSG_SEND_TYPE_URL,
};
return messageTypeUrls[msgType];
}
/**
* Returns the standard fee used by the client for an asset.
* @param {Asset} asset The asset to retrieve the fee for.
* @returns {StdFee} The standard fee.
*/
getStandardFee(asset) {
const denom = this.getDenom(asset);
const defaultGasPrice = stargate.GasPrice.fromString(`0.006${denom}`);
return stargate.calculateFee(150000, defaultGasPrice);
}
/**
* Sign a transaction making a round robin over the clients urls provided to the client
*
* @param {string} sender Sender address
* @param {DecodedTxRaw} unsignedTx Unsigned transaction
* @param {DirectSecp256k1HdWallet} signer Signer
* @param {BigNumber} gasLimit Transaction gas limit
* @returns {TxRaw} The raw signed transaction
*/
roundRobinSign(sender, unsignedTx, signer, gasLimit) {
return __awaiter(this, void 0, void 0, function* () {
for (const url of this.clientUrls[this.network]) {
try {
const signingClient = yield stargate.SigningStargateClient.connectWithSigner(url, signer, {
registry: this.registry,
});
const messages = unsignedTx.body.messages.map((message) => {
return { typeUrl: this.getMsgTypeUrlByType(xchainCosmosSdk.MsgTypes.TRANSFER), value: signingClient.registry.decode(message) };
});
return yield signingClient.sign(sender, messages, {
amount: [],
gas: gasLimit.toString(),
}, unsignedTx.body.memo);
}
catch (_a) { }
}
throw Error('No clients available. Can not sign transaction');
});
}
}
class ClientKeystore extends Client {
constructor(params = defaultClientConfig) {
super(params);
}
/**
* Asynchronous version of getAddress method.
* @param {number} index Derivation path index of the address to be generated.
* @returns {string} A promise that resolves to the generated address.
*/
getAddressAsync() {
return __awaiter(this, arguments, void 0, function* (index = 0) {
return this.getAddress(index);
});
}
/**
* Get the address derived from the provided phrase.
* @param {number | undefined} walletIndex The index of the address derivation path. Default is 0.
* @returns {string} The user address at the specified walletIndex.
*/
getAddress(walletIndex) {
const seed = xchainCrypto.getSeed(this.phrase);
const node = bip32.HDKey.fromMasterSeed(new Uint8Array(seed));
const child = node.derive(this.getFullDerivationPath(walletIndex || 0));
if (!child.privateKey)
throw new Error('child does not have a privateKey');
// TODO: Make this method async and use CosmosJS official address generation strategy
const pubKey = secp__namespace.pointFromScalar(child.privateKey, true);
if (!pubKey)
throw new Error('pubKey is null');
const rawAddress = this.hash160(pubKey);
const words = base.bech32.toWords(new Uint8Array(rawAddress));
const address = base.bech32.encode(this.prefix, words);
return address;
}
transfer(params) {
return __awaiter(this, void 0, void 0, function* () {
const sender = yield this.getAddressAsync(params.walletIndex || 0);
const { rawUnsignedTx } = yield this.prepareTx({
sender,
recipient: params.recipient,
asset: params.asset,
amount: params.amount,
memo: params.memo,
});
const unsignedTx = protoSigning.decodeTxRaw(encoding.fromBase64(rawUnsignedTx));
const signer = yield protoSigning.DirectSecp256k1HdWallet.fromMnemonic(this.phrase, {
prefix: this.prefix,
hdPaths: [xchainCosmosSdk.makeClientPath(this.getFullDerivationPath(params.walletIndex || 0))],
});
const tx = yield this.roundRobinSignAndBroadcast(sender, unsignedTx, signer);
return tx.transactionHash;
});
}
/**
* Hashes a buffer using SHA256 followed by RIPEMD160 or RMD160.
* @param {Uint8Array} buffer The buffer to hash
* @returns {Uint8Array} The hashed buffer
*/
hash160(buffer) {
const sha256Hash = crypto.createHash('sha256').update(buffer).digest();
try {
return crypto.createHash('rmd160').update(sha256Hash).digest();
}
catch (_err) {
return crypto.createHash('ripemd160').update(sha256Hash).digest();
}
}
/**
* Sign a transaction making a round robin over the clients urls provided to the client
*
* @param {string} sender Sender address
* @param {DecodedTxRaw} unsignedTx Unsigned transaction
* @param {DirectSecp256k1HdWallet} signer Signer
* @returns {DeliverTxResponse} The transaction broadcasted
*/
roundRobinSignAndBroadcast(sender, unsignedTx, signer) {
return __awaiter(this, void 0, void 0, function* () {
for (const url of this.clientUrls[this.network]) {
try {
const signingClient = yield stargate.SigningStargateClient.connectWithSigner(url, signer, {
registry: this.registry,
});
const messages = unsignedTx.body.messages.map((message) => {
return { typeUrl: this.getMsgTypeUrlByType(xchainCosmosSdk.MsgTypes.TRANSFER), value: signingClient.registry.decode(message) };
});
const tx = yield signingClient.signAndBroadcast(sender, messages, this.getStandardFee(this.getAssetInfo().asset), unsignedTx.body.memo);
return tx;
}
catch (_a) { }
}
throw Error('No clients available. Can not sign and broadcast transaction');
});
}
}
class ClientLedger extends Client {
constructor(params) {
super(params);
this.transport = params.transport;
}
// Get the current address synchronously
getAddress() {
throw Error('Sync method not supported for Ledger');
}
getApp() {
return __awaiter(this, void 0, void 0, function* () {
if (this.app) {
return this.app;
}
this.app = new CosmosApp__default.default(this.transport);
return this.app;
});
}
getAddressAsync() {
return __awaiter(this, arguments, void 0, function* (index = 0, verify = false) {
const app = yield this.getApp();
const result = yield app.getAddress(this.getFullDerivationPath(index), this.prefix, verify);
return result.address;
});
}
/**
* Transfer Cosmos.
*
* @param {TxParams} params The transfer options including the fee rate.
* @returns {Promise<TxHash|string>} A promise that resolves to the transaction hash or an error message.
*/
transfer(_a) {
return __awaiter(this, arguments, void 0, function* ({ walletIndex, asset, amount, recipient, memo, }) {
const ledgerSigner = new ledgerAmino.LedgerSigner(this.transport);
const fromAddress = yield this.getAddressAsync(walletIndex);
const { rawUnsignedTx } = yield this.prepareTx({ asset, amount, recipient, memo, sender: fromAddress });
const unsignedTx = protoSigning.decodeTxRaw(encoding.fromBase64(rawUnsignedTx));
const tx = yield this.roundRobinSignAndBroadcast(fromAddress, unsignedTx, ledgerSigner);
return tx.transactionHash;
});
}
/**
* Sign a transaction making a round robin over the clients urls provided to the client
*
* @param {string} sender Sender address
* @param {DecodedTxRaw} unsignedTx Unsigned transaction
* @param {OfflineAminoSigner} signer Signer
* @returns {DeliverTxResponse} The transaction broadcasted
*/
roundRobinSignAndBroadcast(sender, unsignedTx, signer) {
return __awaiter(this, void 0, void 0, function* () {
for (const url of this.clientUrls[this.network]) {
try {
const signingClient = yield stargate.SigningStargateClient.connectWithSigner(url, signer, {
registry: this.registry,
});
const messages = unsignedTx.body.messages.map((message) => {
return { typeUrl: this.getMsgTypeUrlByType(xchainCosmosSdk.MsgTypes.TRANSFER), value: signingClient.registry.decode(message) };
});
const tx = yield signingClient.signAndBroadcast(sender, messages, this.getStandardFee(this.getAssetInfo().asset), unsignedTx.body.memo);
return tx;
}
catch (_a) { }
}
throw Error('No clients available. Can not sign and broadcast transaction');
});
}
}
exports.ATOM_DENOM = ATOM_DENOM;
exports.AssetATOM = AssetATOM;
exports.COSMOS_DECIMAL = COSMOS_DECIMAL;
exports.Client = ClientKeystore;
exports.ClientKeystore = ClientKeystore;
exports.ClientLedger = ClientLedger;
exports.DEFAULT_FEE = DEFAULT_FEE;
exports.DEFAULT_GAS_LIMIT = DEFAULT_GAS_LIMIT;
exports.GAIAChain = GAIAChain;
exports.MSG_SEND_TYPE_URL = MSG_SEND_TYPE_URL;
exports.defaultClientConfig = defaultClientConfig;
exports.getChainId = getChainId;
exports.getDefaultClientUrls = getDefaultClientUrls;
exports.getDefaultExplorers = getDefaultExplorers;
exports.getDefaultRootDerivationPaths = getDefaultRootDerivationPaths;
exports.getDenom = getDenom;
exports.getPrefix = getPrefix;