UNPKG

@chorus-one/cosmos

Version:

All-in-one toolkit for building staking dApps on Cosmos SDK based networks

271 lines (270 loc) 12.9 kB
"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); }