Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/sdk-coin-xrp/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ export interface PaymentTransactionExplanation extends BaseTransactionExplanatio
* this flag is still set so consumers can distinguish partial from full delivery.
*/
partialPayment?: boolean;
sendMax?: Amount | MPTAmount;
hasPaths?: boolean;
deliverMin?: Amount | MPTAmount;
}

export interface PaymentSpendLimits {
sendMax?: Amount | MPTAmount;
hasPaths?: boolean;
deliverMin?: Amount | MPTAmount;
}

export interface AccountSetTransactionExplanation extends BaseTransactionExplanation {
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-xrp/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export class Transaction extends BaseTransaction {
// *requested* amount, not what was actually delivered — surface that via the flag so
// consumers don't treat outputAmount as the settled value.
const partialPayment = utils.isPartialPayment(tx.Flags as number);
const spendLimits = utils.getPaymentSpendLimits(tx);

return {
displayOrder: [
Expand All @@ -277,6 +278,7 @@ export class Transaction extends BaseTransaction {
'changeOutputs',
'fee',
...(partialPayment ? ['partialPayment'] : []),
...Object.keys(spendLimits),
],
id: this._id as string,
changeOutputs: [],
Expand All @@ -293,6 +295,7 @@ export class Transaction extends BaseTransaction {
feeRate: undefined,
},
...(partialPayment ? { partialPayment: true } : {}),
...spendLimits,
};
}

Expand Down
10 changes: 9 additions & 1 deletion modules/sdk-coin-xrp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as url from 'url';
import * as xrpl from 'xrpl';
import { Amount, IssuedCurrencyAmount, isMPTAmount, MPTAmount } from 'xrpl';
import { TF_PARTIAL_PAYMENT, VALID_ACCOUNT_SET_FLAGS } from './constants';
import { Address, SignerDetails } from './iface';
import { Address, PaymentSpendLimits, SignerDetails } from './iface';
import { KeyPair as XrpKeyPair } from './keyPair';
import assert from 'assert';

Expand Down Expand Up @@ -242,6 +242,14 @@ class Utils implements BaseUtils {
return (Number(flags) & TF_PARTIAL_PAYMENT) !== 0;
}

public getPaymentSpendLimits(tx: xrpl.Payment): PaymentSpendLimits {
return {
...(tx.SendMax !== undefined ? { sendMax: tx.SendMax } : {}),
...(tx.Paths !== undefined ? { hasPaths: true } : {}),
...(tx.DeliverMin !== undefined ? { deliverMin: tx.DeliverMin } : {}),
};
}

/**
* Extracts the numeric/string delivered amount value from XRP transaction metadata.
* `meta.delivered_amount` is an `Amount | 'unavailable'`:
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-xrp/src/xrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ export class Xrp extends BaseCoin {
const partialPayment = utils.isPartialPayment(transaction.Flags as number);
const deliveredValue = partialPayment ? utils.getDeliveredAmountValue(params.meta) : undefined;
const outputAmount = deliveredValue !== undefined ? deliveredValue : transaction.Amount;
const spendLimits = utils.getPaymentSpendLimits(transaction);
return {
displayOrder: [
'id',
Expand All @@ -362,6 +363,7 @@ export class Xrp extends BaseCoin {
'changeOutputs',
'fee',
...(partialPayment ? ['partialPayment'] : []),
...Object.keys(spendLimits),
],
id: id,
changeOutputs: [],
Expand All @@ -379,6 +381,7 @@ export class Xrp extends BaseCoin {
size: txHex.length / 2,
},
...(partialPayment ? { partialPayment: true } : {}),
...spendLimits,
};
}

Expand Down
51 changes: 50 additions & 1 deletion modules/sdk-coin-xrp/test/unit/xrp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import * as nock from 'nock';
import * as rippleBinaryCodec from 'ripple-binary-codec';
import sinon from 'sinon';
import * as xrpl from 'xrpl';
import { XrpToken } from '../../src';
import { Transaction, XrpToken } from '../../src';
import { coins } from '@bitgo/statics';
import * as testData from '../resources/xrp';
import { SIGNER_BACKUP, SIGNER_BITGO, SIGNER_USER } from '../resources/xrp';
import { getMptBuilderFactory } from './getBuilderFactory';
Expand Down Expand Up @@ -484,6 +485,54 @@ describe('XRP:', function () {
explanation.outputAmount.should.equal('0.005');
});

describe('Payment spend limits (SendMax, Paths, DeliverMin)', function () {
const crossCurrencyPayment = {
TransactionType: 'Payment',
Account: 'rBSpCz8PafXTJHppDcNnex7dYnbe3tSuFG',
Destination: 'rfjub8A4dpSD5nnszUFTsLprxu1W398jwc',
Amount: '1',
SendMax: {
currency: '524C555344000000000000000000000000000000',
issuer: 'rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV',
value: '100000',
},
Paths: [[{ currency: 'USD', issuer: 'rQhWct2fv4Vc4KRjRgMrxa8xPN9Zx9iLKV', type: 48 }]],
DeliverMin: '1',
Flags: 2147483648,
Fee: '45',
Sequence: 7,
};

it('should surface SendMax, Paths and DeliverMin in the explanation', async function () {
const explanation = await basecoin.explainTransaction({ txHex: JSON.stringify(crossCurrencyPayment) });
explanation.sendMax.should.deepEqual(crossCurrencyPayment.SendMax);
explanation.hasPaths.should.equal(true);
explanation.deliverMin.should.equal('1');
explanation.displayOrder.should.containEql('sendMax');
explanation.displayOrder.should.containEql('hasPaths');
explanation.displayOrder.should.containEql('deliverMin');
});

it('should surface the same fields from the lib-level transaction explainer', async function () {
// xrpl rejects DeliverMin without tfPartialPayment, so this payload omits it.
const paymentWithoutDeliverMin = _.omit(crossCurrencyPayment, 'DeliverMin');
const txHex = xrpl.encode(paymentWithoutDeliverMin as xrpl.Payment);
const transaction = new Transaction(coins.get('txrp'));
transaction.fromRawTransaction(txHex);
const explanation = transaction.explainTransaction();
explanation.sendMax.should.deepEqual(crossCurrencyPayment.SendMax);
explanation.hasPaths.should.equal(true);
});

it('should not add spend-limit fields to a plain payment', async function () {
const plainPayment = _.omit(crossCurrencyPayment, ['SendMax', 'Paths', 'DeliverMin']);
const explanation = await basecoin.explainTransaction({ txHex: JSON.stringify(plainPayment) });
explanation.should.not.have.property('sendMax');
explanation.should.not.have.property('hasPaths');
explanation.should.not.have.property('deliverMin');
});
});

describe('Fee Management', () => {
const nockBasecoin: any = bitgo.coin('txrp');

Expand Down
Loading