From 35acbb62c31ee70124e55b4df6ef784e2e62ff16 Mon Sep 17 00:00:00 2001 From: Chris Clark Date: Tue, 29 Sep 2015 11:03:46 -0700 Subject: [PATCH] Support source.amount in getPaths and destination.minAmount in preparePayment --- src/api/common/schemas/adjustment.json | 16 +- .../common/schemas/destinationAdjustment.json | 9 + src/api/common/schemas/get-paths.json | 4 +- src/api/common/schemas/lax-amount.json | 13 + src/api/common/schemas/lax-lax-amount.json | 13 + src/api/common/schemas/max-adjustment.json | 16 +- src/api/common/schemas/min-adjustment.json | 12 + src/api/common/schemas/pathfind.json | 14 +- src/api/common/schemas/payment.json | 4 +- src/api/common/schemas/sourceAdjustment.json | 9 + src/api/common/schemas/tag.json | 6 + src/api/ledger/parse/pathfind.js | 52 ++- src/api/ledger/pathfind.js | 27 +- src/api/transaction/payment.js | 58 +++- src/core/pathfind.js | 7 +- src/core/remote.js | 6 +- test/api-test.js | 17 +- .../api/requests/getpaths/send-all.json | 15 + test/fixtures/api/requests/index.js | 3 +- .../requests/prepare-payment-all-options.json | 1 - .../api/responses/get-paths-send-all.json | 70 ++++ .../api/responses/get-paths-send-usd.json | 3 +- test/fixtures/api/responses/get-paths.json | 6 +- test/fixtures/api/responses/index.js | 13 +- .../prepare-payment-all-options.json | 2 +- .../responses/prepare-payment-min-amount.json | 8 + test/fixtures/api/rippled/index.js | 1 + .../api/rippled/path-find-send-all.json | 313 ++++++++++++++++++ test/mock-rippled.js | 14 +- 29 files changed, 641 insertions(+), 91 deletions(-) create mode 100644 src/api/common/schemas/destinationAdjustment.json create mode 100644 src/api/common/schemas/lax-amount.json create mode 100644 src/api/common/schemas/lax-lax-amount.json create mode 100644 src/api/common/schemas/min-adjustment.json create mode 100644 src/api/common/schemas/sourceAdjustment.json create mode 100644 src/api/common/schemas/tag.json create mode 100644 test/fixtures/api/requests/getpaths/send-all.json create mode 100644 test/fixtures/api/responses/get-paths-send-all.json create mode 100644 test/fixtures/api/responses/prepare-payment-min-amount.json create mode 100644 test/fixtures/api/rippled/path-find-send-all.json diff --git a/src/api/common/schemas/adjustment.json b/src/api/common/schemas/adjustment.json index 901c78a8..d65812f5 100644 --- a/src/api/common/schemas/adjustment.json +++ b/src/api/common/schemas/adjustment.json @@ -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 diff --git a/src/api/common/schemas/destinationAdjustment.json b/src/api/common/schemas/destinationAdjustment.json new file mode 100644 index 00000000..65f02d3e --- /dev/null +++ b/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"} + ] +} diff --git a/src/api/common/schemas/get-paths.json b/src/api/common/schemas/get-paths.json index db658c55..199dc8a0 100644 --- a/src/api/common/schemas/get-paths.json +++ b/src/api/common/schemas/get-paths.json @@ -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"], diff --git a/src/api/common/schemas/lax-amount.json b/src/api/common/schemas/lax-amount.json new file mode 100644 index 00000000..51a3eeef --- /dev/null +++ b/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 +} diff --git a/src/api/common/schemas/lax-lax-amount.json b/src/api/common/schemas/lax-lax-amount.json new file mode 100644 index 00000000..0b27a766 --- /dev/null +++ b/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 +} diff --git a/src/api/common/schemas/max-adjustment.json b/src/api/common/schemas/max-adjustment.json index b4a9eb1e..e63adb53 100644 --- a/src/api/common/schemas/max-adjustment.json +++ b/src/api/common/schemas/max-adjustment.json @@ -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 diff --git a/src/api/common/schemas/min-adjustment.json b/src/api/common/schemas/min-adjustment.json new file mode 100644 index 00000000..ef34447c --- /dev/null +++ b/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 +} diff --git a/src/api/common/schemas/pathfind.json b/src/api/common/schemas/pathfind.json index 2302f2c7..4a37f6e2 100644 --- a/src/api/common/schemas/pathfind.json +++ b/src/api/common/schemas/pathfind.json @@ -7,6 +7,7 @@ "type": "object", "properties": { "address": {"$ref": "address"}, + "amount": {"$ref": "laxAmount"}, "currencies": { "type": "array", "items": { @@ -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 diff --git a/src/api/common/schemas/payment.json b/src/api/common/schemas/payment.json index fa7b3842..40ec6480 100644 --- a/src/api/common/schemas/payment.json +++ b/src/api/common/schemas/payment.json @@ -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", diff --git a/src/api/common/schemas/sourceAdjustment.json b/src/api/common/schemas/sourceAdjustment.json new file mode 100644 index 00000000..77c4806c --- /dev/null +++ b/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"} + ] +} diff --git a/src/api/common/schemas/tag.json b/src/api/common/schemas/tag.json new file mode 100644 index 00000000..55028889 --- /dev/null +++ b/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" +} diff --git a/src/api/ledger/parse/pathfind.js b/src/api/ledger/parse/pathfind.js index 39456d3c..0e996e8e 100644 --- a/src/api/ledger/parse/pathfind.js +++ b/src/api/ledger/parse/pathfind.js @@ -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; diff --git a/src/api/ledger/pathfind.js b/src/api/ledger/pathfind.js index 0af7f769..29a1e793 100644 --- a/src/api/ledger/pathfind.js +++ b/src/api/ledger/pathfind.js @@ -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, src_account: string, dst_amount: string, - dst_account?: string + src_currencies?: Array, src_account: string, + dst_amount: string | Object, dst_account?: string, + src_amount?: string | Object } function addParams(params: PathFindParams, result: {}) { @@ -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 @@ -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, @@ -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, diff --git a/src/api/transaction/payment.js b/src/api/transaction/payment.js index 123f09ed..6103d907 100644 --- a/src/api/transaction/payment.js +++ b/src/api/transaction/payment.js @@ -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'); @@ -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) { @@ -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']); } @@ -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; diff --git a/src/core/pathfind.js b/src/core/pathfind.js index 8a3359c7..1d646f51 100644 --- a/src/core/pathfind.js +++ b/src/core/pathfind.js @@ -11,7 +11,8 @@ const Amount = require('./amount').Amount; * the 'end' and 'superceded' events. */ -function PathFind(remote, src_account, dst_account, dst_amount, src_currencies +function PathFind(remote, src_account, dst_account, dst_amount, + src_currencies, src_amount ) { EventEmitter.call(this); @@ -21,6 +22,7 @@ function PathFind(remote, src_account, dst_account, dst_amount, src_currencies this.dst_account = dst_account; this.dst_amount = dst_amount; this.src_currencies = src_currencies; + this.src_amount = src_amount; } util.inherits(PathFind, EventEmitter); @@ -42,7 +44,8 @@ PathFind.prototype.create = function() { source_account: this.src_account, destination_account: this.dst_account, destination_amount: this.dst_amount, - source_currencies: this.src_currencies + source_currencies: this.src_currencies, + send_max: this.src_amount }); req.once('error', function(err) { diff --git a/src/core/remote.js b/src/core/remote.js index af4d73cf..7268c7ad 100644 --- a/src/core/remote.js +++ b/src/core/remote.js @@ -1824,7 +1824,7 @@ Remote.prototype.createPathFind = function(options, callback) { const pathFind = new PathFind(this, options.src_account, options.dst_account, - options.dst_amount, options.src_currencies); + options.dst_amount, options.src_currencies, options.src_amount); if (this._cur_path_find) { this._cur_path_find.notify_superceded(); @@ -2172,6 +2172,10 @@ Remote.prototype.requestPathFindCreate = function(options, callback) { options.source_currencies.map(Remote.prepareCurrency); } + if (options.send_max) { + request.message.send_max = Amount.json_rewrite(options.send_max); + } + request.callback(callback); return request; }; diff --git a/test/api-test.js b/test/api-test.js index 0be3f414..16a8fc6a 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -48,7 +48,7 @@ describe('RippleAPI', function() { }, instructions); return this.api.preparePayment( address, requests.preparePayment, localInstructions).then( - _.partial(checkResult, responses.preparePayment, 'prepare')); + _.partial(checkResult, responses.preparePayment.normal, 'prepare')); }); it('preparePayment with all options specified', function() { @@ -59,7 +59,7 @@ describe('RippleAPI', function() { }; return this.api.preparePayment( address, requests.preparePaymentAllOptions, localInstructions).then( - _.partial(checkResult, responses.preparePaymentAllOptions, 'prepare')); + _.partial(checkResult, responses.preparePayment.allOptions, 'prepare')); }); }); @@ -67,10 +67,16 @@ describe('RippleAPI', function() { const localInstructions = _.defaults({sequence: 23}, instructions); return this.api.preparePayment( address, requests.preparePaymentNoCounterparty, localInstructions).then( - _.partial(checkResult, responses.preparePaymentNoCounterparty, + _.partial(checkResult, responses.preparePayment.noCounterparty, 'prepare')); }); + it('preparePayment - destination.minAmount', function() { + return this.api.preparePayment(address, responses.getPaths.sendAll[0], + instructions).then(_.partial(checkResult, + responses.preparePayment.minAmount, 'prepare')); + }); + it('prepareOrder - buy order', function() { return this.api.prepareOrder(address, requests.prepareOrder, instructions) .then(_.partial(checkResult, responses.prepareOrder, 'prepare')); @@ -639,6 +645,11 @@ describe('RippleAPI', function() { }); }); + it('getPaths - send all', function() { + return this.api.getPaths(requests.getPaths.sendAll).then( + _.partial(checkResult, responses.getPaths.sendAll, 'getPaths')); + }); + it('getLedgerVersion', function(done) { this.api.getLedgerVersion().then((ver) => { assert.strictEqual(ver, 8819951); diff --git a/test/fixtures/api/requests/getpaths/send-all.json b/test/fixtures/api/requests/getpaths/send-all.json new file mode 100644 index 00000000..e0bd51f9 --- /dev/null +++ b/test/fixtures/api/requests/getpaths/send-all.json @@ -0,0 +1,15 @@ +{ + "source": { + "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "amount": { + "currency": "USD", + "value": "5" + } + }, + "destination": { + "address": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "amount": { + "currency": "USD" + } + } +} diff --git a/test/fixtures/api/requests/index.js b/test/fixtures/api/requests/index.js index 6ef8dc86..5660b449 100644 --- a/test/fixtures/api/requests/index.js +++ b/test/fixtures/api/requests/index.js @@ -25,7 +25,8 @@ module.exports = { XrpToXrpNotEnough: require('./getpaths/xrp2xrp-not-enough'), NotAcceptCurrency: require('./getpaths/not-accept-currency'), NoPaths: require('./getpaths/no-paths'), - NoPathsWithCurrencies: require('./getpaths/no-paths-with-currencies') + NoPathsWithCurrencies: require('./getpaths/no-paths-with-currencies'), + sendAll: require('./getpaths/send-all') }, computeLedgerHash: { header: require('./compute-ledger-hash'), diff --git a/test/fixtures/api/requests/prepare-payment-all-options.json b/test/fixtures/api/requests/prepare-payment-all-options.json index 4cc232fd..007c7f3b 100644 --- a/test/fixtures/api/requests/prepare-payment-all-options.json +++ b/test/fixtures/api/requests/prepare-payment-all-options.json @@ -23,7 +23,6 @@ } ], "invoiceID": "A98FD36C17BE2B8511AD36DC335478E7E89F06262949F36EB88E2D683BBCC50A", - "allowPartialPayment": true, "noDirectRipple": true, "limitQuality": true, "paths": "[[{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\",\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\",\"type\":49,\"type_hex\":\"0000000000000031\"},{\"currency\":\"LTC\",\"issuer\":\"rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX\",\"type\":48,\"type_hex\":\"0000000000000030\"},{\"account\":\"rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX\",\"currency\":\"LTC\",\"issuer\":\"rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX\",\"type\":49,\"type_hex\":\"0000000000000031\"}]]" diff --git a/test/fixtures/api/responses/get-paths-send-all.json b/test/fixtures/api/responses/get-paths-send-all.json new file mode 100644 index 00000000..19a869c6 --- /dev/null +++ b/test/fixtures/api/responses/get-paths-send-all.json @@ -0,0 +1,70 @@ +[ + { + "source": { + "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "amount": { + "currency": "USD", + "value": "5" + } + }, + "destination": { + "address": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "minAmount": { + "currency": "USD", + "value": "4.93463759481038" + } + }, + "paths": "[[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"currency\":\"USD\",\"issuer\":\"rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP\"},{\"account\":\"rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"}]]" + }, + { + "source": { + "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "amount": { + "currency": "USD", + "value": "5" + } + }, + "destination": { + "address": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "minAmount": { + "currency": "USD", + "value": "4.93463759481038" + } + }, + "paths": "[[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"EUR\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"}],[{\"account\":\"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun\"},{\"currency\":\"USD\",\"issuer\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"}]]" + }, + { + "source": { + "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "amount": { + "currency": "USD", + "value": "5" + } + }, + "destination": { + "address": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "minAmount": { + "currency": "USD", + "value": "4.93463759481038" + } + }, + "paths": "[[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"currency\":\"USD\",\"issuer\":\"rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP\"},{\"account\":\"rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"JPY\",\"issuer\":\"rMAz5ZnK73nyNUL4foAvaxdreczCkG3vA6\"},{\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"}]]" + }, + { + "source": { + "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "amount": { + "currency": "USD", + "value": "5" + } + }, + "destination": { + "address": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "minAmount": { + "currency": "USD", + "value": "4.990019960079841" + } + }, + "paths": "[[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"account\":\"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"USD\",\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"}]]" + } +] diff --git a/test/fixtures/api/responses/get-paths-send-usd.json b/test/fixtures/api/responses/get-paths-send-usd.json index d2ba1250..f4d9c50d 100644 --- a/test/fixtures/api/responses/get-paths-send-usd.json +++ b/test/fixtures/api/responses/get-paths-send-usd.json @@ -4,8 +4,7 @@ "address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo", "maxAmount": { "currency": "USD", - "value": "0.000001002", - "counterparty": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo" + "value": "0.000001002" } }, "destination": { diff --git a/test/fixtures/api/responses/get-paths.json b/test/fixtures/api/responses/get-paths.json index 00259882..610bb149 100644 --- a/test/fixtures/api/responses/get-paths.json +++ b/test/fixtures/api/responses/get-paths.json @@ -4,8 +4,7 @@ "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", "maxAmount": { "currency": "JPY", - "value": "0.1117218827811721", - "counterparty": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" + "value": "0.1117218827811721" } }, "destination": { @@ -23,8 +22,7 @@ "address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", "maxAmount": { "currency": "USD", - "value": "0.001002", - "counterparty": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59" + "value": "0.001002" } }, "destination": { diff --git a/test/fixtures/api/responses/index.js b/test/fixtures/api/responses/index.js index 064826d3..5080660a 100644 --- a/test/fixtures/api/responses/index.js +++ b/test/fixtures/api/responses/index.js @@ -9,7 +9,8 @@ module.exports = { getPaths: { XrpToUsd: require('./get-paths.json'), UsdToUsd: require('./get-paths-send-usd.json'), - XrpToXrp: require('./get-paths-xrp-to-xrp.json') + XrpToXrp: require('./get-paths-xrp-to-xrp.json'), + sendAll: require('./get-paths-send-all.json') }, getServerInfo: require('./get-server-info.json'), getSettings: require('./get-settings.json'), @@ -35,10 +36,12 @@ module.exports = { prepareOrderCancellation: require('./prepare-order-cancellation.json'), prepareOrder: require('./prepare-order.json'), prepareOrderSell: require('./prepare-order-sell.json'), - preparePayment: require('./prepare-payment.json'), - preparePaymentAllOptions: require('./prepare-payment-all-options.json'), - preparePaymentNoCounterparty: - require('./prepare-payment-no-counterparty.json'), + preparePayment: { + normal: require('./prepare-payment.json'), + allOptions: require('./prepare-payment-all-options.json'), + noCounterparty: require('./prepare-payment-no-counterparty.json'), + minAmount: require('./prepare-payment-min-amount.json') + }, prepareSettings: { regularKey: require('./prepare-settings-regular-key.json'), flags: require('./prepare-settings.json'), diff --git a/test/fixtures/api/responses/prepare-payment-all-options.json b/test/fixtures/api/responses/prepare-payment-all-options.json index 8fa93a15..abd6f768 100644 --- a/test/fixtures/api/responses/prepare-payment-all-options.json +++ b/test/fixtures/api/responses/prepare-payment-all-options.json @@ -1,5 +1,5 @@ { - "txJSON": "{\"Flags\":458752,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":\"10000\",\"InvoiceID\":\"A98FD36C17BE2B8511AD36DC335478E7E89F06262949F36EB88E2D683BBCC50A\",\"SourceTag\":14,\"DestinationTag\":58,\"Memos\":[{\"Memo\":{\"MemoType\":\"74657374\",\"MemoFormat\":\"706C61696E2F74657874\",\"MemoData\":\"7465787465642064617461\"}}],\"LastLedgerSequence\":8820051,\"Fee\":\"12\",\"Sequence\":23}", + "txJSON": "{\"Flags\":327680,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":\"10000\",\"InvoiceID\":\"A98FD36C17BE2B8511AD36DC335478E7E89F06262949F36EB88E2D683BBCC50A\",\"SourceTag\":14,\"DestinationTag\":58,\"Memos\":[{\"Memo\":{\"MemoType\":\"74657374\",\"MemoFormat\":\"706C61696E2F74657874\",\"MemoData\":\"7465787465642064617461\"}}],\"LastLedgerSequence\":8820051,\"Fee\":\"12\",\"Sequence\":23}", "instructions": { "fee": "12", "sequence": 23, diff --git a/test/fixtures/api/responses/prepare-payment-min-amount.json b/test/fixtures/api/responses/prepare-payment-min-amount.json new file mode 100644 index 00000000..e0fc2000 --- /dev/null +++ b/test/fixtures/api/responses/prepare-payment-min-amount.json @@ -0,0 +1,8 @@ +{ + "txJSON": "{\"Flags\":131072,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX\",\"Amount\":{\"value\":\"9999999999999999e80\",\"currency\":\"USD\",\"issuer\":\"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX\"},\"SendMax\":{\"value\":\"5\",\"currency\":\"USD\",\"issuer\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\"},\"DeliverMin\":{\"value\":\"9999999999999999e80\",\"currency\":\"USD\",\"issuer\":\"ra5nK24KXen9AHvsdFTKHSANinZseWnPcX\"},\"Paths\":[[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"issuer\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\",\"currency\":\"USD\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"},{\"account\":\"rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn\"}],[{\"account\":\"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B\"},{\"currency\":\"XRP\"},{\"issuer\":\"rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP\",\"currency\":\"USD\"},{\"account\":\"rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP\"},{\"account\":\"rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q\"}]],\"LastLedgerSequence\":8820051,\"Fee\":\"12\",\"Sequence\":23}", + "instructions": { + "fee": "12", + "sequence": 23, + "maxLedgerVersion": 8820051 + } +} diff --git a/test/fixtures/api/rippled/index.js b/test/fixtures/api/rippled/index.js index fc7d44b4..fdac6f4e 100644 --- a/test/fixtures/api/rippled/index.js +++ b/test/fixtures/api/rippled/index.js @@ -22,6 +22,7 @@ module.exports = { path_find: { generate: require('./path-find'), sendUSD: require('./path-find-send-usd'), + sendAll: require('./path-find-send-all'), XrpToXrp: require('./path-find-xrp-to-xrp'), srcActNotFound: require('./path-find-srcActNotFound') }, diff --git a/test/fixtures/api/rippled/path-find-send-all.json b/test/fixtures/api/rippled/path-find-send-all.json new file mode 100644 index 00000000..1e7317cd --- /dev/null +++ b/test/fixtures/api/rippled/path-find-send-all.json @@ -0,0 +1,313 @@ +{ + "alternatives": [ + { + "destination_amount": { + "currency": "USD", + "issuer": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "value": "4.93463759481038" + }, + "paths_computed": [ + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + }, + { + "currency": "USD", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + }, + { + "currency": "USD", + "issuer": "rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + } + ] + ], + "source_amount": { + "currency": "USD", + "issuer": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "value": "5" + } + }, + { + "destination_amount": { + "currency": "USD", + "issuer": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "value": "4.93463759481038" + }, + "paths_computed": [ + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + }, + { + "currency": "USD", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "EUR", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "currency": "USD", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + } + ] + ], + "source_amount": { + "currency": "USD", + "issuer": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "value": "5" + } + }, + { + "destination_amount": { + "currency": "USD", + "issuer": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "value": "4.93463759481038" + }, + "paths_computed": [ + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + }, + { + "currency": "USD", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "XRP", + "type": 16, + "type_hex": "0000000000000010" + }, + { + "currency": "USD", + "issuer": "rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rfsEoNBUBbvkf4jPcFe2u9CyaQagLVHGfP", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "JPY", + "issuer": "rMAz5ZnK73nyNUL4foAvaxdreczCkG3vA6", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "currency": "USD", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + } + ] + ], + "source_amount": { + "currency": "USD", + "issuer": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "value": "5" + } + }, + { + "destination_amount": { + "currency": "USD", + "issuer": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "value": "4.990019960079841" + }, + "paths_computed": [ + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "type": 1, + "type_hex": "0000000000000001" + } + ], + [ + { + "account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "type": 1, + "type_hex": "0000000000000001" + }, + { + "currency": "USD", + "issuer": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 48, + "type_hex": "0000000000000030" + }, + { + "account": "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q", + "type": 1, + "type_hex": "0000000000000001" + } + ] + ], + "source_amount": { + "currency": "USD", + "issuer": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "value": "5" + } + } + ], + "destination_account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "destination_amount": { + "currency": "USD", + "issuer": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "value": "-1" + }, + "full_reply": true, + "id": 1, + "source_account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "type": "path_find" +} diff --git a/test/mock-rippled.js b/test/mock-rippled.js index e76f94ae..3db74eb6 100644 --- a/test/mock-rippled.js +++ b/test/mock-rippled.js @@ -266,10 +266,18 @@ module.exports = function(port) { destination_amount: request.destination_amount, destination_address: request.destination_address }); + } else if (request.source_account === addresses.ACCOUNT) { + if (request.destination_account === + 'ra5nK24KXen9AHvsdFTKHSANinZseWnPcX') { + response = createResponse(request, fixtures.path_find.sendAll); + } else { + response = fixtures.path_find.generate.generateIOUPaymentPaths( + request.id, request.source_account, request.destination_account, + request.destination_amount); + } } else { - response = fixtures.path_find.generate.generateIOUPaymentPaths( - request.id, request.source_account, request.destination_account, - request.destination_amount); + assert(false, 'Unrecognized path find request: ' + + JSON.stringify(request)); } // delay response to simulate calculation time so we can test queuing setTimeout(() => conn.send(response), 20);