Skip to content

Commit 08c0c4a

Browse files
committed
feat(sdk-coin-xrp): add wrw support cold wallet xrpl token
TICKET: WIN-3813
1 parent c5262dd commit 08c0c4a

File tree

7 files changed

+251
-55
lines changed

7 files changed

+251
-55
lines changed

modules/bitgo/test/v2/unit/recovery.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { EcdsaRangeProof, EcdsaTypes } from '@bitgo/sdk-lib-mpc';
88
import { TransactionFactory } from '@ethereumjs/tx';
99
import * as sinon from 'sinon';
1010
import { ethLikeDKLSKeycard, ethLikeGG18Keycard } from '../fixtures/tss/recoveryFixtures';
11+
import { Utils } from '@bitgo/sdk-coin-xrp';
1112

1213
const recoveryNocks = require('../lib/recovery-nocks');
1314

@@ -54,12 +55,12 @@ describe('Recovery:', function () {
5455
})
5556
.then(function (recovery) {
5657
recovery.txHex.should.equal(
57-
'120000228000000024000000042E00003039201B0015519161400000024F37EDC068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E0107321026C91974146427889C801BD26CE31CE0E10307A69DFE4139DE45E5E35933A6B03744630440220692763F79B6C61D50BE57C613C589EF33FC7A5063169F7E21ABDBF9BFB84A26C022062DC329F27F678AAE51C85896298C54D9D1174E891D52199DED13898F94ECA1A8114ABB5B7C843F3AA8D8EFACC3C5A7D9B0484C17442E1E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D27446304402203F8DAA9F0B26D902A20BBDE426B80941E2E75784EE40290203607BAEEFF080E802207C651C9DE5DB949A3231A70F199939624FB92B36BDA23D2A5858643CA0C288EC8114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
58+
'12000022800000002400000004201B0015519161400000024F37EDC068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E0107321026C91974146427889C801BD26CE31CE0E10307A69DFE4139DE45E5E35933A6B03744630440220263C30D0CBB59076D0541E1D8919B0D467FDC9CFC63D2FB43E22A26924FB174E02204EFFD8F2269D1F701FF3D816A215884226BCB6668222B962FD124A9F8264E98D8114ABB5B7C843F3AA8D8EFACC3C5A7D9B0484C17442E1E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D274473045022100D3D94038EE6D5A2E4E9C2B79ED176066C7FE8019B2ACD1F028FD3212A7BA4E6F02205E39C9734873441D8018D2A72C42F31D1863B65215AFC9674820A8A3135E81248114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
5859
);
59-
recovery.id.should.equal('0123383D6E12E9F7B3A13727CCE4D15895014FB3957D29610D308E300EA742C1');
60+
recovery.id.should.equal('25B104FD0B9649AB38950202741356C75DBEEAD3E9F1F43097C8A649D3FE8E94');
6061
recovery.outputAmount.should.equal('9919000000');
6162
recovery.outputs.length.should.equal(1);
62-
recovery.outputs[0].address.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2?dt=12345');
63+
recovery.outputs[0].address.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2');
6364
recovery.outputs[0].amount.should.equal('9919000000');
6465
recovery.fee.fee.should.equal('30');
6566
});
@@ -82,12 +83,12 @@ describe('Recovery:', function () {
8283
})
8384
.then(function (recovery) {
8485
recovery.txHex.should.equal(
85-
'120000228000000024000000042E00003039201B0015519161400000024F37EDC068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D27446304402203F8DAA9F0B26D902A20BBDE426B80941E2E75784EE40290203607BAEEFF080E802207C651C9DE5DB949A3231A70F199939624FB92B36BDA23D2A5858643CA0C288EC8114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
86+
'12000022800000002400000004201B0015519161400000024F37EDC068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D274473045022100D3D94038EE6D5A2E4E9C2B79ED176066C7FE8019B2ACD1F028FD3212A7BA4E6F02205E39C9734873441D8018D2A72C42F31D1863B65215AFC9674820A8A3135E81248114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
8687
);
87-
recovery.id.should.equal('397C13D060B4BE43E7F2EEAE5B35E27DA306A7F6766A38C4F0570E359E71D090');
88+
recovery.id.should.equal('11D09331E7AC76D4817F08D359F8CEB06A10DFCBE4B7ABAE45C7A1131443C6F1');
8889
recovery.outputAmount.should.equal('9919000000');
8990
recovery.outputs.length.should.equal(1);
90-
recovery.outputs[0].address.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2?dt=12345');
91+
recovery.outputs[0].address.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2');
9192
recovery.outputs[0].amount.should.equal('9919000000');
9293
recovery.fee.fee.should.equal('30');
9394
});
@@ -109,16 +110,18 @@ describe('Recovery:', function () {
109110
recoveryDestination: 'rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2?dt=12345',
110111
})
111112
.then(function (recovery) {
112-
const json = JSON.parse(recovery.txHex);
113-
json.TransactionType.should.equal('Payment');
114-
json.Account.should.equal('raGZWRkRBUWdQJsKYEzwXJNbCZMTqX56aA');
115-
json.Destination.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2');
116-
json.DestinationTag.should.equal(12345);
117-
json.Amount.should.equal('9919000000');
118-
json.Flags.should.equal(2147483648);
119-
json.LastLedgerSequence.should.equal(1397137);
120-
json.Fee.should.equal('30');
121-
json.Sequence.should.equal(4);
113+
recovery.txHex.should.equal(
114+
'12000022800000002400000004201B0015519161400000024F37EDC068400000000000001E811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267'
115+
);
116+
const tx: any = Utils.decodeTransaction(recovery.txHex);
117+
tx.TransactionType.should.equal('Payment');
118+
tx.Account.should.equal('raGZWRkRBUWdQJsKYEzwXJNbCZMTqX56aA');
119+
tx.Destination.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2');
120+
tx.Amount.should.equal('9919000000');
121+
tx.Flags.should.equal(2147483648);
122+
tx.LastLedgerSequence.should.equal(1397137);
123+
tx.Fee.should.equal('30');
124+
tx.Sequence.should.equal(4);
122125
});
123126
});
124127
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class TokenTransferBuilder extends TransactionBuilder {
4949
* @param {string} address - the address with optional destination tag
5050
* @returns {TransactionBuilder} This transaction builder
5151
*/
52-
to(address: string): TransactionBuilder {
52+
to(address: string): TokenTransferBuilder {
5353
const { address: xrpAddress, destinationTag } = utils.getAddressDetails(address);
5454
this._destination = xrpAddress;
5555
this._destinationTag = destinationTag;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class TransferBuilder extends TransactionBuilder {
4646
* @param {string} address - the address with optional destination tag
4747
* @returns {TransactionBuilder} This transaction builder
4848
*/
49-
to(address: string): TransactionBuilder {
49+
to(address: string): TransferBuilder {
5050
const { address: xrpAddress, destinationTag } = utils.getAddressDetails(address);
5151
this._destination = xrpAddress;
5252
this._destinationTag = destinationTag;

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,24 @@ class Utils implements BaseUtils {
249249
issuer: token.issuerAddress,
250250
};
251251
}
252+
253+
/**
254+
* Decodes a serialized XRPL transaction.
255+
*
256+
* @param {string} txHex - The serialized transaction in hex.
257+
* @returns {Object} - Decoded transaction object.
258+
* @throws {Error} - If decoding fails or input is invalid.
259+
*/
260+
public decodeTransaction(txHex: string) {
261+
if (typeof txHex !== 'string' || txHex.trim() === '') {
262+
throw new Error('Invalid transaction hex. Expected a non-empty string.');
263+
}
264+
try {
265+
return xrpl.decode(txHex);
266+
} catch (error) {
267+
throw new Error(`Failed to decode transaction: ${error.message}`);
268+
}
269+
}
252270
}
253271

254272
const utils = new Utils();

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

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import { KeyPair as XrpKeyPair } from './lib/keyPair';
4141
import utils from './lib/utils';
4242
import ripple from './ripple';
43+
import { TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib';
4344

4445
export class Xrp extends BaseCoin {
4546
protected _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -541,29 +542,31 @@ export class Xrp extends BaseCoin {
541542
accountLines,
542543
keys,
543544
isKrsRecovery,
545+
isUnsignedSweep,
544546
userAddress,
545547
backupAddress,
546548
};
547549

548550
return this.recoverXrpToken(params, tokenName, tokenParams);
549551
}
550552

551-
const transaction = {
552-
TransactionType: 'Payment',
553-
Account: params.rootAddress, // source address
554-
Destination: destinationAddress,
555-
DestinationTag: destinationTag,
556-
Amount: recoverableBalance.toFixed(0),
557-
Flags: 2147483648,
558-
LastLedgerSequence: currentLedger + 1000000, // give it 1 million ledgers' time (~1 month, suitable for KRS)
559-
Fee: openLedgerFee.times(3).toFixed(0), // the factor three is for the multisigning
560-
Sequence: sequenceId,
561-
};
562-
const txJSON: string = JSON.stringify(transaction);
553+
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
554+
const txBuilder = factory.getTransferBuilder() as TransferBuilder;
555+
txBuilder
556+
.to(destinationAddress as string)
557+
.amount(recoverableBalance.toFixed(0))
558+
.sender(params.rootAddress)
559+
.flags(2147483648)
560+
.lastLedgerSequence(currentLedger + 1000000) // give it 1 million ledgers' time (~1 month, suitable for KRS)
561+
.fee(openLedgerFee.times(3).toFixed(0)) // the factor three is for the multisigning
562+
.sequence(sequenceId);
563+
564+
const tx = await txBuilder.build();
565+
const serializedTx = tx.toBroadcastFormat();
563566

564567
if (isUnsignedSweep) {
565568
return {
566-
txHex: txJSON,
569+
txHex: serializedTx,
567570
coin: this.getChain(),
568571
};
569572
}
@@ -572,7 +575,7 @@ export class Xrp extends BaseCoin {
572575
throw new Error(`userKey is not a private key`);
573576
}
574577
const userKey = keys[0].privateKey.toString('hex');
575-
const userSignature = ripple.signWithPrivateKey(txJSON, userKey, { signAs: userAddress });
578+
const userSignature = ripple.signWithPrivateKey(serializedTx, userKey, { signAs: userAddress });
576579

577580
let signedTransaction: string;
578581

@@ -583,7 +586,7 @@ export class Xrp extends BaseCoin {
583586
throw new Error(`backupKey is not a private key`);
584587
}
585588
const backupKey = keys[1].privateKey.toString('hex');
586-
const backupSignature = ripple.signWithPrivateKey(txJSON, backupKey, { signAs: backupAddress });
589+
const backupSignature = ripple.signWithPrivateKey(serializedTx, backupKey, { signAs: backupAddress });
587590
signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
588591
}
589592

@@ -603,8 +606,6 @@ export class Xrp extends BaseCoin {
603606
public async recoverXrpToken(params, tokenName, tokenParams) {
604607
const { currency, issuer } = utils.getXrpCurrencyFromTokenName(tokenName);
605608

606-
// const accountLines = JSON.parse(tokenParams.accountLines);
607-
// const lines = accountLines.body.result.lines;
608609
const lines = tokenParams.accountLines.body.result.lines;
609610

610611
let amount;
@@ -622,33 +623,40 @@ export class Xrp extends BaseCoin {
622623
throw new Error(`Does not have funds to recover`);
623624
}
624625

626+
const decimalPlaces = coins.get(tokenName).decimalPlaces;
627+
amount = new BigNumber(amount).shiftedBy(decimalPlaces).toFixed();
628+
625629
const FLAG_VALUE = 2147483648;
626630

627-
const transaction = {
628-
TransactionType: 'Payment',
629-
Amount: {
630-
value: amount,
631-
currency,
632-
issuer,
633-
}, // source address
634-
Destination: tokenParams.destinationAddress,
635-
DestinationTag: tokenParams.destinationTag,
636-
Account: params.rootAddress,
637-
Flags: FLAG_VALUE,
638-
LastLedgerSequence: tokenParams.currentLedger + 1000000, // give it 1 million ledgers' time (~1 month, suitable for KRS)
639-
Fee: tokenParams.openLedgerFee.times(3).toFixed(0), // the factor three is for the multisigning
640-
Sequence: tokenParams.sequenceId,
641-
};
642-
const txJSON: string = JSON.stringify(transaction);
631+
const factory = new TransactionBuilderFactory(coins.get(tokenName));
632+
const txBuilder = factory.getTokenTransferBuilder() as TokenTransferBuilder;
633+
txBuilder
634+
.to(tokenParams.destinationAddress)
635+
.amount(amount)
636+
.sender(params.rootAddress)
637+
.flags(FLAG_VALUE)
638+
.lastLedgerSequence(tokenParams.currentLedger + 1000000) // give it 1 million ledgers' time (~1 month, suitable for KRS)
639+
.fee(tokenParams.openLedgerFee.times(3).toFixed(0)) // the factor three is for the multisigning
640+
.sequence(tokenParams.sequenceId);
641+
642+
const tx = await txBuilder.build();
643+
const serializedTx = tx.toBroadcastFormat();
644+
645+
const { keys, isKrsRecovery, isUnsignedSweep, userAddress, backupAddress } = tokenParams;
643646

644-
const { keys, isKrsRecovery, userAddress, backupAddress } = tokenParams;
647+
if (isUnsignedSweep) {
648+
return {
649+
txHex: serializedTx,
650+
coin: this.getChain(),
651+
};
652+
}
645653

646654
if (!keys[0].privateKey) {
647655
throw new Error(`userKey is not a private key`);
648656
}
649657

650658
const userKey = keys[0].privateKey.toString('hex');
651-
const userSignature = ripple.signWithPrivateKey(txJSON, userKey, { signAs: userAddress });
659+
const userSignature = ripple.signWithPrivateKey(serializedTx, userKey, { signAs: userAddress });
652660

653661
let signedTransaction: string;
654662

@@ -659,7 +667,7 @@ export class Xrp extends BaseCoin {
659667
throw new Error(`backupKey is not a private key`);
660668
}
661669
const backupKey = keys[1].privateKey.toString('hex');
662-
const backupSignature = ripple.signWithPrivateKey(txJSON, backupKey, { signAs: backupAddress });
670+
const backupSignature = ripple.signWithPrivateKey(serializedTx, backupKey, { signAs: backupAddress });
663671
signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
664672
}
665673

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,105 @@ export const accountlinesResponse = {
195195
},
196196
};
197197

198+
export const accountInfoResponseUnsigned = {
199+
body: {
200+
result: {
201+
account_data: {
202+
Account: 'raGZWRkRBUWdQJsKYEzwXJNbCZMTqX56aA',
203+
Balance: '99997952',
204+
Flags: 1179648,
205+
LedgerEntryType: 'AccountRoot',
206+
OwnerCount: 2,
207+
PreviousTxnID: '1216237378659EC1849D45D210B45C96B7B091B4134BA3A00509B8F9DCBA59C4',
208+
PreviousTxnLgrSeq: 1851142,
209+
Sequence: 807096,
210+
index: 'BDD920F6F4C21E2EDF8E604257AB2D3EFF3CD8374330B98989A58E9AB27FDC9D',
211+
signer_lists: [
212+
{
213+
Flags: 65536,
214+
LedgerEntryType: 'SignerList',
215+
OwnerNode: '0',
216+
PreviousTxnID: '1216237378659EC1849D45D210B45C96B7B091B4134BA3A00509B8F9DCBA59C4',
217+
PreviousTxnLgrSeq: 1851142,
218+
SignerEntries: [
219+
{
220+
SignerEntry: {
221+
Account: 'rGmQHwvb5SZRbyhp4JBHdpRzSmgqADxPbE',
222+
SignerWeight: 1,
223+
},
224+
},
225+
{
226+
SignerEntry: {
227+
Account: 'rGevN87RpWBbdLxKCF4FAqWgRoSyMJA81f',
228+
SignerWeight: 1,
229+
},
230+
},
231+
{
232+
SignerEntry: {
233+
Account: 'r3mykfPQZt4eJZKLUGMNVB49eDSJiE9zh3',
234+
SignerWeight: 1,
235+
},
236+
},
237+
],
238+
SignerListID: 0,
239+
SignerQuorum: 2,
240+
index: '00B47042E37B5F11E6325D7BECAA08D165C6681DB4F6528AF7D1CA6ED50075B7',
241+
},
242+
],
243+
},
244+
account_flags: {
245+
allowTrustLineClawback: false,
246+
defaultRipple: false,
247+
depositAuth: false,
248+
disableMasterKey: false,
249+
disallowIncomingCheck: false,
250+
disallowIncomingNFTokenOffer: false,
251+
disallowIncomingPayChan: false,
252+
disallowIncomingTrustline: false,
253+
disallowIncomingXRP: false,
254+
globalFreeze: false,
255+
noFreeze: false,
256+
passwordSpent: false,
257+
requireAuthorization: false,
258+
requireDestinationTag: false,
259+
},
260+
ledger_current_index: 1851200,
261+
queue_data: {
262+
txn_count: 0,
263+
},
264+
validated: false,
265+
},
266+
status: 'success',
267+
type: 'response',
268+
},
269+
};
270+
271+
export const accountlinesResponseUnsigned = {
272+
body: {
273+
result: {
274+
account: 'raGZWRkRBUWdQJsKYEzwXJNbCZMTqX56aA',
275+
ledger_hash: 'E6F38D1D7B94153BF7FFC8D8CC1DF57D57151D26FC2EB7647B5631786B955EFF',
276+
ledger_index: 1848964,
277+
lines: [
278+
{
279+
account: 'rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV',
280+
balance: '4',
281+
currency: '524C555344000000000000000000000000000000',
282+
limit: '1000000000',
283+
limit_peer: '0',
284+
no_ripple: false,
285+
no_ripple_peer: false,
286+
quality_in: 0,
287+
quality_out: 0,
288+
},
289+
],
290+
validated: true,
291+
},
292+
status: 'success',
293+
type: 'response',
294+
},
295+
};
296+
198297
export const feeResponse = {
199298
body: {
200299
id: 'fee_websocket_example',

0 commit comments

Comments
 (0)