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') {