Skip to content

Commit 095c408

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

File tree

7 files changed

+262
-71
lines changed

7 files changed

+262
-71
lines changed

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

Lines changed: 22 additions & 18 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,13 +55,13 @@ describe('Recovery:', function () {
5455
})
5556
.then(function (recovery) {
5657
recovery.txHex.should.equal(
57-
'120000228000000024000000042E00003039201B0015519161400000024F37EDC068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E0107321026C91974146427889C801BD26CE31CE0E10307A69DFE4139DE45E5E35933A6B03744630440220692763F79B6C61D50BE57C613C589EF33FC7A5063169F7E21ABDBF9BFB84A26C022062DC329F27F678AAE51C85896298C54D9D1174E891D52199DED13898F94ECA1A8114ABB5B7C843F3AA8D8EFACC3C5A7D9B0484C17442E1E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D27446304402203F8DAA9F0B26D902A20BBDE426B80941E2E75784EE40290203607BAEEFF080E802207C651C9DE5DB949A3231A70F199939624FB92B36BDA23D2A5858643CA0C288EC8114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
58+
'120000228000000024000000042E00003039201B0015519161400000024E06C0C068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E0107321026C91974146427889C801BD26CE31CE0E10307A69DFE4139DE45E5E35933A6B037446304402204AA3D2F344729B0BB9075C4AEA07EBB2EAF6D3F36309BCAEF10B2C9734AC943E022032D55EC19E27B2E90E3D9444FD26CC06FD47BB3E3D85B0FCC0CC4DE7038563FD8114ABB5B7C843F3AA8D8EFACC3C5A7D9B0484C17442E1E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D2744630440220568F1D49F5810458E7204A1D2D23B86B694505327E8410A215AB9C9324EA8A3102207A93211ACFB5E9C1441B701A7954B72A3054265BA3FD61965D709E4C4E9080F38114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
5859
);
59-
recovery.id.should.equal('0123383D6E12E9F7B3A13727CCE4D15895014FB3957D29610D308E300EA742C1');
60-
recovery.outputAmount.should.equal('9919000000');
60+
recovery.id.should.equal('F2005B392E9454FF1E8217B816C87866A56770382B8FCAC0AAE2FA8D12A53B98');
61+
recovery.outputAmount.should.equal('9899000000');
6162
recovery.outputs.length.should.equal(1);
6263
recovery.outputs[0].address.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2?dt=12345');
63-
recovery.outputs[0].amount.should.equal('9919000000');
64+
recovery.outputs[0].amount.should.equal('9899000000');
6465
recovery.fee.fee.should.equal('30');
6566
});
6667
});
@@ -82,13 +83,13 @@ describe('Recovery:', function () {
8283
})
8384
.then(function (recovery) {
8485
recovery.txHex.should.equal(
85-
'120000228000000024000000042E00003039201B0015519161400000024F37EDC068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D27446304402203F8DAA9F0B26D902A20BBDE426B80941E2E75784EE40290203607BAEEFF080E802207C651C9DE5DB949A3231A70F199939624FB92B36BDA23D2A5858643CA0C288EC8114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
86+
'120000228000000024000000042E00003039201B0015519161400000024E06C0C068400000000000001E7300811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267F3E010732102F4E376133012F5404990C7E1DF83A9F943B30D55F0D856632C8E8378FCEB70D2744630440220568F1D49F5810458E7204A1D2D23B86B694505327E8410A215AB9C9324EA8A3102207A93211ACFB5E9C1441B701A7954B72A3054265BA3FD61965D709E4C4E9080F38114ACEF9F0A2FCEC44A9A213444A9E6C57E2D02856AE1F1'
8687
);
87-
recovery.id.should.equal('397C13D060B4BE43E7F2EEAE5B35E27DA306A7F6766A38C4F0570E359E71D090');
88-
recovery.outputAmount.should.equal('9919000000');
88+
recovery.id.should.equal('6EA1728B0CC0C047E54AAF578D81822EDE1107908B979868299657E74A8E18C0');
89+
recovery.outputAmount.should.equal('9899000000');
8990
recovery.outputs.length.should.equal(1);
9091
recovery.outputs[0].address.should.equal('rsv2kremJSSFbbaLqrf8fWxxN5QnsynNm2?dt=12345');
91-
recovery.outputs[0].amount.should.equal('9919000000');
92+
recovery.outputs[0].amount.should.equal('9899000000');
9293
recovery.fee.fee.should.equal('30');
9394
});
9495
});
@@ -109,16 +110,19 @@ 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+
'120000228000000024000000042E00003039201B0015519161400000024E06C0C068400000000000001E811439CA010E0E0198150F8DDD5768CCD2B095701D8C8314201276ADC469C4F10D1369E0F5C5A7DEF37B2267'
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.DestinationTag.should.equal(12345);
121+
tx.Amount.should.equal('9899000000');
122+
tx.Flags.should.equal(2147483648);
123+
tx.LastLedgerSequence.should.equal(1397137);
124+
tx.Fee.should.equal('30');
125+
tx.Sequence.should.equal(4);
122126
});
123127
});
124128
});

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: 48 additions & 47 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>;
@@ -443,6 +444,7 @@ export class Xrp extends BaseCoin {
443444
const balance = new BigNumber(addressDetails.body.result.account_data.Balance);
444445
const signerLists = addressDetails.body.result.account_data.signer_lists;
445446
const accountFlags = addressDetails.body.result.account_data.Flags;
447+
const ownerCount = new BigNumber(addressDetails.body.result.account_data.OwnerCount);
446448

447449
// make sure there is only one signer list set
448450
if (signerLists.length !== 1) {
@@ -499,15 +501,13 @@ export class Xrp extends BaseCoin {
499501
}
500502

501503
// recover the funds
502-
const reserve = baseReserve.plus(reserveDelta);
504+
const totalReserveDelta = reserveDelta.times(ownerCount);
505+
const reserve = baseReserve.plus(totalReserveDelta);
503506
const recoverableBalance = balance.minus(reserve);
504507

505508
const rawDestination = params.recoveryDestination;
506509
const destinationDetails = url.parse(rawDestination);
507-
const destinationAddress = destinationDetails.pathname;
508510

509-
// parse destination tag from query
510-
let destinationTag: number | undefined;
511511
if (destinationDetails.query) {
512512
const queryDetails = querystring.parse(destinationDetails.query);
513513
if (Array.isArray(queryDetails.dt)) {
@@ -516,11 +516,6 @@ export class Xrp extends BaseCoin {
516516
`destination tag can appear at most once, but ${queryDetails.dt.length} destination tags were found`
517517
);
518518
}
519-
520-
const parsedTag = parseInt(queryDetails.dt as string, 10);
521-
if (Number.isInteger(parsedTag)) {
522-
destinationTag = parsedTag;
523-
}
524519
}
525520

526521
if (recoverableBalance.toNumber() <= 0) {
@@ -532,38 +527,39 @@ export class Xrp extends BaseCoin {
532527
const tokenName = params?.tokenName;
533528
if (!!tokenName) {
534529
const tokenParams = {
535-
destinationAddress,
536-
destinationTag,
530+
recoveryDestination: params.recoveryDestination,
537531
recoverableBalance,
538532
currentLedger,
539533
openLedgerFee,
540534
sequenceId,
541535
accountLines,
542536
keys,
543537
isKrsRecovery,
538+
isUnsignedSweep,
544539
userAddress,
545540
backupAddress,
546541
};
547542

548543
return this.recoverXrpToken(params, tokenName, tokenParams);
549544
}
550545

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);
546+
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
547+
const txBuilder = factory.getTransferBuilder() as TransferBuilder;
548+
txBuilder
549+
.to(params.recoveryDestination as string)
550+
.amount(recoverableBalance.toFixed(0))
551+
.sender(params.rootAddress)
552+
.flags(2147483648)
553+
.lastLedgerSequence(currentLedger + 1000000) // give it 1 million ledgers' time (~1 month, suitable for KRS)
554+
.fee(openLedgerFee.times(3).toFixed(0)) // the factor three is for the multisigning
555+
.sequence(sequenceId);
556+
557+
const tx = await txBuilder.build();
558+
const serializedTx = tx.toBroadcastFormat();
563559

564560
if (isUnsignedSweep) {
565561
return {
566-
txHex: txJSON,
562+
txHex: serializedTx,
567563
coin: this.getChain(),
568564
};
569565
}
@@ -572,7 +568,7 @@ export class Xrp extends BaseCoin {
572568
throw new Error(`userKey is not a private key`);
573569
}
574570
const userKey = keys[0].privateKey.toString('hex');
575-
const userSignature = ripple.signWithPrivateKey(txJSON, userKey, { signAs: userAddress });
571+
const userSignature = ripple.signWithPrivateKey(serializedTx, userKey, { signAs: userAddress });
576572

577573
let signedTransaction: string;
578574

@@ -583,7 +579,7 @@ export class Xrp extends BaseCoin {
583579
throw new Error(`backupKey is not a private key`);
584580
}
585581
const backupKey = keys[1].privateKey.toString('hex');
586-
const backupSignature = ripple.signWithPrivateKey(txJSON, backupKey, { signAs: backupAddress });
582+
const backupSignature = ripple.signWithPrivateKey(serializedTx, backupKey, { signAs: backupAddress });
587583
signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
588584
}
589585

@@ -603,8 +599,6 @@ export class Xrp extends BaseCoin {
603599
public async recoverXrpToken(params, tokenName, tokenParams) {
604600
const { currency, issuer } = utils.getXrpCurrencyFromTokenName(tokenName);
605601

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

610604
let amount;
@@ -622,33 +616,40 @@ export class Xrp extends BaseCoin {
622616
throw new Error(`Does not have funds to recover`);
623617
}
624618

619+
const decimalPlaces = coins.get(tokenName).decimalPlaces;
620+
amount = new BigNumber(amount).shiftedBy(decimalPlaces).toFixed();
621+
625622
const FLAG_VALUE = 2147483648;
626623

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);
624+
const factory = new TransactionBuilderFactory(coins.get(tokenName));
625+
const txBuilder = factory.getTokenTransferBuilder() as TokenTransferBuilder;
626+
txBuilder
627+
.to(tokenParams.recoveryDestination)
628+
.amount(amount)
629+
.sender(params.rootAddress)
630+
.flags(FLAG_VALUE)
631+
.lastLedgerSequence(tokenParams.currentLedger + 1000000) // give it 1 million ledgers' time (~1 month, suitable for KRS)
632+
.fee(tokenParams.openLedgerFee.times(3).toFixed(0)) // the factor three is for the multisigning
633+
.sequence(tokenParams.sequenceId);
634+
635+
const tx = await txBuilder.build();
636+
const serializedTx = tx.toBroadcastFormat();
643637

644-
const { keys, isKrsRecovery, userAddress, backupAddress } = tokenParams;
638+
const { keys, isKrsRecovery, isUnsignedSweep, userAddress, backupAddress } = tokenParams;
639+
640+
if (isUnsignedSweep) {
641+
return {
642+
txHex: serializedTx,
643+
coin: this.getChain(),
644+
};
645+
}
645646

646647
if (!keys[0].privateKey) {
647648
throw new Error(`userKey is not a private key`);
648649
}
649650

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

653654
let signedTransaction: string;
654655

@@ -659,7 +660,7 @@ export class Xrp extends BaseCoin {
659660
throw new Error(`backupKey is not a private key`);
660661
}
661662
const backupKey = keys[1].privateKey.toString('hex');
662-
const backupSignature = ripple.signWithPrivateKey(txJSON, backupKey, { signAs: backupAddress });
663+
const backupSignature = ripple.signWithPrivateKey(serializedTx, backupKey, { signAs: backupAddress });
663664
signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
664665
}
665666

0 commit comments

Comments
 (0)