Merge branch 'develop' into release

This commit is contained in:
Geert Weening
2015-10-05 15:04:18 -07:00
63 changed files with 1425 additions and 370 deletions

24
npm-shrinkwrap.json generated
View File

@@ -117,12 +117,12 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.2.tgz"
},
"ripple-address-codec": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-1.6.0.tgz",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-2.0.1.tgz",
"dependencies": {
"x-address-codec": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/x-address-codec/-/x-address-codec-0.6.0.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/x-address-codec/-/x-address-codec-0.7.0.tgz",
"dependencies": {
"base-x": {
"version": "1.0.1",
@@ -149,6 +149,22 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
},
"ripple-address-codec": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-1.6.0.tgz",
"dependencies": {
"x-address-codec": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/x-address-codec/-/x-address-codec-0.6.0.tgz",
"dependencies": {
"base-x": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-1.0.1.tgz"
}
}
}
}
}
}
},

View File

@@ -26,7 +26,7 @@
"is-my-json-valid": "^2.12.2",
"lodash": "^3.1.0",
"lru-cache": "~2.5.0",
"ripple-address-codec": "^1.6.0",
"ripple-address-codec": "^2.0.1",
"ripple-keypairs": "^0.9.0",
"ripple-lib-transactionparser": "^0.5.1",
"ripple-lib-value": "0.1.0",

View File

@@ -31,6 +31,8 @@ function loadSchemas() {
require('./schemas/currency.json'),
require('./schemas/get-account-info.json'),
require('./schemas/get-balances.json'),
require('./schemas/get-balance-sheet'),
require('./schemas/balance-sheet-options.json'),
require('./schemas/get-ledger.json'),
require('./schemas/get-orderbook.json'),
require('./schemas/get-orders.json'),

View File

@@ -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

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "balance-sheet-options",
"description": "Options for getBalanceSheet",
"type": "object",
"properties": {
"excludeAddresses": {
"type": "array",
"items": {"$ref": "address"},
"uniqueItems": true
},
"ledgerVersion": {"$ref": "ledgerVersion"}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "destinationAdjustment",
"type": "object",
"oneOf": [
{"$ref": "adjustment"},
{"$ref": "minAdjustment"}
]
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "getBalanceSheet",
"description": "getBalanceSheet response",
"type": "object",
"properties": {
"balances": {
"type": "array",
"items": {"$ref": "amount"}
},
"assets": {
"type": "array",
"items": {"$ref": "amount"}
},
"obligations": {
"type": "array",
"items": {
"type": "object",
"required": ["currency", "value"],
"additionalProperties": false,
"properties": {
"currency": {"$ref": "currency"},
"value": {"$ref": "value"}
}
}
}
},
"additionalProperties": false
}

View File

@@ -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"],

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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",

View File

@@ -0,0 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "sourceAdjustment",
"type": "object",
"oneOf": [
{"$ref": "adjustment"},
{"$ref": "maxAdjustment"}
]
}

View File

@@ -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"
}

View File

@@ -71,7 +71,8 @@ function composeAsync(wrapper: Wrapper, callback: Callback): Callback {
function convertErrors(callback: Callback): () => void {
return function(error, data) {
if (error && !(error instanceof errors.RippleError)) {
const error_ = new errors.RippleError(error);
const message = _.get(error, ['remote', 'error_message'], error.message);
const error_ = new errors.RippleError(message);
error_.data = data;
callback(error_, data);
} else if (error) {

View File

@@ -82,6 +82,7 @@ module.exports = {
getAccountInfoOptions: _.partial(validateOptions, 'settings-options'),
getTrustlinesOptions: _.partial(validateOptions, 'trustlines-options'),
getBalancesOptions: _.partial(validateOptions, 'trustlines-options'),
getBalanceSheetOptions: _.partial(validateOptions, 'balance-sheet-options'),
getOrdersOptions: _.partial(validateOptions, 'orders-options'),
getOrderbookOptions: _.partial(validateOptions, 'orders-options'),
getTransactionOptions: _.partial(validateOptions, 'transaction-options'),

View File

@@ -16,6 +16,7 @@ const getTransaction = require('./ledger/transaction');
const getTransactions = require('./ledger/transactions');
const getTrustlines = require('./ledger/trustlines');
const getBalances = require('./ledger/balances');
const getBalanceSheet = require('./ledger/balance-sheet');
const getPaths = require('./ledger/pathfind');
const getOrders = require('./ledger/orders');
const getOrderbook = require('./ledger/orderbook');
@@ -66,6 +67,7 @@ _.assign(RippleAPI.prototype, {
getTransactions,
getTrustlines,
getBalances,
getBalanceSheet,
getPaths,
getOrders,
getOrderbook,

View File

@@ -0,0 +1,68 @@
'use strict';
const _ = require('lodash');
const utils = require('./utils');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
const convertErrors = utils.common.convertErrors;
function formatBalanceSheet(balanceSheet) {
const result = {};
if (!_.isUndefined(balanceSheet.balances)) {
result.balances = [];
_.forEach(balanceSheet.balances, (balances, counterparty) => {
_.forEach(balances, (balance) => {
result.balances.push(Object.assign({counterparty}, balance));
});
});
}
if (!_.isUndefined(balanceSheet.assets)) {
result.assets = [];
_.forEach(balanceSheet.assets, (assets, counterparty) => {
_.forEach(assets, (balance) => {
result.assets.push(Object.assign({counterparty}, balance));
});
});
}
if (!_.isUndefined(balanceSheet.obligations)) {
result.obligations = _.map(balanceSheet.obligations, (value, currency) =>
({currency, value}));
}
return result;
}
function getBalanceSheetAsync(address, options, callback) {
validate.address(address);
validate.getBalanceSheetOptions(options);
const requestOptions = Object.assign({}, {
account: address,
strict: true,
hotwallet: options.excludeAddresses,
ledger: options.ledgerVersion
});
const requestCallback = composeAsync(
formatBalanceSheet, convertErrors(callback));
this.remote.getLedgerSequence((err, ledgerVersion) => {
if (err) {
callback(err);
return;
}
if (_.isUndefined(requestOptions.ledger)) {
requestOptions.ledger = ledgerVersion;
}
this.remote.requestGatewayBalances(requestOptions, requestCallback);
});
}
function getBalanceSheet(address: string, options = {}) {
return utils.promisify(getBalanceSheetAsync).call(this, address, options);
}
module.exports = getBalanceSheet;

View File

@@ -7,6 +7,9 @@ const getTrustlines = require('./trustlines');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
const convertErrors = utils.common.convertErrors;
import type {Remote} from '../../core/remote';
import type {GetLedgerSequenceCallback} from '../../core/remote';
function getTrustlineBalanceAmount(trustline) {
return {
@@ -31,14 +34,25 @@ function getTrustlinesAsync(account, options, callback) {
.catch(callback);
}
function getLedgerVersionHelper(remote: Remote, optionValue?: number,
callback: GetLedgerSequenceCallback
) {
if (optionValue !== undefined && optionValue !== null) {
callback(null, optionValue);
} else {
remote.getLedgerSequence(callback);
}
}
function getBalancesAsync(account, options, callback) {
validate.address(account);
validate.getBalancesOptions(options);
const ledgerVersion = options.ledgerVersion
|| this.remote.getLedgerSequence();
async.parallel({
xrp: _.partial(utils.getXRPBalance, this.remote, account, ledgerVersion),
xrp: async.seq(
_.partial(getLedgerVersionHelper, this.remote, options.ledgerVersion),
_.partial(utils.getXRPBalance, this.remote, account)
),
trustlines: _.partial(getTrustlinesAsync.bind(this), account, options)
}, composeAsync(formatBalances, convertErrors(callback)));
}

View File

@@ -1,6 +1,7 @@
/* @flow */
'use strict';
const _ = require('lodash');
const async = require('async');
const utils = require('./utils');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
@@ -26,17 +27,17 @@ function getOrdersAsync(account, options, callback) {
validate.address(account);
validate.getOrdersOptions(options);
const ledgerVersion = options.ledgerVersion
|| this.remote.getLedgerSequence();
const getter = _.partial(requestAccountOffers, this.remote, account,
ledgerVersion);
options.ledgerVersion);
utils.getRecursive(getter, options.limit,
composeAsync((orders) => _.sortBy(orders,
(order) => order.properties.sequence), callback));
}
function getOrders(account: string, options = {}) {
return utils.promisify(getOrdersAsync).call(this, account, options);
return utils.promisify(async.seq(
utils.getLedgerOptionsWithLedgerVersion,
getOrdersAsync)).call(this, account, options);
}
module.exports = getOrders;

View File

@@ -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;

View File

@@ -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: {}) {
@@ -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,

View File

@@ -52,10 +52,10 @@ function getTransactionAsync(identifier: string, options: TransactionOptions,
validate.getTransactionOptions(options);
const remote = this.remote;
const maxLedgerVersion =
options.maxLedgerVersion || remote.getLedgerSequence();
function callbackWrapper(error_?: Error, tx?: Object) {
function callbackWrapper(error_?: Error, tx?: Object,
maxLedgerVersion?: number
) {
let error = error_;
if (!error && tx && tx.validated !== true) {
@@ -89,11 +89,19 @@ function getTransactionAsync(identifier: string, options: TransactionOptions,
}
}
function maxLedgerGetter(error_?: Error, tx?: Object) {
this.getLedgerVersion().then((version) => {
const maxLedgerVersion = options.maxLedgerVersion || version;
callbackWrapper(error_, tx, maxLedgerVersion);
}, callbackWrapper);
}
async.waterfall([
_.partial(remote.requestTx.bind(remote),
{hash: identifier, binary: false}),
_.partial(attachTransactionDate, remote)
], callbackWrapper);
], maxLedgerGetter.bind(this));
}
function getTransaction(identifier: string,

View File

@@ -70,8 +70,10 @@ function formatPartialResponse(address, options, data) {
function getAccountTx(remote, address, options, marker, limit, callback) {
const params = {
account: address,
// -1 is equivalent to earliest available validated ledger
ledger_index_min: options.minLedgerVersion || -1,
ledger_index_max: options.maxLedgerVersion || remote.getLedgerSequence(),
// -1 is equivalent to most recent available validated ledger
ledger_index_max: options.maxLedgerVersion || -1,
forward: options.earliestFirst,
binary: options.binary,
limit: utils.clamp(limit, 10, 400),
@@ -121,7 +123,7 @@ function getTransactionsAsync(account, options, callback) {
validate.address(account);
validate.getTransactionsOptions(options);
const defaults = {maxLedgerVersion: this.remote.getLedgerSequence()};
const defaults = {maxLedgerVersion: -1};
if (options.start) {
getTransaction.call(this, options.start).then(tx => {
const ledgerVersion = tx.outcome.ledgerVersion;

View File

@@ -1,6 +1,7 @@
/* @flow */
'use strict';
const _ = require('lodash');
const async = require('async');
const utils = require('./utils');
const validate = utils.common.validate;
const composeAsync = utils.common.composeAsync;
@@ -42,15 +43,15 @@ function getTrustlinesAsync(account: string, options: {currency: string,
validate.address(account);
validate.getTrustlinesOptions(options);
const ledgerVersion = options.ledgerVersion
|| this.remote.getLedgerSequence();
const getter = _.partial(getAccountLines, this.remote, account,
ledgerVersion, options);
options.ledgerVersion, options);
utils.getRecursive(getter, options.limit, callback);
}
function getTrustlines(account: string, options = {}) {
return utils.promisify(getTrustlinesAsync).call(this, account, options);
return utils.promisify(async.seq(
utils.getLedgerOptionsWithLedgerVersion,
getTrustlinesAsync)).call(this, account, options);
}
module.exports = getTrustlines;

View File

@@ -103,17 +103,32 @@ function hasCompleteLedgerRange(remote: Remote, minLedgerVersion?: number,
const firstLedgerVersion = 32570; // earlier versions have been lost
return remote.getServer().hasLedgerRange(
minLedgerVersion || firstLedgerVersion,
maxLedgerVersion || remote.getLedgerSequence());
maxLedgerVersion || remote.getLedgerSequenceSync());
}
function isPendingLedgerVersion(remote: Remote, maxLedgerVersion: ?number
): boolean {
const currentLedger = remote.getLedgerSequence();
const currentLedger = remote.getLedgerSequenceSync();
return currentLedger < (maxLedgerVersion || 0);
}
function getLedgerOptionsWithLedgerVersion(account: string, options: Object,
callback: (err?: ?Error, account?: string, options: Object) => void
) {
if (Boolean(options) && options.ledgerVersion !== undefined &&
options.ledgerVersion !== null
) {
callback(null, account, options);
} else {
this.getLedgerVersion().then((version) => {
callback(null, account, _.assign({}, options, {ledgerVersion: version}));
}, callback);
}
}
module.exports = {
getXRPBalance,
getLedgerOptionsWithLedgerVersion,
compareTransactions,
renameCounterpartyToIssuer,
renameCounterpartyToIssuerInOrder,

View File

@@ -53,8 +53,7 @@ function getServerInfoAsync(
): void {
this.remote.requestServerInfo((error, response) => {
if (error) {
const message =
_.get(error, ['remote', 'error_message'], error.message);
const message = _.get(error, ['remote', 'error_message'], error.message);
callback(new common.errors.RippledNetworkError(message));
} else {
callback(null,
@@ -63,28 +62,40 @@ function getServerInfoAsync(
});
}
function getFee(): number {
return common.dropsToXrp(this.remote.createTransaction()._computeFee());
function getFee(): ?number {
if (!this.remote.getConnectedServers().length) {
throw new common.errors.RippledNetworkError('No servers available.');
}
const fee = this.remote.createTransaction()._computeFee();
return fee === undefined ? undefined : common.dropsToXrp(fee);
}
function getLedgerVersion(): number {
return this.remote.getLedgerSequence();
function getLedgerVersion(): Promise<number> {
return common.promisify(this.remote.getLedgerSequence).call(this.remote);
}
function connect(): Promise<void> {
return common.promisify(callback => {
this.remote.connect(() => callback(null));
try {
this.remote.connect(() => callback(null));
} catch(error) {
callback(new common.errors.RippledNetworkError(error.message));
}
})();
}
function disconnect(): Promise<void> {
return common.promisify(callback => {
this.remote.disconnect(() => callback(null));
try {
this.remote.disconnect(() => callback(null));
} catch(error) {
callback(new common.errors.RippledNetworkError(error.message));
}
})();
}
function getServerInfo(): Promise<GetServerInfoResponse> {
return common.promisify(getServerInfoAsync.bind(this))();
return common.promisify(getServerInfoAsync).call(this);
}
function rippleTimeToISO8601(rippleTime: string): string {

View File

@@ -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;

View File

@@ -1,8 +1,10 @@
/* @flow */
'use strict';
const _ = require('lodash');
const async = require('async');
const BigNumber = require('bignumber.js');
const common = require('../common');
const composeAsync = common.composeAsync;
function setTransactionBitFlags(transaction: any, values: any, flags: any
): void {
@@ -19,9 +21,11 @@ function setTransactionBitFlags(transaction: any, values: any, flags: any
}
}
function getFeeDrops(remote) {
function getFeeDrops(remote, callback) {
const feeUnits = 10; // all transactions currently have a fee of 10 fee units
return remote.feeTx(feeUnits).to_text();
remote.feeTxAsync(feeUnits, (err, data) => {
callback(err, data ? data.to_text() : undefined);
});
}
function formatPrepareResponse(txJSON) {
@@ -39,42 +43,64 @@ function formatPrepareResponse(txJSON) {
type Callback = (err: ?(typeof Error),
data: {txJSON: string, instructions: any}) => void;
function prepareTransaction(transaction: any, remote: any, instructions: any,
callback: Callback): void {
callback: Callback
): void {
common.validate.instructions(instructions);
transaction.complete();
const account = transaction.getAccount();
const txJSON = transaction.tx_json;
if (instructions.maxLedgerVersion !== undefined) {
txJSON.LastLedgerSequence = parseInt(instructions.maxLedgerVersion, 10);
} else {
const offset = instructions.maxLedgerVersionOffset !== undefined ?
parseInt(instructions.maxLedgerVersionOffset, 10) : 3;
txJSON.LastLedgerSequence = remote.getLedgerSequence() + offset;
}
if (instructions.fee !== undefined) {
txJSON.Fee = common.xrpToDrops(instructions.fee);
} else {
const serverFeeDrops = getFeeDrops(remote);
if (instructions.maxFee !== undefined) {
const maxFeeDrops = common.xrpToDrops(instructions.maxFee);
txJSON.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString();
function prepareMaxLedgerVersion(callback_) {
if (instructions.maxLedgerVersion !== undefined) {
txJSON.LastLedgerSequence = parseInt(instructions.maxLedgerVersion, 10);
callback_();
} else {
txJSON.Fee = serverFeeDrops;
const offset = instructions.maxLedgerVersionOffset !== undefined ?
parseInt(instructions.maxLedgerVersionOffset, 10) : 3;
remote.getLedgerSequence((error, ledgerVersion) => {
txJSON.LastLedgerSequence = ledgerVersion + offset;
callback_(error);
});
}
}
if (instructions.sequence !== undefined) {
txJSON.Sequence = parseInt(instructions.sequence, 10);
callback(null, formatPrepareResponse(txJSON));
} else {
remote.findAccount(account).getNextSequence(function(error, sequence) {
txJSON.Sequence = sequence;
callback(error, formatPrepareResponse(txJSON));
});
function prepareFee(callback_) {
if (instructions.fee !== undefined) {
txJSON.Fee = common.xrpToDrops(instructions.fee);
callback_();
} else {
getFeeDrops(remote, composeAsync((serverFeeDrops) => {
if (instructions.maxFee !== undefined) {
const maxFeeDrops = common.xrpToDrops(instructions.maxFee);
txJSON.Fee = BigNumber.min(serverFeeDrops, maxFeeDrops).toString();
} else {
txJSON.Fee = serverFeeDrops;
}
}, callback_));
}
}
function prepareSequence(callback_) {
if (instructions.sequence !== undefined) {
txJSON.Sequence = parseInt(instructions.sequence, 10);
callback_(null, formatPrepareResponse(txJSON));
} else {
remote.findAccount(account).getNextSequence(function(error, sequence) {
txJSON.Sequence = sequence;
callback_(error, formatPrepareResponse(txJSON));
});
}
}
async.series([
prepareMaxLedgerVersion,
prepareFee,
prepareSequence
], common.convertErrors(function(error, results) {
callback(error, results && results[2]);
}));
}
module.exports = {

View File

@@ -17,7 +17,7 @@ const util = require('util');
const {deriveAddress} = require('ripple-keypairs');
const {EventEmitter} = require('events');
const {TransactionManager} = require('./transactionmanager');
const {UInt160} = require('./uint160');
const {isValidAddress} = require('ripple-address-codec');
/**
* @constructor Account
@@ -25,14 +25,13 @@ const {UInt160} = require('./uint160');
* @param {String} account
*/
function Account(remote, account) {
function Account(remote, address) {
EventEmitter.call(this);
const self = this;
this._remote = remote;
this._account = UInt160.from_json(account);
this._account_id = this._account.to_json();
this._address = address;
this._subs = 0;
// Ledger entry object
@@ -43,7 +42,7 @@ function Account(remote, account) {
if (_.includes(Account.subscribeEvents, type)) {
if (!self._subs && self._remote._connected) {
self._remote.requestSubscribe()
.addAccount(self._account_id)
.addAccount(self._address)
.broadcast().request();
}
self._subs += 1;
@@ -57,7 +56,7 @@ function Account(remote, account) {
self._subs -= 1;
if (!self._subs && self._remote._connected) {
self._remote.requestUnsubscribe()
.addAccount(self._account_id)
.addAccount(self._address)
.broadcast().request();
}
}
@@ -66,8 +65,8 @@ function Account(remote, account) {
this.on('removeListener', listenerRemoved);
function attachAccount(request) {
if (self._account.is_valid() && self._subs) {
request.addAccount(self._account_id);
if (isValidAddress(self._address) && self._subs) {
request.addAccount(self._address);
}
}
@@ -81,7 +80,7 @@ function Account(remote, account) {
let changed = false;
transaction.mmeta.each(function(an) {
const isAccount = an.fields.Account === self._account_id;
const isAccount = an.fields.Account === self._address;
const isAccountRoot = isAccount && (an.entryType === 'AccountRoot');
if (isAccountRoot) {
@@ -111,7 +110,7 @@ util.inherits(Account, EventEmitter);
Account.subscribeEvents = ['transaction', 'entry'];
Account.prototype.toJson = function() {
return this._account.to_json();
return this._address;
};
/**
@@ -121,7 +120,7 @@ Account.prototype.toJson = function() {
*/
Account.prototype.isValid = function() {
return this._account.is_valid();
return isValidAddress(this._address);
};
/**
@@ -131,7 +130,7 @@ Account.prototype.isValid = function() {
*/
Account.prototype.getInfo = function(callback) {
return this._remote.requestAccountInfo({account: this._account_id}, callback);
return this._remote.requestAccountInfo({account: this._address}, callback);
};
/**
@@ -210,7 +209,7 @@ Account.prototype.lines = function(callback_) {
}
}
this._remote.requestAccountLines({account: this._account_id}, accountLines);
this._remote.requestAccountLines({account: this._address}, accountLines);
return this;
};
@@ -275,7 +274,7 @@ Account.prototype.notifyTx = function(transaction) {
return;
}
const isThisAccount = (account === this._account_id);
const isThisAccount = (account === this._address);
this.emit(isThisAccount ? 'transaction-outbound' : 'transaction-inbound',
transaction);
@@ -331,7 +330,7 @@ Account.prototype.publicKeyIsActive = function(public_key, callback) {
// Catch the case of unfunded accounts
if (!account_info_res) {
if (public_key_as_uint160 === self._account_id) {
if (public_key_as_uint160 === self._address) {
async_callback(null, true);
} else {
async_callback(null, false);
@@ -373,20 +372,17 @@ Account.prototype.publicKeyIsActive = function(public_key, callback) {
* @returns {RippleAddress} Ripple Address
*/
Account._publicKeyToAddress = function(public_key) {
// Based on functions in /src/js/ripple/keypair.js
function hexToUInt160(publicKey) {
return deriveAddress(publicKey);
}
if (UInt160.is_valid(public_key)) {
if (isValidAddress(public_key)) {
return public_key;
} else if (/^[0-9a-fA-F]+$/.test(public_key)) {
return hexToUInt160(public_key);
return deriveAddress(public_key);
} else { // eslint-disable-line no-else-return
throw new Error('Public key is invalid. Must be a UInt160 or a hex string');
}
};
exports.Account = Account;
module.exports = {
Account
};
// vim:sw=2:sts=2:ts=8:et

View File

@@ -6,9 +6,10 @@
const assert = require('assert');
const extend = require('extend');
const utils = require('./utils');
const UInt160 = require('./uint160').UInt160;
const Currency = require('./currency').Currency;
const {XRPValue, IOUValue} = require('ripple-lib-value');
const {isValidAddress} = require('ripple-address-codec');
const {ACCOUNT_ONE, ACCOUNT_ZERO} = require('./constants');
type Value = XRPValue | IOUValue;
@@ -21,7 +22,7 @@ function Amount(value = new XRPValue(NaN)) {
this._value = value;
this._is_native = true; // Default to XRP. Only valid if value is not NaN.
this._currency = new Currency();
this._issuer = new UInt160();
this._issuer = 'NaN';
}
/**
@@ -105,7 +106,7 @@ Amount.NaN = function() {
};
Amount.from_components_unsafe = function(value: Value, currency: Currency,
issuer: UInt160, isNative: boolean
issuer: string, isNative: boolean
) {
const result = new Amount(value);
result._is_native = isNative;
@@ -119,18 +120,14 @@ Amount.from_components_unsafe = function(value: Value, currency: Currency,
// be sure that _is_native is set properly BEFORE calling _set_value
Amount.prototype._set_value = function(value: Value) {
this._value = value.isZero() && value.isNegative() ?
value.negate() : value;
this._check_limits();
};
// Returns a new value which is the absolute value of this.
Amount.prototype.abs = function() {
return this._copy(this._value.abs());
};
Amount.prototype.add = function(addend) {
@@ -228,7 +225,7 @@ Amount.prototype.ratio_human = function(denom, opts) {
//
// To compensate, we multiply the numerator by 10^xns_precision.
if (denominator._is_native) {
numerator._set_value(numerator.multiply(bi_xns_unit));
numerator._set_value(numerator._value.multiply(bi_xns_unit));
}
return numerator.divide(denominator);
@@ -402,7 +399,7 @@ Amount.prototype.equals = function(d, ignore_issuer) {
&& this._is_native === d._is_native
&& this._value.equals(d._value)
&& (this._is_native || (this._currency.equals(d._currency)
&& (ignore_issuer || this._issuer.equals(d._issuer))));
&& (ignore_issuer || this._issuer === d._issuer)));
};
// True if Amounts are valid and both native or non-native.
@@ -428,9 +425,8 @@ Amount.prototype.is_valid = function() {
};
Amount.prototype.is_valid_full = function() {
return this.is_valid()
&& this._currency.is_valid()
&& this._issuer.is_valid();
return this.is_valid() && this._currency.is_valid()
&& isValidAddress(this._issuer) && this._issuer !== ACCOUNT_ZERO;
};
Amount.prototype.is_zero = function() {
@@ -532,7 +528,7 @@ Amount.prototype.parse_human = function(j, options) {
};
Amount.prototype.parse_issuer = function(issuer) {
this._issuer = UInt160.from_json(issuer);
this._issuer = issuer;
return this;
};
@@ -583,7 +579,7 @@ function(quality, counterCurrency, counterIssuer, opts) {
const offset = parseInt(offset_hex, 16) - 100;
this._currency = Currency.from_json(counterCurrency);
this._issuer = UInt160.from_json(counterIssuer);
this._issuer = counterIssuer;
this._is_native = this._currency.is_native();
if (this._is_native && baseCurrency.is_native()) {
@@ -643,7 +639,7 @@ function(quality, counterCurrency, counterIssuer, opts) {
Amount.prototype.parse_number = function(n) {
this._is_native = false;
this._currency = Currency.from_json(1);
this._issuer = UInt160.from_json(1);
this._issuer = ACCOUNT_ONE;
this._set_value(new IOUValue(n));
return this;
};
@@ -659,15 +655,15 @@ Amount.prototype.parse_json = function(j) {
if (m) {
this._currency = Currency.from_json(m[2]);
if (m[3]) {
this._issuer = UInt160.from_json(m[3]);
this._issuer = m[3];
} else {
this._issuer = UInt160.from_json('1');
this._issuer = 'NaN';
}
this.parse_value(m[1]);
} else {
this.parse_native(j);
this._currency = Currency.from_json('0');
this._issuer = UInt160.from_json('0');
this._issuer = ACCOUNT_ZERO;
}
break;
@@ -686,9 +682,10 @@ Amount.prototype.parse_json = function(j) {
// Parse the passed value to sanitize and copy it.
this._currency.parse_json(j.currency, true); // Never XRP.
if (typeof j.issuer === 'string') {
this._issuer.parse_json(j.issuer);
if (typeof j.issuer !== 'string') {
throw new Error('issuer must be a string');
}
this._issuer = j.issuer;
this.parse_value(j.value);
}
@@ -736,12 +733,7 @@ Amount.prototype.set_currency = function(c) {
};
Amount.prototype.set_issuer = function(issuer) {
if (issuer instanceof UInt160) {
this._issuer = issuer;
} else {
this._issuer = UInt160.from_json(issuer);
}
this._issuer = issuer;
return this;
};
@@ -939,7 +931,7 @@ Amount.prototype.to_human_full = function(options) {
const opts = options || {};
const value = this.to_human(opts);
const currency = this._currency.to_human();
const issuer = this._issuer.to_json(opts);
const issuer = this._issuer;
const base = value + '/' + currency;
return this.is_native() ? base : (base + '/' + issuer);
};
@@ -955,21 +947,21 @@ Amount.prototype.to_json = function() {
this._currency.to_hex() : this._currency.to_json()
};
if (this._issuer.is_valid()) {
amount_json.issuer = this._issuer.to_json();
if (isValidAddress(this._issuer)) {
amount_json.issuer = this._issuer;
}
return amount_json;
};
Amount.prototype.to_text_full = function(opts) {
Amount.prototype.to_text_full = function() {
if (!this.is_valid()) {
return 'NaN';
}
return this._is_native
? this.to_human() + '/XRP'
: this.to_text() + '/' + this._currency.to_json()
+ '/' + this._issuer.to_json(opts);
+ '/' + this._issuer;
};
// For debugging.
@@ -998,11 +990,8 @@ Amount.prototype.not_equals_why = function(d, ignore_issuer) {
if (!this._currency.equals(d._currency)) {
return 'Non-XRP currency differs.';
}
if (!ignore_issuer && !this._issuer.equals(d._issuer)) {
return 'Non-XRP issuer differs: '
+ d._issuer.to_json()
+ '/'
+ this._issuer.to_json();
if (!ignore_issuer && this._issuer !== d._issuer) {
return 'Non-XRP issuer differs: ' + d._issuer + '/' + this._issuer;
}
}
};

6
src/core/constants.js Normal file
View File

@@ -0,0 +1,6 @@
'use strict';
module.exports = {
ACCOUNT_ZERO: 'rrrrrrrrrrrrrrrrrrrrrhoLvTp',
ACCOUNT_ONE: 'rrrrrrrrrrrrrrrrrrrrBZbvji'
};

View File

@@ -1,7 +1,10 @@
var extend = require('extend');
var utils = require('./utils');
var UInt160 = require('./uint160').UInt160;
var Amount = require('./amount').Amount;
'use strict';
const extend = require('extend');
const utils = require('./utils');
const UInt160 = require('./uint160').UInt160;
const Amount = require('./amount').Amount;
const ACCOUNT_ZERO = require('./constants').ACCOUNT_ZERO;
const {isValidAddress} = require('ripple-address-codec');
/**
* Meta data processing facility
@@ -11,8 +14,6 @@ var Amount = require('./amount').Amount;
*/
function Meta(data) {
var self = this;
this.nodes = [ ];
if (typeof data !== 'object') {
@@ -24,7 +25,7 @@ function Meta(data) {
}
data.AffectedNodes.forEach(this.addNode, this);
};
}
Meta.NODE_TYPES = [
'CreatedNode',
@@ -53,10 +54,10 @@ Meta.ACCOUNT_FIELDS = [
*/
Meta.prototype.getNodeType = function(node) {
var result = null;
let result = null;
for (var i=0; i<Meta.NODE_TYPES.length; i++) {
var type = Meta.NODE_TYPES[i];
for (let i = 0; i < Meta.NODE_TYPES.length; i++) {
const type = Meta.NODE_TYPES[i];
if (node.hasOwnProperty(type)) {
result = type;
break;
@@ -83,20 +84,22 @@ Meta.prototype.isAccountField = function(field) {
*/
Meta.prototype.addNode = function(node) {
this._affectedAccounts = void(0);
this._affectedBooks = void(0);
this._affectedAccounts = undefined;
this._affectedBooks = undefined;
var result = { };
const result = { };
if ((result.nodeType = this.getNodeType(node))) {
node = node[result.nodeType];
result.diffType = result.nodeType;
result.entryType = node.LedgerEntryType;
result.ledgerIndex = node.LedgerIndex;
result.fields = extend({ }, node.PreviousFields, node.NewFields, node.FinalFields);
result.fieldsPrev = node.PreviousFields || { };
result.fieldsNew = node.NewFields || { };
result.fieldsFinal = node.FinalFields || { };
result.nodeType = this.getNodeType(node);
if (result.nodeType) {
const _node = node[result.nodeType];
result.diffType = result.nodeType;
result.entryType = _node.LedgerEntryType;
result.ledgerIndex = _node.LedgerIndex;
result.fields = extend({ }, _node.PreviousFields,
_node.NewFields, _node.FinalFields);
result.fieldsPrev = _node.PreviousFields || { };
result.fieldsNew = _node.NewFields || { };
result.fieldsFinal = _node.FinalFields || { };
// getAffectedBooks will set this
// result.bookKey = undefined;
@@ -126,36 +129,36 @@ Meta.prototype.getNodes = function(options) {
}
return true;
});
} else {
return this.nodes;
}
return this.nodes;
};
Meta.prototype.getAffectedAccounts = function(from) {
Meta.prototype.getAffectedAccounts = function() {
if (this._affectedAccounts) {
return this._affectedAccounts;
}
var accounts = [ ];
const accounts = [ ];
// This code should match the behavior of the C++ method:
// TransactionMetaSet::getAffectedAccounts
for (var i=0; i<this.nodes.length; i++) {
var node = this.nodes[i];
var fields = (node.nodeType === 'CreatedNode')
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
const fields = (node.nodeType === 'CreatedNode')
? node.fieldsNew
: node.fieldsFinal;
for (var fieldName in fields) {
var field = fields[fieldName];
for (const fieldName in fields) {
const field = fields[fieldName];
if (this.isAccountField(fieldName) && UInt160.is_valid(field)) {
accounts.push(field);
} else if (~Meta.AMOUNT_FIELDS_AFFECTING_ISSUER.indexOf(fieldName)) {
var amount = Amount.from_json(field);
var issuer = amount.issuer();
if (issuer.is_valid() && !issuer.is_zero()) {
accounts.push(issuer.to_json());
} else if (
Meta.AMOUNT_FIELDS_AFFECTING_ISSUER.indexOf(fieldName) !== -1) {
const amount = Amount.from_json(field);
const issuer = amount.issuer();
if (isValidAddress(issuer) && issuer !== ACCOUNT_ZERO) {
accounts.push(issuer);
}
}
}
@@ -171,29 +174,29 @@ Meta.prototype.getAffectedBooks = function() {
return this._affectedBooks;
}
var books = [ ];
const books = [ ];
for (var i=0; i<this.nodes.length; i++) {
var node = this.nodes[i];
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
if (node.entryType !== 'Offer') {
continue;
}
var gets = Amount.from_json(node.fields.TakerGets);
var pays = Amount.from_json(node.fields.TakerPays);
var getsKey = gets.currency().to_json();
var paysKey = pays.currency().to_json();
const gets = Amount.from_json(node.fields.TakerGets);
const pays = Amount.from_json(node.fields.TakerPays);
let getsKey = gets.currency().to_json();
let paysKey = pays.currency().to_json();
if (getsKey !== 'XRP') {
getsKey += '/' + gets.issuer().to_json();
getsKey += '/' + gets.issuer();
}
if (paysKey !== 'XRP') {
paysKey += '/' + pays.issuer().to_json();
paysKey += '/' + pays.issuer();
}
var key = getsKey + ':' + paysKey;
const key = getsKey + ':' + paysKey;
// Hell of a lot of work, so we are going to cache this. We can use this
// later to good effect in OrderBook.notify to make sure we only process
@@ -243,12 +246,12 @@ Meta.prototype.getAffectedBooks = function() {
*/
[
'forEach',
'map',
'filter',
'every',
'some',
'reduce'
'forEach',
'map',
'filter',
'every',
'some',
'reduce'
].forEach(function(fn) {
Meta.prototype[fn] = function() {
return Array.prototype[fn].apply(this.nodes, arguments);

View File

@@ -846,7 +846,7 @@ OrderBook.prototype.onTransaction = function(transaction) {
if (--this._transactionsLeft === 0 && !this._waitingForOffers) {
const lastClosedLedger = this._remote.getLedgerSequence();
const lastClosedLedger = this._remote.getLedgerSequenceSync();
if (this._isAutobridgeable) {
if (this._canRunAutobridgeCalc()) {
if (this._legOneBook._lastUpdateLedgerSequence === lastClosedLedger ||
@@ -1080,7 +1080,7 @@ OrderBook.prototype.notify = function(transaction) {
this.emit('transaction', transaction);
this._lastUpdateLedgerSequence = this._remote.getLedgerSequence();
this._lastUpdateLedgerSequence = this._remote.getLedgerSequenceSync();
if (!takerGetsTotal.is_zero()) {
this.emit('trade', takerPaysTotal, takerGetsTotal);

View File

@@ -29,8 +29,7 @@ function createAmount(value, currency_, counterparty_) {
Currency.from_json(currency_);
const counterparty = counterparty_ instanceof UInt160 ?
counterparty_ :
UInt160.from_json(counterparty_);
counterparty_.to_json() : counterparty_;
return Amount.from_components_unsafe(new IOUValue(value),
currency, counterparty, false);

View File

@@ -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) {

View File

@@ -36,6 +36,8 @@ const utils = require('./utils');
const hashprefixes = require('./hashprefixes');
const log = require('./log').internal.sub('remote');
export type GetLedgerSequenceCallback = (err?: ?Error, index?: number) => void;
/**
* Interface to manage connections to rippled servers
*
@@ -518,7 +520,34 @@ Remote.prototype._handleMessage = function(message, server) {
}
};
Remote.prototype.getLedgerSequence = function() {
/**
*
* @param {Function} [callback]
* @api public
*/
Remote.prototype.getLedgerSequence = function(callback = function() {}) {
if (!this._servers.length) {
callback(new Error('No servers available.'));
return;
}
if (_.isFinite(this._ledger_current_index)) {
// the "current" ledger is the one after the most recently closed ledger
callback(null, this._ledger_current_index - 1);
} else {
this.once('ledger_closed', () => {
callback(null, this._ledger_current_index - 1);
});
}
};
/**
*
* @api private
*/
Remote.prototype.getLedgerSequenceSync = function(): number {
if (!this._ledger_current_index) {
throw new Error('Ledger sequence has not yet been initialized');
}
@@ -1795,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();
@@ -1803,9 +1832,20 @@ Remote.prototype.createPathFind = function(options, callback) {
if (callback) {
pathFind.on('update', (data) => {
if (data.full_reply) {
pathFind.close();
if (data.full_reply && !data.closed) {
this._cur_path_find = null;
callback(null, data);
// "A client can only have one pathfinding request open at a time.
// If another pathfinding request is already open on the same
// connection, the old request is automatically closed and replaced
// with the new request."
// - ripple.com/build/rippled-apis/#path-find-create
if (this._queued_path_finds.length > 0) {
const pathfind = this._queued_path_finds.shift();
this.createPathFind(pathfind.options, pathfind.callback);
} else {
pathFind.close();
}
}
});
pathFind.on('error', callback);
@@ -2132,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;
};
@@ -2145,19 +2189,8 @@ Remote.prototype.requestPathFindCreate = function(options, callback) {
Remote.prototype.requestPathFindClose = function(callback) {
const request = new Request(this, 'path_find');
request.message.subcommand = 'close';
request.callback((error, data) => {
this._cur_path_find = null;
if (this._queued_path_finds.length > 0) {
const pathfind = this._queued_path_finds.shift();
this.createPathFind(pathfind.options, pathfind.callback);
}
if (callback) {
callback(error, data);
}
});
request.callback(callback);
return request;
};
@@ -2247,6 +2280,29 @@ Remote.prototype.requestConnect = function(ip, port, callback) {
return request;
};
Remote.prototype.requestGatewayBalances = function(options, callback) {
assert(_.isObject(options), 'Options missing');
assert(options.account, 'Account missing');
const request = new Request(this, 'gateway_balances');
request.message.account = UInt160.json_rewrite(options.account);
if (!_.isUndefined(options.hotwallet)) {
request.message.hotwallet = options.hotwallet;
}
if (!_.isUndefined(options.strict)) {
request.message.strict = options.strict;
}
if (!_.isUndefined(options.ledger)) {
request.selectLedger(options.ledger);
}
request.callback(callback);
return request;
};
/**
* Create a Transaction
*
@@ -2307,6 +2363,32 @@ Remote.prototype.feeTx = function(units) {
return server._feeTx(units);
};
/**
* Same as feeTx, but will wait to connect to server if currently
* disconnected.
*
* @param {Number} fee units
* @param {Function} callback
*/
Remote.prototype.feeTxAsync = function(units, callback) {
if (!this._servers.length) {
callback(new Error('No servers available.'));
return;
}
let server = this.getServer();
if (!server) {
this.once('connected', () => {
server = this.getServer();
callback(null, server._feeTx(units));
});
} else {
callback(null, server._feeTx(units));
}
};
/**
* Get the current recommended transaction fee unit.
*

View File

@@ -34,23 +34,29 @@ function Request(remote, command) {
command: command,
id: undefined
};
this._timeout = this.remote.submission_timeout;
}
util.inherits(Request, EventEmitter);
// Send the request to a remote.
Request.prototype.request = function(servers, callback_) {
const self = this;
const callback = typeof servers === 'function' ? servers : callback_;
const self = this;
if (this.requested) {
throw new Error('Already requested');
}
this.emit('before');
this.callback(callback);
// emit handler can set requested flag
if (this.requested) {
return this;
}
this.requested = true;
this.callback(callback);
this.on('error', function() {});
this.emit('request', this.remote);
@@ -65,17 +71,25 @@ Request.prototype.request = function(servers, callback_) {
}
}
function onReconnect() {
doRequest();
}
const timeout = setTimeout(() => {
if (typeof callback === 'function') {
callback(new RippleError('tejTimeout'));
}
this.emit('timeout');
// just in case
this.emit = _.noop;
this.cancel();
}, this._timeout);
function onResponse() {
self.remote.removeListener('connected', onReconnect);
clearTimeout(timeout);
}
if (this.remote.isConnected()) {
this.remote.on('connected', onReconnect);
this.remote.on('connected', doRequest);
}
this.once('response', onResponse);
doRequest();
@@ -259,38 +273,11 @@ Request.prototype.callback = function(callback, successEvent, errorEvent) {
return this;
};
Request.prototype.timeout = function(duration, callback) {
const self = this;
function requested() {
self.timeout(duration, callback);
Request.prototype.setTimeout = function(delay) {
if (!_.isFinite(delay)) {
throw new Error('delay must be number');
}
if (!this.requested) {
// Defer until requested
return this.once('request', requested);
}
const emit = this.emit;
let timed_out = false;
const timeout = setTimeout(function() {
timed_out = true;
if (typeof callback === 'function') {
callback();
}
emit.call(self, 'timeout');
self.cancel();
}, duration);
this.emit = function() {
if (!timed_out) {
clearTimeout(timeout);
emit.apply(self, arguments);
}
};
this._timeout = delay;
return this;
};

View File

@@ -1,30 +1,42 @@
var util = require('util');
var extend = require('extend');
'use strict';
function RippleError(code, message) {
switch (typeof code) {
case 'object':
extend(this, code);
break;
const util = require('util');
const _ = require('lodash');
case 'string':
this.result = code;
this.result_message = message;
break;
function RippleError(code?: any, message?: string) {
if (code instanceof Error) {
this.result = code;
this.result_message = code.message;
} else {
switch (typeof code) {
case 'object':
_.extend(this, code);
break;
case 'string':
this.result = code;
this.result_message = message;
break;
}
}
this.engine_result = this.result = (this.result || this.engine_result || this.error || 'Error');
this.engine_result_message = this.result_message = (this.result_message || this.engine_result_message || this.error_message || 'Error');
this.result_message = this.message = (this.result_message);
this.engine_result = this.result = (this.result || this.engine_result ||
this.error || 'Error');
this.engine_result_message = this.result_message = (this.result_message ||
this.engine_result_message || this.error_message || 'Error');
this.message = this.result_message;
var stack;
let stack;
if (!!Error.captureStackTrace) {
if (Boolean(Error.captureStackTrace)) {
Error.captureStackTrace(this, code || this);
} else if ((stack = new Error().stack)) {
this.stack = stack;
} else {
stack = new Error().stack;
if (Boolean(stack)) {
this.stack = stack;
}
}
};
}
util.inherits(RippleError, Error);

View File

@@ -100,7 +100,8 @@ SerializedType.serialize_varint = function(so, val) {
SerializedType.prototype.parse_varint = function(so) {
const b1 = so.read(1)[0];
let b2, b3;
let b2;
let b3;
let result;
if (b1 > 254) {
@@ -416,7 +417,8 @@ exports.Quality = new SerializedType({
value = new BigNumber(val);
}
let hi = 0, lo = 0;
let hi = 0;
let lo = 0;
const offset = value.e - 15;
if (val !== 0) {
@@ -483,7 +485,8 @@ const STAmount = exports.Amount = new SerializedType({
valueBytes[0] |= 0x40;
}
} else {
let hi = 0, lo = 0;
let hi = 0;
let lo = 0;
// First bit: non-native
hi |= 1 << 31;
@@ -518,7 +521,7 @@ const STAmount = exports.Amount = new SerializedType({
STCurrency.serialize(so, currency, true);
// Issuer (160-bit hash)
so.append(amount.issuer().to_bytes());
so.append(UInt160.from_json(amount.issuer()).to_bytes());
}
},
parse: function(so) {
@@ -834,7 +837,7 @@ exports.STMemo = new SerializedType({
output.parsed_memo_data = convertHexToString(output.MemoData);
}
/* eslint-disable no-empty */
} catch(e) {
} catch (e) {
// empty
// we'll fail in case the content does not match what the MemoFormat
// described

View File

@@ -14,6 +14,7 @@ const SerializedObject = require('./serializedobject').SerializedObject;
const RippleError = require('./rippleerror').RippleError;
const hashprefixes = require('./hashprefixes');
const log = require('./log').internal.sub('transaction');
const {isValidAddress} = require('ripple-address-codec');
/**
* @constructor Transaction
@@ -580,7 +581,7 @@ Transaction.prototype.setLastLedgerSequence = function(sequence) {
assert(this.remote, 'Unable to set LastLedgerSequence, missing Remote');
this._setUInt32('LastLedgerSequence',
this.remote.getLedgerSequence() + 1
this.remote.getLedgerSequenceSync() + 1
+ this.getLastLedgerSequenceOffset());
}
@@ -722,7 +723,7 @@ Transaction.prototype._setAmount = function(name, amount, options_) {
if (!(isNative || parsedAmount.currency().is_valid())) {
throw new Error(name + ' must have a valid currency');
}
if (!(isNative || parsedAmount.issuer().is_valid())) {
if (!(isNative || isValidAddress(parsedAmount.issuer()))) {
throw new Error(name + ' must have a valid issuer');
}

View File

@@ -21,7 +21,7 @@ function TransactionManager(account) {
const self = this;
this._account = account;
this._accountID = account._account_id;
this._accountID = account._address;
this._remote = account._remote;
this._nextSequence = undefined;
this._maxFee = this._remote.max_fee;
@@ -681,7 +681,7 @@ TransactionManager.prototype._request = function(tx) {
}
}
tx.submitIndex = this._remote.getLedgerSequence() + 1;
tx.submitIndex = this._remote.getLedgerSequenceSync() + 1;
if (tx.attempts === 0) {
tx.initialSubmitIndex = tx.submitIndex;
@@ -698,7 +698,8 @@ TransactionManager.prototype._request = function(tx) {
tx.emit('postsubmit');
submitRequest.timeout(self._submissionTimeout, requestTimeout);
submitRequest.setTimeout(self._submissionTimeout);
submitRequest.once('timeout', requestTimeout);
};
/**

View File

@@ -1048,6 +1048,17 @@ describe('Amount', function() {
});
describe('ratio_human', function() {
it('Divide USD by XRP', function() {
const a = Amount.from_json({
value: '0.08161672093323858',
currency: 'USD',
issuer: 'rLFPPebckMYZf3urdomLsaqRGmQ6zHVrrK'
});
const b = Amount.from_json('15000000');
const c = a.ratio_human(b);
assert.deepEqual(c.to_json(), {value: '0.005441114728882572',
currency: 'USD', issuer: 'rLFPPebckMYZf3urdomLsaqRGmQ6zHVrrK'});
});
it('Divide USD by XAU (dem)', function() {
assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh');
});

View File

@@ -29,7 +29,6 @@ const orderbook = {
};
function checkResult(expected, schemaName, response) {
// console.log(JSON.stringify(response, null, 2));
assert.deepEqual(response, expected);
if (schemaName) {
schemaValidator.schemaValidate(schemaName, response);
@@ -48,27 +47,35 @@ 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() {
const localInstructions = {
maxLedgerVersion: this.api.getLedgerVersion() + 100,
fee: '0.000012'
};
return this.api.preparePayment(
address, requests.preparePaymentAllOptions, localInstructions).then(
_.partial(checkResult, responses.preparePaymentAllOptions, 'prepare'));
return this.api.getLedgerVersion().then((ver) => {
const localInstructions = {
maxLedgerVersion: ver + 100,
fee: '0.000012'
};
return this.api.preparePayment(
address, requests.preparePaymentAllOptions, localInstructions).then(
_.partial(checkResult, responses.preparePayment.allOptions, 'prepare'));
});
});
it('preparePayment without counterparty set', 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'));
@@ -194,6 +201,11 @@ describe('RippleAPI', function() {
_.partial(checkResult, responses.getBalances, 'getBalances'));
});
it('getBalanceSheet', function() {
return this.api.getBalanceSheet(address).then(
_.partial(checkResult, responses.getBalanceSheet, 'getBalanceSheet'));
});
describe('getTransaction', () => {
it('getTransaction - payment', function() {
return this.api.getTransaction(hashes.VALID_TRANSACTION_HASH).then(
@@ -570,6 +582,18 @@ describe('RippleAPI', function() {
_.partial(checkResult, responses.getPaths.XrpToUsd, 'getPaths'));
});
it('getPaths - queuing', function() {
return Promise.all([
this.api.getPaths(requests.getPaths.normal),
this.api.getPaths(requests.getPaths.UsdToUsd),
this.api.getPaths(requests.getPaths.XrpToXrp)
]).then(results => {
checkResult(responses.getPaths.XrpToUsd, 'getPaths', results[0]);
checkResult(responses.getPaths.UsdToUsd, 'getPaths', results[1]);
checkResult(responses.getPaths.XrpToXrp, 'getPaths', results[2]);
});
});
// @TODO
// need decide what to do with currencies/XRP:
// if add 'XRP' in currencies, then there will be exception in
@@ -625,8 +649,16 @@ describe('RippleAPI', function() {
});
});
it('getLedgerVersion', function() {
assert.strictEqual(this.api.getLedgerVersion(), 8819951);
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);
done();
}, done);
});
it('getLedger', function() {

View File

@@ -0,0 +1,15 @@
{
"source": {
"address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
"amount": {
"currency": "USD",
"value": "5"
}
},
"destination": {
"address": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"amount": {
"currency": "USD"
}
}
}

View File

@@ -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'),

View File

@@ -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\"}]]"

View File

@@ -0,0 +1,54 @@
{
"balances": [
{
"counterparty": "rKm4uWpg9tfwbVSeATv4KxDe6mpE9yPkgJ",
"currency": "EUR",
"value": "29826.1965999999"
},
{
"counterparty": "rKm4uWpg9tfwbVSeATv4KxDe6mpE9yPkgJ",
"currency": "USD",
"value": "10.0"
},
{
"counterparty": "ra7JkEzrgeKHdzKgo4EUUVBnxggY4z37kt",
"currency": "USD",
"value": "13857.70416"
}
],
"assets": [
{
"counterparty": "r9F6wk8HkXrgYWoJ7fsv4VrUBVoqDVtzkH",
"currency": "BTC",
"value": "5444166510000000e-26"
},
{
"counterparty": "r9F6wk8HkXrgYWoJ7fsv4VrUBVoqDVtzkH",
"currency": "USD",
"value": "100.0"
},
{
"counterparty": "rwmUaXsWtXU4Z843xSYwgt1is97bgY8yj6",
"currency": "BTC",
"value": "8700000000000000e-30"
}
],
"obligations": [
{
"currency": "BTC",
"value": "5908.324927635318"
},
{
"currency": "EUR",
"value": "992471.7419793958"
},
{
"currency": "GBP",
"value": "4991.38706013193"
},
{
"currency": "USD",
"value": "1997134.20229482"
}
]
}

View File

@@ -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\"}]]"
}
]

View File

@@ -4,8 +4,7 @@
"address": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
"maxAmount": {
"currency": "USD",
"value": "0.000001002",
"counterparty": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo"
"value": "0.000001002"
}
},
"destination": {

View File

@@ -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": {

View File

@@ -4,12 +4,14 @@ module.exports = {
generateAddress: require('./generate-address.json'),
getAccountInfo: require('./get-account-info.json'),
getBalances: require('./get-balances.json'),
getBalanceSheet: require('./get-balance-sheet.json'),
getOrderbook: require('./get-orderbook.json'),
getOrders: require('./get-orders.json'),
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 +37,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'),

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -0,0 +1,52 @@
{
"id": 0,
"status": "success",
"type": "response",
"result": {
"account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
"assets": {
"r9F6wk8HkXrgYWoJ7fsv4VrUBVoqDVtzkH": [
{
"currency": "BTC",
"value": "5444166510000000e-26"
},
{
"currency": "USD",
"value": "100.0"
}
],
"rwmUaXsWtXU4Z843xSYwgt1is97bgY8yj6": [
{
"currency": "BTC",
"value": "8700000000000000e-30"
}
]
},
"balances": {
"rKm4uWpg9tfwbVSeATv4KxDe6mpE9yPkgJ": [
{
"currency": "EUR",
"value": "29826.1965999999"
},
{
"currency": "USD",
"value": "10.0"
}
],
"ra7JkEzrgeKHdzKgo4EUUVBnxggY4z37kt": [
{
"currency": "USD",
"value": "13857.70416"
}
]
},
"obligations": {
"BTC": "5908.324927635318",
"EUR": "992471.7419793958",
"GBP": "4991.38706013193",
"USD": "1997134.20229482"
},
"ledger_current_index": 9592219,
"validated": true
}
}

View File

@@ -16,12 +16,14 @@ module.exports = {
},
account_offers: require('./account-offers'),
account_tx: require('./account-tx'),
gateway_balances: require('./gateway-balances'),
book_offers: require('./book-offers'),
server_info: require('./server-info'),
server_info_error: require('./server-info-error'),
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')
},

View File

@@ -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"
}

View File

@@ -266,12 +266,25 @@ 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));
}
conn.send(response);
// delay response to simulate calculation time so we can test queuing
setTimeout(() => conn.send(response), 20);
});
mock.on('request_gateway_balances', function(request, conn) {
conn.send(createResponse(request, fixtures.gateway_balances));
});
return mock;

View File

@@ -1966,6 +1966,22 @@ describe('Remote', function() {
});
});
it('Construct gateway_balances request', function() {
const request = remote.requestGatewayBalances({
account: 'rGr9PjmVe7MqEXTSbd3njhgJc2s5vpHV54',
hotwallet: 'rwxBjBC9fPzyQ9GgPZw6YYLNeRTSx5',
strict: true
});
assert.deepEqual(request.message, {
command: 'gateway_balances',
id: undefined,
account: 'rGr9PjmVe7MqEXTSbd3njhgJc2s5vpHV54',
hotwallet: 'rwxBjBC9fPzyQ9GgPZw6YYLNeRTSx5',
strict: true
});
});
it('Construct Payment transaction', function() {
const tx = remote.createTransaction('Payment', {
account: TX_JSON.Account,

View File

@@ -40,13 +40,12 @@ const SERVER_INFO = {
};
describe('Request', function() {
it('Send request', function(done) {
it('Send request', function() {
const remote = {
request: function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
done();
},
on: function() {
},
@@ -60,7 +59,42 @@ describe('Request', function() {
request.request();
// Should only request once
request.request();
assert.throws(function() {
request.request();
}, Error);
});
it('Send request - reconnect', function(done) {
const server = makeServer('wss://localhost:5006');
let emitted = 0;
const remote = new Remote();
remote._connected = true;
remote._servers = [server];
server._request = function(req) {
assert(req instanceof Request);
assert.strictEqual(typeof req.message, 'object');
assert.strictEqual(req.message.command, 'server_info');
if (++emitted === 1) {
setTimeout(function() {
remote.emit('connected');
}, 2);
} if (emitted === 2) {
setTimeout(function() {
req.emit('success', SERVER_INFO);
req.emit('response', SERVER_INFO);
}, 2);
}
};
const request = new Request(remote, 'server_info');
request.callback(function() {
assert.strictEqual(emitted, 2);
done();
});
});
it('Send request -- filterRequest', function(done) {
@@ -536,6 +570,7 @@ describe('Request', function() {
setTimeout(function() {
successEmitted = true;
req.emit('success', SERVER_INFO);
req.emit('response', SERVER_INFO);
}, 200);
};
@@ -544,8 +579,9 @@ describe('Request', function() {
remote._servers = [server];
const request = new Request(remote, 'server_info');
request.setTimeout(10);
request.timeout(10, function() {
request.on('timeout', function() {
setTimeout(function() {
assert(successEmitted);
done();
@@ -566,7 +602,8 @@ describe('Request', function() {
assert.strictEqual(req.message.command, 'server_info');
setTimeout(function() {
req.emit('success', SERVER_INFO);
}, 200);
req.emit('response', SERVER_INFO);
}, 20);
};
const remote = new Remote();
@@ -581,13 +618,15 @@ describe('Request', function() {
timedOut = true;
});
request.timeout(1000);
request.setTimeout(100);
request.callback(function(err, res) {
assert(!timedOut);
assert.ifError(err);
assert.deepEqual(res, SERVER_INFO);
done();
setTimeout(function() {
assert(!timedOut, 'must not timeout');
assert.ifError(err);
assert.deepEqual(res, SERVER_INFO);
done();
}, 100);
});
});
@@ -1207,4 +1246,22 @@ describe('Request', function() {
]
});
});
it('Emit "before" only once', function(done) {
const remote = new Remote();
remote._connected = true;
const request = new Request(remote, 'server_info');
let beforeCalled = 0;
request.on('before', () => {
beforeCalled++;
});
request.request(function() {});
assert.strictEqual(beforeCalled, 1);
done();
});
});

View File

@@ -696,7 +696,7 @@ describe('TransactionManager', function() {
assert.strictEqual(summary.submissionAttempts, 0);
assert.strictEqual(summary.submitIndex, undefined);
assert.strictEqual(summary.initialSubmitIndex, undefined);
assert.strictEqual(summary.lastLedgerSequence, remote.getLedgerSequence() + 1 + Remote.DEFAULTS.last_ledger_offset);
assert.strictEqual(summary.lastLedgerSequence, remote.getLedgerSequenceSync() + 1 + Remote.DEFAULTS.last_ledger_offset);
assert.strictEqual(summary.state, 'failed');
assert.strictEqual(summary.finalized, true);
assert.deepEqual(summary.result, {