From c8eab8fe17f7d481317b18695e35dc04609ddfe0 Mon Sep 17 00:00:00 2001 From: Hitansh Madan Date: Sun, 10 Dec 2023 14:29:57 +0530 Subject: [PATCH] refactor(sdk-coin-atom): use abstract-cosmos Ticket: WIN-1192 --- modules/abstract-cosmos/src/cosmosCoin.ts | 1 + modules/sdk-coin-atom/package.json | 9 +- modules/sdk-coin-atom/src/atom.ts | 654 +----------------- .../src/lib/StakingActivateBuilder.ts | 29 - .../src/lib/StakingDeactivateBuilder.ts | 29 - .../src/lib/StakingWithdrawRewardsBuilder.ts | 29 - modules/sdk-coin-atom/src/lib/constants.ts | 7 - modules/sdk-coin-atom/src/lib/iface.ts | 53 -- modules/sdk-coin-atom/src/lib/index.ts | 14 +- modules/sdk-coin-atom/src/lib/keyPair.ts | 50 +- modules/sdk-coin-atom/src/lib/transaction.ts | 309 --------- .../src/lib/transactionBuilder.ts | 250 ------- .../src/lib/transactionBuilderFactory.ts | 37 +- .../sdk-coin-atom/src/lib/transferBuilder.ts | 29 - modules/sdk-coin-atom/src/lib/utils.ts | 637 +---------------- modules/sdk-coin-atom/src/tatom.ts | 14 - modules/sdk-coin-atom/test/unit/atom.ts | 11 +- .../sdk-coin-atom/test/unit/transaction.ts | 11 +- modules/sdk-coin-atom/tsconfig.json | 3 + modules/statics/src/base.ts | 2 +- 20 files changed, 71 insertions(+), 2107 deletions(-) delete mode 100644 modules/sdk-coin-atom/src/lib/StakingActivateBuilder.ts delete mode 100644 modules/sdk-coin-atom/src/lib/StakingDeactivateBuilder.ts delete mode 100644 modules/sdk-coin-atom/src/lib/StakingWithdrawRewardsBuilder.ts delete mode 100644 modules/sdk-coin-atom/src/lib/iface.ts delete mode 100644 modules/sdk-coin-atom/src/lib/transaction.ts delete mode 100644 modules/sdk-coin-atom/src/lib/transactionBuilder.ts delete mode 100644 modules/sdk-coin-atom/src/lib/transferBuilder.ts diff --git a/modules/abstract-cosmos/src/cosmosCoin.ts b/modules/abstract-cosmos/src/cosmosCoin.ts index 880cd07505..23396cac84 100644 --- a/modules/abstract-cosmos/src/cosmosCoin.ts +++ b/modules/abstract-cosmos/src/cosmosCoin.ts @@ -300,6 +300,7 @@ export class CosmosCoin extends BaseCoin { return [userKeyDerivedCombined, backupKeyCombined]; } + // TODO(BG-78714): Reduce code duplication between this and eth.ts private async signRecoveryTSS( userKeyCombined: ECDSA.KeyCombined, backupKeyCombined: ECDSA.KeyCombined, diff --git a/modules/sdk-coin-atom/package.json b/modules/sdk-coin-atom/package.json index 4e3a319323..426ec73e11 100644 --- a/modules/sdk-coin-atom/package.json +++ b/modules/sdk-coin-atom/package.json @@ -40,19 +40,14 @@ ] }, "dependencies": { + "@bitgo/abstract-cosmos": "^1.15.0", "@bitgo/sdk-core": "^16.0.0", "@bitgo/sdk-lib-mpc": "^8.23.0", "@bitgo/statics": "^37.0.0", - "@bitgo/utxo-lib": "^9.24.0", "@cosmjs/amino": "^0.29.5", - "@cosmjs/crypto": "^0.29.5", "@cosmjs/encoding": "^0.29.5", - "@cosmjs/proto-signing": "^0.29.5", "@cosmjs/stargate": "^0.29.5", - "bignumber.js": "^9.1.1", - "cosmjs-types": "^0.6.1", - "lodash": "^4.17.21", - "superagent": "^3.8.3" + "bignumber.js": "^9.1.1" }, "devDependencies": { "@bitgo/sdk-api": "^1.32.0", diff --git a/modules/sdk-coin-atom/src/atom.ts b/modules/sdk-coin-atom/src/atom.ts index 9fffe38306..29d6d01bac 100644 --- a/modules/sdk-coin-atom/src/atom.ts +++ b/modules/sdk-coin-atom/src/atom.ts @@ -1,78 +1,14 @@ -import { - BaseCoin, - BaseTransaction, - BitGoBase, - Ecdsa, - ECDSA, - ECDSAMethodTypes, - Environments, - ExplanationResult, - hexToBigInt, - InvalidAddressError, - InvalidMemoIdError, - KeyPair, - MPCAlgorithm, - ParsedTransaction, - ParseTransactionOptions, - SignedTransaction, - SigningError, - SignTransactionOptions, - TransactionType, - UnexpectedAddressError, - VerifyAddressOptions, - VerifyTransactionOptions, -} from '@bitgo/sdk-core'; -import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes } from '@bitgo/sdk-lib-mpc'; -import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics'; -import { bip32 } from '@bitgo/utxo-lib'; -import { BigNumber } from 'bignumber.js'; -import { createHash, Hash, randomBytes } from 'crypto'; -import * as _ from 'lodash'; -import utils from './lib/utils'; -import url from 'url'; -import querystring from 'querystring'; -import { KeyPair as AtomKeyPair, Transaction, TransactionBuilderFactory } from './lib'; -import * as request from 'superagent'; -import { Buffer } from 'buffer'; -import { FeeData, SendMessage } from './lib/iface'; -import { Coin } from '@cosmjs/stargate'; +import { CosmosCoin, CosmosKeyPair, GasAmountDetails } from '@bitgo/abstract-cosmos'; +import { BaseCoin, BitGoBase, Environments } from '@bitgo/sdk-core'; +import { BaseUnit, BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; +import { KeyPair, TransactionBuilderFactory } from './lib'; import { GAS_AMOUNT, GAS_LIMIT } from './lib/constants'; +import utils from './lib/utils'; -/** - * Atom accounts support memo Id based addresses - */ -interface AddressDetails { - address: string; - memoId?: string | undefined; -} - -/** - * Atom accounts support memo Id based addresses - */ -interface AtomCoinSpecific { - rootAddress: string; -} - -interface RecoveryOptions { - userKey?: string; // Box A - backupKey?: string; // Box B - bitgoKey: string; // Box C - recoveryDestination: string; - krsProvider?: string; - walletPassphrase?: string; - startingScanIndex?: number; - scan?: number; -} - -interface AtomTx { - serializedTx: string; - scanIndex: number; -} - -export class Atom extends BaseCoin { +export class Atom extends CosmosCoin { protected readonly _staticsCoin: Readonly; protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { - super(bitgo); + super(bitgo, staticsCoin); if (!staticsCoin) { throw new Error('missing required constructor parameter staticsCoin'); @@ -90,41 +26,6 @@ export class Atom extends BaseCoin { return 1e6; } - /** @inheritDoc **/ - getChain(): string { - return this._staticsCoin.name; - } - - /** @inheritDoc **/ - getFamily(): CoinFamily { - return this._staticsCoin.family; - } - - /** @inheritDoc **/ - getFullName(): string { - return this._staticsCoin.fullName; - } - - /** @inheritDoc */ - supportsTss(): boolean { - return true; - } - - /** @inheritDoc **/ - getMPCAlgorithm(): MPCAlgorithm { - return 'ecdsa'; - } - - /** @inheritDoc **/ - isValidPub(pub: string): boolean { - return utils.isValidPublicKey(pub); - } - - /** @inheritDoc **/ - isValidPrv(prv: string): boolean { - return utils.isValidPrivateKey(prv); - } - getBuilder(): TransactionBuilderFactory { return new TransactionBuilderFactory(coins.get(this.getChain())); } @@ -135,314 +36,21 @@ export class Atom extends BaseCoin { } /** @inheritDoc **/ - async verifyTransaction(params: VerifyTransactionOptions): Promise { - let totalAmount = new BigNumber(0); - const coinConfig = coins.get(this.getChain()); - const { txPrebuild, txParams } = params; - const rawTx = txPrebuild.txHex; - if (!rawTx) { - throw new Error('missing required tx prebuild property txHex'); - } - const transaction = await new TransactionBuilderFactory(coinConfig).from(rawTx).build(); - const explainedTx = transaction.explainTransaction(); - - if (txParams.recipients && txParams.recipients.length > 0) { - const filteredRecipients = txParams.recipients?.map((recipient) => _.pick(recipient, ['address', 'amount'])); - const filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount'])); - - if (!_.isEqual(filteredOutputs, filteredRecipients)) { - throw new Error('Tx outputs does not match with expected txParams recipients'); - } - // WithdrawDelegatorRewards transaction doesn't have amount - if (transaction.type !== TransactionType.StakingWithdraw) { - for (const recipients of txParams.recipients) { - totalAmount = totalAmount.plus(recipients.amount); - } - if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { - throw new Error('Tx total amount does not match with expected total amount field'); - } - } - } - return true; + getDenomination(): string { + return BaseUnit.ATOM; } /** @inheritDoc **/ - async parseTransaction(params: ParseTransactionOptions & { txHex: string }): Promise { - const transactionExplanation = await this.explainTransaction({ txHex: params.txHex }); - if (!transactionExplanation) { - throw new Error('Invalid transaction'); - } - - if (transactionExplanation.outputs.length <= 0) { - return { - inputs: [], - outputs: [], - }; - } - const senderAddress = transactionExplanation.outputs[0].address; - const feeAmount = new BigNumber(transactionExplanation.fee.fee === '' ? '0' : transactionExplanation.fee.fee); - const inputs = [ - { - address: senderAddress, - amount: new BigNumber(transactionExplanation.outputAmount).plus(feeAmount).toFixed(), - }, - ]; - const outputs = transactionExplanation.outputs.map((output) => { - return { - address: output.address, - amount: new BigNumber(output.amount).toFixed(), - }; - }); + getGasAmountDetails(): GasAmountDetails { return { - inputs, - outputs, - }; - } - - /** @inheritDoc **/ - async explainTransaction(options: { txHex: string }): Promise { - if (!options.txHex) { - throw new Error('missing required txHex parameter'); - } - try { - const transactionBuilder = new TransactionBuilderFactory(coins.get(this.getChain())).from(options.txHex); - const transaction = await transactionBuilder.build(); - return transaction.explainTransaction(); - } catch (e) { - throw new Error('Invalid transaction: ' + e.message); - } - } - - /** @inheritDoc **/ - generateKeyPair(seed?: Buffer): KeyPair { - if (!seed) { - // An extended private key has both a normal 256 bit private key and a 256 - // bit chain code, both of which must be random. 512 bits is therefore the - // maximum entropy and gives us maximum security against cracking. - seed = randomBytes(512 / 8); - } - const extendedKey = bip32.fromSeed(seed); - return { - pub: extendedKey.neutered().toBase58(), - prv: extendedKey.toBase58(), - }; - } - - /** - * Sign a transaction with a single private key - * @param params parameters in the form of { txPrebuild: {txHex}, prv } - * @returns signed transaction in the form of { txHex } - */ - async signTransaction( - params: SignTransactionOptions & { txPrebuild: { txHex: string }; prv: string } - ): Promise { - const txHex = params?.txPrebuild?.txHex; - const privateKey = params?.prv; - if (!txHex) { - throw new SigningError('missing required txPrebuild parameter: params.txPrebuild.txHex'); - } - if (!privateKey) { - throw new SigningError('missing required prv parameter: params.prv'); - } - const txBuilder = new TransactionBuilderFactory(coins.get(this.getChain())).from(params.txPrebuild.txHex); - txBuilder.sign({ key: params.prv }); - const transaction: BaseTransaction = await txBuilder.build(); - if (!transaction) { - throw new SigningError('Failed to build signed transaction'); - } - const serializedTx = transaction.toBroadcastFormat(); - return { - txHex: serializedTx, - }; - } - - /** - * Builds a funds recovery transaction without BitGo - * @param {RecoveryOptions} params parameters needed to construct and - * (maybe) sign the transaction - * - * @returns {AtomTx} the serialized transaction hex string and index - * of the address being swept - */ - async recover(params: RecoveryOptions): Promise { - // Step 1: Check if params contains the required parameters - if (!params.bitgoKey) { - throw new Error('missing bitgoKey'); - } - - if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) { - throw new Error('invalid recoveryDestination'); - } - - if (!params.userKey) { - throw new Error('missing userKey'); - } - - if (!params.backupKey) { - throw new Error('missing backupKey'); - } - - if (!params.walletPassphrase) { - throw new Error('missing wallet passphrase'); - } - - // Step 2: Fetch the bitgo key from params - const bitgoKey = params.bitgoKey.replace(/\s/g, ''); - - // Step 3: Instantiate the ECDSA signer and fetch the address details - const MPC = new Ecdsa(); - const chainId = await this.getChainId(); - const currPath = 'm/0'; - const publicKey = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 66); - const senderAddress = this.getAddressFromPublicKey(publicKey); - - // Step 4: Fetch account details such as accountNo, balance and check for sufficient funds once gasAmount has been deducted - const [accountNumber, sequenceNo] = await this.getAccountDetails(senderAddress); - const balance = new BigNumber(await this.getAccountBalance(senderAddress)); - const gasBudget: FeeData = { - amount: [{ denom: 'uatom', amount: GAS_AMOUNT }], + gasAmount: GAS_AMOUNT, gasLimit: GAS_LIMIT, }; - const gasAmount = new BigNumber(gasBudget.amount[0].amount); - const actualBalance = balance.minus(gasAmount); - - if (actualBalance.isLessThanOrEqualTo(0)) { - throw new Error('Did not have enough funds to recover'); - } - - // Step 5: Once sufficient funds are present, construct the recover tx messsage - const amount: Coin[] = [ - { - denom: 'uatom', - amount: actualBalance.toFixed(), - }, - ]; - const sendMessage: SendMessage[] = [ - { - fromAddress: senderAddress, - toAddress: params.recoveryDestination, - amount: amount, - }, - ]; - - // Step 6: Build the unsigned tx using the constructed message - const txnBuilder = this.getBuilder().getTransferBuilder(); - txnBuilder - .messages(sendMessage) - .gasBudget(gasBudget) - .publicKey(publicKey) - .sequence(Number(sequenceNo)) - .accountNumber(Number(accountNumber)) - .chainId(chainId); - const unsignedTransaction = (await txnBuilder.build()) as Transaction; - let serializedTx = unsignedTransaction.toBroadcastFormat(); - const signableHex = unsignedTransaction.signablePayload.toString('hex'); - const userKey = params.userKey.replace(/\s/g, ''); - const backupKey = params.backupKey.replace(/\s/g, ''); - const [userKeyCombined, backupKeyCombined] = ((): [ - ECDSAMethodTypes.KeyCombined | undefined, - ECDSAMethodTypes.KeyCombined | undefined - ] => { - const [userKeyCombined, backupKeyCombined] = this.getKeyCombinedFromTssKeyShares( - userKey, - backupKey, - params.walletPassphrase - ); - return [userKeyCombined, backupKeyCombined]; - })(); - - if (!userKeyCombined || !backupKeyCombined) { - throw new Error('Missing combined key shares for user or backup'); - } - - // Step 7: Sign the tx - const signature = await this.signRecoveryTSS(userKeyCombined, backupKeyCombined, signableHex); - const signableBuffer = Buffer.from(signableHex, 'hex'); - MPC.verify(signableBuffer, signature, createHash('sha256')); - const atomKeyPair = new AtomKeyPair({ pub: publicKey }); - txnBuilder.addSignature({ pub: atomKeyPair.getKeys().pub }, Buffer.from(signature.r + signature.s, 'hex')); - const signedTransaction = await txnBuilder.build(); - serializedTx = signedTransaction.toBroadcastFormat(); - - return { serializedTx: serializedTx, scanIndex: 0 }; - } - - /** - * Get balance from public node - */ - protected async getBalanceFromNode(senderAddress: string): Promise { - const nodeUrl = this.getPublicNodeUrl(); - const getBalancePath = 'cosmos/bank/v1beta1/balances/'; - const fullEndpoint = nodeUrl + getBalancePath + senderAddress; - try { - return await request.get(fullEndpoint).send(); - } catch (e) { - console.debug(e); - } - throw new Error(`Unable to call endpoint ${getBalancePath + senderAddress} from node: ${nodeUrl}`); - } - - /** - * Helper to fetch chainId - */ - protected async getChainId(): Promise { - const response = await this.getChainIdFromNode(); - if (response.status !== 200) { - throw new Error('Account not found'); - } - return response.body.block.header.chain_id; - } - - /** - * Get chain id from public node - */ - protected async getChainIdFromNode(): Promise { - const nodeUrl = this.getPublicNodeUrl(); - const getLatestBlockPath = 'cosmos/base/tendermint/v1beta1/blocks/latest'; - const fullEndpoint = nodeUrl + getLatestBlockPath; - try { - return await request.get(fullEndpoint).send(); - } catch (e) { - console.debug(e); - } - throw new Error(`Unable to call endpoint ${getLatestBlockPath} from node: ${nodeUrl}`); - } - - /** - * Helper to fetch account number - */ - protected async getAccountDetails(senderAddress: string): Promise { - const response = await this.getAccountFromNode(senderAddress); - if (response.status !== 200) { - throw new Error('Account not found'); - } - return [response.body.account.account_number, response.body.account.sequence]; - } - - /** - * Get account number from public node - */ - protected async getAccountFromNode(senderAddress: string): Promise { - const nodeUrl = this.getPublicNodeUrl(); - const getAccountPath = 'cosmos/auth/v1beta1/accounts/'; - const fullEndpoint = nodeUrl + getAccountPath + senderAddress; - try { - return await request.get(fullEndpoint).send(); - } catch (e) { - console.debug(e); - } - throw new Error(`Unable to call endpoint ${getAccountPath + senderAddress} from node: ${nodeUrl}`); } - /** - * Helper to fetch account balance - */ - protected async getAccountBalance(senderAddress: string): Promise { - const response = await this.getBalanceFromNode(senderAddress); - if (response.status !== 200) { - throw new Error('Account not found'); - } - return response.body.balances[0].amount; + /** @inheritDoc **/ + getKeyPair(publicKey: string): CosmosKeyPair { + return new KeyPair({ pub: publicKey }); } /** @@ -453,238 +61,6 @@ export class Atom extends BaseCoin { } getAddressFromPublicKey(pubKey: string): string { - return new AtomKeyPair({ pub: pubKey }).getAddress(); - } - - /** @inheritDoc **/ - async isWalletAddress(params: VerifyAddressOptions): Promise { - const addressDetails = this.getAddressDetails(params.address); - - if (!this.isValidAddress(addressDetails.address)) { - throw new InvalidAddressError(`invalid address: ${addressDetails.address}`); - } - const rootAddress = (params.coinSpecific as AtomCoinSpecific).rootAddress; - if (addressDetails.address !== rootAddress) { - throw new UnexpectedAddressError(`address validation failure: ${addressDetails.address} vs ${rootAddress}`); - } - return true; - } - - getHashFunction(): Hash { - return createHash('sha256'); - } - - /** - * Process address into address and memo id - * - * @param address the address - * @returns object containing address and memo id - */ - getAddressDetails(address: string): AddressDetails { - const destinationDetails = url.parse(address); - const destinationAddress = destinationDetails.pathname || ''; - - // address doesn't have a memo id - if (destinationDetails.pathname === address) { - return { - address: address, - memoId: undefined, - }; - } - - if (!destinationDetails.query) { - throw new InvalidAddressError(`invalid address: ${address}`); - } - - const queryDetails = querystring.parse(destinationDetails.query); - if (!queryDetails.memoId) { - // if there are more properties, the query details need to contain the memo id property - throw new InvalidAddressError(`invalid address: ${address}`); - } - - if (Array.isArray(queryDetails.memoId)) { - throw new InvalidAddressError( - `memoId may only be given at most once, but found ${queryDetails.memoId.length} instances in address ${address}` - ); - } - - if (Array.isArray(queryDetails.memoId) && queryDetails.memoId.length !== 1) { - // valid addresses can only contain one memo id - throw new InvalidAddressError(`invalid address '${address}', must contain exactly one memoId`); - } - - const [memoId] = _.castArray(queryDetails.memoId) || undefined; - if (!this.isValidMemoId(memoId)) { - throw new InvalidMemoIdError(`invalid address: '${address}', memoId is not valid`); - } - - return { - address: destinationAddress, - memoId, - }; - } - - /** - * Return boolean indicating whether a memo id is valid - * - * @param memoId memo id - * @returns true if memo id is valid - */ - isValidMemoId(memoId: string): boolean { - return utils.isValidMemoId(memoId); - } - - private getKeyCombinedFromTssKeyShares( - userPublicOrPrivateKeyShare: string, - backupPrivateOrPublicKeyShare: string, - walletPassphrase?: string - ): [ECDSAMethodTypes.KeyCombined, ECDSAMethodTypes.KeyCombined] { - let backupPrv; - let userPrv; - try { - backupPrv = this.bitgo.decrypt({ - input: backupPrivateOrPublicKeyShare, - password: walletPassphrase, - }); - userPrv = this.bitgo.decrypt({ - input: userPublicOrPrivateKeyShare, - password: walletPassphrase, - }); - } catch (e) { - throw new Error(`Error decrypting backup keychain: ${e.message}`); - } - - const userSigningMaterial = JSON.parse(userPrv) as ECDSAMethodTypes.SigningMaterial; - const backupSigningMaterial = JSON.parse(backupPrv) as ECDSAMethodTypes.SigningMaterial; - - if (!userSigningMaterial.backupNShare) { - throw new Error('Invalid user key - missing backupNShare'); - } - - if (!backupSigningMaterial.userNShare) { - throw new Error('Invalid backup key - missing userNShare'); - } - - const MPC = new Ecdsa(); - - const userKeyCombined = MPC.keyCombine(userSigningMaterial.pShare, [ - userSigningMaterial.bitgoNShare, - userSigningMaterial.backupNShare, - ]); - - const userSigningKeyDerived = MPC.keyDerive( - userSigningMaterial.pShare, - [userSigningMaterial.bitgoNShare, userSigningMaterial.backupNShare], - 'm/0' - ); - - const userKeyDerivedCombined = { - xShare: userSigningKeyDerived.xShare, - yShares: userKeyCombined.yShares, - }; - - const backupKeyCombined = MPC.keyCombine(backupSigningMaterial.pShare, [ - userSigningKeyDerived.nShares[2], - backupSigningMaterial.bitgoNShare, - ]); - - if ( - userKeyDerivedCombined.xShare.y !== backupKeyCombined.xShare.y || - userKeyDerivedCombined.xShare.chaincode !== backupKeyCombined.xShare.chaincode - ) { - throw new Error('Common keychains do not match'); - } - - return [userKeyDerivedCombined, backupKeyCombined]; - } - - // TODO(BG-78714): Reduce code duplication between this and eth.ts - private async signRecoveryTSS( - userKeyCombined: ECDSA.KeyCombined, - backupKeyCombined: ECDSA.KeyCombined, - txHex: string, - { - rangeProofChallenge, - }: { - rangeProofChallenge?: EcdsaTypes.SerializedNtilde; - } = {} - ): Promise { - const MPC = new Ecdsa(); - const signerOneIndex = userKeyCombined.xShare.i; - const signerTwoIndex = backupKeyCombined.xShare.i; - - // Since this is a user <> backup signing, we will reuse the same range proof challenge - rangeProofChallenge = - rangeProofChallenge ?? EcdsaTypes.serializeNtildeWithProofs(await EcdsaRangeProof.generateNtilde()); - - const userToBackupPaillierChallenge = await EcdsaPaillierProof.generateP( - hexToBigInt(userKeyCombined.yShares[signerTwoIndex].n) - ); - const backupToUserPaillierChallenge = await EcdsaPaillierProof.generateP( - hexToBigInt(backupKeyCombined.yShares[signerOneIndex].n) - ); - - const userXShare = MPC.appendChallenge( - userKeyCombined.xShare, - rangeProofChallenge, - EcdsaTypes.serializePaillierChallenge({ p: userToBackupPaillierChallenge }) - ); - const userYShare = MPC.appendChallenge( - userKeyCombined.yShares[signerTwoIndex], - rangeProofChallenge, - EcdsaTypes.serializePaillierChallenge({ p: backupToUserPaillierChallenge }) - ); - const backupXShare = MPC.appendChallenge( - backupKeyCombined.xShare, - rangeProofChallenge, - EcdsaTypes.serializePaillierChallenge({ p: backupToUserPaillierChallenge }) - ); - const backupYShare = MPC.appendChallenge( - backupKeyCombined.yShares[signerOneIndex], - rangeProofChallenge, - EcdsaTypes.serializePaillierChallenge({ p: userToBackupPaillierChallenge }) - ); - - const signShares: ECDSA.SignShareRT = await MPC.signShare(userXShare, userYShare); - - const signConvertS21 = await MPC.signConvertStep1({ - xShare: backupXShare, - yShare: backupYShare, // YShare corresponding to the other participant signerOne - kShare: signShares.kShare, - }); - const signConvertS12 = await MPC.signConvertStep2({ - aShare: signConvertS21.aShare, - wShare: signShares.wShare, - }); - const signConvertS21_2 = await MPC.signConvertStep3({ - muShare: signConvertS12.muShare, - bShare: signConvertS21.bShare, - }); - - const [signCombineOne, signCombineTwo] = [ - MPC.signCombine({ - gShare: signConvertS12.gShare, - signIndex: { - i: signConvertS12.muShare.i, - j: signConvertS12.muShare.j, - }, - }), - MPC.signCombine({ - gShare: signConvertS21_2.gShare, - signIndex: { - i: signConvertS21_2.signIndex.i, - j: signConvertS21_2.signIndex.j, - }, - }), - ]; - - const MESSAGE = Buffer.from(txHex, 'hex'); - - const [signA, signB] = [ - MPC.sign(MESSAGE, signCombineOne.oShare, signCombineTwo.dShare, createHash('sha256')), - MPC.sign(MESSAGE, signCombineTwo.oShare, signCombineOne.dShare, createHash('sha256')), - ]; - - return MPC.constructSignature([signA, signB]); + return new KeyPair({ pub: pubKey }).getAddress(); } } diff --git a/modules/sdk-coin-atom/src/lib/StakingActivateBuilder.ts b/modules/sdk-coin-atom/src/lib/StakingActivateBuilder.ts deleted file mode 100644 index a47e9e15ca..0000000000 --- a/modules/sdk-coin-atom/src/lib/StakingActivateBuilder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; - -import { delegateMsgTypeUrl } from './constants'; -import { DelegateOrUndelegeteMessage } from './iface'; -import { TransactionBuilder } from './transactionBuilder'; -import utils from './utils'; - -export class StakingActivateBuilder extends TransactionBuilder { - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - protected get transactionType(): TransactionType { - return TransactionType.StakingActivate; - } - - /** @inheritdoc */ - messages(delegateMessages: DelegateOrUndelegeteMessage[]): this { - this._messages = delegateMessages.map((delegateMessage) => { - utils.validateDelegateOrUndelegateMessage(delegateMessage); - return { - typeUrl: delegateMsgTypeUrl, - value: delegateMessage, - }; - }); - return this; - } -} diff --git a/modules/sdk-coin-atom/src/lib/StakingDeactivateBuilder.ts b/modules/sdk-coin-atom/src/lib/StakingDeactivateBuilder.ts deleted file mode 100644 index 8e67afe6a4..0000000000 --- a/modules/sdk-coin-atom/src/lib/StakingDeactivateBuilder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; - -import { undelegateMsgTypeUrl } from './constants'; -import { DelegateOrUndelegeteMessage } from './iface'; -import { TransactionBuilder } from './transactionBuilder'; -import utils from './utils'; - -export class StakingDeactivateBuilder extends TransactionBuilder { - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - protected get transactionType(): TransactionType { - return TransactionType.StakingDeactivate; - } - - /** @inheritdoc */ - messages(undelegateMessages: DelegateOrUndelegeteMessage[]): this { - this._messages = undelegateMessages.map((undelegateMessage) => { - utils.validateDelegateOrUndelegateMessage(undelegateMessage); - return { - typeUrl: undelegateMsgTypeUrl, - value: undelegateMessage, - }; - }); - return this; - } -} diff --git a/modules/sdk-coin-atom/src/lib/StakingWithdrawRewardsBuilder.ts b/modules/sdk-coin-atom/src/lib/StakingWithdrawRewardsBuilder.ts deleted file mode 100644 index 25ed92d9ba..0000000000 --- a/modules/sdk-coin-atom/src/lib/StakingWithdrawRewardsBuilder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; - -import { withdrawDelegatorRewardMsgTypeUrl } from './constants'; -import { WithdrawDelegatorRewardsMessage } from './iface'; -import { TransactionBuilder } from './transactionBuilder'; -import utils from './utils'; - -export class StakingWithdrawRewardsBuilder extends TransactionBuilder { - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - protected get transactionType(): TransactionType { - return TransactionType.StakingWithdraw; - } - - /** @inheritdoc */ - messages(withdrawRewardsMessages: WithdrawDelegatorRewardsMessage[]): this { - this._messages = withdrawRewardsMessages.map((withdrawRewardsMessage) => { - utils.validateWithdrawRewardsMessage(withdrawRewardsMessage); - return { - typeUrl: withdrawDelegatorRewardMsgTypeUrl, - value: withdrawRewardsMessage, - }; - }); - return this; - } -} diff --git a/modules/sdk-coin-atom/src/lib/constants.ts b/modules/sdk-coin-atom/src/lib/constants.ts index f4042f628e..95b0560002 100644 --- a/modules/sdk-coin-atom/src/lib/constants.ts +++ b/modules/sdk-coin-atom/src/lib/constants.ts @@ -1,12 +1,5 @@ -export const DEFAULT_SEED_SIZE_BYTES = 16; -export const sendMsgTypeUrl = '/cosmos.bank.v1beta1.MsgSend'; -export const delegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgDelegate'; -export const undelegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgUndelegate'; -export const withdrawDelegatorRewardMsgTypeUrl = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward'; export const validDenoms = ['natom', 'uatom', 'matom', 'atom']; export const accountAddressRegex = /^(cosmos)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; export const validatorAddressRegex = /^(cosmosvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; -export const contractAddressRegex = /^(cosmos)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; -export const UNAVAILABLE_TEXT = 'UNAVAILABLE'; export const GAS_AMOUNT = '100000'; export const GAS_LIMIT = 200000; diff --git a/modules/sdk-coin-atom/src/lib/iface.ts b/modules/sdk-coin-atom/src/lib/iface.ts deleted file mode 100644 index 7b45ca8b8a..0000000000 --- a/modules/sdk-coin-atom/src/lib/iface.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { TransactionExplanation as BaseTransactionExplanation, TransactionType } from '@bitgo/sdk-core'; -import { Coin } from '@cosmjs/stargate'; - -export interface TransactionExplanation extends BaseTransactionExplanation { - type: TransactionType; -} - -export interface MessageData { - typeUrl: string; - value: SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage; -} - -export interface SendMessage { - fromAddress: string; - toAddress: string; - amount: Coin[]; -} - -export interface DelegateOrUndelegeteMessage { - delegatorAddress: string; - validatorAddress: string; - amount: Coin; -} - -export interface WithdrawDelegatorRewardsMessage { - delegatorAddress: string; - validatorAddress: string; -} - -export interface FeeData { - amount: Coin[]; - gasLimit: number; -} - -/** - * The transaction data returned from the toJson() function of a transaction - */ -export interface TxData extends AtomTransaction { - id?: string; - type?: TransactionType; - accountNumber: number; - chainId: string; -} - -export interface AtomTransaction { - readonly sequence: number; - readonly sendMessages: MessageData[]; - readonly gasBudget: FeeData; - readonly publicKey?: string; - readonly signature?: Uint8Array; - readonly hash?: string; - readonly memo?: string; -} diff --git a/modules/sdk-coin-atom/src/lib/index.ts b/modules/sdk-coin-atom/src/lib/index.ts index 0787e20f9c..87cc346db5 100644 --- a/modules/sdk-coin-atom/src/lib/index.ts +++ b/modules/sdk-coin-atom/src/lib/index.ts @@ -1,12 +1,10 @@ +import * as Constants from './constants'; import * as Utils from './utils'; -import * as Interface from './iface'; +export { + CosmosTransaction as Transaction, + CosmosTransactionBuilder as TransactionBuilder, +} from '@bitgo/abstract-cosmos'; export { KeyPair } from './keyPair'; -export { Transaction } from './transaction'; -export { TransactionBuilder } from './transactionBuilder'; -export { TransferBuilder } from './transferBuilder'; export { TransactionBuilderFactory } from './transactionBuilderFactory'; -export { StakingActivateBuilder } from './StakingActivateBuilder'; -export { StakingDeactivateBuilder } from './StakingDeactivateBuilder'; -export { StakingWithdrawRewardsBuilder } from './StakingWithdrawRewardsBuilder'; -export { Interface, Utils }; +export { Constants, Utils }; diff --git a/modules/sdk-coin-atom/src/lib/keyPair.ts b/modules/sdk-coin-atom/src/lib/keyPair.ts index 406a586149..0a5868d48e 100644 --- a/modules/sdk-coin-atom/src/lib/keyPair.ts +++ b/modules/sdk-coin-atom/src/lib/keyPair.ts @@ -1,55 +1,13 @@ -import { - DefaultKeys, - isPrivateKey, - isPublicKey, - isSeed, - KeyPairOptions, - Secp256k1ExtendedKeyPair, -} from '@bitgo/sdk-core'; -import { bip32 } from '@bitgo/utxo-lib'; +import { KeyPairOptions } from '@bitgo/sdk-core'; import { pubkeyToAddress } from '@cosmjs/amino'; -import { randomBytes } from 'crypto'; - -import { DEFAULT_SEED_SIZE_BYTES } from './constants'; +import { CosmosKeyPair, PubKeyType } from '@bitgo/abstract-cosmos'; /** * Cosmos keys and address management. */ -export class KeyPair extends Secp256k1ExtendedKeyPair { - /** - * Public constructor. By default, creates a key pair with a random master seed. - * @param { KeyPairOptions } source Either a master seed, a private key (extended or raw), or a public key - * (extended, compressed, or uncompressed) - */ +export class KeyPair extends CosmosKeyPair { constructor(source?: KeyPairOptions) { super(source); - if (!source) { - const seed = randomBytes(DEFAULT_SEED_SIZE_BYTES); - this.hdNode = bip32.fromSeed(seed); - } else if (isSeed(source)) { - this.hdNode = bip32.fromSeed(source.seed); - } else if (isPrivateKey(source)) { - this.recordKeysFromPrivateKey(source.prv); - } else if (isPublicKey(source)) { - this.recordKeysFromPublicKey(source.pub); - } else { - throw new Error('Invalid key pair options'); - } - - if (this.hdNode) { - this.keyPair = Secp256k1ExtendedKeyPair.toKeyPair(this.hdNode); - } - } - - /** - * Cosmos default keys format: raw private and compressed public key - * @returns { DefaultKeys } The keys in the protocol default key format - */ - getKeys(): DefaultKeys { - return { - pub: this.getPublicKey({ compressed: true }).toString('hex'), - prv: this.getPrivateKey()?.toString('hex'), - }; } /** @inheritdoc */ @@ -57,7 +15,7 @@ export class KeyPair extends Secp256k1ExtendedKeyPair { const base64String = Buffer.from(this.getKeys().pub.slice(0, 66), 'hex').toString('base64'); return pubkeyToAddress( { - type: 'tendermint/PubKeySecp256k1', + type: PubKeyType.secp256k1, value: base64String, }, 'cosmos' diff --git a/modules/sdk-coin-atom/src/lib/transaction.ts b/modules/sdk-coin-atom/src/lib/transaction.ts deleted file mode 100644 index 0a3e9a520c..0000000000 --- a/modules/sdk-coin-atom/src/lib/transaction.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { - BaseKey, - BaseTransaction, - Entry, - InvalidTransactionError, - ParseTransactionError, - TransactionRecipient, - TransactionType, -} from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { fromHex, toBase64 } from '@cosmjs/encoding'; -import { makeSignBytes } from '@cosmjs/proto-signing'; -import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; -import { UNAVAILABLE_TEXT } from './constants'; -import { - AtomTransaction, - DelegateOrUndelegeteMessage, - SendMessage, - TransactionExplanation, - TxData, - WithdrawDelegatorRewardsMessage, -} from './iface'; -import utils from './utils'; - -export class Transaction extends BaseTransaction { - private _atomTransaction: AtomTransaction; - private _accountNumber: number; - private _chainId: string; - - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - get atomTransaction(): AtomTransaction { - return this._atomTransaction; - } - - set atomTransaction(atomTransaction: Readonly) { - this._atomTransaction = atomTransaction; - } - - get chainId(): string { - return this._chainId; - } - - set chainId(chainId: string) { - this._chainId = chainId; - } - - get accountNumber(): number { - return this._accountNumber; - } - - set accountNumber(accountNumber: number) { - this._accountNumber = accountNumber; - } - - /** @inheritDoc **/ - get id(): string { - if (this._id) { - return this._id; - } else if (this._atomTransaction?.hash !== undefined) { - return this._atomTransaction.hash; - } - return UNAVAILABLE_TEXT; - } - - /** @inheritdoc */ - canSign(key: BaseKey): boolean { - return true; - } - - /** @inheritdoc */ - toBroadcastFormat(): string { - if (!this._atomTransaction) { - throw new InvalidTransactionError('Empty transaction'); - } - return this.serialize(); - } - - /** @inheritdoc */ - toJson(): TxData { - if (!this._atomTransaction) { - throw new ParseTransactionError('Empty transaction'); - } - const tx = this._atomTransaction; - return { - id: this.id, - type: this._type, - sequence: tx.sequence, - sendMessages: tx.sendMessages, - gasBudget: tx.gasBudget, - publicKey: tx.publicKey, - signature: tx.signature, - accountNumber: this._accountNumber, - chainId: this._chainId, - hash: tx.hash, - memo: tx.memo, - }; - } - - /** @inheritDoc */ - explainTransaction(): TransactionExplanation { - const result = this.toJson(); - const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type']; - const outputs: TransactionRecipient[] = []; - - const explanationResult: TransactionExplanation = { - displayOrder, - id: this.id, - outputs, - outputAmount: '0', - changeOutputs: [], - changeAmount: '0', - fee: { fee: this.atomTransaction.gasBudget.amount[0].amount }, - type: this.type, - }; - return this.explainTransactionInternal(result, explanationResult); - } - - /** - * Set the transaction type. - * @param {TransactionType} transactionType The transaction type to be set. - */ - set transactionType(transactionType: TransactionType) { - this._type = transactionType; - } - - /** - * Sets this transaction payload - * @param rawTransaction raw transaction in base64 encoded string - */ - enrichTransactionDetailsFromRawTransaction(rawTransaction: string): void { - if (utils.isValidHexString(rawTransaction)) { - this.atomTransaction = utils.deserializeAtomTransaction(toBase64(fromHex(rawTransaction))); - } else { - this.atomTransaction = utils.deserializeAtomTransaction(rawTransaction); - } - if (this.atomTransaction.signature) { - this.addSignature(Buffer.from(this.atomTransaction.signature).toString('hex')); - } - const typeUrl = this.atomTransaction.sendMessages[0].typeUrl; - const transactionType = utils.getTransactionTypeFromTypeUrl(typeUrl); - if (transactionType === undefined) { - throw new Error('Transaction type is not supported ' + typeUrl); - } - this.transactionType = transactionType; - } - - /** - * Add a signature to the transaction - * @param {string} signature in hex format - */ - addSignature(signature: string) { - this._signatures = []; - this._signatures.push(signature); - } - - /** - * Serialize the transaction to a JSON string - * @returns {string} serialized base64 encoded transaction - */ - serialize(): string { - const txRaw = utils.createTxRawFromAtomTransaction(this.atomTransaction); - if (this.atomTransaction?.publicKey !== undefined && this._signatures.length > 0) { - const signedRawTx = utils.createSignedTxRaw(this.atomTransaction.publicKey, this._signatures[0], txRaw); - return toBase64(TxRaw.encode(signedRawTx).finish()); - } - return toBase64(TxRaw.encode(txRaw).finish()); - } - - /** @inheritdoc **/ - get signablePayload(): Buffer { - return Buffer.from(makeSignBytes(utils.createSignDoc(this.atomTransaction, this._accountNumber, this._chainId))); - } - - /** - * Returns a complete explanation for a transfer transaction - * Currently only supports one message per transfer. - * @param {TxData} json The transaction data in json format - * @param {TransactionExplanation} explanationResult The transaction explanation to be completed - * @returns {TransactionExplanation} - */ - explainTransactionInternal(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation { - let outputs: TransactionRecipient[]; - let outputAmount; - switch (json.type) { - case TransactionType.Send: - explanationResult.type = TransactionType.Send; - outputAmount = BigInt(0); - outputs = json.sendMessages.map((message) => { - const sendMessage = message.value as SendMessage; - outputAmount = outputAmount + BigInt(sendMessage.amount[0].amount); - return { - address: sendMessage.toAddress, - amount: sendMessage.amount[0].amount, - }; - }); - break; - case TransactionType.StakingActivate: - explanationResult.type = TransactionType.StakingActivate; - outputAmount = BigInt(0); - outputs = json.sendMessages.map((message) => { - const delegateMessage = message.value as DelegateOrUndelegeteMessage; - outputAmount = outputAmount + BigInt(delegateMessage.amount.amount); - return { - address: delegateMessage.validatorAddress, - amount: delegateMessage.amount.amount, - }; - }); - break; - case TransactionType.StakingDeactivate: - explanationResult.type = TransactionType.StakingDeactivate; - outputAmount = BigInt(0); - outputs = json.sendMessages.map((message) => { - const delegateMessage = message.value as DelegateOrUndelegeteMessage; - outputAmount = outputAmount + BigInt(delegateMessage.amount.amount); - return { - address: delegateMessage.validatorAddress, - amount: delegateMessage.amount.amount, - }; - }); - break; - case TransactionType.StakingWithdraw: - explanationResult.type = TransactionType.StakingWithdraw; - outputs = json.sendMessages.map((message) => { - const withdrawMessage = message.value as WithdrawDelegatorRewardsMessage; - return { - address: withdrawMessage.validatorAddress, - amount: UNAVAILABLE_TEXT, - }; - }); - break; - default: - throw new InvalidTransactionError('Transaction type not supported'); - } - if (json.memo) { - outputs.forEach((output) => { - output.memo = json.memo; - }); - } - return { - ...explanationResult, - outputAmount: outputAmount?.toString(), - outputs, - }; - } - - loadInputsAndOutputs(): void { - if (this.type === undefined || !this.atomTransaction) { - throw new InvalidTransactionError('Transaction type or atomTransaction is not set'); - } - - const outputs: Entry[] = []; - const inputs: Entry[] = []; - switch (this.type) { - case TransactionType.Send: - this.atomTransaction.sendMessages.forEach((message) => { - const sendMessage = message.value as SendMessage; - inputs.push({ - address: sendMessage.fromAddress, - value: sendMessage.amount[0].amount, - coin: this._coinConfig.name, - }); - outputs.push({ - address: sendMessage.toAddress, - value: sendMessage.amount[0].amount, - coin: this._coinConfig.name, - }); - }); - break; - case TransactionType.StakingActivate: - case TransactionType.StakingDeactivate: - this.atomTransaction.sendMessages.forEach((message) => { - const delegateMessage = message.value as DelegateOrUndelegeteMessage; - inputs.push({ - address: delegateMessage.delegatorAddress, - value: delegateMessage.amount.amount, - coin: this._coinConfig.name, - }); - outputs.push({ - address: delegateMessage.validatorAddress, - value: delegateMessage.amount.amount, - coin: this._coinConfig.name, - }); - }); - break; - case TransactionType.StakingWithdraw: - this.atomTransaction.sendMessages.forEach((message) => { - const withdrawMessage = message.value as WithdrawDelegatorRewardsMessage; - inputs.push({ - address: withdrawMessage.delegatorAddress, - value: UNAVAILABLE_TEXT, - coin: this._coinConfig.name, - }); - outputs.push({ - address: withdrawMessage.validatorAddress, - value: UNAVAILABLE_TEXT, - coin: this._coinConfig.name, - }); - }); - break; - default: - throw new InvalidTransactionError('Transaction type not supported'); - } - this._inputs = inputs; - this._outputs = outputs; - } -} diff --git a/modules/sdk-coin-atom/src/lib/transactionBuilder.ts b/modules/sdk-coin-atom/src/lib/transactionBuilder.ts deleted file mode 100644 index 1ed4849ce7..0000000000 --- a/modules/sdk-coin-atom/src/lib/transactionBuilder.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { - BaseAddress, - BaseKey, - BaseTransactionBuilder, - BuildTransactionError, - InvalidTransactionError, - PublicKey as BasePublicKey, - SigningError, - TransactionType, -} from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { Secp256k1, sha256 } from '@cosmjs/crypto'; -import { makeSignBytes } from '@cosmjs/proto-signing'; -import BigNumber from 'bignumber.js'; - -import { - DelegateOrUndelegeteMessage, - FeeData, - MessageData, - SendMessage, - WithdrawDelegatorRewardsMessage, -} from './iface'; -import { KeyPair } from './keyPair'; -import { Transaction } from './transaction'; -import utils from './utils'; - -export abstract class TransactionBuilder extends BaseTransactionBuilder { - protected _transaction: Transaction; - protected _sequence: number; - protected _messages: MessageData[]; - protected _gasBudget: FeeData; - private _accountNumber?: number; - private _signature: Buffer; - private _chainId?: string; - private _publicKey?: string; - private _signer: KeyPair; - private _memo?: string; - - constructor(_coinConfig: Readonly) { - super(_coinConfig); - this._transaction = new Transaction(_coinConfig); - } - - /** - * The transaction type. - */ - protected abstract get transactionType(): TransactionType; - - /** @inheritdoc */ - protected get transaction(): Transaction { - return this._transaction; - } - - /** @inheritdoc */ - protected set transaction(transaction: Transaction) { - this._transaction = transaction; - } - - /** @inheritDoc */ - addSignature(publicKey: BasePublicKey, signature: Buffer): void { - this._signature = signature; - this._publicKey = publicKey.pub; - } - - /** - * Sets gas budget of this transaction - * Gas budget consist of fee amount and gas limit. Division feeAmount/gasLimit represents - * the gas-fee and it should be more than minimum required gas-fee to process the transaction - * @param {FeeData} gasBudget - * @returns {TransactionBuilder} this transaction builder - */ - gasBudget(gasBudget: FeeData): this { - utils.validateGasBudget(gasBudget); - this._gasBudget = gasBudget; - return this; - } - - /** - * Sets sequence of this transaction. - * @param {number} sequence - sequence data for tx signer - * @returns {TransactionBuilder} This transaction builder - */ - sequence(sequence: number): this { - utils.validateSequence(sequence); - this._sequence = sequence; - return this; - } - - /** - * Sets messages to the transaction body. Message type will be different based on the transaction type - * - For @see TransactionType.StakingActivate required type is @see DelegateOrUndelegeteMessage - * - For @see TransactionType.StakingDeactivate required type is @see DelegateOrUndelegeteMessage - * - For @see TransactionType.Send required type is @see SendMessage - * - For @see TransactionType.StakingWithdraw required type is @see WithdrawDelegatorRewardsMessage - * @param {(SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage)[]} messages - * @returns {TransactionBuilder} This transaction builder - */ - abstract messages(messages: (SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage)[]): this; - - publicKey(publicKey: string | undefined): this { - this._publicKey = publicKey; - return this; - } - - accountNumber(accountNumber: number | undefined): this { - this._accountNumber = accountNumber; - return this; - } - - chainId(chainId: string | undefined): this { - this._chainId = chainId; - return this; - } - - memo(memo: string | undefined): this { - this._memo = memo; - return this; - } - - /** - * Initialize the transaction builder fields using the decoded transaction data - * @param {Transaction} tx the transaction data - */ - initBuilder(tx: Transaction): void { - this._transaction = tx; - const txData = tx.toJson(); - this.gasBudget(txData.gasBudget); - this.messages( - txData.sendMessages.map((message) => { - return message.value; - }) - ); - this.sequence(txData.sequence); - this.publicKey(txData.publicKey); - this.accountNumber(txData.accountNumber); - this.chainId(txData.chainId); - this.memo(txData.memo); - if (tx.signature && tx.signature.length > 0) { - this.addSignature({ pub: txData.publicKey } as any, Buffer.from(tx.signature[0], 'hex')); - } - } - - /** @inheritdoc */ - protected fromImplementation(rawTransaction: string): Transaction { - const tx = new Transaction(this._coinConfig); - tx.enrichTransactionDetailsFromRawTransaction(rawTransaction); - this.initBuilder(tx); - return this.transaction; - } - - /** @inheritdoc */ - protected async buildImplementation(): Promise { - this.transaction.transactionType = this.transactionType; - if (this._accountNumber) { - this.transaction.accountNumber = this._accountNumber; - } - if (this._chainId) { - this.transaction.chainId = this._chainId; - } - this.transaction.atomTransaction = utils.createAtomTransaction( - this._sequence, - this._messages, - this._gasBudget, - this._publicKey, - this._memo - ); - - const privateKey = this._signer?.getPrivateKey(); - if (privateKey !== undefined && this.transaction.atomTransaction.publicKey !== undefined) { - const signDoc = utils.createSignDoc(this.transaction.atomTransaction, this._accountNumber, this._chainId); - const txnHash = sha256(makeSignBytes(signDoc)); - const signature = await Secp256k1.createSignature(txnHash, privateKey); - const compressedSig = Buffer.concat([signature.r(), signature.s()]); - this.addSignature({ pub: this.transaction.atomTransaction.publicKey }, compressedSig); - } - - if (this._signature !== undefined) { - this.transaction.addSignature(this._signature.toString('hex')); - this.transaction.atomTransaction = utils.createAtomTransactionWithHash( - this._sequence, - this._messages, - this._gasBudget, - this._publicKey, - this._signature, - this._memo - ); - } - this.transaction.loadInputsAndOutputs(); - return this.transaction; - } - - /** @inheritdoc */ - protected signImplementation(key: BaseKey): Transaction { - this.validateKey(key); - if (this._accountNumber === undefined) { - throw new SigningError('accountNumber is required before signing'); - } - if (this._chainId === undefined) { - throw new SigningError('chainId is required before signing'); - } - this._signer = new KeyPair({ prv: key.key }); - this._publicKey = this._signer.getKeys().pub; - return this.transaction; - } - - validateAddress(address: BaseAddress, addressFormat?: string): void { - if (!(utils.isValidAddress(address.address) || utils.isValidValidatorAddress(address.address))) { - throw new BuildTransactionError('transactionBuilder: address isValidAddress check failed: ' + address.address); - } - } - - /** @inheritdoc */ - validateValue(value: BigNumber): void { - if (value.isLessThan(0)) { - throw new BuildTransactionError('Value cannot be less than zero'); - } - } - - /** @inheritdoc */ - validateKey(key: BaseKey): void { - try { - new KeyPair({ prv: key.key }); - } catch { - throw new BuildTransactionError(`Key validation failed`); - } - } - - /** @inheritdoc */ - validateRawTransaction(rawTransaction: string): void { - if (!rawTransaction) { - throw new InvalidTransactionError('Invalid raw transaction: Undefined rawTransaction'); - } - try { - } catch (e) { - throw new InvalidTransactionError('Invalid raw transaction: ' + e.message); - } - const atomTransaction = utils.deserializeAtomTransaction(rawTransaction); - utils.validateAtomTransaction(atomTransaction); - } - - /** @inheritdoc */ - validateTransaction(transaction: Transaction): void { - utils.validateAtomTransaction({ - sequence: this._sequence, - sendMessages: this._messages, - gasBudget: this._gasBudget, - publicKey: this._publicKey, - }); - } -} diff --git a/modules/sdk-coin-atom/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-atom/src/lib/transactionBuilderFactory.ts index c2097eb780..c4158cbc76 100644 --- a/modules/sdk-coin-atom/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-atom/src/lib/transactionBuilderFactory.ts @@ -1,11 +1,14 @@ +import { + CosmosTransaction, + CosmosTransactionBuilder, + CosmosTransferBuilder, + StakingActivateBuilder, + StakingDeactivateBuilder, + StakingWithdrawRewardsBuilder, +} from '@bitgo/abstract-cosmos'; import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; -import { TransactionBuilder } from './transactionBuilder'; -import { TransferBuilder } from './transferBuilder'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { Transaction } from './transaction'; -import { StakingActivateBuilder } from './StakingActivateBuilder'; -import { StakingDeactivateBuilder } from './StakingDeactivateBuilder'; -import { StakingWithdrawRewardsBuilder } from './StakingWithdrawRewardsBuilder'; +import utils from './utils'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -13,8 +16,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { } /** @inheritdoc */ - from(raw: string): TransactionBuilder { - const tx = new Transaction(this._coinConfig); + from(raw: string): CosmosTransactionBuilder { + const tx = new CosmosTransaction(this._coinConfig, utils); tx.enrichTransactionDetailsFromRawTransaction(raw); try { switch (tx.type) { @@ -35,20 +38,20 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { } /** @inheritdoc */ - getTransferBuilder(tx?: Transaction): TransferBuilder { - return this.initializeBuilder(tx, new TransferBuilder(this._coinConfig)); + getTransferBuilder(tx?: CosmosTransaction): CosmosTransferBuilder { + return this.initializeBuilder(tx, new CosmosTransferBuilder(this._coinConfig, utils)); } /** @inheritdoc */ - getStakingActivateBuilder(tx?: Transaction): StakingActivateBuilder { - return this.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig)); + getStakingActivateBuilder(tx?: CosmosTransaction): StakingActivateBuilder { + return this.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig, utils)); } /** @inheritdoc */ - getStakingDeactivateBuilder(tx?: Transaction): StakingDeactivateBuilder { - return this.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig)); + getStakingDeactivateBuilder(tx?: CosmosTransaction): StakingDeactivateBuilder { + return this.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig, utils)); } /** @inheritdoc */ - getStakingWithdrawRewardsBuilder(tx?: Transaction): StakingWithdrawRewardsBuilder { - return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig)); + getStakingWithdrawRewardsBuilder(tx?: CosmosTransaction): StakingWithdrawRewardsBuilder { + return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, utils)); } /** @inheritdoc */ getWalletInitializationBuilder(): void { @@ -62,7 +65,7 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { * @param {TransactionBuilder} builder - the builder to be initialized * @returns {TransactionBuilder} the builder initialized */ - private initializeBuilder(tx: Transaction | undefined, builder: T): T { + private initializeBuilder(tx: CosmosTransaction | undefined, builder: T): T { if (tx) { builder.initBuilder(tx); } diff --git a/modules/sdk-coin-atom/src/lib/transferBuilder.ts b/modules/sdk-coin-atom/src/lib/transferBuilder.ts deleted file mode 100644 index 4a5836dc54..0000000000 --- a/modules/sdk-coin-atom/src/lib/transferBuilder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TransactionType } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig } from '@bitgo/statics'; - -import { sendMsgTypeUrl } from './constants'; -import { SendMessage } from './iface'; -import { TransactionBuilder } from './transactionBuilder'; -import utils from './utils'; - -export class TransferBuilder extends TransactionBuilder { - constructor(_coinConfig: Readonly) { - super(_coinConfig); - } - - protected get transactionType(): TransactionType { - return TransactionType.Send; - } - - /** @inheritdoc */ - messages(sendMessages: SendMessage[]): this { - this._messages = sendMessages.map((sendMessage) => { - utils.validateSendMessage(sendMessage); - return { - typeUrl: sendMsgTypeUrl, - value: sendMessage, - }; - }); - return this; - } -} diff --git a/modules/sdk-coin-atom/src/lib/utils.ts b/modules/sdk-coin-atom/src/lib/utils.ts index 0ba495ab89..2ab5a705c7 100644 --- a/modules/sdk-coin-atom/src/lib/utils.ts +++ b/modules/sdk-coin-atom/src/lib/utils.ts @@ -1,555 +1,21 @@ -import { - BaseUtils, - InvalidTransactionError, - NotImplementedError, - ParseTransactionError, - TransactionType, -} from '@bitgo/sdk-core'; -import { encodeSecp256k1Pubkey, encodeSecp256k1Signature } from '@cosmjs/amino'; -import { fromBase64, fromBech32, fromHex, toHex } from '@cosmjs/encoding'; -import { - DecodedTxRaw, - decodePubkey, - decodeTxRaw, - EncodeObject, - encodePubkey, - makeAuthInfoBytes, - makeSignDoc, - Registry, -} from '@cosmjs/proto-signing'; -import { Coin, defaultRegistryTypes } from '@cosmjs/stargate'; +import { CosmosUtils } from '@bitgo/abstract-cosmos'; +import { InvalidTransactionError } from '@bitgo/sdk-core'; +import { Coin } from '@cosmjs/stargate'; import BigNumber from 'bignumber.js'; -import { SignDoc, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; -import { Any } from 'cosmjs-types/google/protobuf/any'; -import * as crypto from 'crypto'; - import * as constants from './constants'; -import { - AtomTransaction, - DelegateOrUndelegeteMessage, - FeeData, - MessageData, - SendMessage, - WithdrawDelegatorRewardsMessage, -} from './iface'; -import { KeyPair } from './keyPair'; - -export class Utils implements BaseUtils { - private registry = new Registry([...defaultRegistryTypes]); - - /** @inheritdoc */ - isValidBlockId(hash: string): boolean { - return this.validateBlake2b(hash); - } - - /** @inheritdoc */ - isValidPrivateKey(key: string): boolean { - try { - new KeyPair({ prv: key }); - return true; - } catch { - return false; - } - } - - /** @inheritdoc */ - isValidPublicKey(key: string): boolean { - try { - new KeyPair({ pub: key }); - return true; - } catch { - return false; - } - } - - /** @inheritdoc */ - isValidSignature(signature: string): boolean { - throw new NotImplementedError('isValidSignature not implemented'); - } +export class Utils extends CosmosUtils { /** @inheritdoc */ - isValidTransactionId(txId: string): boolean { - return this.validateBlake2b(txId); - } - - /** - * Checks if transaction hash is in valid black2b format - */ - validateBlake2b(hash: string): boolean { - if (hash?.length !== 64) { - return false; - } - return hash.match(/^[a-zA-Z0-9]+$/) !== null; - } - - /** - * Checks if a cosmos like Bech32 address matches given regular expression and - * validates memoId if present - * @param {string} address - * @param {RegExp} regExp Regular expression to validate the root address against after trimming the memoId - * @returns {boolean} true if address is valid - */ - protected isValidCosmosLikeAddressWithMemoId(address: string, regExp: RegExp): boolean { - if (typeof address !== 'string') return false; - const addressArray = address.split('?memoId='); - if ( - ![1, 2].includes(addressArray.length) || // should have at most one occurrence of 'memoId=' - !this.isValidBech32AddressMatchingRegex(addressArray[0], regExp) || - (addressArray[1] && !this.isValidMemoId(addressArray[1])) - ) { - return false; - } - return true; - } - - /** - * Checks if address is valid Bech32 and matches given regular expression - * @param {string} address - * @param {RegExp} regExp Regular expression to validate the address against - * @returns {boolean} true if address is valid - */ - protected isValidBech32AddressMatchingRegex(address: string, regExp: RegExp): boolean { - try { - fromBech32(address); - } catch (e) { - return false; - } - return regExp.test(address); - } - - /** - * Return boolean indicating whether a memo id is valid - * - * @param memoId memo id - * @returns true if memo id is valid - */ - isValidMemoId(memoId: string): boolean { - let memoIdNumber: BigNumber; - try { - memoIdNumber = new BigNumber(memoId); - } catch (e) { - return false; - } - return memoIdNumber.gte(0) && memoIdNumber.isInteger(); - } - - /** - * Validates if the address matches with regex @see accountAddressRegex - * - * @param {string} address - * @returns {boolean} - the validation result - */ isValidAddress(address: string): boolean { return this.isValidCosmosLikeAddressWithMemoId(address, constants.accountAddressRegex); } - /** - * Validates if the address matches with regex @see accountAddressRegex - * - * @param {string} address - * @returns {boolean} - the validation result - */ + /** @inheritdoc */ isValidValidatorAddress(address: string): boolean { return this.isValidBech32AddressMatchingRegex(address, constants.validatorAddressRegex); } - /** - * Validates whether amounts are in range - * - * @param {number[]} amounts - the amounts to validate - * @returns {boolean} - the validation result - */ - isValidAmounts(amounts: number[]): boolean { - for (const amount of amounts) { - if (!this.isValidAmount(amount)) { - return false; - } - } - return true; - } - - /** - * Validates whether amount is in range - * @param {number} amount - * @returns {boolean} the validation result - */ - isValidAmount(amount: number): boolean { - const bigNumberAmount = new BigNumber(amount); - if (!bigNumberAmount.isInteger() || bigNumberAmount.isLessThanOrEqualTo(0)) { - return false; - } - return true; - } - - /** - * Decodes raw tx data into messages, signing info, and fee data - * @param {string} txHex - raw base64 tx - * @returns {DecodedTxRaw} Decoded transaction - */ - getDecodedTxFromRawBase64(txRaw: string): DecodedTxRaw { - try { - return decodeTxRaw(fromBase64(txRaw)); - } catch (e) { - throw new ParseTransactionError('Error decoding TxRaw base64 encoded string: ' + e.message); - } - } - - /** - * Returns the array of messages in the body of the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {EncodeObject[]} messages along with type url - */ - private getEncodedMessagesFromDecodedTx(decodedTx: DecodedTxRaw): EncodeObject[] { - return decodedTx.body.messages; - } - - /** - * Pulls the sequence number from a DecodedTxRaw AuthInfo property - * @param {DecodedTxRaw} decodedTx - * @returns {number} sequence - */ - getSequenceFromDecodedTx(decodedTx: DecodedTxRaw): number { - return Number(decodedTx.authInfo.signerInfos[0].sequence); - } - - /** - * Pulls the typeUrl from the encoded message of a DecodedTxRaw - * @param {DecodedTxRaw} decodedTx - * @returns {string} cosmos proto type url - */ - getTypeUrlFromDecodedTx(decodedTx: DecodedTxRaw): string { - const encodedMessage = this.getEncodedMessagesFromDecodedTx(decodedTx)[0]; - return encodedMessage.typeUrl; - } - - /** - * Returns the fee data from the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {FeeData} fee data - */ - getGasBudgetFromDecodedTx(decodedTx: DecodedTxRaw): FeeData { - return { - amount: decodedTx.authInfo.fee?.amount as Coin[], - gasLimit: Number(decodedTx.authInfo.fee?.gasLimit), - }; - } - - /** - * Returns the publicKey from the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {string | undefined} publicKey in hex format if it exists, undefined otherwise - */ - getPublicKeyFromDecodedTx(decodedTx: DecodedTxRaw): string | undefined { - const publicKeyUInt8Array = decodedTx.authInfo.signerInfos?.[0].publicKey?.value; - if (publicKeyUInt8Array) { - return toHex(fromBase64(decodePubkey(decodedTx.authInfo.signerInfos?.[0].publicKey)?.value)); - } - return undefined; - } - - /** - * Returns the array of MessageData[] from the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {MessageData[]} Send transaction message data - */ - getSendMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData[] { - return decodedTx.body.messages.map((message) => { - const value = this.registry.decode(message); - return { - value: { - fromAddress: value.fromAddress, - toAddress: value.toAddress, - amount: value.amount, - }, - typeUrl: message.typeUrl, - }; - }); - } - - /** - * Returns the array of MessageData[] from the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {MessageData[]} Delegate of undelegate transaction message data - */ - getDelegateOrUndelegateMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData[] { - return decodedTx.body.messages.map((message) => { - const value = this.registry.decode(message); - return { - value: { - delegatorAddress: value.delegatorAddress, - validatorAddress: value.validatorAddress, - amount: value.amount, - }, - typeUrl: message.typeUrl, - }; - }); - } - - /** - * Returns the array of MessageData[] from the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {MessageData[]} WithdrawDelegatorRewards transaction message data - */ - getWithdrawRewardsMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData[] { - return decodedTx.body.messages.map((message) => { - const value = this.registry.decode(message); - return { - value: { - delegatorAddress: value.delegatorAddress, - validatorAddress: value.validatorAddress, - }, - typeUrl: message.typeUrl, - }; - }); - } - - /** - * Returns the array of MessageData[] from the decoded transaction - * @param {DecodedTxRaw} decodedTx - * @returns {MessageData[]} Delegate of undelegate transaction message data - */ - getWithdrawDelegatorRewardsMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData[] { - return decodedTx.body.messages.map((message) => { - const value = this.registry.decode(message); - return { - value: { - delegatorAddress: value.delegatorAddress, - validatorAddress: value.validatorAddress, - }, - typeUrl: message.typeUrl, - }; - }); - } - - /** - * Determines bitgo transaction type based on cosmos proto type url - * @param {string} typeUrl - * @returns {TransactionType | undefined} TransactionType if url is supported else undefined - */ - getTransactionTypeFromTypeUrl(typeUrl: string): TransactionType | undefined { - switch (typeUrl) { - case constants.sendMsgTypeUrl: - return TransactionType.Send; - case constants.delegateMsgTypeUrl: - return TransactionType.StakingActivate; - case constants.undelegateMsgTypeUrl: - return TransactionType.StakingDeactivate; - case constants.withdrawDelegatorRewardMsgTypeUrl: - return TransactionType.StakingWithdraw; - default: - return undefined; - } - } - - /** - * Creates a txRaw from an atom transaction @see AtomTransaction - * @Precondition atomTransaction.publicKey must be defined - * @param {AtomTransaction} atomTransaction - * @returns {TxRaw} Unsigned raw transaction - */ - createTxRawFromAtomTransaction(atomTransaction: AtomTransaction): TxRaw { - if (!atomTransaction.publicKey) { - throw new Error('publicKey is required to create a txRaw'); - } - const encodedPublicKey: Any = encodePubkey(encodeSecp256k1Pubkey(fromHex(atomTransaction.publicKey))); - const messages = atomTransaction.sendMessages as unknown as Any[]; - let txBodyValue; - if (atomTransaction.memo) { - txBodyValue = { - memo: atomTransaction.memo, - messages: messages, - }; - } else { - txBodyValue = { - messages: messages, - }; - } - - const txBodyBytes = this.registry.encodeTxBody(txBodyValue); - const sequence = atomTransaction.sequence; - const authInfoBytes = makeAuthInfoBytes( - [{ pubkey: encodedPublicKey, sequence }], - atomTransaction.gasBudget.amount, - atomTransaction.gasBudget.gasLimit, - undefined, - undefined, - undefined - ); - return TxRaw.fromPartial({ - bodyBytes: txBodyBytes, - authInfoBytes: authInfoBytes, - }); - } - - /** - * Encodes a signature into a txRaw - * @param {string} publicKeyHex publicKey in hex encoded string format - * @param {string} signatureHex signature in hex encoded string format - * @param {TxRaw} unsignedTx raw transaction - * @returns {TxRaw} Signed raw transaction - */ - createSignedTxRaw( - publicKeyHex: string, - signatureHex: string, - unsignedTx: { bodyBytes: Uint8Array; authInfoBytes: Uint8Array } - ): TxRaw { - const stdSignature = encodeSecp256k1Signature(fromHex(publicKeyHex), fromHex(signatureHex)); - return TxRaw.fromPartial({ - bodyBytes: unsignedTx.bodyBytes, - authInfoBytes: unsignedTx.authInfoBytes, - signatures: [fromBase64(stdSignature.signature)], - }); - } - - /** - * Decodes a raw transaction into a DecodedTxRaw and checks if it has non empty signatures - * @param {string} rawTransaction - * @returns {boolean} true if transaction is signed else false - */ - isSignedRawTx(rawTransaction: string): boolean { - const decodedTx = this.getDecodedTxFromRawBase64(rawTransaction); - if (decodedTx.signatures.length > 0) { - return true; - } - return false; - } - - /** - * Deserializes base64 enocded raw transaction string into @see AtomTransaction - * @param {string} rawTx base64 enocded raw transaction string - * @returns {AtomTransaction} Deserialized atomTransaction - */ - deserializeAtomTransaction(rawTx: string): AtomTransaction { - const decodedTx = utils.getDecodedTxFromRawBase64(rawTx); - const typeUrl = utils.getTypeUrlFromDecodedTx(decodedTx); - const type: TransactionType | undefined = utils.getTransactionTypeFromTypeUrl(typeUrl); - let sendMessageData: MessageData[]; - if (type === TransactionType.Send) { - sendMessageData = utils.getSendMessageDataFromDecodedTx(decodedTx); - } else if (type === TransactionType.StakingActivate || type === TransactionType.StakingDeactivate) { - sendMessageData = utils.getDelegateOrUndelegateMessageDataFromDecodedTx(decodedTx); - } else if (type === TransactionType.StakingWithdraw) { - sendMessageData = utils.getWithdrawRewardsMessageDataFromDecodedTx(decodedTx); - } else { - throw new Error('Transaction type not supported: ' + typeUrl); - } - const sequence = utils.getSequenceFromDecodedTx(decodedTx); - const gasBudget = utils.getGasBudgetFromDecodedTx(decodedTx); - const publicKey = utils.getPublicKeyFromDecodedTx(decodedTx); - const signature = decodedTx.signatures?.[0] !== undefined ? Buffer.from(decodedTx.signatures[0]) : undefined; - return this.createAtomTransactionWithHash( - sequence, - sendMessageData, - gasBudget, - publicKey, - signature, - decodedTx.body?.memo - ); - } - - createAtomTransaction( - sequence: number, - messages: MessageData[], - gasBudget: FeeData, - publicKey?: string, - memo?: string - ): AtomTransaction { - const atomTxn = { - sequence: sequence, - sendMessages: messages, - gasBudget: gasBudget, - publicKey: publicKey, - memo: memo, - }; - this.validateAtomTransaction(atomTxn); - return atomTxn; - } - - createAtomTransactionWithHash( - sequence: number, - messages: MessageData[], - gasBudget: FeeData, - publicKey?: string, - signature?: Buffer, - memo?: string - ): AtomTransaction { - const atomTxn = this.createAtomTransaction(sequence, messages, gasBudget, publicKey, memo); - let hash = constants.UNAVAILABLE_TEXT; - if (signature !== undefined) { - const unsignedTx = this.createTxRawFromAtomTransaction(atomTxn); - const signedTx = TxRaw.fromPartial({ - bodyBytes: unsignedTx.bodyBytes, - authInfoBytes: unsignedTx.authInfoBytes, - signatures: [signature], - }); - hash = crypto - .createHash('sha256') - .update(TxRaw.encode(signedTx).finish()) - .digest() - .toString('hex') - .toLocaleUpperCase('en-US'); - return { ...atomTxn, hash: hash, signature: signature }; - } - return { ...atomTxn, hash: hash }; - } - - validateAtomTransaction(tx: AtomTransaction): void { - this.validateSequence(tx.sequence); - this.validateGasBudget(tx.gasBudget); - this.validatePublicKey(tx.publicKey); - if (tx.sendMessages === undefined || tx.sendMessages.length === 0) { - throw new InvalidTransactionError('Invalid transaction: messages is required'); - } else { - tx.sendMessages.forEach((message) => this.validateMessageData(message)); - } - } - - validateMessageData(messageData: MessageData): void { - if (messageData == null) { - throw new InvalidTransactionError(`Invalid MessageData: undefined`); - } - if (messageData.typeUrl == null || utils.getTransactionTypeFromTypeUrl(messageData.typeUrl) == null) { - throw new InvalidTransactionError(`Invalid MessageData typeurl: ` + messageData.typeUrl); - } - const type = utils.getTransactionTypeFromTypeUrl(messageData.typeUrl); - if (type === TransactionType.Send) { - const value = messageData.value as SendMessage; - if (value.toAddress == null) { - throw new InvalidTransactionError(`Invalid MessageData value.toAddress: ` + value.toAddress); - } - if (value.fromAddress == null) { - throw new InvalidTransactionError(`Invalid MessageData value.fromAddress: ` + value.fromAddress); - } - } else if (type === TransactionType.StakingActivate || type === TransactionType.StakingDeactivate) { - const value = messageData.value as DelegateOrUndelegeteMessage; - if (value.validatorAddress == null) { - throw new InvalidTransactionError(`Invalid MessageData value.validatorAddress: ` + value.validatorAddress); - } - if (value.delegatorAddress == null) { - throw new InvalidTransactionError(`Invalid MessageData value.delegatorAddress: ` + value.delegatorAddress); - } - this.validateAmount((messageData.value as DelegateOrUndelegeteMessage).amount); - } else if (type === TransactionType.StakingWithdraw) { - const value = messageData.value as WithdrawDelegatorRewardsMessage; - if (value.validatorAddress == null) { - throw new InvalidTransactionError(`Invalid MessageData value.validatorAddress: ` + value.validatorAddress); - } - if (value.delegatorAddress == null) { - throw new InvalidTransactionError(`Invalid MessageData value.delegatorAddress: ` + value.delegatorAddress); - } - } else { - throw new InvalidTransactionError(`Invalid MessageData TypeUrl is not supported: ` + messageData.typeUrl); - } - if (type !== TransactionType.StakingWithdraw) { - } - } - - validateAmountData(amountArray: Coin[]): void { - amountArray.forEach((coinAmount) => { - this.validateAmount(coinAmount); - }); - } - + /** @inheritdoc */ validateAmount(amount: Coin): void { const amountBig = BigNumber(amount.amount); if (amountBig.isLessThanOrEqualTo(0)) { @@ -559,97 +25,6 @@ export class Utils implements BaseUtils { throw new InvalidTransactionError('transactionBuilder: validateAmount: Invalid denom: ' + amount.denom); } } - - validateGasBudget(gasBudget: FeeData): void { - if (gasBudget.gasLimit <= 0) { - throw new InvalidTransactionError('Invalid gas limit ' + gasBudget.gasLimit); - } - this.validateAmountData(gasBudget.amount); - } - - validateSequence(sequence: number) { - if (sequence < 0) { - throw new InvalidTransactionError('Invalid sequence: less than zero'); - } - } - - validatePublicKey(publicKey: string | undefined) { - if (publicKey !== undefined) { - try { - new KeyPair({ pub: publicKey }); - } catch { - throw new InvalidTransactionError(`Key validation failed`); - } - } - } - - /** - * Creates a sign doc from an atom transaction @see AtomTransaction - * @Precondition atomTransaction.accountNumber and atomTransaction.chainId must be defined - * @param {AtomTransaction} atomTransaction - * @returns {SignDoc} sign doc - */ - createSignDoc( - atomTransaction: AtomTransaction, - accountNumber: number | undefined, - chainId: string | undefined - ): SignDoc { - if (!accountNumber) { - throw new Error('accountNumber is required to create a sign doc'); - } - if (!chainId) { - throw new Error('chainId is required to create a sign doc'); - } - if (!atomTransaction) { - throw new Error('atomTransaction is required to create a sign doc'); - } - const txRaw = utils.createTxRawFromAtomTransaction(atomTransaction); - return makeSignDoc(txRaw.bodyBytes, txRaw.authInfoBytes, chainId, accountNumber); - } - - validateDelegateOrUndelegateMessage(delegateMessage: DelegateOrUndelegeteMessage) { - if (!delegateMessage.validatorAddress || !utils.isValidValidatorAddress(delegateMessage.validatorAddress)) { - throw new InvalidTransactionError( - `Invalid DelegateOrUndelegeteMessage validatorAddress: ` + delegateMessage.validatorAddress - ); - } - if (!delegateMessage.delegatorAddress || !utils.isValidAddress(delegateMessage.delegatorAddress)) { - throw new InvalidTransactionError( - `Invalid DelegateOrUndelegeteMessage delegatorAddress: ` + delegateMessage.delegatorAddress - ); - } - this.validateAmount(delegateMessage.amount); - } - - validateWithdrawRewardsMessage(withdrawRewardsMessage: WithdrawDelegatorRewardsMessage) { - if ( - !withdrawRewardsMessage.validatorAddress || - !utils.isValidValidatorAddress(withdrawRewardsMessage.validatorAddress) - ) { - throw new InvalidTransactionError( - `Invalid WithdrawDelegatorRewardsMessage validatorAddress: ` + withdrawRewardsMessage.validatorAddress - ); - } - if (!withdrawRewardsMessage.delegatorAddress || !utils.isValidAddress(withdrawRewardsMessage.delegatorAddress)) { - throw new InvalidTransactionError( - `Invalid WithdrawDelegatorRewardsMessage delegatorAddress: ` + withdrawRewardsMessage.delegatorAddress - ); - } - } - - validateSendMessage(sendMessage: SendMessage) { - if (!sendMessage.toAddress || !utils.isValidAddress(sendMessage.toAddress)) { - throw new InvalidTransactionError(`Invalid SendMessage toAddress: ` + sendMessage.toAddress); - } - if (!sendMessage.fromAddress || !utils.isValidAddress(sendMessage.fromAddress)) { - throw new InvalidTransactionError(`Invalid SendMessage fromAddress: ` + sendMessage.fromAddress); - } - this.validateAmountData(sendMessage.amount); - } - - isValidHexString(hexString: string): boolean { - return /^[0-9A-Fa-f]*$/.test(hexString); - } } const utils = new Utils(); diff --git a/modules/sdk-coin-atom/src/tatom.ts b/modules/sdk-coin-atom/src/tatom.ts index 8b1d5fab4e..1dd4bcb9c6 100644 --- a/modules/sdk-coin-atom/src/tatom.ts +++ b/modules/sdk-coin-atom/src/tatom.ts @@ -22,18 +22,4 @@ export class Tatom extends Atom { static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { return new Tatom(bitgo, staticsCoin); } - - /** - * Identifier for the blockchain which supports this coin - */ - public getChain(): string { - return this._staticsCoin.name; - } - - /** - * Complete human-readable name of this coin - */ - public getFullName(): string { - return this._staticsCoin.fullName; - } } diff --git a/modules/sdk-coin-atom/test/unit/atom.ts b/modules/sdk-coin-atom/test/unit/atom.ts index 3129bdc95f..15a0a5a0aa 100644 --- a/modules/sdk-coin-atom/test/unit/atom.ts +++ b/modules/sdk-coin-atom/test/unit/atom.ts @@ -1,21 +1,21 @@ +import { CosmosTransaction, SendMessage } from '@bitgo/abstract-cosmos'; import { BitGoAPI } from '@bitgo/sdk-api'; import { EcdsaRangeProof, EcdsaTypes } from '@bitgo/sdk-lib-mpc'; -import { mockSerializedChallengeWithProofs, TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { TestBitGo, TestBitGoAPI, mockSerializedChallengeWithProofs } from '@bitgo/sdk-test'; import { coins } from '@bitgo/statics'; import BigNumber from 'bignumber.js'; import sinon from 'sinon'; -import { Atom, Tatom, Transaction } from '../../src'; +import { Atom, Tatom } from '../../src'; import { GAS_AMOUNT } from '../../src/lib/constants'; -import { SendMessage } from '../../src/lib/iface'; import utils from '../../src/lib/utils'; import { - address, TEST_DELEGATE_TX, TEST_SEND_MANY_TX, TEST_SEND_TX, TEST_TX_WITH_MEMO, TEST_UNDELEGATE_TX, TEST_WITHDRAW_REWARDS_TX, + address, wrwUser, } from '../resources/atom'; import should = require('should'); @@ -430,12 +430,11 @@ describe('ATOM', function () { }); res.should.not.be.empty(); res.should.hasOwnProperty('serializedTx'); - res.should.hasOwnProperty('scanIndex'); sandBox.assert.calledOnce(basecoin.getAccountBalance); sandBox.assert.calledOnce(basecoin.getAccountDetails); sandBox.assert.calledOnce(basecoin.getChainId); - const atomTxn = new Transaction(coin); + const atomTxn = new CosmosTransaction(coin, utils); atomTxn.enrichTransactionDetailsFromRawTransaction(res.serializedTx); const atomTxnJson = atomTxn.toJson(); const sendMessage = atomTxnJson.sendMessages[0].value as SendMessage; diff --git a/modules/sdk-coin-atom/test/unit/transaction.ts b/modules/sdk-coin-atom/test/unit/transaction.ts index ce79fe4693..a0dfb42cff 100644 --- a/modules/sdk-coin-atom/test/unit/transaction.ts +++ b/modules/sdk-coin-atom/test/unit/transaction.ts @@ -1,10 +1,15 @@ +import { + CosmosTransaction, + DelegateOrUndelegeteMessage, + SendMessage, + WithdrawDelegatorRewardsMessage, +} from '@bitgo/abstract-cosmos'; import { toHex, TransactionType } from '@bitgo/sdk-core'; import { coins } from '@bitgo/statics'; import { fromBase64 } from '@cosmjs/encoding'; import should from 'should'; - import { Transaction } from '../../src'; -import { DelegateOrUndelegeteMessage, SendMessage, WithdrawDelegatorRewardsMessage } from '../../src/lib/iface'; +import utils from '../../src/lib/utils'; import * as testData from '../resources/atom'; describe('Atom Transaction', () => { @@ -12,7 +17,7 @@ describe('Atom Transaction', () => { const config = coins.get('tatom'); beforeEach(() => { - tx = new Transaction(config); + tx = new CosmosTransaction(config, utils); }); describe('Empty transaction', () => { diff --git a/modules/sdk-coin-atom/tsconfig.json b/modules/sdk-coin-atom/tsconfig.json index 339ace24a2..d40e35bb27 100644 --- a/modules/sdk-coin-atom/tsconfig.json +++ b/modules/sdk-coin-atom/tsconfig.json @@ -18,6 +18,9 @@ }, { "path": "../sdk-test" + }, + { + "path": "../abstract-cosmos" } ] } diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 2cd203c841..c24a6e6993 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -1438,7 +1438,7 @@ export enum KeyCurve { * This enum contains the base units for the coins that BitGo supports */ export enum BaseUnit { - ATOM = 'uATOM', + ATOM = 'uatom', ETH = 'wei', BTC = 'satoshi', BSC = 'jager',