Skip to content

Commit

Permalink
Support source.amount in getPaths and destination.minAmount in prepar…
Browse files Browse the repository at this point in the history
…ePayment
  • Loading branch information
Chris Clark committed Oct 1, 2015
1 parent 8edc3b1 commit 35acbb6
Show file tree
Hide file tree
Showing 29 changed files with 641 additions and 91 deletions.
16 changes: 2 additions & 14 deletions src/api/common/schemas/adjustment.json
Expand Up @@ -4,20 +4,8 @@
"type": "object",
"properties": {
"address": {"$ref": "address"},
"amount": {
"type": "object",
"properties": {
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"value": {"$ref": "value"}
},
"required": ["currency", "value"],
"additionalProperties": false
},
"tag": {
"description": "A string representing an unsigned 32-bit integer most commonly used to refer to a sender's hosted account at a Ripple gateway",
"$ref": "uint32"
}
"amount": {"$ref": "amount"},
"tag": {"$ref": "tag"}
},
"required": ["address", "amount"],
"additionalProperties": false
Expand Down
9 changes: 9 additions & 0 deletions src/api/common/schemas/destinationAdjustment.json
@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "destinationAdjustment",
"type": "object",
"oneOf": [
{"$ref": "adjustment"},
{"$ref": "minAdjustment"}
]
}
4 changes: 2 additions & 2 deletions src/api/common/schemas/get-paths.json
Expand Up @@ -5,8 +5,8 @@
"items": {
"type": "object",
"properties": {
"source": {"$ref": "maxAdjustment"},
"destination": {"$ref": "adjustment"},
"source": {"$ref": "sourceAdjustment"},
"destination": {"$ref": "destinationAdjustment"},
"paths": {"type": "string"}
},
"required": ["source", "destination", "paths"],
Expand Down
13 changes: 13 additions & 0 deletions src/api/common/schemas/lax-amount.json
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "laxAmount",
"description": "Amount where counterparty is optional",
"type": "object",
"properties": {
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"value": {"$ref": "value"}
},
"required": ["currency", "value"],
"additionalProperties": false
}
13 changes: 13 additions & 0 deletions src/api/common/schemas/lax-lax-amount.json
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "laxLaxAmount",
"description": "Amount where counterparty and value are optional",
"type": "object",
"properties": {
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"value": {"$ref": "value"}
},
"required": ["currency"],
"additionalProperties": false
}
16 changes: 2 additions & 14 deletions src/api/common/schemas/max-adjustment.json
Expand Up @@ -4,20 +4,8 @@
"type": "object",
"properties": {
"address": {"$ref": "address"},
"maxAmount": {
"type": "object",
"properties": {
"currency": {"$ref": "currency"},
"counterparty": {"$ref": "address"},
"value": {"$ref": "value"}
},
"required": ["currency", "value"],
"additionalProperties": false
},
"tag": {
"description": "A string representing an unsigned 32-bit integer most commonly used to refer to a sender's hosted account at a Ripple gateway",
"$ref": "uint32"
}
"maxAmount": {"$ref": "laxAmount"},
"tag": {"$ref": "tag"}
},
"required": ["address", "maxAmount"],
"additionalProperties": false
Expand Down
12 changes: 12 additions & 0 deletions src/api/common/schemas/min-adjustment.json
@@ -0,0 +1,12 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "minAdjustment",
"type": "object",
"properties": {
"address": {"$ref": "address"},
"minAmount": {"$ref": "laxAmount"},
"tag": {"$ref": "tag"}
},
"required": ["address", "minAmount"],
"additionalProperties": false
}
14 changes: 13 additions & 1 deletion src/api/common/schemas/pathfind.json
Expand Up @@ -7,6 +7,7 @@
"type": "object",
"properties": {
"address": {"$ref": "address"},
"amount": {"$ref": "laxAmount"},
"currencies": {
"type": "array",
"items": {
Expand All @@ -19,12 +20,23 @@
"additionalProperties": false
},
"uniqueItems": true
},
"not": {
"required": ["amount", "currencies"]
}
},
"additionalProperties": false,
"required": ["address"]
},
"destination": {"$ref": "adjustment"}
"destination": {
"type": "object",
"properties": {
"address": {"$ref": "address"},
"amount": {"$ref": "laxLaxAmount"}
},
"required": ["address", "amount"],
"additionalProperties": false
}
},
"required": ["source", "destination"],
"additionalProperties": false
Expand Down
4 changes: 2 additions & 2 deletions src/api/common/schemas/payment.json
Expand Up @@ -3,8 +3,8 @@
"title": "payment",
"type": "object",
"properties": {
"source": {"$ref": "maxAdjustment"},
"destination": {"$ref": "adjustment"},
"source": {"$ref": "sourceAdjustment"},
"destination": {"$ref": "destinationAdjustment"},
"paths": {"type": "string"},
"memos": {
"type": "array",
Expand Down
9 changes: 9 additions & 0 deletions src/api/common/schemas/sourceAdjustment.json
@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "sourceAdjustment",
"type": "object",
"oneOf": [
{"$ref": "adjustment"},
{"$ref": "maxAdjustment"}
]
}
6 changes: 6 additions & 0 deletions src/api/common/schemas/tag.json
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "tag",
"description": "A string representing an unsigned 32-bit integer most commonly used to refer to a sender's hosted account at a Ripple gateway",
"$ref": "uint32"
}
52 changes: 36 additions & 16 deletions src/api/ledger/parse/pathfind.js
Expand Up @@ -8,22 +8,42 @@ function parsePaths(paths) {
_.omit(step, ['type', 'type_hex'])));
}

function parsePathfind(sourceAddress: string,
destinationAmount: Object, pathfindResult: Object
): Object {
return pathfindResult.alternatives.map(function(alternative) {
return {
source: {
address: sourceAddress,
maxAmount: parseAmount(alternative.source_amount)
},
destination: {
address: pathfindResult.destination_account,
amount: destinationAmount
},
paths: JSON.stringify(parsePaths(alternative.paths_computed))
};
});
function removeAnyCounterpartyEncoding(address: string, amount: Object) {
return amount.counterparty === address ?
_.omit(amount, 'counterparty') : amount;
}

function createAdjustment(address: string, adjustmentWithoutAddress: Object) {
const amountKey = _.keys(adjustmentWithoutAddress)[0];
const amount = adjustmentWithoutAddress[amountKey];
return _.set({address: address}, amountKey,
removeAnyCounterpartyEncoding(address, amount));
}

function parseAlternative(sourceAddress: string, destinationAddress: string,
destinationAmount: Object, alternative: Object
) {
// we use "maxAmount"/"minAmount" here so that the result can be passed
// directly to preparePayment
const amounts = (alternative.destination_amount !== undefined) ?
{source: {amount: parseAmount(alternative.source_amount)},
destination: {minAmount: parseAmount(alternative.destination_amount)}} :
{source: {maxAmount: parseAmount(alternative.source_amount)},
destination: {amount: parseAmount(destinationAmount)}};

return {
source: createAdjustment(sourceAddress, amounts.source),
destination: createAdjustment(destinationAddress, amounts.destination),
paths: JSON.stringify(parsePaths(alternative.paths_computed))
};
}

function parsePathfind(pathfindResult: Object): Object {
const sourceAddress = pathfindResult.source_account;
const destinationAddress = pathfindResult.destination_account;
const destinationAmount = pathfindResult.destination_amount;
return pathfindResult.alternatives.map(_.partial(parseAlternative,
sourceAddress, destinationAddress, destinationAmount));
}

module.exports = parsePathfind;
27 changes: 20 additions & 7 deletions src/api/ledger/pathfind.js
Expand Up @@ -4,15 +4,18 @@ const _ = require('lodash');
const async = require('async');
const BigNumber = require('bignumber.js');
const utils = require('./utils');
const validate = utils.common.validate;
const parsePathfind = require('./parse/pathfind');
const validate = utils.common.validate;
const NotFoundError = utils.common.errors.NotFoundError;
const ValidationError = utils.common.errors.ValidationError;
const composeAsync = utils.common.composeAsync;
const convertErrors = utils.common.convertErrors;
const toRippledAmount = utils.common.toRippledAmount;

type PathFindParams = {
src_currencies?: Array<string>, src_account: string, dst_amount: string,
dst_account?: string
src_currencies?: Array<string>, src_account: string,
dst_amount: string | Object, dst_account?: string,
src_amount?: string | Object
}

function addParams(params: PathFindParams, result: {}) {
Expand All @@ -29,10 +32,11 @@ type PathFind = {
}

function requestPathFind(remote, pathfind: PathFind, callback) {
const destinationAmount = _.assign({value: -1}, pathfind.destination.amount);
const params: PathFindParams = {
src_account: pathfind.source.address,
dst_account: pathfind.destination.address,
dst_amount: utils.common.toRippledAmount(pathfind.destination.amount)
dst_amount: toRippledAmount(destinationAmount)
};
if (typeof params.dst_amount === 'object' && !params.dst_amount.issuer) {
// Convert blank issuer to sender's address
Expand All @@ -44,7 +48,17 @@ function requestPathFind(remote, pathfind: PathFind, callback) {
}
if (pathfind.source.currencies && pathfind.source.currencies.length > 0) {
params.src_currencies = pathfind.source.currencies.map(amount =>
_.omit(utils.common.toRippledAmount(amount), 'value'));
_.omit(toRippledAmount(amount), 'value'));
}
if (pathfind.source.amount) {
if (pathfind.destination.amount.value !== undefined) {
throw new ValidationError('Cannot specify both source.amount'
+ ' and destination.amount.value in getPaths');
}
params.src_amount = toRippledAmount(pathfind.source.amount);
if (params.src_amount.currency && !params.src_amount.issuer) {
params.src_amount.issuer = pathfind.source.address;
}
}

remote.createPathFind(params,
Expand Down Expand Up @@ -81,8 +95,7 @@ function conditionallyAddDirectXRPPath(remote, address, paths, callback) {

function formatResponse(pathfind, paths) {
if (paths.alternatives && paths.alternatives.length > 0) {
const address = pathfind.source.address;
return parsePathfind(address, pathfind.destination.amount, paths);
return parsePathfind(paths);
}
if (paths.destination_currencies !== undefined &&
!_.includes(paths.destination_currencies,
Expand Down
58 changes: 46 additions & 12 deletions src/api/transaction/payment.js
Expand Up @@ -5,6 +5,7 @@ const utils = require('./utils');
const validate = utils.common.validate;
const toRippledAmount = utils.common.toRippledAmount;
const Transaction = utils.common.core.Transaction;
const ValidationError = utils.common.errors.ValidationError;

function isXRPToXRPPayment(payment) {
const sourceCurrency = _.get(payment, 'source.maxAmount.currency');
Expand All @@ -23,24 +24,49 @@ function applyAnyCounterpartyEncoding(payment) {
// https://ripple.com/build/transactions/
// #special-issuer-values-for-sendmax-and-amount
// https://ripple.com/build/ripple-rest/#counterparties-in-payments
if (isIOUWithoutCounterparty(payment.source.maxAmount)) {
payment.source.maxAmount.counterparty = payment.source.address;
}
if (isIOUWithoutCounterparty(payment.destination.amount)) {
payment.destination.amount.counterparty = payment.destination.address;
}
_.forEach([payment.source, payment.destination], (adjustment) => {
_.forEach(['amount', 'minAmount', 'maxAmount'], (key) => {
if (isIOUWithoutCounterparty(adjustment[key])) {
adjustment[key].counterparty = adjustment.address;
}
});
});
}

function createPaymentTransaction(account, payment) {
function createMaximalAmount(amount) {
const maxXRPValue = '100000000000';
const maxIOUValue = '9999999999999999e80';
const maxValue = amount.currency === 'XRP' ? maxXRPValue : maxIOUValue;
return _.assign(amount, {value: maxValue});
}

function createPaymentTransaction(account, paymentArgument) {
const payment = _.cloneDeep(paymentArgument);
applyAnyCounterpartyEncoding(payment);
validate.address(account);
validate.payment(payment);

if ((payment.source.maxAmount && payment.destination.minAmount) ||
(payment.source.amount && payment.destination.amount)) {
throw new ValidationError('payment must specify either (source.maxAmount '
+ 'and destination.amount) or (source.amount and destination.minAmount)');
}

// when using destination.minAmount, rippled still requires that we set
// a destination amount in addition to DeliverMin. the destination amount
// is interpreted as the maximum amount to send. we want to be sure to
// send the whole source amount, so we set the destination amount to the
// maximum possible amount. otherwise it's possible that the destination
// cap could be hit before the source cap.
const amount = payment.destination.minAmount && !isXRPToXRPPayment(payment) ?
createMaximalAmount(payment.destination.minAmount) :
(payment.destination.amount || payment.destination.minAmount);

const transaction = new Transaction();
transaction.payment({
from: payment.source.address,
to: payment.destination.address,
amount: toRippledAmount(payment.destination.amount)
amount: toRippledAmount(amount)
});

if (payment.invoiceID) {
Expand All @@ -57,9 +83,6 @@ function createPaymentTransaction(account, payment) {
transaction.addMemo(memo.type, memo.format, memo.data)
);
}
if (payment.allowPartialPayment) {
transaction.setFlags(['PartialPayment']);
}
if (payment.noDirectRipple) {
transaction.setFlags(['NoRippleDirect']);
}
Expand All @@ -71,11 +94,22 @@ function createPaymentTransaction(account, payment) {
// temREDUNDANT_SEND_MAX removed in:
// https://github.com/ripple/rippled/commit/
// c522ffa6db2648f1d8a987843e7feabf1a0b7de8/
transaction.sendMax(toRippledAmount(payment.source.maxAmount));
if (payment.allowPartialPayment || payment.destination.minAmount) {
transaction.setFlags(['PartialPayment']);
}

transaction.setSendMax(toRippledAmount(
payment.source.maxAmount || payment.source.amount));

if (payment.destination.minAmount) {
transaction.setDeliverMin(toRippledAmount(payment.destination.minAmount));
}

if (payment.paths) {
transaction.paths(JSON.parse(payment.paths));
}
} else if (payment.allowPartialPayment) {
throw new ValidationError('XRP to XRP payments cannot be partial payments');
}

return transaction;
Expand Down

0 comments on commit 35acbb6

Please sign in to comment.