diff --git a/modules/sdk-coin-algo/src/algo.ts b/modules/sdk-coin-algo/src/algo.ts index 5e1e514279..5b7e1c88fd 100644 --- a/modules/sdk-coin-algo/src/algo.ts +++ b/modules/sdk-coin-algo/src/algo.ts @@ -579,6 +579,44 @@ export class Algo extends BaseCoin { } async verifyTransaction(params: VerifyTransactionOptions): Promise { + const coinConfig = coins.get(this.getChain()); + const { txPrebuild, txParams } = params; + + // Validate the presence of txHex + const rawTx = txPrebuild.txHex; + if (!rawTx) { + throw new Error('Missing required tx prebuild property: txHex'); + } + + // Parse the transaction + const transaction = new AlgoLib.Transaction(coinConfig); + transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64')); + const explainedTx = transaction.explainTransaction(); + + // Validate recipients + if (txParams.recipients) { + const filteredRecipients = txParams.recipients.map((recipient) => ({ + address: recipient.address, + amount: BigInt(recipient.amount), + })); + + const filteredOutputs = explainedTx.outputs.map((output) => ({ + address: output.address, + amount: BigInt(output.amount), + })); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Transaction outputs do not match the expected recipients in txParams.'); + } + + // Validate total amount + const totalAmount = txParams.recipients.reduce((sum, recipient) => sum.plus(recipient.amount), new BigNumber(0)); + + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Transaction total amount does not match the expected total amount.'); + } + } + return true; } diff --git a/modules/sdk-coin-algo/src/lib/transaction.ts b/modules/sdk-coin-algo/src/lib/transaction.ts index 57fa89b30b..e97391af36 100644 --- a/modules/sdk-coin-algo/src/lib/transaction.ts +++ b/modules/sdk-coin-algo/src/lib/transaction.ts @@ -252,6 +252,25 @@ export class Transaction extends BaseTransaction { return result; } + /** + * Sets this transaction payload + * + * @param rawTx - The raw transaction in hex format + */ + fromRawTransaction(rawTransaction: string): void { + try { + // Decode the raw transaction using Algorand SDK + const decodedTx = algosdk.decodeUnsignedTransaction(Buffer.from(rawTransaction, 'hex')); + + // Extract and set transaction details + this._algoTransaction = decodedTx; + this._sender = algosdk.encodeAddress(decodedTx.from.publicKey); + this._signatures = []; // Reset signatures as this is a raw transaction + } catch (e) { + throw new Error('Invalid raw transaction: ' + e.message); + } + } + /** * Load the input and output data on this transaction. */ diff --git a/modules/sdk-coin-cspr/src/cspr.ts b/modules/sdk-coin-cspr/src/cspr.ts index 09d33939f8..66cdc3413e 100644 --- a/modules/sdk-coin-cspr/src/cspr.ts +++ b/modules/sdk-coin-cspr/src/cspr.ts @@ -25,6 +25,7 @@ import { VerifyAddressOptions, VerifyTransactionOptions, } from '@bitgo/sdk-core'; +import _ from 'lodash'; interface SignTransactionOptions extends BaseSignTransactionOptions { txPrebuild: TransactionPrebuild; @@ -110,7 +111,44 @@ export class Cspr extends BaseCoin { } async verifyTransaction(params: VerifyTransactionOptions): Promise { - // TODO: Implement when available on the SDK. + const coinConfig = coins.get(this.getChain()); + const { txPrebuild, txParams } = params; + + // Validate the presence of txHex + const rawTx = txPrebuild.txHex; + if (!rawTx) { + throw new Error('Missing required tx prebuild property: txHex'); + } + + // Parse the transaction + const transaction = new CsprLib.Transaction(coinConfig); + transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64')); + const explainedTx = transaction.explainTransaction(); + + // Validate recipients + if (txParams.recipients) { + const filteredRecipients = txParams.recipients.map((recipient) => ({ + address: recipient.address, + amount: BigInt(recipient.amount), + })); + + const filteredOutputs = explainedTx.outputs.map((output) => ({ + address: output.address, + amount: BigInt(output.amount), + })); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Transaction outputs do not match the expected recipients in txParams.'); + } + + // Validate total amount + const totalAmount = txParams.recipients.reduce((sum, recipient) => sum.plus(recipient.amount), new BigNumber(0)); + + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Transaction total amount does not match the expected total amount.'); + } + } + return true; } diff --git a/modules/sdk-coin-cspr/src/lib/transaction.ts b/modules/sdk-coin-cspr/src/lib/transaction.ts index 2fdbd20168..02aa0b4591 100644 --- a/modules/sdk-coin-cspr/src/lib/transaction.ts +++ b/modules/sdk-coin-cspr/src/lib/transaction.ts @@ -254,6 +254,32 @@ export class Transaction extends BaseTransaction { } } + /** + * Sets this transaction payload + * + * @param rawTx - The raw transaction in hex format + */ + async fromRawTransaction(rawTransaction: string): Promise { + try { + // Decode the raw transaction using Casper's DeployUtil + const deploy = DeployUtil.deployFromJson(JSON.parse(Buffer.from(rawTransaction, 'hex').toString())); + + if (!deploy) { + throw new Error('Failed to decode raw transaction'); + } + + // Extract and set transaction details + if (deploy.ok) { + this._deploy = deploy.val; + } else { + throw new Error('Failed to decode raw transaction: ' + (deploy.err as any)?.message || 'Unknown error'); + } + this._signatures = []; // Reset signatures as this is a raw transaction + } catch (e) { + throw new Error('Invalid raw transaction: ' + e.message); + } + } + get casperTx(): DeployUtil.Deploy { return this._deploy; } diff --git a/modules/sdk-coin-dot/src/dot.ts b/modules/sdk-coin-dot/src/dot.ts index 3434a7a87d..c0bea6fa8b 100644 --- a/modules/sdk-coin-dot/src/dot.ts +++ b/modules/sdk-coin-dot/src/dot.ts @@ -646,12 +646,51 @@ export class Dot extends BaseCoin { } async verifyTransaction(params: VerifyTransactionOptions): Promise { - const { txParams } = params; + const coinConfig = coins.get(this.getChain()); + const { txPrebuild, txParams } = params; + + // Ensure only one recipient is allowed if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) { throw new Error( - `${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.` + `${this.getChain()} doesn't support sending to more than one destination address within a single transaction. Use only a single recipient.` ); } + + // Validate the presence of txHex + const rawTx = txPrebuild.txHex; + if (!rawTx) { + throw new Error('Missing required tx prebuild property: txHex'); + } + + // Parse the transaction + const transaction = new Transaction(coinConfig); + transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64')); + const explainedTx = transaction.explainTransaction(); + + // Validate recipients + if (txParams.recipients) { + const filteredRecipients = txParams.recipients.map((recipient) => ({ + address: recipient.address, + amount: BigInt(recipient.amount), + })); + + const filteredOutputs = explainedTx.outputs.map((output) => ({ + address: output.address, + amount: BigInt(output.amount), + })); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Transaction outputs do not match the expected recipients in txParams.'); + } + + // Validate total amount + const totalAmount = txParams.recipients.reduce((sum, recipient) => sum.plus(recipient.amount), new BigNumber(0)); + + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Transaction total amount does not match the expected total amount.'); + } + } + return true; } diff --git a/modules/sdk-coin-dot/src/lib/transaction.ts b/modules/sdk-coin-dot/src/lib/transaction.ts index 153d0d307a..6a945c3c22 100644 --- a/modules/sdk-coin-dot/src/lib/transaction.ts +++ b/modules/sdk-coin-dot/src/lib/transaction.ts @@ -448,6 +448,29 @@ export class Transaction extends BaseTransaction { ]; } + /** + * Sets this transaction payload + * + * @param rawTx - The raw transaction in hex format + */ + fromRawTransaction(rawTransaction: string): void { + try { + // Decode the raw transaction using the Polkadot txwrapper + const decodedTx = decode(rawTransaction, { + metadataRpc: this._dotTransaction.metadataRpc, + registry: this._registry, + isImmortalEra: utils.isZeroHex(this._dotTransaction.era), + }) as unknown as DecodedTx; + + // Extract and set transaction details + this._sender = decodedTx.address; + this._dotTransaction = decodedTx as unknown as UnsignedTransaction; + this._signatures = []; // Reset signatures as this is a raw transaction + } catch (e) { + throw new Error('Invalid raw transaction: ' + e.message); + } + } + private decodeInputsAndOutputsForBatch(decodedTx: DecodedTx) { const sender = decodedTx.address; this._inputs = []; diff --git a/modules/sdk-coin-stx/src/stx.ts b/modules/sdk-coin-stx/src/stx.ts index 5c4fca5ee6..848a8e2e03 100644 --- a/modules/sdk-coin-stx/src/stx.ts +++ b/modules/sdk-coin-stx/src/stx.ts @@ -15,9 +15,11 @@ import { ClarityType, cvToString, cvToValue } from '@stacks/transactions'; import { ExplainTransactionOptions, StxSignTransactionOptions, StxTransactionExplanation } from './types'; import { StxLib } from '.'; -import { TransactionBuilderFactory } from './lib'; +import { Transaction, TransactionBuilderFactory } from './lib'; import { TransactionBuilder } from './lib/transactionBuilder'; import { findTokenNameByContract } from './lib/utils'; +import _ from 'lodash'; +import BigNumber from 'bignumber.js'; export class Stx extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -67,12 +69,43 @@ export class Stx extends BaseCoin { } async verifyTransaction(params: VerifyTransactionOptions): Promise { - const { txParams } = params; + const coinConfig = coins.get(this.getChain()); + const { txPrebuild: txPrebuild, txParams: txParams } = params; if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) { throw new Error( `${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.` ); } + const transaction = new Transaction(coinConfig); + const rawTx = txPrebuild.txHex; + if (!rawTx) { + throw new Error('missing required tx prebuild property txHex'); + } + + transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64')); + const explainedTx = transaction.explainTransaction(); + if (txParams.recipients !== undefined) { + const filteredRecipients = txParams.recipients.map((recipient) => ({ + address: recipient.address, + amount: BigInt(recipient.amount), + })); + + const filteredOutputs = explainedTx.outputs.map((output) => ({ + address: output.address, + amount: BigInt(output.amount), + })); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Transaction outputs do not match the expected recipients in txParams.'); + } + + // Validate total amount + const totalAmount = txParams.recipients.reduce((sum, recipient) => sum.plus(recipient.amount), new BigNumber(0)); + + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Transaction total amount does not match the expected total amount.'); + } + } return true; } diff --git a/modules/sdk-coin-xtz/src/lib/transaction.ts b/modules/sdk-coin-xtz/src/lib/transaction.ts index e0bd38ebd1..cd1ca6c6be 100644 --- a/modules/sdk-coin-xtz/src/lib/transaction.ts +++ b/modules/sdk-coin-xtz/src/lib/transaction.ts @@ -158,6 +158,26 @@ export class Transaction extends BaseTransaction { }); } + /** + * Sets this transaction payload + * + * @param rawTx - The raw transaction in hex format + */ + async fromRawTransaction(rawTransaction: string): Promise { + try { + // Decode the raw transaction using Taquito's local-forging library + const decodedTx = localForger.parse(rawTransaction); + + // Extract and set transaction details + this._encodedTransaction = rawTransaction; + this._parsedTransaction = await decodedTx; + this._source = (await decodedTx).contents[0]?.source || ''; + this._signatures = []; // Reset signatures as this is a raw transaction + } catch (e) { + throw new Error('Invalid raw transaction: ' + e.message); + } + } + /** * Record the most important fields for a Transaction operation. * diff --git a/modules/sdk-coin-xtz/src/xtz.ts b/modules/sdk-coin-xtz/src/xtz.ts index 67e54368a7..ea5a70f908 100644 --- a/modules/sdk-coin-xtz/src/xtz.ts +++ b/modules/sdk-coin-xtz/src/xtz.ts @@ -16,7 +16,8 @@ import { import { bip32 } from '@bitgo/secp256k1'; import { CoinFamily, coins, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import BigNumber from 'bignumber.js'; -import { Interface, KeyPair, TransactionBuilder, Utils } from './lib'; +import { Interface, KeyPair, Transaction, TransactionBuilder, Utils } from './lib'; +import _ from 'lodash'; export class Xtz extends BaseCoin { protected readonly _staticsCoin: Readonly; @@ -116,12 +117,48 @@ export class Xtz extends BaseCoin { } async verifyTransaction(params: VerifyTransactionOptions): Promise { - const { txParams } = params; + const coinConfig = coins.get(this.getChain()); + const { txPrebuild, txParams } = params; if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) { throw new Error( `${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.` ); } + // Validate the presence of txHex + const rawTx = txPrebuild.txHex; + if (!rawTx) { + throw new Error('Missing required tx prebuild property: txHex'); + } + + // Parse the transaction + const transaction = new Transaction(coinConfig); + transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64')); + const explainedTx = transaction.explainTransaction(); + + // Validate recipients + if (txParams.recipients) { + const filteredRecipients = txParams.recipients.map((recipient) => ({ + address: recipient.address, + amount: BigInt(recipient.amount), + })); + + const filteredOutputs = explainedTx.outputs.map((output) => ({ + address: output.address, + amount: BigInt(output.amount), + })); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Transaction outputs do not match the expected recipients in txParams.'); + } + + // Validate total amount + const totalAmount = txParams.recipients.reduce((sum, recipient) => sum.plus(recipient.amount), new BigNumber(0)); + + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Transaction total amount does not match the expected total amount.'); + } + } + return true; } diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index 97cde95429..d076414888 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -37,6 +37,7 @@ import { KeychainsTriplet } from '../../../baseCoin'; import { exchangeEddsaCommitments } from '../../../tss/common'; import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc'; import { IRequestTracer } from '../../../../api'; +import { Wallet } from '../../../wallet'; /** * Utility functions for TSS work flows. @@ -586,7 +587,12 @@ export class EddsaUtils extends baseTSSUtils { assert(txRequestResolved.transactions || txRequestResolved.unsignedTxs, 'Unable to find transactions in txRequest'); const unsignedTx = apiVersion === 'full' ? txRequestResolved.transactions![0].unsignedTx : txRequestResolved.unsignedTxs[0]; - + await this.baseCoin.verifyTransaction({ + txPrebuild: { txHex: unsignedTx.signableHex }, + wallet: this.wallet as unknown as Wallet, + txParams: (params as any).txParams || { recipients: [] }, + walletType: this.wallet.multisigType(), + }); const signingKey = MPC.keyDerive( userSigningMaterial.uShare, [userSigningMaterial.bitgoYShare, userSigningMaterial.backupYShare],