Skip to content

Commit f4a95ca

Browse files
Merge pull request #5100 from BitGo/WIN-3761
feat(sdk-coin-xrp): add support for token enablement
2 parents 51426b1 + 8972a6e commit f4a95ca

File tree

3 files changed

+148
-2
lines changed

3 files changed

+148
-2
lines changed

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

Lines changed: 10 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,15 @@ export interface AccountSetTransactionExplanation extends BaseTransactionExplana
9293
};
9394
}
9495

96+
export interface TrustSetTransactionExplanation extends BaseTransactionExplanation {
97+
account: string;
98+
limitAmount: {
99+
currency: string;
100+
issuer: string;
101+
value: string;
102+
};
103+
}
104+
95105
export interface SignerListSetTransactionExplanation extends BaseTransactionExplanation {
96106
signerListSet: {
97107
signerQuorum: number;

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

Lines changed: 64 additions & 2 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,35 @@ export class Xrp extends BaseCoin {
222230
setFlag: transaction.SetFlag,
223231
},
224232
};
233+
} else if (transaction.TransactionType === 'TrustSet') {
234+
return {
235+
displayOrder: [
236+
'id',
237+
'outputAmount',
238+
'changeAmount',
239+
'outputs',
240+
'changeOutputs',
241+
'fee',
242+
'account',
243+
'limitAmount',
244+
],
245+
id: id,
246+
changeOutputs: [],
247+
outputAmount: 0,
248+
changeAmount: 0,
249+
outputs: [],
250+
fee: {
251+
fee: transaction.Fee,
252+
feeRate: undefined,
253+
size: txHex.length / 2,
254+
},
255+
account: transaction.Account,
256+
limitAmount: {
257+
currency: transaction.LimitAmount.currency,
258+
issuer: transaction.LimitAmount.issuer,
259+
value: transaction.LimitAmount.value,
260+
},
261+
};
225262
}
226263

227264
const address =
@@ -254,6 +291,7 @@ export class Xrp extends BaseCoin {
254291
* @returns {boolean}
255292
*/
256293
public async verifyTransaction({ txParams, txPrebuild }: VerifyTransactionOptions): Promise<boolean> {
294+
const coinConfig = coins.get(this.getChain()) as XrpCoin;
257295
const explanation = await this.explainTransaction({
258296
txHex: txPrebuild.txHex,
259297
});
@@ -270,10 +308,34 @@ export class Xrp extends BaseCoin {
270308
return amount1.toFixed() === amount2.toFixed();
271309
};
272310

273-
if (!comparator(output, expectedOutput)) {
311+
if ((txParams.type === undefined || txParams.type === 'payment') && !comparator(output, expectedOutput)) {
274312
throw new Error('transaction prebuild does not match expected output');
275313
}
276314

315+
if (txParams.type === 'enabletoken') {
316+
if (txParams.recipients?.length !== 1) {
317+
throw new Error('Only one recipient is allowed.');
318+
}
319+
const recipient = txParams.recipients[0];
320+
if (!recipient.tokenName) {
321+
throw new Error('Recipient must include a token name.');
322+
}
323+
const recipientCurrency = utils.getXrpCurrencyFromTokenName(recipient.tokenName).currency;
324+
if (coinConfig.isToken) {
325+
if (recipientCurrency !== coinConfig.currencyCode) {
326+
throw new Error('Incorrect token name specified in recipients');
327+
}
328+
}
329+
if (!('account' in explanation) || !('limitAmount' in explanation) || !explanation.limitAmount.currency) {
330+
throw new Error('Explanation is missing required keys (account or limitAmount with currency)');
331+
}
332+
const baseAddress = explanation.account;
333+
const currency = explanation.limitAmount.currency;
334+
335+
if (recipient.address !== baseAddress || recipientCurrency !== currency) {
336+
throw new Error('Tx outputs does not match with expected txParams recipients');
337+
}
338+
}
277339
return true;
278340
}
279341

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

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

0 commit comments

Comments
 (0)