From d274c8ec46cdffe4ecbf1a7e1bc368a51e33c8c2 Mon Sep 17 00:00:00 2001 From: Prabhsharan Singh Date: Thu, 2 Jul 2026 14:29:06 +0530 Subject: [PATCH] fix(sdk-coin-xrp): route MPT token enablement to enableMpt type via coin config Add getEnableTokenType to TokenEnablementConfig so coins can return the correct transaction type. XRP uses this to return 'enableMpt' for XrpMptCoin tokens instead of hardcoded 'enabletoken', avoiding the IOU TrustSet path that throws for MPT tokens. Remove redundant xrpl.decode fallbacks in ripple.ts and xrp.ts since binary/rippleBinaryCodec are aliased to xrpl which supports all tx types including MPTokenAuthorize. Simplify express handleV2EnableTokens by removing the offline MPT/IOU split. Add unit tests for getTokenEnablementConfig and MPTokenAuthorize explainTransaction paths. TICKET: CGD-2013 Co-Authored-By: Claude Sonnet 4.6 --- modules/sdk-coin-xrp/src/ripple.ts | 2 +- modules/sdk-coin-xrp/src/xrp.ts | 24 ++++++-- modules/sdk-coin-xrp/test/unit/xrp.ts | 55 +++++++++++++++++++ .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 1 + modules/sdk-core/src/bitgo/wallet/wallet.ts | 3 +- 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/modules/sdk-coin-xrp/src/ripple.ts b/modules/sdk-coin-xrp/src/ripple.ts index 8c3b5796ec..dea8633ed0 100644 --- a/modules/sdk-coin-xrp/src/ripple.ts +++ b/modules/sdk-coin-xrp/src/ripple.ts @@ -50,7 +50,7 @@ const signWithPrivateKey = function (txHex, privateKey, options) { } catch (e) { try { tx = JSON.parse(txHex); - } catch (e) { + } catch (e2) { throw new Error('txHex needs to be either hex or JSON string for XRP'); } } diff --git a/modules/sdk-coin-xrp/src/xrp.ts b/modules/sdk-coin-xrp/src/xrp.ts index 31818992b2..ab2b33b70f 100644 --- a/modules/sdk-coin-xrp/src/xrp.ts +++ b/modules/sdk-coin-xrp/src/xrp.ts @@ -25,7 +25,7 @@ import { UnexpectedAddressError, VerifyTransactionOptions, } from '@bitgo/sdk-core'; -import { coins, BaseCoin as StaticsBaseCoin, XrpCoin } from '@bitgo/statics'; +import { coins, BaseCoin as StaticsBaseCoin, XrpCoin, XrpMptCoin } from '@bitgo/statics'; import * as rippleKeypairs from 'ripple-keypairs'; import * as xrpl from 'xrpl'; @@ -132,6 +132,8 @@ export class Xrp extends BaseCoin { return { requiresTokenEnablement: true, supportsMultipleTokenEnablements: false, + getEnableTokenType: (tokenName: string) => + coins.has(tokenName) && coins.get(tokenName) instanceof XrpMptCoin ? 'enableMpt' : 'enabletoken', }; } @@ -219,7 +221,7 @@ export class Xrp extends BaseCoin { try { transaction = JSON.parse(txHex); txHex = rippleBinaryCodec.encode(transaction); - } catch (e) { + } catch (e2) { throw new Error('txHex needs to be either hex or JSON string for XRP'); } } @@ -329,7 +331,7 @@ export class Xrp extends BaseCoin { } catch (e) { try { transaction = JSON.parse(txHex); - } catch (e) { + } catch (e2) { throw new Error('txHex needs to be either hex or JSON string for XRP'); } } @@ -337,10 +339,17 @@ export class Xrp extends BaseCoin { return transaction.TransactionType; } - verifyTxType(txPrebuildDecoded: TransactionExplanation, txHexPrebuild: string | undefined): void { + verifyTxType(txPrebuildDecoded: TransactionExplanation, txHexPrebuild: string | undefined, isMpt = false): 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 (isMpt) { + if (transactionType !== XrpTransactionType.MPTokenAuthorize) + throw new Error(`tx type ${transactionType} does not match expected type MPTokenAuthorize`); + return; + } + 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 @@ -438,6 +447,13 @@ export class Xrp extends BaseCoin { // 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 === 'enableMpt') { + if (verification?.verifyTokenEnablement) { + this.verifyTxType(explanation, txPrebuild.txHex, true); + } + return true; + } + if (txParams.type === 'enabletoken' && verification?.verifyTokenEnablement) { this.verifyTxType(explanation, txPrebuild.txHex); this.verifyActivationAddress(txParams, explanation); diff --git a/modules/sdk-coin-xrp/test/unit/xrp.ts b/modules/sdk-coin-xrp/test/unit/xrp.ts index 8a8807293c..d3cf64bf2c 100644 --- a/modules/sdk-coin-xrp/test/unit/xrp.ts +++ b/modules/sdk-coin-xrp/test/unit/xrp.ts @@ -158,6 +158,38 @@ describe('XRP:', function () { unsignedExplanation.fee.fee.should.equal('45'); }); + it('should explain an MPTokenAuthorize transaction from hex', async function () { + const factory = getMptBuilderFactory(testData.MPT_ISSUANCE_ID); + const sender = testData.TEST_MULTI_SIG_ACCOUNT.address.split('?')[0]; + + const builder = factory.getMPTokenAuthorizeBuilder(); + builder.sender(sender); + builder.mptIssuanceId(testData.MPT_ISSUANCE_ID); + builder.sequence(1600000); + builder.fee('12'); + builder.flags(2147483648); + + const txHex = (await builder.build()).toBroadcastFormat(); + const explanation = await basecoin.explainTransaction({ txHex }); + + explanation.id.should.be.a.String(); + explanation.fee.fee.should.equal('12'); + }); + + it('should explain an MPTokenAuthorize transaction from JSON string', async function () { + const txJson = JSON.stringify({ + TransactionType: 'MPTokenAuthorize', + Account: 'rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG', + MPTokenIssuanceID: testData.MPT_ISSUANCE_ID, + Fee: '12', + Sequence: 1600000, + Flags: 2147483648, + }); + const explanation = await basecoin.explainTransaction({ txHex: txJson }); + + explanation.fee.fee.should.equal('12'); + }); + it('should be able to sign an XRP transaction', async function () { const txPrebuild = { txHex: @@ -1038,4 +1070,27 @@ describe('XRP:', function () { ); }); }); + + describe('getTokenEnablementConfig', function () { + it('should return enableMpt for a registered MPT token', function () { + const config = basecoin.getTokenEnablementConfig(); + config.getEnableTokenType('txrp:feesec').should.equal('enableMpt'); + }); + + it('should return enabletoken for a registered IOU token', function () { + const config = basecoin.getTokenEnablementConfig(); + config.getEnableTokenType('txrp:rlusd').should.equal('enabletoken'); + }); + + it('should return enabletoken for an unknown token name', function () { + const config = basecoin.getTokenEnablementConfig(); + config.getEnableTokenType('txrp:unknown-token').should.equal('enabletoken'); + }); + + it('should require token enablement and not support multiple enablements in one tx', function () { + const config = basecoin.getTokenEnablementConfig(); + config.requiresTokenEnablement.should.equal(true); + config.supportsMultipleTokenEnablements.should.equal(false); + }); + }); }); diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 8d4cda4a27..860ad08c90 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -608,6 +608,7 @@ export interface TokenEnablementConfig { requiresTokenEnablement: boolean; supportsMultipleTokenEnablements: boolean; validateWallet?: (walletType: string) => void; + getEnableTokenType?: (tokenName: string) => string; } export interface MessagePrep { diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 28121bf96b..4c5a65f999 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -3975,7 +3975,8 @@ export class Wallet implements IWallet { const buildParams: PrebuildTransactionOptions = _.pick(params, this.prebuildWhitelistedParams()); if (!buildParams.type) { - buildParams.type = 'enabletoken'; + const tokenName = params.enableTokens[0]?.name; + buildParams.type = (tokenName && teConfig.getEnableTokenType?.(tokenName)) ?? 'enabletoken'; } // Check if we build with intent if (this._wallet.multisigType === 'tss') {