Skip to content

Commit d3fc28f

Browse files
committed
fix(sdk-coin-xrp): update XRP signers sorting to use numeric comparison
Ticket: WIN-5390
1 parent 56bd7c3 commit d3fc28f

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,22 @@
77
import * as rippleKeypairs from 'ripple-keypairs';
88
import * as xrpl from 'xrpl';
99
import { ECPair } from '@bitgo/secp256k1';
10+
import BigNumber from 'bignumber.js';
1011

1112
import * as binary from 'ripple-binary-codec';
1213

14+
/**
15+
* Convert an XRP address to a BigNumber for numeric comparison.
16+
* This is needed for proper sorting of signers as required by the XRP protocol.
17+
*
18+
* @param address - The XRP address to convert
19+
* @returns BigNumber representation of the address
20+
*/
21+
function addressToBigNumber(address: string): BigNumber {
22+
const hex = Buffer.from(xrpl.decodeAccountID(address)).toString('hex');
23+
return new BigNumber(hex, 16);
24+
}
25+
1326
function computeSignature(tx, privateKey, signAs) {
1427
const signingData = signAs ? binary.encodeForMultisigning(tx, signAs) : binary.encodeForSigning(tx);
1528
return rippleKeypairs.sign(signingData, privateKey);
@@ -53,9 +66,17 @@ const signWithPrivateKey = function (txHex, privateKey, options) {
5366
TxnSignature: computeSignature(tx, privateKey, options.signAs),
5467
};
5568
// Ordering of private key signing matters, or the Ripple fullnode will throw an 'Unsorted Signers array' error.
56-
// Additional signers must be added to the front of the signers array list.
57-
if (tx.TxnSignature || tx.Signers) {
58-
tx.Signers.unshift({ Signer: signer });
69+
// XRP requires Signers array to be sorted based on numeric value of signer addresses (lowest first)
70+
if (tx.Signers) {
71+
// Add the current signer
72+
tx.Signers.push({ Signer: signer });
73+
74+
// Sort the Signers array by numeric value of Account (address) to ensure proper ordering
75+
tx.Signers.sort((a, b) => {
76+
const addressBN1 = addressToBigNumber(a.Signer.Account);
77+
const addressBN2 = addressToBigNumber(b.Signer.Account);
78+
return addressBN1.comparedTo(addressBN2);
79+
});
5980
} else {
6081
tx.Signers = [{ Signer: signer }];
6182
}

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import assert from 'assert';
1010
import * as rippleBinaryCodec from 'ripple-binary-codec';
1111
import sinon from 'sinon';
1212
import * as testData from '../resources/xrp';
13+
import { SIGNER_USER, SIGNER_BACKUP, SIGNER_BITGO } from '../resources/xrp';
1314
import * as _ from 'lodash';
1415
import { XrpToken } from '../../src';
1516
import * as xrpl from 'xrpl';
@@ -224,6 +225,58 @@ describe('XRP:', function () {
224225
coSignedHexTransaction.id.should.equal(coSignedJsonTransaction.id);
225226
});
226227

228+
it('should correctly sort signers by numeric value of addresses', function () {
229+
// Use the test signers from the test resources
230+
// These are known valid key pairs where the private key corresponds to the address
231+
const signers = [SIGNER_USER, SIGNER_BACKUP, SIGNER_BITGO];
232+
233+
// Unsigned transaction
234+
const unsignedTxHex =
235+
'120000228000000024000000072E00000000201B0018D07161400000000003DE2968400000000000002D8114726D0D8A26568D5D9680AC80577C912236717191831449EE221CCACC4DD2BF8862B22B0960A84FC771D9';
236+
237+
// Sign with first signer
238+
let signedTx = ripple.signWithPrivateKey(unsignedTxHex, signers[0].prv, {
239+
signAs: signers[0].address,
240+
});
241+
242+
// Sign with second signer
243+
signedTx = ripple.signWithPrivateKey(signedTx.signedTransaction, signers[1].prv, {
244+
signAs: signers[1].address,
245+
});
246+
247+
// Sign with third signer
248+
signedTx = ripple.signWithPrivateKey(signedTx.signedTransaction, signers[2].prv, {
249+
signAs: signers[2].address,
250+
});
251+
252+
// Decode the signed transaction
253+
const decodedTx = rippleBinaryCodec.decode(signedTx.signedTransaction);
254+
255+
// Verify that the Signers array exists and has the correct length
256+
assert(Array.isArray(decodedTx.Signers));
257+
(decodedTx.Signers as Array<any>).length.should.equal(3);
258+
259+
// Extract the addresses from the Signers array
260+
const signerAddresses = (decodedTx.Signers as Array<any>).map((signer) => signer.Signer.Account);
261+
262+
// Convert addresses to BigNumber for numeric comparison
263+
const addressToBigNumber = (address) => {
264+
const hex = Buffer.from(xrpl.decodeAccountID(address)).toString('hex');
265+
return BigInt('0x' + hex);
266+
};
267+
268+
// Convert the addresses to BigNumber values
269+
const signerValues = signerAddresses.map(addressToBigNumber);
270+
271+
// Verify that the Signers array is sorted in ascending order by numeric value
272+
for (let i = 0; i < signerValues.length - 1; i++) {
273+
assert(
274+
signerValues[i] < signerValues[i + 1],
275+
`Signers not properly sorted: ${signerValues[i]} should be less than ${signerValues[i + 1]}`
276+
);
277+
}
278+
});
279+
227280
it('Should be unable to explain bogus XRP transaction', async function () {
228281
await basecoin
229282
.explainTransaction({ txHex: 'abcdefgH' })

0 commit comments

Comments
 (0)