@chorus-one/cosmos
Version:
All-in-one toolkit for building staking dApps on Cosmos SDK based networks
271 lines (270 loc) • 12.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.publicKeyToEthBasedAddress = exports.publicKeyToAddress = exports.getEthermintAccount = exports.getAccount = exports.genSignedTx = exports.genSignDocSignature = exports.genSignableTx = exports.getGas = exports.genBeginRedelegateMsg = exports.genDelegateOrUndelegateMsg = exports.genWithdrawRewardsMsg = exports.denomToMacroAmount = exports.macroToDenomAmount = void 0;
const stargate_1 = require("@cosmjs/stargate");
const tx_1 = require("cosmjs-types/cosmos/tx/v1beta1/tx");
const encoding_1 = require("@cosmjs/encoding");
const signing_1 = require("cosmjs-types/cosmos/tx/signing/v1beta1/signing");
const tx_2 = require("cosmjs-types/cosmos/staking/v1beta1/tx");
const tx_3 = require("cosmjs-types/cosmos/distribution/v1beta1/tx");
const math_1 = require("@cosmjs/math");
const crypto_1 = require("@cosmjs/crypto");
const secp256k1_1 = require("secp256k1");
const utils_1 = require("@chorus-one/utils");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const proto_signing_1 = require("@cosmjs/proto-signing");
const amino_1 = require("@cosmjs/amino");
const amino_2 = require("@cosmjs/amino");
function createDefaultTypes() {
return {
...(0, stargate_1.createAuthzAminoConverters)(),
...(0, stargate_1.createBankAminoConverters)(),
...(0, stargate_1.createDistributionAminoConverters)(),
...(0, stargate_1.createGovAminoConverters)(),
...(0, stargate_1.createStakingAminoConverters)(),
...(0, stargate_1.createIbcAminoConverters)(),
...(0, stargate_1.createVestingAminoConverters)()
};
}
function toCoin(amount, // in lowest denom (e.g. uatom)
expectedDenom // e.g. uatom
) {
const total = amount.match(/\d+/)?.at(0);
const denom = amount.match(/[^\d.-]+/)?.at(0);
if (total === undefined) {
throw Error('failed to extract total amount of tokens from: ' + amount);
}
if (denom !== undefined && denom !== expectedDenom) {
throw new Error('denom mismatch, expected: ' + expectedDenom + ' got: ' + denom);
}
return (0, stargate_1.coin)(total, expectedDenom);
}
function macroToDenomAmount(amount, // in macro denom (e.g. ATOM)
denomMultiplier) {
(0, utils_1.checkMaxDecimalPlaces)(denomMultiplier);
if (BigInt(denomMultiplier) === BigInt(0)) {
throw new Error('denomMultiplier cannot be 0');
}
if ((0, bignumber_js_1.default)(amount).isNaN()) {
throw new Error('invalid amount: ' + amount + ' failed to parse to number');
}
const macroAmount = (0, bignumber_js_1.default)(denomMultiplier).multipliedBy(amount);
if (macroAmount.isNegative()) {
throw new Error('amount cannot be negative');
}
const decimalPlaces = macroAmount.decimalPlaces();
if (decimalPlaces !== null && decimalPlaces > 0) {
throw new Error(`exceeded maximum denominator precision, amount: ${macroAmount.toString()}, precision: .${macroAmount.precision()}`);
}
return macroAmount.toString(10);
}
exports.macroToDenomAmount = macroToDenomAmount;
function denomToMacroAmount(amount, // in denom (e.g. uatom, adydx)
denomMultiplier) {
(0, utils_1.checkMaxDecimalPlaces)(denomMultiplier);
if (BigInt(denomMultiplier) === BigInt(0)) {
throw new Error('denomMultiplier cannot be 0');
}
if ((0, bignumber_js_1.default)(amount).isNaN()) {
throw new Error('invalid amount: ' + amount + ' failed to parse to number');
}
return (0, bignumber_js_1.default)(amount).dividedBy(denomMultiplier).toString(10);
}
exports.denomToMacroAmount = denomToMacroAmount;
function genWithdrawRewardsMsg(delegatorAddress, validatorAddress) {
const withdrawRewardsMsg = {
typeUrl: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward',
value: tx_3.MsgWithdrawDelegatorReward.fromPartial({
delegatorAddress: delegatorAddress,
validatorAddress: validatorAddress
})
};
return withdrawRewardsMsg;
}
exports.genWithdrawRewardsMsg = genWithdrawRewardsMsg;
function genDelegateOrUndelegateMsg(networkConfig, msgType, delegatorAddress, validatorAddress, amount // in lowest denom (e.g. uatom)
) {
const coins = toCoin(amount, networkConfig.denom);
if (!['delegate', 'undelegate'].some((x) => x === msgType)) {
throw new Error('invalid type: ' + msgType);
}
const delegateMsg = {
typeUrl: msgType === 'delegate' ? '/cosmos.staking.v1beta1.MsgDelegate' : '/cosmos.staking.v1beta1.MsgUndelegate',
value: tx_2.MsgDelegate.fromPartial({
delegatorAddress: delegatorAddress,
validatorAddress: validatorAddress,
amount: coins
})
};
return delegateMsg;
}
exports.genDelegateOrUndelegateMsg = genDelegateOrUndelegateMsg;
function genBeginRedelegateMsg(networkConfig, delegatorAddress, validatorSrcAddress, validatorDstAddress, amount // in lowest denom (e.g. uatom)
) {
const coins = toCoin(amount, networkConfig.denom);
const beginRedelegateMsg = {
typeUrl: '/cosmos.staking.v1beta1.MsgBeginRedelegate',
value: tx_2.MsgBeginRedelegate.fromPartial({
delegatorAddress: delegatorAddress,
validatorSrcAddress: validatorSrcAddress,
validatorDstAddress,
amount: coins
})
};
return beginRedelegateMsg;
}
exports.genBeginRedelegateMsg = genBeginRedelegateMsg;
async function getGas(client, networkConfig, signerAddress, signer, msg, memo) {
const extraGas = networkConfig.extraGas ? Number(networkConfig.extraGas) : 0;
if (typeof networkConfig.gas === 'number' && networkConfig.gas > 0) {
return Number(networkConfig.gas) + extraGas;
}
if (networkConfig.gas !== 'auto') {
throw new Error('gas must be either a number or "auto"');
}
const registry = new proto_signing_1.Registry(stargate_1.defaultRegistryTypes);
const anyMsgs = [registry.encodeAsAny(msg)];
const signerPubkey = await signer.getPublicKey(signerAddress);
const pk = crypto_1.Secp256k1.compressPubkey(signerPubkey);
const pubkey = (0, amino_1.encodeSecp256k1Pubkey)(pk);
const { sequence } = await client.getSequence(signerAddress);
const { gasInfo } = await client.getCosmosQueryClient().tx.simulate(anyMsgs, memo ?? '', pubkey, sequence);
if (gasInfo?.gasUsed === undefined) {
throw new Error('failed to get gas estimate');
}
// it's highly unlikely gas will reach the boundry of Number.MAX_SAFE_INTEGER
return (0, bignumber_js_1.default)(gasInfo.gasUsed.toString(10), 10).toNumber() + extraGas;
}
exports.getGas = getGas;
async function genSignableTx(networkConfig, chainID, msg, accountNumber, accountSequence, gas, memo) {
const aminoTypes = new stargate_1.AminoTypes(createDefaultTypes());
const feeAmt = networkConfig.fee
? (0, bignumber_js_1.default)(networkConfig.fee)
: (0, bignumber_js_1.default)(gas).multipliedBy(networkConfig.gasPrice);
const fee = {
amount: [(0, stargate_1.coin)(feeAmt.toFixed(0, bignumber_js_1.default.ROUND_CEIL).toString(), networkConfig.denom)],
gas: gas.toString(10)
};
const signDoc = (0, amino_1.makeSignDoc)([msg].map((msg) => aminoTypes.toAmino(msg)), fee, chainID, memo, accountNumber, accountSequence);
return signDoc;
}
exports.genSignableTx = genSignableTx;
async function genSignDocSignature(signer, signerAccount, signDoc, isEVM) {
// The LCD doesn't have to return a public key for an acocunt, therefore
// we do a best effort check to assert the pubkey type is ethsecp256k1
if (isEVM && signerAccount.pubkey !== undefined) {
if (!signerAccount.pubkey?.type.toLowerCase().includes('ethsecp256k1')) {
throw new Error('signer account pubkey type is not ethsecp256k1');
}
}
const msg = isEVM ? (0, crypto_1.keccak256)((0, amino_1.serializeSignDoc)(signDoc)) : new crypto_1.Sha256((0, amino_1.serializeSignDoc)(signDoc)).digest();
const message = Buffer.from(msg).toString('hex');
const note = (0, utils_1.SafeJSONStringify)(signDoc, 2);
const data = { signDoc };
const signerAddress = signerAccount.address;
return await signer.sign(signerAddress, { message, data }, { note });
}
exports.genSignDocSignature = genSignDocSignature;
function genSignedTx(signDoc, signature, pk, pkType) {
const signMode = signing_1.SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
// cosmos signature doesn't use `v` field, only .r and .s
const signatureBytes = new Uint8Array([
...Buffer.from(signature.r ?? '', 'hex'),
...Buffer.from(signature.s ?? '', 'hex')
]);
const secppk = (0, amino_1.encodeSecp256k1Pubkey)(pk);
const pubkey = (0, proto_signing_1.encodePubkey)(secppk);
if (pkType !== undefined) {
pubkey.typeUrl = pkType;
}
// https://github.com/cosmos/cosmjs/blob/main/packages/stargate/src/signingstargateclient.ts#L331
const aminoTypes = new stargate_1.AminoTypes(createDefaultTypes());
const signedTxBody = {
messages: signDoc.msgs.map((msg) => aminoTypes.fromAmino(msg)),
memo: signDoc.memo
};
const signedTxBodyEncodeObject = {
typeUrl: '/cosmos.tx.v1beta1.TxBody',
value: signedTxBody
};
const registry = new proto_signing_1.Registry(stargate_1.defaultRegistryTypes);
const signedTxBodyBytes = registry.encode(signedTxBodyEncodeObject);
const signedGasLimit = math_1.Int53.fromString(signDoc.fee.gas).toNumber();
const signedSequence = math_1.Int53.fromString(signDoc.sequence).toNumber();
const signedAuthInfoBytes = (0, proto_signing_1.makeAuthInfoBytes)([{ pubkey, sequence: signedSequence }], signDoc.fee.amount, signedGasLimit, signDoc.fee.granter, signDoc.fee.payer, signMode);
const cosmosSignature = (0, amino_1.encodeSecp256k1Signature)(pk, signatureBytes);
const txRaw = tx_1.TxRaw.fromPartial({
bodyBytes: signedTxBodyBytes,
authInfoBytes: signedAuthInfoBytes,
signatures: [(0, encoding_1.fromBase64)(cosmosSignature.signature)]
});
return txRaw;
}
exports.genSignedTx = genSignedTx;
/** @ignore */
async function getAccount(client, lcdUrl, address) {
// try to get account from the RPC endpoint
const cosmosAccount = await client.getAccount(address);
if (cosmosAccount == null) {
throw new Error('failed to query account: ' + address + ' are you sure the account exists?');
}
// if this is an ethermint / evm account, we need to fetch
// account information from the LCD endpoint (due to lack of codec)
if (cosmosAccount.address != 'ethermint_account') {
return cosmosAccount;
}
return await getEthermintAccount(lcdUrl, address);
}
exports.getAccount = getAccount;
/** @ignore */
async function getEthermintAccount(lcdUrl, address) {
const r = await fetch(lcdUrl + '/cosmos/auth/v1beta1/accounts/' + address);
if (r.status !== 200) {
throw new Error('failed to query account with LCD endpoint, address: ' + address + ' are you sure the account exists?');
}
const data = await r.json();
const base = data['account']['base_account'];
const pubkey = base['pub_key'] === null
? {
type: guessPubkeyType(data['account']['@type']),
value: null
}
: {
type: base['pub_key']['@type'],
value: base['pub_key']['key']
};
return {
address: base['address'],
pubkey,
accountNumber: parseInt(base['account_number']),
sequence: parseInt(base['sequence'])
};
}
exports.getEthermintAccount = getEthermintAccount;
/** @ignore */
function publicKeyToAddress(pk, bechPrefix) {
const pkCompressed = Buffer.from((0, secp256k1_1.publicKeyConvert)(pk, true));
return (0, encoding_1.toBech32)(bechPrefix, (0, amino_2.rawSecp256k1PubkeyToRawAddress)(pkCompressed));
}
exports.publicKeyToAddress = publicKeyToAddress;
/** @ignore */
function publicKeyToEthBasedAddress(pk, bechPrefix) {
const pkUncompressed = Buffer.from((0, secp256k1_1.publicKeyConvert)(pk, false));
const hash = (0, crypto_1.keccak256)(pkUncompressed.subarray(1));
const ethAddress = hash.slice(-20);
return (0, encoding_1.toBech32)(bechPrefix, ethAddress);
}
exports.publicKeyToEthBasedAddress = publicKeyToEthBasedAddress;
/** @ignore */
function guessPubkeyType(accountType) {
if (accountType.startsWith('/ethermint')) {
return '/ethermint.crypto.v1.ethsecp256k1.PubKey';
}
if (accountType.startsWith('/injective')) {
return '/injective.crypto.v1beta1.ethsecp256k1.PubKey';
}
throw new Error('unknown account type: ' + accountType);
}