Skip to content

Commit a580fa8

Browse files
committed
feat(sdk-coin-xrp): support MPToken via xrpl codec and enableMpt wallet type
Ticket: CGD-1827 TICKET: CGD-1827
1 parent 38ee654 commit a580fa8

5 files changed

Lines changed: 130 additions & 4 deletions

File tree

modules/sdk-coin-xrp/src/ripple.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import * as xrpl from 'xrpl';
99
import { ECPair } from '@bitgo/secp256k1';
1010
import BigNumber from 'bignumber.js';
1111

12-
import * as binary from 'ripple-binary-codec';
12+
// xrpl re-exports ripple-binary-codec@2.7.0 which supports MPTokenAuthorize.
13+
// The standalone ripple-binary-codec dep is 2.1.0 (pre-MPT), so use xrpl as the codec.
14+
const binary = xrpl;
1315

1416
/**
1517
* Convert an XRP address to a BigNumber for numeric comparison.
@@ -24,7 +26,7 @@ function addressToBigNumber(address: string): BigNumber {
2426
}
2527

2628
function computeSignature(tx, privateKey, signAs) {
27-
const signingData = signAs ? binary.encodeForMultisigning(tx, signAs) : binary.encodeForSigning(tx);
29+
const signingData = signAs ? binary.encodeForMultiSigning(tx, signAs) : binary.encodeForSigning(tx);
2830
return rippleKeypairs.sign(signingData, privateKey);
2931
}
3032

modules/sdk-coin-xrp/src/xrp.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ import {
2626
VerifyTransactionOptions,
2727
} from '@bitgo/sdk-core';
2828
import { coins, BaseCoin as StaticsBaseCoin, XrpCoin } from '@bitgo/statics';
29-
import * as rippleBinaryCodec from 'ripple-binary-codec';
3029
import * as rippleKeypairs from 'ripple-keypairs';
3130
import * as xrpl from 'xrpl';
3231

32+
// xrpl re-exports ripple-binary-codec@2.7.0 which supports MPTokenAuthorize.
33+
// The standalone ripple-binary-codec dep is 2.1.0 (pre-MPT), so use xrpl as the codec.
34+
const rippleBinaryCodec = xrpl;
35+
3336
import { AccountDeleteBuilder, TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib';
3437
import {
3538
ExplainTransactionOptions,

modules/sdk-coin-xrp/test/unit/xrp.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import * as xrpl from 'xrpl';
1515
import { XrpToken } from '../../src';
1616
import * as testData from '../resources/xrp';
1717
import { SIGNER_BACKUP, SIGNER_BITGO, SIGNER_USER } from '../resources/xrp';
18+
import { getMptBuilderFactory } from './getBuilderFactory';
1819

1920
nock.disableNetConnect();
2021

@@ -202,6 +203,41 @@ describe('XRP:', function () {
202203
(signedTransaction.Signers as Array<string>).length.should.equal(2);
203204
});
204205

206+
it('should multi-sign an MPTokenAuthorize transaction using the xrpl codec (encodeForMultiSigning)', async function () {
207+
// Build an unsigned MPTokenAuthorize tx using the builder so we get a real XRPL-encoded hex.
208+
// This exercises the ripple.ts encodeForMultiSigning path via the xrpl codec (v2.7.0)
209+
// which supports the MPTokenAuthorize transaction type absent in ripple-binary-codec v2.1.0.
210+
const factory = getMptBuilderFactory(testData.MPT_ISSUANCE_ID);
211+
const sender = testData.TEST_MULTI_SIG_ACCOUNT.address.split('?')[0]; // strip destination tag
212+
213+
const builder = factory.getMPTokenAuthorizeBuilder();
214+
builder.sender(sender);
215+
builder.mptIssuanceId(testData.MPT_ISSUANCE_ID);
216+
builder.sequence(1600000);
217+
builder.fee('12');
218+
builder.flags(2147483648);
219+
220+
const unsignedTx = await builder.build();
221+
const unsignedHex = unsignedTx.toBroadcastFormat();
222+
223+
// Sign with first signer
224+
const firstSigned = ripple.signWithPrivateKey(unsignedHex, SIGNER_USER.prv, {
225+
signAs: SIGNER_USER.address,
226+
});
227+
228+
// Add second signature
229+
const fullySigned = ripple.signWithPrivateKey(firstSigned.signedTransaction, SIGNER_BITGO.prv, {
230+
signAs: SIGNER_BITGO.address,
231+
});
232+
233+
// Must use xrpl.decode (ripple-binary-codec v2.7.0) — the standalone
234+
// ripple-binary-codec v2.1.0 doesn't know the MPTokenAuthorize type.
235+
const decoded = xrpl.decode(fullySigned.signedTransaction);
236+
(decoded.TransactionType as string).should.equal('MPTokenAuthorize');
237+
assert(Array.isArray(decoded.Signers));
238+
(decoded.Signers as Array<unknown>).length.should.equal(2);
239+
});
240+
205241
it('should be able to cosign XRP transaction in any form', function () {
206242
const unsignedTxHex =
207243
'120000228000000024000000072E00000000201B0018D07161400000000003DE2968400000000000002D8114726D0D8A26568D5D9680AC80577C912236717191831449EE221CCACC4DD2BF8862B22B0960A84FC771D9';

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4058,7 +4058,10 @@ export class Wallet implements IWallet {
40584058
teConfig.validateWallet(this._wallet.type);
40594059
}
40604060

4061-
if (typeof params.prebuildTx === 'string' || params.prebuildTx?.buildParams?.type !== 'enabletoken') {
4061+
if (
4062+
typeof params.prebuildTx === 'string' ||
4063+
(params.prebuildTx?.buildParams?.type !== 'enabletoken' && params.prebuildTx?.buildParams?.type !== 'enableMpt')
4064+
) {
40624065
throw new Error('Invalid build of token enablement.');
40634066
}
40644067

modules/sdk-core/test/unit/bitgo/wallet/tokenApproval.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,86 @@ describe('Wallet - Token Approval', function () {
150150
await wallet.buildErc20TokenApproval('USDC', 'passphrase123').should.be.rejectedWith('signing error');
151151
});
152152
});
153+
154+
describe('sendTokenEnablement', function () {
155+
let teWallet: Wallet;
156+
let teBaseCoin: any;
157+
let teBitGo: any;
158+
159+
beforeEach(function () {
160+
teBitGo = {
161+
post: sinon.stub(),
162+
get: sinon.stub(),
163+
setRequestTracer: sinon.stub(),
164+
};
165+
166+
teBaseCoin = {
167+
getFamily: sinon.stub().returns('txrp'),
168+
getFullName: sinon.stub().returns('Testnet XRP'),
169+
url: sinon.stub(),
170+
keychains: sinon.stub(),
171+
supportsTss: sinon.stub().returns(false),
172+
getMPCAlgorithm: sinon.stub(),
173+
getTokenEnablementConfig: sinon.stub().returns({ requiresTokenEnablement: true }),
174+
};
175+
176+
// custodial wallet so the path after validation calls initiateTransaction
177+
const walletData = {
178+
id: 'te-wallet-id',
179+
coin: 'txrp',
180+
type: 'custodial',
181+
keys: ['user-key', 'backup-key', 'bitgo-key'],
182+
};
183+
184+
teWallet = new Wallet(teBitGo, teBaseCoin, walletData);
185+
});
186+
187+
it('should throw "Invalid build of token enablement." when prebuildTx is a string', async function () {
188+
await teWallet
189+
.sendTokenEnablement({ prebuildTx: 'raw-hex-string' as any })
190+
.should.be.rejectedWith('Invalid build of token enablement.');
191+
});
192+
193+
it('should throw "Invalid build of token enablement." when buildParams.type is undefined', async function () {
194+
await teWallet
195+
.sendTokenEnablement({ prebuildTx: { buildParams: {} } as any })
196+
.should.be.rejectedWith('Invalid build of token enablement.');
197+
});
198+
199+
it('should throw "Invalid build of token enablement." when buildParams.type is an unrecognised type', async function () {
200+
await teWallet
201+
.sendTokenEnablement({ prebuildTx: { buildParams: { type: 'transfer' } } as any })
202+
.should.be.rejectedWith('Invalid build of token enablement.');
203+
});
204+
205+
it('should pass validation and proceed when buildParams.type is "enabletoken"', async function () {
206+
const initiateStub = sinon.stub(teWallet as any, 'initiateTransaction').resolves({ txid: 'abc123' });
207+
208+
const result = await teWallet.sendTokenEnablement({
209+
prebuildTx: { buildParams: { type: 'enabletoken' } } as any,
210+
});
211+
212+
result.should.eql({ txid: 'abc123' });
213+
sinon.assert.calledOnce(initiateStub);
214+
});
215+
216+
it('should pass validation and proceed when buildParams.type is "enableMpt"', async function () {
217+
const initiateStub = sinon.stub(teWallet as any, 'initiateTransaction').resolves({ txid: 'mpt456' });
218+
219+
const result = await teWallet.sendTokenEnablement({
220+
prebuildTx: { buildParams: { type: 'enableMpt' } } as any,
221+
});
222+
223+
result.should.eql({ txid: 'mpt456' });
224+
sinon.assert.calledOnce(initiateStub);
225+
});
226+
227+
it('should throw when the coin does not require token enablement', async function () {
228+
teBaseCoin.getTokenEnablementConfig.returns({ requiresTokenEnablement: false });
229+
230+
await teWallet
231+
.sendTokenEnablement({ prebuildTx: { buildParams: { type: 'enableMpt' } } as any })
232+
.should.be.rejectedWith(/does not require token enablement transactions/);
233+
});
234+
});
153235
});

0 commit comments

Comments
 (0)