Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions modules/sdk-coin-xrp/src/ripple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import * as xrpl from 'xrpl';
import { ECPair } from '@bitgo/secp256k1';
import BigNumber from 'bignumber.js';

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

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

function computeSignature(tx, privateKey, signAs) {
const signingData = signAs ? binary.encodeForMultisigning(tx, signAs) : binary.encodeForSigning(tx);
const signingData = signAs ? binary.encodeForMultiSigning(tx, signAs) : binary.encodeForSigning(tx);
return rippleKeypairs.sign(signingData, privateKey);
}

Expand Down
5 changes: 4 additions & 1 deletion modules/sdk-coin-xrp/src/xrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ import {
VerifyTransactionOptions,
} from '@bitgo/sdk-core';
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';

// xrpl re-exports ripple-binary-codec@2.7.0 which supports MPTokenAuthorize.
// The standalone ripple-binary-codec dep is 2.1.0 (pre-MPT), so use xrpl as the codec.
const rippleBinaryCodec = xrpl;

import { AccountDeleteBuilder, TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib';
import {
ExplainTransactionOptions,
Expand Down
36 changes: 36 additions & 0 deletions modules/sdk-coin-xrp/test/unit/xrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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';
import { getMptBuilderFactory } from './getBuilderFactory';

nock.disableNetConnect();

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

it('should multi-sign an MPTokenAuthorize transaction using the xrpl codec (encodeForMultiSigning)', async function () {
// Build an unsigned MPTokenAuthorize tx using the builder so we get a real XRPL-encoded hex.
// This exercises the ripple.ts encodeForMultiSigning path via the xrpl codec (v2.7.0)
// which supports the MPTokenAuthorize transaction type absent in ripple-binary-codec v2.1.0.
const factory = getMptBuilderFactory(testData.MPT_ISSUANCE_ID);
const sender = testData.TEST_MULTI_SIG_ACCOUNT.address.split('?')[0]; // strip destination tag

const builder = factory.getMPTokenAuthorizeBuilder();
builder.sender(sender);
builder.mptIssuanceId(testData.MPT_ISSUANCE_ID);
builder.sequence(1600000);
builder.fee('12');
builder.flags(2147483648);

const unsignedTx = await builder.build();
const unsignedHex = unsignedTx.toBroadcastFormat();

// Sign with first signer
const firstSigned = ripple.signWithPrivateKey(unsignedHex, SIGNER_USER.prv, {
signAs: SIGNER_USER.address,
});

// Add second signature
const fullySigned = ripple.signWithPrivateKey(firstSigned.signedTransaction, SIGNER_BITGO.prv, {
signAs: SIGNER_BITGO.address,
});

// Must use xrpl.decode (ripple-binary-codec v2.7.0) — the standalone
// ripple-binary-codec v2.1.0 doesn't know the MPTokenAuthorize type.
const decoded = xrpl.decode(fullySigned.signedTransaction);
(decoded.TransactionType as string).should.equal('MPTokenAuthorize');
assert(Array.isArray(decoded.Signers));
(decoded.Signers as Array<unknown>).length.should.equal(2);
});

it('should be able to cosign XRP transaction in any form', function () {
const unsignedTxHex =
'120000228000000024000000072E00000000201B0018D07161400000000003DE2968400000000000002D8114726D0D8A26568D5D9680AC80577C912236717191831449EE221CCACC4DD2BF8862B22B0960A84FC771D9';
Expand Down
5 changes: 4 additions & 1 deletion modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4058,7 +4058,10 @@ export class Wallet implements IWallet {
teConfig.validateWallet(this._wallet.type);
}

if (typeof params.prebuildTx === 'string' || params.prebuildTx?.buildParams?.type !== 'enabletoken') {
if (
typeof params.prebuildTx === 'string' ||
(params.prebuildTx?.buildParams?.type !== 'enabletoken' && params.prebuildTx?.buildParams?.type !== 'enableMpt')
) {
throw new Error('Invalid build of token enablement.');
}

Expand Down
82 changes: 82 additions & 0 deletions modules/sdk-core/test/unit/bitgo/wallet/tokenApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,86 @@ describe('Wallet - Token Approval', function () {
await wallet.buildErc20TokenApproval('USDC', 'passphrase123').should.be.rejectedWith('signing error');
});
});

describe('sendTokenEnablement', function () {
let teWallet: Wallet;
let teBaseCoin: any;
let teBitGo: any;

beforeEach(function () {
teBitGo = {
post: sinon.stub(),
get: sinon.stub(),
setRequestTracer: sinon.stub(),
};

teBaseCoin = {
getFamily: sinon.stub().returns('txrp'),
getFullName: sinon.stub().returns('Testnet XRP'),
url: sinon.stub(),
keychains: sinon.stub(),
supportsTss: sinon.stub().returns(false),
getMPCAlgorithm: sinon.stub(),
getTokenEnablementConfig: sinon.stub().returns({ requiresTokenEnablement: true }),
};

// custodial wallet so the path after validation calls initiateTransaction
const walletData = {
id: 'te-wallet-id',
coin: 'txrp',
type: 'custodial',
keys: ['user-key', 'backup-key', 'bitgo-key'],
};

teWallet = new Wallet(teBitGo, teBaseCoin, walletData);
});

it('should throw "Invalid build of token enablement." when prebuildTx is a string', async function () {
await teWallet
.sendTokenEnablement({ prebuildTx: 'raw-hex-string' as any })
.should.be.rejectedWith('Invalid build of token enablement.');
});

it('should throw "Invalid build of token enablement." when buildParams.type is undefined', async function () {
await teWallet
.sendTokenEnablement({ prebuildTx: { buildParams: {} } as any })
.should.be.rejectedWith('Invalid build of token enablement.');
});

it('should throw "Invalid build of token enablement." when buildParams.type is an unrecognised type', async function () {
await teWallet
.sendTokenEnablement({ prebuildTx: { buildParams: { type: 'transfer' } } as any })
.should.be.rejectedWith('Invalid build of token enablement.');
});

it('should pass validation and proceed when buildParams.type is "enabletoken"', async function () {
const initiateStub = sinon.stub(teWallet as any, 'initiateTransaction').resolves({ txid: 'abc123' });

const result = await teWallet.sendTokenEnablement({
prebuildTx: { buildParams: { type: 'enabletoken' } } as any,
});

result.should.eql({ txid: 'abc123' });
sinon.assert.calledOnce(initiateStub);
});

it('should pass validation and proceed when buildParams.type is "enableMpt"', async function () {
const initiateStub = sinon.stub(teWallet as any, 'initiateTransaction').resolves({ txid: 'mpt456' });

const result = await teWallet.sendTokenEnablement({
prebuildTx: { buildParams: { type: 'enableMpt' } } as any,
});

result.should.eql({ txid: 'mpt456' });
sinon.assert.calledOnce(initiateStub);
});

it('should throw when the coin does not require token enablement', async function () {
teBaseCoin.getTokenEnablementConfig.returns({ requiresTokenEnablement: false });

await teWallet
.sendTokenEnablement({ prebuildTx: { buildParams: { type: 'enableMpt' } } as any })
.should.be.rejectedWith(/does not require token enablement transactions/);
});
});
});
Loading