Skip to content

Commit 6c39d14

Browse files
authored
Merge pull request #5077 from BitGo/WIN-3720
feat(sdk-coin-xrp): add non-bitgo recovery for xrpl token
2 parents 5682ced + ef10edd commit 6c39d14

File tree

5 files changed

+423
-2
lines changed

5 files changed

+423
-2
lines changed

modules/bitgo/test/v2/lib/recovery-nocks.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,24 @@ module.exports.nockXrpRecovery = function nockXrpRecovery() {
145145
},
146146
status: 'success',
147147
},
148+
})
149+
.post('/', {
150+
method: 'account_lines',
151+
params: [
152+
{
153+
account: 'raGZWRkRBUWdQJsKYEzwXJNbCZMTqX56aA',
154+
ledger_index: 'validated',
155+
},
156+
],
157+
})
158+
.reply(200, {
159+
result: {
160+
account: 'rMficzfw4t5iGu9hhB23eKwDjM879vJWTR',
161+
ledger_hash: 'E6F38D1D7B94153BF7FFC8D8CC1DF57D57151D26FC2EB7647B5631786B955EFF',
162+
ledger_index: 1848964,
163+
lines: [],
164+
validated: true,
165+
},
148166
});
149167
};
150168

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface RecoveryOptions {
6767
bitgoKey?: string;
6868
walletPassphrase: string;
6969
krsProvider?: string;
70+
tokenName?: string;
7071
}
7172

7273
export interface HalfSignedTransaction {

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

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,24 @@ export class Xrp extends BaseCoin {
329329
params: [
330330
{
331331
account: params.rootAddress,
332-
strict: true,
333332
ledger_index: 'current',
334333
queue: true,
334+
strict: true,
335335
signer_lists: true,
336336
},
337337
],
338338
};
339339

340+
const accountLinesParams = {
341+
method: 'account_lines',
342+
params: [
343+
{
344+
account: params.rootAddress,
345+
ledger_index: 'validated',
346+
},
347+
],
348+
};
349+
340350
if (isKrsRecovery) {
341351
checkKrsProvider(this, params.krsProvider);
342352
}
@@ -348,10 +358,11 @@ export class Xrp extends BaseCoin {
348358

349359
const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false });
350360

351-
const { addressDetails, feeDetails, serverDetails } = await promiseProps({
361+
const { addressDetails, feeDetails, serverDetails, accountLines } = await promiseProps({
352362
addressDetails: this.bitgo.post(rippledUrl).send(accountInfoParams),
353363
feeDetails: this.bitgo.post(rippledUrl).send({ method: 'fee' }),
354364
serverDetails: this.bitgo.post(rippledUrl).send({ method: 'server_info' }),
365+
accountLines: this.bitgo.post(rippledUrl).send(accountLinesParams),
355366
});
356367

357368
const openLedgerFee = new BigNumber(feeDetails.body.result.drops.open_ledger_fee);
@@ -452,6 +463,25 @@ export class Xrp extends BaseCoin {
452463
);
453464
}
454465

466+
const tokenName = params?.tokenName;
467+
if (!!tokenName) {
468+
const tokenParams = {
469+
destinationAddress,
470+
destinationTag,
471+
recoverableBalance,
472+
currentLedger,
473+
openLedgerFee,
474+
sequenceId,
475+
accountLines,
476+
keys,
477+
isKrsRecovery,
478+
userAddress,
479+
backupAddress,
480+
};
481+
482+
return this.recoverXrpToken(params, tokenName, tokenParams);
483+
}
484+
455485
const transaction = {
456486
TransactionType: 'Payment',
457487
Account: params.rootAddress, // source address
@@ -504,6 +534,82 @@ export class Xrp extends BaseCoin {
504534
return transactionExplanation;
505535
}
506536

537+
public async recoverXrpToken(params, tokenName, tokenParams) {
538+
const { currency, issuer } = utils.getXrpCurrencyFromTokenName(tokenName);
539+
540+
// const accountLines = JSON.parse(tokenParams.accountLines);
541+
// const lines = accountLines.body.result.lines;
542+
const lines = tokenParams.accountLines.body.result.lines;
543+
544+
let amount;
545+
for (const line of lines) {
546+
if (line.currency === currency && line.account === issuer) {
547+
amount = line.balance;
548+
break;
549+
}
550+
}
551+
552+
if (amount === undefined) {
553+
throw new Error(`Does not have Trustline with ${issuer}`);
554+
}
555+
if (amount === '0') {
556+
throw new Error(`Does not have funds to recover`);
557+
}
558+
559+
const FLAG_VALUE = 2147483648;
560+
561+
const transaction = {
562+
TransactionType: 'Payment',
563+
Amount: {
564+
value: amount,
565+
currency,
566+
issuer,
567+
}, // source address
568+
Destination: tokenParams.destinationAddress,
569+
DestinationTag: tokenParams.destinationTag,
570+
Account: params.rootAddress,
571+
Flags: FLAG_VALUE,
572+
LastLedgerSequence: tokenParams.currentLedger + 1000000, // give it 1 million ledgers' time (~1 month, suitable for KRS)
573+
Fee: tokenParams.openLedgerFee.times(3).toFixed(0), // the factor three is for the multisigning
574+
Sequence: tokenParams.sequenceId,
575+
};
576+
const txJSON: string = JSON.stringify(transaction);
577+
578+
const { keys, isKrsRecovery, userAddress, backupAddress } = tokenParams;
579+
580+
if (!keys[0].privateKey) {
581+
throw new Error(`userKey is not a private key`);
582+
}
583+
584+
const userKey = keys[0].privateKey.toString('hex');
585+
const userSignature = ripple.signWithPrivateKey(txJSON, userKey, { signAs: userAddress });
586+
587+
let signedTransaction: string;
588+
589+
if (isKrsRecovery) {
590+
signedTransaction = userSignature.signedTransaction;
591+
} else {
592+
if (!keys[1].privateKey) {
593+
throw new Error(`backupKey is not a private key`);
594+
}
595+
const backupKey = keys[1].privateKey.toString('hex');
596+
const backupSignature = ripple.signWithPrivateKey(txJSON, backupKey, { signAs: backupAddress });
597+
signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
598+
}
599+
600+
const transactionExplanation: RecoveryInfo = (await this.explainTransaction({
601+
txHex: signedTransaction,
602+
})) as RecoveryInfo;
603+
604+
transactionExplanation.txHex = signedTransaction;
605+
606+
if (isKrsRecovery) {
607+
transactionExplanation.backupKey = params.backupKey;
608+
transactionExplanation.coin = this.getChain();
609+
}
610+
return transactionExplanation;
611+
}
612+
507613
/**
508614
* Generate a new keypair for this coin.
509615
* @param seed Seed from which the new keypair should be generated, otherwise a random seed is used

0 commit comments

Comments
 (0)