Skip to content

Commit 683f82e

Browse files
committed
feat(sdk-coin-xrp): add support for token enablement
TICKET: WIN-3761
1 parent 3a3ed54 commit 683f82e

File tree

3 files changed

+151
-3
lines changed

3 files changed

+151
-3
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface SupplementGenerateWalletOptions {
8383
export type TransactionExplanation =
8484
| BaseTransactionExplanation
8585
| AccountSetTransactionExplanation
86+
| TrustSetTransactionExplanation
8687
| SignerListSetTransactionExplanation;
8788

8889
export interface AccountSetTransactionExplanation extends BaseTransactionExplanation {
@@ -92,6 +93,14 @@ export interface AccountSetTransactionExplanation extends BaseTransactionExplana
9293
};
9394
}
9495

96+
export interface TrustSetTransactionExplanation extends BaseTransactionExplanation {
97+
limitAmount: {
98+
tokenName: string;
99+
address: string;
100+
amount: string;
101+
};
102+
}
103+
95104
export interface SignerListSetTransactionExplanation extends BaseTransactionExplanation {
96105
signerListSet: {
97106
signerQuorum: number;

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

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import {
1616
ParsedTransaction,
1717
ParseTransactionOptions,
1818
promiseProps,
19+
TokenEnablementConfig,
1920
UnexpectedAddressError,
2021
VerifyTransactionOptions,
2122
} from '@bitgo/sdk-core';
22-
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
23+
import { BaseCoin as StaticsBaseCoin, coins, XrpCoin } from '@bitgo/statics';
2324
import * as rippleBinaryCodec from 'ripple-binary-codec';
2425
import * as rippleKeypairs from 'ripple-keypairs';
2526
import * as xrpl from 'xrpl';
@@ -107,6 +108,13 @@ export class Xrp extends BaseCoin {
107108
return this.bitgo.get(this.url('/public/feeinfo')).result();
108109
}
109110

111+
public getTokenEnablementConfig(): TokenEnablementConfig {
112+
return {
113+
requiresTokenEnablement: true,
114+
supportsMultipleTokenEnablements: false,
115+
};
116+
}
117+
110118
/**
111119
* Assemble keychain and half-sign prebuilt transaction
112120
* @param params
@@ -222,6 +230,25 @@ export class Xrp extends BaseCoin {
222230
setFlag: transaction.SetFlag,
223231
},
224232
};
233+
} else if (transaction.TransactionType === 'TrustSet') {
234+
return {
235+
displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'limitAmount'],
236+
id: id,
237+
changeOutputs: [],
238+
outputAmount: 0,
239+
changeAmount: 0,
240+
outputs: [],
241+
fee: {
242+
fee: transaction.Fee,
243+
feeRate: undefined,
244+
size: txHex.length / 2,
245+
},
246+
limitAmount: {
247+
tokenName: transaction.LimitAmount.currency,
248+
address: transaction.LimitAmount.issuer,
249+
amount: transaction.LimitAmount.value,
250+
},
251+
};
225252
}
226253

227254
const address =
@@ -254,6 +281,7 @@ export class Xrp extends BaseCoin {
254281
* @returns {boolean}
255282
*/
256283
public async verifyTransaction({ txParams, txPrebuild }: VerifyTransactionOptions): Promise<boolean> {
284+
const coinConfig = coins.get(this.getChain()) as XrpCoin;
257285
const explanation = await this.explainTransaction({
258286
txHex: txPrebuild.txHex,
259287
});
@@ -270,8 +298,34 @@ export class Xrp extends BaseCoin {
270298
return amount1.toFixed() === amount2.toFixed();
271299
};
272300

273-
if (!comparator(output, expectedOutput)) {
274-
throw new Error('transaction prebuild does not match expected output');
301+
if (txParams.recipients) {
302+
// for enabletoken, recipient output amount is 0
303+
const recipients = txParams.recipients.map((recipient) => ({
304+
...recipient,
305+
}));
306+
if (coinConfig.isToken) {
307+
recipients.forEach((recipient) => {
308+
if (
309+
recipient.tokenName !== undefined &&
310+
utils.getXrpCurrencyFromTokenName(recipient.tokenName).currency !== coinConfig.currencyCode
311+
) {
312+
throw new Error('Incorrect token name specified in recipients');
313+
}
314+
recipient.tokenName = coinConfig.currencyCode;
315+
});
316+
}
317+
318+
// verify recipients from params and explainedTx
319+
const filteredRecipients = recipients?.map((recipient) => _.pick(recipient, ['address', 'amount', 'tokenName']));
320+
const filteredOutputs = 'limitAmount' in explanation ? [explanation.limitAmount] : [];
321+
322+
if (!_.isEqual(filteredOutputs, filteredRecipients)) {
323+
throw new Error('Tx outputs does not match with expected txParams recipients');
324+
}
325+
} else {
326+
if (!comparator(output, expectedOutput)) {
327+
throw new Error('transaction prebuild does not match expected output');
328+
}
275329
}
276330

277331
return true;

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ 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 * as _ from 'lodash';
14+
import { XrpToken } from '../../src';
1315

1416
nock.disableNetConnect();
1517

@@ -18,10 +20,15 @@ bitgo.safeRegister('txrp', Txrp.createInstance);
1820

1921
describe('XRP:', function () {
2022
let basecoin;
23+
let token;
2124

2225
before(function () {
26+
XrpToken.createTokenConstructors().forEach(({ name, coinConstructor }) => {
27+
bitgo.safeRegister(name, coinConstructor);
28+
});
2329
bitgo.initializeTestVars();
2430
basecoin = bitgo.coin('txrp');
31+
token = bitgo.coin('txrp:rlusd');
2532
});
2633

2734
after(function () {
@@ -294,4 +301,82 @@ describe('XRP:', function () {
294301
);
295302
});
296303
});
304+
305+
describe('Verify Transaction', () => {
306+
let newTxPrebuild;
307+
let newTxParams;
308+
309+
const txPrebuild = {
310+
txHex: `{"TransactionType":"TrustSet","Account":"rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG","LimitAmount":{"currency":"524C555344000000000000000000000000000000","issuer":"rnox8i6h9GoAbuwr73JtaDxXoncLLUCpXH","value":"1000000000"},"Flags":2147483648,"Fee":"45","Sequence":7}`,
311+
};
312+
313+
const txParams = {
314+
recipients: [
315+
{
316+
address: 'rnox8i6h9GoAbuwr73JtaDxXoncLLUCpXH',
317+
amount: '10',
318+
},
319+
{
320+
address: 'raBSn6ipeWXYe7rNbNafZSx9dV2fU3zRyP',
321+
amount: '15',
322+
},
323+
],
324+
};
325+
326+
before(function () {
327+
newTxPrebuild = () => {
328+
return _.cloneDeep(txPrebuild);
329+
};
330+
newTxParams = () => {
331+
return _.cloneDeep(txParams);
332+
};
333+
});
334+
335+
it('should verify token trustline transactions', async function () {
336+
const txParams = newTxParams();
337+
const txPrebuild = newTxPrebuild();
338+
339+
txParams.recipients = [
340+
{
341+
address: 'rnox8i6h9GoAbuwr73JtaDxXoncLLUCpXH',
342+
amount: '1000000000',
343+
tokenName: 'txrp:rlusd',
344+
},
345+
];
346+
347+
const validTransaction = await token.verifyTransaction({
348+
txParams,
349+
txPrebuild,
350+
});
351+
validTransaction.should.equal(true);
352+
});
353+
354+
it('should fail verify trustline transaction with mismatch recipients', async function () {
355+
const txParams = newTxParams();
356+
const txPrebuild = newTxPrebuild();
357+
txParams.recipients = [
358+
{
359+
address: 'raBSn6ipeWXYe7rNbNafZSx9dV2fU3zRyP',
360+
amount: '1000000000',
361+
tokenName: 'txrp:rlusd',
362+
},
363+
];
364+
await token
365+
.verifyTransaction({ txParams, txPrebuild })
366+
.should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
367+
});
368+
369+
it('should fail to verify trustline transaction with incorrect token name', async function () {
370+
const txParams = newTxParams();
371+
const txPrebuild = newTxPrebuild();
372+
txParams.recipients = [
373+
{
374+
address: 'rnox8i6h9GoAbuwr73JtaDxXoncLLUCpXH',
375+
amount: '1000000000',
376+
tokenName: 'txrp:usd',
377+
},
378+
];
379+
await token.verifyTransaction({ txParams, txPrebuild }).should.be.rejectedWith('txrp:usd is not supported');
380+
});
381+
});
297382
});

0 commit comments

Comments
 (0)