diff --git a/modules/sdk-coin-xrp/src/xrp.ts b/modules/sdk-coin-xrp/src/xrp.ts index f128b25b33..fd05e7c756 100644 --- a/modules/sdk-coin-xrp/src/xrp.ts +++ b/modules/sdk-coin-xrp/src/xrp.ts @@ -21,14 +21,16 @@ import { ParseTransactionOptions, promiseProps, TokenEnablementConfig, + TransactionParams, UnexpectedAddressError, VerifyTransactionOptions, } from '@bitgo/sdk-core'; -import { BaseCoin as StaticsBaseCoin, coins, XrpCoin } from '@bitgo/statics'; +import { coins, BaseCoin as StaticsBaseCoin, XrpCoin } from '@bitgo/statics'; import * as rippleBinaryCodec from 'ripple-binary-codec'; import * as rippleKeypairs from 'ripple-keypairs'; import * as xrpl from 'xrpl'; +import { TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib'; import { ExplainTransactionOptions, FeeInfo, @@ -40,11 +42,11 @@ import { SupplementGenerateWalletOptions, TransactionExplanation, VerifyAddressOptions, + XrpTransactionType, } from './lib/iface'; import { KeyPair as XrpKeyPair } from './lib/keyPair'; import utils from './lib/utils'; import ripple from './ripple'; -import { TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib'; export class Xrp extends BaseCoin { protected _staticsCoin: Readonly; @@ -298,6 +300,98 @@ export class Xrp extends BaseCoin { }; } + getTransactionTypeRawTxHex(txHex: string): XrpTransactionType | undefined { + let transaction; + if (!txHex) { + throw new Error('missing required param txHex'); + } + try { + transaction = rippleBinaryCodec.decode(txHex); + } catch (e) { + try { + transaction = JSON.parse(txHex); + } catch (e) { + throw new Error('txHex needs to be either hex or JSON string for XRP'); + } + } + + return transaction.TransactionType; + } + + verifyTxType(txPrebuildDecoded: TransactionExplanation, txHexPrebuild: string | undefined): void { + if (!txHexPrebuild) throw new Error('Missing txHexPrebuild to verify token type for enabletoken tx'); + const transactionType = this.getTransactionTypeRawTxHex(txHexPrebuild); + if (transactionType === undefined) throw new Error('Missing TransactionType on token enablement tx'); + if (transactionType !== XrpTransactionType.TrustSet) + throw new Error(`tx type ${transactionType} does not match expected type TrustSet`); + // decoded payload type could come as undefined or any of the enabletoken like types but never as something else like Send, etc + const actualTypeFromDecoded = + 'type' in txPrebuildDecoded && typeof txPrebuildDecoded.type === 'string' ? txPrebuildDecoded.type : undefined; + if ( + !actualTypeFromDecoded || + actualTypeFromDecoded === 'enabletoken' || + actualTypeFromDecoded === 'AssociatedTokenAccountInitialization' + ) + return; + + throw new Error(`tx type ${actualTypeFromDecoded} does not match the expected type enabletoken`); + } + + verifyTokenName( + txParams: TransactionParams, + txPrebuildDecoded: TransactionExplanation, + txHexPrebuild: string | undefined, + coinConfig: XrpCoin + ): void { + if (!txHexPrebuild) throw new Error('Missing txHexPrebuild param required for token enablement.'); + if (!txParams.recipients || txParams.recipients.length === 0) + throw new Error('Missing recipients param for token enablement.'); + const fullTokenName = txParams.recipients[0].tokenName; + if (fullTokenName === undefined) + throw new Error('Param tokenName is required for token enablement. Recipient must include a token name.'); + + if (!('limitAmount' in txPrebuildDecoded)) throw new Error('Missing limitAmount param for token enablement.'); + + // we check currency on both the txHex but also the explained payload + const expectedCurrency = utils.getXrpCurrencyFromTokenName(fullTokenName).currency; + if (coinConfig.isToken && expectedCurrency !== txPrebuildDecoded.limitAmount.currency) + throw new Error('Invalid token issuer or currency on token enablement tx'); + } + + verifyActivationAddress(txParams: TransactionParams, txPrebuildDecoded: TransactionExplanation): void { + if (txParams.recipients === undefined || txParams.recipients.length === 0) + throw new Error('Missing recipients param for token enablement.'); + + if (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.` + ); + } + if (!('account' in txPrebuildDecoded)) throw new Error('missing account on token enablement tx'); + + const activationAddress = txParams.recipients[0].address; + const accountAddress = txPrebuildDecoded.account; + if (activationAddress !== accountAddress) throw new Error("Account address doesn't match with activation address."); + } + + verifyTokenIssuer(txParams: TransactionParams, txPrebuildDecoded: TransactionExplanation): void { + if (txPrebuildDecoded === undefined || !('limitAmount' in txPrebuildDecoded)) + throw new Error('missing token issuer on token enablement tx'); + const { issuer, currency } = txPrebuildDecoded.limitAmount; + if (!utils.getXrpToken(issuer, currency)) + throw new Error('Invalid token issuer or currency on token enablement tx'); + } + + verifyRequiredKeys(txParams: TransactionParams, txPrebuildDecoded: TransactionExplanation): void { + if ( + !('account' in txPrebuildDecoded) || + !('limitAmount' in txPrebuildDecoded) || + !txPrebuildDecoded.limitAmount.currency + ) { + throw new Error('Explanation is missing required keys (account or limitAmount with currency)'); + } + } + /** * Verify that a transaction prebuild complies with the original intention * @param txParams params object passed to send @@ -305,12 +399,22 @@ export class Xrp extends BaseCoin { * @param wallet * @returns {boolean} */ - public async verifyTransaction({ txParams, txPrebuild }: VerifyTransactionOptions): Promise { + public async verifyTransaction({ txParams, txPrebuild, verification }: VerifyTransactionOptions): Promise { const coinConfig = coins.get(this.getChain()) as XrpCoin; const explanation = await this.explainTransaction({ txHex: txPrebuild.txHex, }); + // Explaining a tx strips out certain data, for extra measurement we're checking vs the explained tx + // but also vs the tx pre explained. + if (txParams.type === 'enabletoken' && verification?.verifyTokenEnablement) { + this.verifyTxType(explanation, txPrebuild.txHex); + this.verifyActivationAddress(txParams, explanation); + this.verifyTokenIssuer(txParams, explanation); + this.verifyTokenName(txParams, explanation, txPrebuild.txHex, coinConfig); + this.verifyRequiredKeys(txParams, explanation); + } + const output = [...explanation.outputs, ...explanation.changeOutputs][0]; const expectedOutput = txParams.recipients && txParams.recipients[0]; @@ -331,32 +435,6 @@ export class Xrp extends BaseCoin { throw new Error('transaction prebuild does not match expected output'); } - if (txParams.type === 'enabletoken') { - if (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 recipient = txParams.recipients[0]; - if (!recipient.tokenName) { - throw new Error('Recipient must include a token name.'); - } - const recipientCurrency = utils.getXrpCurrencyFromTokenName(recipient.tokenName).currency; - if (coinConfig.isToken) { - if (recipientCurrency !== coinConfig.currencyCode) { - throw new Error('Incorrect token name specified in recipients'); - } - } - if (!('account' in explanation) || !('limitAmount' in explanation) || !explanation.limitAmount.currency) { - throw new Error('Explanation is missing required keys (account or limitAmount with currency)'); - } - const baseAddress = explanation.account; - const currency = explanation.limitAmount.currency; - - if (recipient.address !== baseAddress || recipientCurrency !== currency) { - throw new Error('Tx outputs does not match with expected txParams recipients'); - } - } return true; } diff --git a/modules/sdk-coin-xrp/test/resources/xrp.ts b/modules/sdk-coin-xrp/test/resources/xrp.ts index 752627548e..c790b493b2 100644 --- a/modules/sdk-coin-xrp/test/resources/xrp.ts +++ b/modules/sdk-coin-xrp/test/resources/xrp.ts @@ -396,3 +396,373 @@ export const serverInfoResponse = { type: 'response', }, }; + +export const enableTokenFixtures = { + txParams: { + type: 'enabletoken', + recipients: [{ tokenName: 'txrp:xsgd', address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', amount: '0' }], + wallet: { + id: '68c03cb61b1c78c28ae61700522f4816', + users: [{ user: '65f9c6797236825d1b4e1f82a1035b46', permissions: ['admin', 'spend', 'view'] }], + coin: 'txrp', + label: 'XRP blind signing', + m: 2, + n: 3, + keys: [ + '68c03cab77eefb0633092f9fe1c34063', + '68c03cab8beaecb95c9de2a3eb422c76', + '68c03cac9ab70e1c21952a968a24379e', + ], + keySignatures: {}, + enterprise: '66632c6b42b03d265a939048beaaee55', + organization: '66632c6d42b03d265a939107d2f586e5', + bitgoOrg: 'BitGo Trust', + tags: ['68c03cb61b1c78c28ae61700522f4816', '66632c6b42b03d265a939048beaaee55'], + disableTransactionNotifications: false, + freeze: {}, + deleted: false, + approvalsRequired: 1, + isCold: false, + coinSpecific: { + rootAddress: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', + pendingChainInitialization: false, + creationFailure: [], + trustedTokens: [{ token: 'txrp:rlusd', state: 'active', limit: '9999999999999999000000000000000' }], + }, + admin: { + policy: { + date: '2025-09-09T14:42:00.066Z', + id: '68c03cb81b1c78c28ae618422499d3ec', + label: 'default', + rules: [], + version: 0, + latest: true, + }, + }, + clientFlags: [], + walletFlags: [], + allowBackupKeySigning: false, + recoverable: true, + startDate: '2025-09-09T14:41:58.000Z', + type: 'hot', + buildDefaults: {}, + customChangeKeySignatures: {}, + hasLargeNumberOfAddresses: false, + multisigType: 'onchain', + hasReceiveTransferPolicy: false, + creator: '65f9c6797236825d1b4e1f82a1035b46', + walletFullyCreated: true, + config: {}, + balanceString: '7199955', + confirmedBalanceString: '7199955', + spendableBalanceString: '5799955', + reservedBalanceString: '1400000', + unspendableBalances: { + sendQueueUnspendableBalance: '0', + pendingApprovalUnspendableBalance: '0', + reservedBalance: '1400000', + }, + receiveAddress: { + id: '68c03e228911403f04166f8951f3a5af', + address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg?dt=1', + chain: 0, + index: 1, + coin: 'txrp', + wallet: '68c03cb61b1c78c28ae61700522f4816', + }, + pendingApprovals: [], + }, + enableTokens: [{ name: 'txrp:xsgd', address: '' }], + walletPassphrase: 'F5R*KDzwg3Wjg3s', + prebuildTx: { + txHex: + '{"Account":"rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg","Fee":"45","Sequence":10481223,"Flags":2147483648,"TransactionType":"TrustSet","LimitAmount":{"value":"9999999999999999","currency":"5853474400000000000000000000000000000000","issuer":"rKgjEa9gEyyumaJsfkPq9uSAyaecQRmvYD"}}', + txInfo: { + Account: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', + Fee: '45', + Sequence: 10481223, + Flags: 2147483648, + TransactionType: 'TrustSet', + LimitAmount: { + value: '9999999999999999', + currency: '5853474400000000000000000000000000000000', + issuer: 'rKgjEa9gEyyumaJsfkPq9uSAyaecQRmvYD', + }, + }, + feeInfo: { + fee: '45', + feeString: '45', + date: '2025-09-11T12:17:33.316Z', + height: 10535172, + xrpBaseReserve: '1000000', + xrpIncReserve: '200000', + xrpOpenLedgerFee: '10', + xrpMedianFee: '5000', + medianFee: '1000000', + baseReserve: '200000', + incReserve: '10', + openLedgerFee: '5000', + }, + coin: 'txrp', + walletId: '68c03cb61b1c78c28ae61700522f4816', + buildParams: { + type: 'enabletoken', + recipients: [{ tokenName: 'txrp:xsgd', address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', amount: '0' }], + wallet: { + id: '68c03cb61b1c78c28ae61700522f4816', + users: [{ user: '65f9c6797236825d1b4e1f82a1035b46', permissions: ['admin', 'spend', 'view'] }], + coin: 'txrp', + label: 'XRP blind signing', + m: 2, + n: 3, + keys: [ + '68c03cab77eefb0633092f9fe1c34063', + '68c03cab8beaecb95c9de2a3eb422c76', + '68c03cac9ab70e1c21952a968a24379e', + ], + keySignatures: {}, + enterprise: '66632c6b42b03d265a939048beaaee55', + organization: '66632c6d42b03d265a939107d2f586e5', + bitgoOrg: 'BitGo Trust', + tags: ['68c03cb61b1c78c28ae61700522f4816', '66632c6b42b03d265a939048beaaee55'], + disableTransactionNotifications: false, + freeze: {}, + deleted: false, + approvalsRequired: 1, + isCold: false, + coinSpecific: { + rootAddress: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', + pendingChainInitialization: false, + creationFailure: [], + trustedTokens: [{ token: 'txrp:rlusd', state: 'active', limit: '9999999999999999000000000000000' }], + }, + admin: { + policy: { + date: '2025-09-09T14:42:00.066Z', + id: '68c03cb81b1c78c28ae618422499d3ec', + label: 'default', + rules: [], + version: 0, + latest: true, + }, + }, + clientFlags: [], + walletFlags: [], + allowBackupKeySigning: false, + recoverable: true, + startDate: '2025-09-09T14:41:58.000Z', + type: 'hot', + buildDefaults: {}, + customChangeKeySignatures: {}, + hasLargeNumberOfAddresses: false, + multisigType: 'onchain', + hasReceiveTransferPolicy: false, + creator: '65f9c6797236825d1b4e1f82a1035b46', + walletFullyCreated: true, + config: {}, + balanceString: '7199955', + confirmedBalanceString: '7199955', + spendableBalanceString: '5799955', + reservedBalanceString: '1400000', + unspendableBalances: { + sendQueueUnspendableBalance: '0', + pendingApprovalUnspendableBalance: '0', + reservedBalance: '1400000', + }, + receiveAddress: { + id: '68c03e228911403f04166f8951f3a5af', + address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg?dt=1', + chain: 0, + index: 1, + coin: 'txrp', + wallet: '68c03cb61b1c78c28ae61700522f4816', + }, + pendingApprovals: [], + }, + }, + }, + }, + txPrebuildRaw: { + txHex: + '{"Account":"rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg","Fee":"45","Sequence":10481223,"Flags":2147483648,"TransactionType":"TrustSet","LimitAmount":{"value":"9999999999999999","currency":"5853474400000000000000000000000000000000","issuer":"rKgjEa9gEyyumaJsfkPq9uSAyaecQRmvYD"}}', + txInfo: { + Account: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', + Fee: '45', + Sequence: 10481223, + Flags: 2147483648, + TransactionType: 'TrustSet', + LimitAmount: { + value: '9999999999999999', + currency: '5853474400000000000000000000000000000000', + issuer: 'rKgjEa9gEyyumaJsfkPq9uSAyaecQRmvYD', + }, + }, + feeInfo: { + fee: '45', + feeString: '45', + date: '2025-09-11T12:17:33.316Z', + height: 10535172, + xrpBaseReserve: '1000000', + xrpIncReserve: '200000', + xrpOpenLedgerFee: '10', + xrpMedianFee: '5000', + medianFee: '1000000', + baseReserve: '200000', + incReserve: '10', + openLedgerFee: '5000', + }, + coin: 'txrp', + walletId: '68c03cb61b1c78c28ae61700522f4816', + buildParams: { + type: 'enabletoken', + recipients: [{ tokenName: 'txrp:xsgd', address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', amount: '0' }], + wallet: { + id: '68c03cb61b1c78c28ae61700522f4816', + users: [{ user: '65f9c6797236825d1b4e1f82a1035b46', permissions: ['admin', 'spend', 'view'] }], + coin: 'txrp', + label: 'XRP blind signing', + m: 2, + n: 3, + keys: [ + '68c03cab77eefb0633092f9fe1c34063', + '68c03cab8beaecb95c9de2a3eb422c76', + '68c03cac9ab70e1c21952a968a24379e', + ], + keySignatures: {}, + enterprise: '66632c6b42b03d265a939048beaaee55', + organization: '66632c6d42b03d265a939107d2f586e5', + bitgoOrg: 'BitGo Trust', + tags: ['68c03cb61b1c78c28ae61700522f4816', '66632c6b42b03d265a939048beaaee55'], + disableTransactionNotifications: false, + freeze: {}, + deleted: false, + approvalsRequired: 1, + isCold: false, + coinSpecific: { + rootAddress: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', + pendingChainInitialization: false, + creationFailure: [], + trustedTokens: [{ token: 'txrp:rlusd', state: 'active', limit: '9999999999999999000000000000000' }], + }, + admin: { + policy: { + date: '2025-09-09T14:42:00.066Z', + id: '68c03cb81b1c78c28ae618422499d3ec', + label: 'default', + rules: [], + version: 0, + latest: true, + }, + }, + clientFlags: [], + walletFlags: [], + allowBackupKeySigning: false, + recoverable: true, + startDate: '2025-09-09T14:41:58.000Z', + type: 'hot', + buildDefaults: {}, + customChangeKeySignatures: {}, + hasLargeNumberOfAddresses: false, + multisigType: 'onchain', + hasReceiveTransferPolicy: false, + creator: '65f9c6797236825d1b4e1f82a1035b46', + walletFullyCreated: true, + config: {}, + balanceString: '7199955', + confirmedBalanceString: '7199955', + spendableBalanceString: '5799955', + reservedBalanceString: '1400000', + unspendableBalances: { + sendQueueUnspendableBalance: '0', + pendingApprovalUnspendableBalance: '0', + reservedBalance: '1400000', + }, + receiveAddress: { + id: '68c03e228911403f04166f8951f3a5af', + address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg?dt=1', + chain: 0, + index: 1, + coin: 'txrp', + wallet: '68c03cb61b1c78c28ae61700522f4816', + }, + pendingApprovals: [], + }, + }, + }, + walletData: { + id: '68c03cb61b1c78c28ae61700522f4816', + users: [{ user: '65f9c6797236825d1b4e1f82a1035b46', permissions: ['admin', 'spend', 'view'] }], + coin: 'txrp', + label: 'XRP blind signing', + m: 2, + n: 3, + keys: ['68c03cab77eefb0633092f9fe1c34063', '68c03cab8beaecb95c9de2a3eb422c76', '68c03cac9ab70e1c21952a968a24379e'], + keySignatures: {}, + enterprise: '66632c6b42b03d265a939048beaaee55', + organization: '66632c6d42b03d265a939107d2f586e5', + bitgoOrg: 'BitGo Trust', + tags: ['68c03cb61b1c78c28ae61700522f4816', '66632c6b42b03d265a939048beaaee55'], + disableTransactionNotifications: false, + freeze: {}, + deleted: false, + approvalsRequired: 1, + isCold: false, + coinSpecific: { + rootAddress: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg', + pendingChainInitialization: false, + creationFailure: [], + trustedTokens: [{ token: 'txrp:rlusd', state: 'active', limit: '9999999999999999000000000000000' }], + }, + admin: { + policy: { + date: '2025-09-09T14:42:00.066Z', + id: '68c03cb81b1c78c28ae618422499d3ec', + label: 'default', + rules: [], + version: 0, + latest: true, + }, + }, + clientFlags: [], + walletFlags: [], + allowBackupKeySigning: false, + recoverable: true, + startDate: '2025-09-09T14:41:58.000Z', + type: 'hot', + buildDefaults: {}, + customChangeKeySignatures: {}, + hasLargeNumberOfAddresses: false, + multisigType: 'onchain', + hasReceiveTransferPolicy: false, + creator: '65f9c6797236825d1b4e1f82a1035b46', + walletFullyCreated: true, + config: {}, + balanceString: '7199955', + confirmedBalanceString: '7199955', + spendableBalanceString: '5799955', + reservedBalanceString: '1400000', + unspendableBalances: { + sendQueueUnspendableBalance: '0', + pendingApprovalUnspendableBalance: '0', + reservedBalance: '1400000', + }, + receiveAddress: { + id: '68c03e228911403f04166f8951f3a5af', + address: 'rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg?dt=1', + chain: 0, + index: 1, + coin: 'txrp', + wallet: '68c03cb61b1c78c28ae61700522f4816', + }, + pendingApprovals: [], + }, + txHexWrongType: + '{"TransactionType":"Payment","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","Destination":"rfjub8A4dpSD5nnszUFTsLprxu1W398jwc","DestinationTag":0,"Amount":"253481","Flags":2147483648,"LastLedgerSequence":1626225,"Fee":"45","Sequence":7}', + txHexWrongActivationAddress: + '{"Account":"rJsDSa9rZTieCg5tJkBYBEmXTGpXLeUL9y","Fee":"45","Sequence":10481223,"Flags":2147483648,"TransactionType":"TrustSet","LimitAmount":{"value":"9999999999999999","currency":"5853474400000000000000000000000000000000","issuer":"rKgjEa9gEyyumaJsfkPq9uSAyaecQRmvYD"}}', + txHexWrongTokenName: + '{"Account":"rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg","Fee":"45","Sequence":10481223,"Flags":2147483648,"TransactionType":"TrustSet","LimitAmount":{"value":"9999999999999999","currency":"5853473100000000000000000000000000000000","issuer":"rKgjEa9gEyyumaJsfkPq9uSAyaecQRmvYD"}}', + + txHexWrongIssuerAddress: + '{"Account":"rsAKF2b8ZkrQ1f295ChWiZLrCv5W1c7Xbg","Fee":"45","Sequence":10481223,"Flags":2147483648,"TransactionType":"TrustSet","LimitAmount":{"value":"9999999999999999","currency":"5853474400000000000000000000000000000000","issuer":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG"}}', +}; diff --git a/modules/sdk-coin-xrp/test/unit/xrp.ts b/modules/sdk-coin-xrp/test/unit/xrp.ts index 0ac5044754..8acc2ae752 100644 --- a/modules/sdk-coin-xrp/test/unit/xrp.ts +++ b/modules/sdk-coin-xrp/test/unit/xrp.ts @@ -1,19 +1,20 @@ import 'should'; -import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; -import { Txrp } from '../../src/txrp'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import ripple from '../../src/ripple'; +import { Txrp } from '../../src/txrp'; -import * as nock from 'nock'; +import { Wallet } from '@bitgo/sdk-core'; import assert from 'assert'; +import * as _ from 'lodash'; +import * as nock from 'nock'; import * as rippleBinaryCodec from 'ripple-binary-codec'; import sinon from 'sinon'; -import * as testData from '../resources/xrp'; -import { SIGNER_USER, SIGNER_BACKUP, SIGNER_BITGO } from '../resources/xrp'; -import * as _ from 'lodash'; -import { XrpToken } from '../../src'; import * as xrpl from 'xrpl'; +import { XrpToken } from '../../src'; +import * as testData from '../resources/xrp'; +import { SIGNER_BACKUP, SIGNER_BITGO, SIGNER_USER } from '../resources/xrp'; nock.disableNetConnect(); @@ -445,7 +446,7 @@ describe('XRP:', function () { let newTxPrebuild; const txPrebuild = { - txHex: `{"TransactionType":"TrustSet","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","LimitAmount":{"currency":"524C555344000000000000000000000000000000","issuer":"rnox8i6h9GoAbuwr73JtaDxXoncLLUCpXH","value":"1000000000"},"Flags":2147483648,"Fee":"45","Sequence":7}`, + txHex: `{"TransactionType":"TrustSet","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","LimitAmount":{"currency":"524C555344000000000000000000000000000000","issuer":"rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV","value":"99999999"},"Flags":2147483648,"Fee":"45","Sequence":7}`, }; before(function () { @@ -524,8 +525,8 @@ describe('XRP:', function () { type: 'enabletoken', }; await token - .verifyTransaction({ txParams, txPrebuild }) - .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients'); + .verifyTransaction({ txParams, txPrebuild, verification: { verifyTokenEnablement: true } }) + .should.be.rejectedWith("Account address doesn't match with activation address."); }); it('should fail to verify trustline transaction with incorrect token name', async function () { @@ -540,7 +541,9 @@ describe('XRP:', function () { ], type: 'enabletoken', }; - await token.verifyTransaction({ txParams, txPrebuild }).should.be.rejectedWith('txrp:usd is not supported'); + await token + .verifyTransaction({ txParams, txPrebuild, verification: { verifyTokenEnablement: true } }) + .should.be.rejectedWith('txrp:usd is not supported'); }); it('should verify token transfers with recipoent has dt', async function () { @@ -685,4 +688,87 @@ describe('XRP:', function () { sinon.restore(); }); }); + + describe('blind signing token enablement protection', () => { + it('should verify as valid the enabletoken intent when prebuild tx matchs user intent ', async function () { + const { txParams, txPrebuildRaw, walletData } = testData.enableTokenFixtures; + const wallet = new Wallet(bitgo, basecoin, walletData); + const sameIntentTx = await basecoin.verifyTransaction({ + txParams, + txPrebuild: txPrebuildRaw, + wallet, + verification: { verifyTokenEnablement: true }, + }); + + sameIntentTx.should.equal(true); + }); + + it('should verify token name', async function () { + const { txParams, txPrebuildRaw, txHexWrongTokenName, walletData } = testData.enableTokenFixtures; + const wallet = new Wallet(bitgo, basecoin, walletData); + const txPrebuildWrongTokenName = { ...txPrebuildRaw, txHex: txHexWrongTokenName }; + + await assert.rejects( + async () => + await basecoin.verifyTransaction({ + txParams, + txPrebuild: txPrebuildWrongTokenName, + wallet, + verification: { verifyTokenEnablement: true }, + }), + { message: 'Invalid token issuer or currency on token enablement tx' } + ); + }); + + it('should verify transaction type', async function () { + const { txParams, txPrebuildRaw, txHexWrongType, walletData } = testData.enableTokenFixtures; + const wallet = new Wallet(bitgo, basecoin, walletData); + const txPrebuildWrongType = { ...txPrebuildRaw, txHex: txHexWrongType }; + + await assert.rejects( + async () => + await basecoin.verifyTransaction({ + txParams, + txPrebuild: txPrebuildWrongType, + wallet, + verification: { verifyTokenEnablement: true }, + }), + { message: 'tx type Payment does not match expected type TrustSet' } + ); + }); + + it('should verify activation address', async function () { + const { txParams, txPrebuildRaw, txHexWrongActivationAddress, walletData } = testData.enableTokenFixtures; + const wallet = new Wallet(bitgo, basecoin, walletData); + const txPrebuildWrongActivationAddr = { ...txPrebuildRaw, txHex: txHexWrongActivationAddress }; + + await assert.rejects( + async () => + await basecoin.verifyTransaction({ + txParams, + txPrebuild: txPrebuildWrongActivationAddr, + wallet, + verification: { verifyTokenEnablement: true }, + }), + { message: "Account address doesn't match with activation address." } + ); + }); + + it('should verify issuer address', async function () { + const { txParams, txPrebuildRaw, txHexWrongIssuerAddress, walletData } = testData.enableTokenFixtures; + const wallet = new Wallet(bitgo, basecoin, walletData); + const txPrebuildWrongIssuerAddress = { ...txPrebuildRaw, txHex: txHexWrongIssuerAddress }; + + await assert.rejects( + async () => + await basecoin.verifyTransaction({ + txParams, + txPrebuild: txPrebuildWrongIssuerAddress, + wallet, + verification: { verifyTokenEnablement: true }, + }), + { message: 'Invalid token issuer or currency on token enablement tx' } + ); + }); + }); });