Merge notifications functionality into getAccountTransactions

This commit is contained in:
Chris Clark
2015-06-24 15:50:20 -07:00
parent 77f1351e5b
commit 64e86f403e
7 changed files with 92 additions and 450 deletions

View File

@@ -6,7 +6,6 @@ const balances = require('./ledger/balances');
const settings = require('./ledger/settings');
const transactions = require('./ledger/transactions');
const trustlines = require('./ledger/trustlines');
const notifications = require('./ledger/notifications');
const payments = require('./ledger/payments');
const orders = require('./ledger/orders');
const preparePayment = require('./transaction/payment');
@@ -41,8 +40,6 @@ RippleAPI.prototype = {
getSettings: settings.getSettings,
getTransaction: transactions.getTransaction,
getAccountTransactions: transactions.getAccountTransactions,
getNotification: notifications.getNotification,
getNotifications: notifications.getNotifications,
preparePayment: preparePayment,
prepareTrustline: prepareTrustline,

View File

@@ -0,0 +1,40 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "acct-tx-options",
"description": "Options for getAccountTransactions",
"type": "object",
"properties": {
"start": {"$ref": "hash256"},
"limit": {
"type": "integer",
"minimum": 1
},
"minLedgerVersion": {"$ref": "ledgerVersion"},
"maxLedgerVersion": {"$ref": "ledgerVersion"},
"earliestFirst": {"type": "boolean"},
"excludeFailures": {"type": "boolean"},
"outgoing": {"type": "boolean"},
"incoming": {"type": "boolean"},
"types": {
"type": "array",
"items": {
"enum": [
"payment",
"trustline",
"order",
"orderCancellation",
"settings"
]
}
},
"binary": {"type": "boolean"}
},
"additionalProperties": false,
"not": {
"anyOf": [
{"required": ["incoming", "outgoing"]},
{"required": ["start", "minLedgerVersion"]},
{"required": ["start", "maxLedgerVersion"]}
]
}
}

View File

@@ -33,8 +33,8 @@ function validateLedgerRange(options) {
}
}
function validateOptions(options) {
schemaValidate('options', options);
function validateOptions(schema, options) {
schemaValidate(schema, options);
validateLedgerRange(options);
}
@@ -52,6 +52,7 @@ module.exports = {
trustline: _.partial(schemaValidate, 'trustline'),
txJSON: _.partial(schemaValidate, 'tx'),
blob: _.partial(schemaValidate, 'blob'),
options: validateOptions,
getAccountTransactionsOptions: _.partial(validateOptions, 'acct-tx-options'),
options: _.partial(validateOptions, 'options'),
instructions: _.partial(schemaValidate, 'instructions')
};

View File

@@ -1,329 +0,0 @@
/* eslint-disable valid-jsdoc */
'use strict';
const _ = require('lodash');
const async = require('async');
const transactions = require('./transactions');
const NotificationParser = require('./parse/notification');
const utils = require('./utils');
const validate = utils.common.validate;
const server = utils.common.server;
/**
* Find the previous and next transaction hashes.
* Report errors to the client using res.json
* or pass the notificationDetails with the added fields
* back to the callback.
*
* @param {Remote} $.remote
* @param {Express.js Response} res
* @param {RippleAddress} notificationDetails.account
* @param {Ripple Transaction in JSON Format} notificationDetails.transaction
* @param {Hex-encoded String} notificationDetails.identifier
* @param {Function} callback
*
* @callback
* @param {Error} error
* @param {Object} notificationDetails
**/
function attachPreviousAndNextTransactionIdentifiers(api,
notificationDetails, topCallback) {
// Get all of the transactions affecting the specified
// account in the given ledger. This is done so that
// we can query for one more than that number on either
// side to ensure that we'll find the next and previous
// transactions, no matter how many transactions the
// given account had in the same ledger
function getAccountTransactionsInBaseTransactionLedger(callback) {
const params = {
account: notificationDetails.account,
ledger_index_min: notificationDetails.transaction.ledger_index,
ledger_index_max: notificationDetails.transaction.ledger_index,
exclude_failed: false,
max: 99999999,
limit: 200 // arbitrary, just checking number of transactions in ledger
};
transactions.getAccountTransactions(api, params, callback);
}
// Query for one more than the numTransactionsInLedger
// going forward and backwards to get a range of transactions
// that will definitely include the next and previous transactions
function getNextAndPreviousTransactions(txns, callback) {
const numTransactionsInLedger = txns.length;
async.concat([false, true], function(earliestFirst, concat_callback) {
const params = {
account: notificationDetails.account,
max: numTransactionsInLedger + 1,
min: numTransactionsInLedger + 1,
limit: numTransactionsInLedger + 1,
earliestFirst: earliestFirst
};
// In rippled -1 corresponds to the first or last ledger
// in its database, depending on whether it is the min or max value
if (params.earliestFirst) {
params.ledger_index_max = -1;
params.ledger_index_min = notificationDetails.transaction.ledger_index;
} else {
params.ledger_index_max = notificationDetails.transaction.ledger_index;
params.ledger_index_min = -1;
}
transactions.getAccountTransactions(api, params, concat_callback);
}, callback);
}
// Sort the transactions returned by ledger_index and remove duplicates
function sortTransactions(allTransactions, callback) {
allTransactions.push(notificationDetails.transaction);
const txns = _.uniq(allTransactions, function(tx) {
return tx.hash;
});
txns.sort(utils.compareTransactions);
callback(null, txns);
}
// Find the baseTransaction amongst the results. Because the
// transactions have been sorted, the next and previous transactions
// will be the ones on either side of the base transaction
function findPreviousAndNextTransactions(txns, callback) {
// Find the index in the array of the baseTransaction
const baseTransactionIndex = _.findIndex(txns, function(possibility) {
return possibility.hash === notificationDetails.transaction.hash;
});
// The previous transaction is the one with an index in
// the array of baseTransactionIndex - 1
if (baseTransactionIndex > 0) {
const previous_transaction = txns[baseTransactionIndex - 1];
notificationDetails.previous_transaction_identifier =
previous_transaction.hash;
}
// The next transaction is the one with an index in
// the array of baseTransactionIndex + 1
if (baseTransactionIndex + 1 < txns.length) {
const next_transaction = txns[baseTransactionIndex + 1];
notificationDetails.next_transaction_identifier = next_transaction.hash;
}
callback(null, notificationDetails);
}
const steps = [
getAccountTransactionsInBaseTransactionLedger,
getNextAndPreviousTransactions,
sortTransactions,
findPreviousAndNextTransactions
];
async.waterfall(steps, topCallback);
}
/**
* Get a notification corresponding to the specified
* account and transaction identifier. Send errors back
* to the client using the res.json method or pass
* the notification json to the callback function.
*
* @param {Remote} $.remote
* @param {RippleAddress} req.params.account
* @param {Hex-encoded String} req.params.identifier
* @param {Express.js Response} res
* @param {Function} callback
*
* @callback
* @param {Error} error
* @param {Notification} notification
*/
function getNotificationHelper(api, account, identifier, urlBase, topCallback) {
function getTransaction(callback) {
try {
transactions.getTransaction(api, account, identifier, {}, callback);
} catch(err) {
callback(err);
}
}
function checkLedger(baseTransaction, callback) {
server.remoteHasLedger(api.remote, baseTransaction.ledger_index,
function(error, remoteHasLedger) {
if (error) {
return callback(error);
}
if (remoteHasLedger) {
callback(null, baseTransaction);
} else {
callback(new utils.common.errors.NotFoundError(
'Cannot Get Notification. ' +
'This transaction is not in the ripple\'s complete ledger set. ' +
'Because there is a gap in the rippled\'s historical database it ' +
'is not possible to determine the transactions that precede this one')
);
}
});
}
function prepareNotificationDetails(baseTransaction, callback) {
const notificationDetails = {
account: account,
identifier: identifier,
transaction: baseTransaction
};
attachPreviousAndNextTransactionIdentifiers(api, notificationDetails,
callback);
}
function formatNotificationResponse(notificationDetails, callback) {
const notification = NotificationParser.parse(notificationDetails, urlBase);
callback(null, {notification: notification});
}
const steps = [
getTransaction,
checkLedger,
prepareNotificationDetails,
formatNotificationResponse
];
async.waterfall(steps, topCallback);
}
/**
* Get a notification corresponding to the specified
* account and transaction identifier. Uses the res.json
* method to send errors or a notification back to the client.
*
* @param {Remote} $.remote
* @param {/lib/config-loader} $.config
* @param {RippleAddress} req.params.account
* @param {Hex-encoded String} req.params.identifier
*/
function getNotification(account, identifier, urlBase, callback) {
validate.address(account);
validate.identifier(identifier);
return getNotificationHelper(this, account, identifier, urlBase, callback);
}
/**
* Get a notifications corresponding to the specified
* account.
*
* This function calls transactions.getAccountTransactions
* recursively to retrieve results_per_page number of transactions
* and filters the results using client-specified parameters.
*
* @param {RippleAddress} account
* @param {string} urlBase - The url to use for the transaction status URL
*
* @param {string} options.source_account
* @param {Number} options.ledger_min
* @param {Number} options.ledger_max
* @param {string} [false] options.earliest_first
* @param {string[]} options.types - @see transactions.getAccountTransactions
*
*/
// TODO: If given ledger range, check for ledger gaps
function getNotifications(account, urlBase, options, callback) {
validate.address(account);
const self = this;
function getTransactions(_callback) {
const resultsPerPage = options.results_per_page ||
transactions.DEFAULT_RESULTS_PER_PAGE;
const offset = resultsPerPage * ((options.page || 1) - 1);
const args = {
account: account,
direction: options.direction,
min: resultsPerPage,
max: resultsPerPage,
ledger_index_min: options.ledger_min,
ledger_index_max: options.ledger_max,
offset: offset,
earliestFirst: options.earliest_first
};
transactions.getAccountTransactions(self, args, _callback);
}
function parseNotifications(baseTransactions, _callback) {
const numTransactions = baseTransactions.length;
function parseNotification(transaction, __callback) {
const args = {
account: account,
identifier: transaction.hash,
transaction: transaction
};
// Attaching previous and next identifiers
const idx = baseTransactions.indexOf(transaction);
const previous = baseTransactions[idx + 1];
const next = baseTransactions[idx - 1];
if (!options.earliest_first) {
args.previous_transaction_identifier = previous ?
previous.hash : undefined;
args.next_transaction_identifier = next ? next.hash : undefined;
} else {
args.previous_transaction_identifier = next ? next.hash : undefined;
args.next_transaction_identifier = previous ? previous.hash : undefined;
}
args.previous_transaction_identifier = args.previous_hash;
args.next_transaction_identifier = args.next_hash;
const firstAndPaging = options.page &&
(options.earliest_first ?
args.previous_transaction_identifier === undefined :
args.next_transaction_identifier === undefined);
const last = idx === numTransactions - 1;
if (firstAndPaging || last) {
attachPreviousAndNextTransactionIdentifiers(self, args,
function(err, _args) {
return __callback(err, NotificationParser.parse(_args, urlBase));
}
);
} else {
return __callback(null, NotificationParser.parse(args, urlBase));
}
}
return async.map(baseTransactions, parseNotification, _callback);
}
function formatResponse(notifications, _callback) {
_callback(null, {notifications: notifications});
}
const steps = [
getTransactions,
_.partial(utils.attachDate, self),
parseNotifications,
formatResponse
];
return async.waterfall(steps, callback);
}
module.exports = {
getNotification: getNotification,
getNotifications: getNotifications
};

View File

@@ -1,104 +0,0 @@
/* @flow */
/* eslint-disable valid-jsdoc */
'use strict';
const ripple = require('../utils').common.core;
/**
* Convert a Ripple transaction in the JSON format,
* along with some additional pieces of information,
* into a Notification object.
*
* @param {Ripple Transaction in JSON Format} notification_details.transaction
* @param {RippleAddress} notification_details.account
* @param {Hex-encoded String}
* notification_details.previous_transaction_identifier
* @param {Hex-encoded String}
* notification_details.next_transaction_identifier
*
* @returns {Notification}
*/
function NotificationParser() {}
NotificationParser.prototype.parse = function(notification_details, urlBase) {
const transaction = notification_details.transaction;
const account = notification_details.account;
const previous_transaction_identifier =
notification_details.previous_transaction_identifier;
const next_transaction_identifier =
notification_details.next_transaction_identifier;
const metadata = transaction.meta || { };
const notification = {
account: account,
type: transaction.TransactionType.toLowerCase(),
direction: '', // set below
state: (metadata.TransactionResult === 'tesSUCCESS'
? 'validated' : 'failed'),
result: metadata.TransactionResult || '',
ledger: '' + transaction.ledger_index,
hash: transaction.hash,
timestamp: '',// set below
transaction_url: '', // set below
previous_transaction_identifier:
notification_details.previous_transaction_identifier || '',
previous_notification_url: '', // set below
next_transaction_identifier:
notification_details.next_transaction_identifier || '',
next_notification_url: '' // set below
};
notification.timestamp = transaction.date ?
new Date(ripple.utils.time.fromRipple(transaction.date)).toISOString() : '';
// Direction
if (account === transaction.Account) {
notification.direction = 'outgoing';
} else if (transaction.TransactionType === 'Payment'
&& transaction.Destination !== account) {
notification.direction = 'passthrough';
} else {
notification.direction = 'incoming';
}
// Notification URL
if (notification.type === 'payment') {
notification.transaction_url = urlBase + '/v1/accounts/'
+ notification.account
+ '/payments/'
+ notification.hash;
} else if (notification.type === 'offercreate'
|| notification.type === 'offercancel') {
notification.type = 'order';
notification.transaction_url = urlBase + '/v1/accounts/'
+ notification.account
+ '/orders/' + notification.hash;
} else if (notification.type === 'trustset') {
notification.type = 'trustline';
notification.transaction_url = urlBase + '/v1/transactions/'
+ notification.hash;
} else if (notification.type === 'accountset') {
notification.type = 'settings';
notification.transaction_url = urlBase + '/v1/transactions/'
+ notification.hash;
}
// Next notification URL
if (next_transaction_identifier) {
notification.next_notification_url = urlBase + '/v1/accounts/'
+ notification.account + '/notifications/' + next_transaction_identifier;
}
// Previous notification URL
if (previous_transaction_identifier) {
notification.previous_notification_url = urlBase + '/v1/accounts/'
+ notification.account + '/notifications/'
+ previous_transaction_identifier;
}
return notification;
};
module.exports = new NotificationParser();

View File

@@ -6,6 +6,7 @@ const utils = require('./utils');
const parseTransaction = require('./parse/transaction');
const validate = utils.common.validate;
const errors = utils.common.errors;
const composeAsync = utils.common.composeAsync;
const DEFAULT_LIMIT = 100;
const MIN_LEDGER_VERSION = 32570; // earlier versions have been completely lost
@@ -81,7 +82,6 @@ function parseAccountTxTransaction(tx) {
return parseTransaction(tx.tx);
}
function transactionFilter(address, filters, tx) {
if (filters.excludeFailures && tx.outcome.result !== 'tesSUCCESS') {
return false;
@@ -98,14 +98,20 @@ function transactionFilter(address, filters, tx) {
return true;
}
function orderFilter(options, tx) {
return !options.startTx || (options.earliestFirst ?
utils.compareTransactions(tx, options.startTx) > 0 :
utils.compareTransactions(tx, options.startTx) < 0);
}
function getAccountTx(remote, address, options, marker, limit, callback) {
const params = {
account: address,
ledger_index_min: options.ledgerVersion || options.minLedgerVersion || -1,
ledger_index_max: options.ledgerVersion || options.maxLedgerVersion || -1,
ledger_index_min: options.minLedgerVersion || -1,
ledger_index_max: options.maxLedgerVersion || -1,
forward: options.earliestFirst,
binary: options.binary,
limit: Math.min(limit || DEFAULT_LIMIT, 10),
limit: Math.max(limit || DEFAULT_LIMIT, 10),
marker: marker
};
@@ -116,20 +122,40 @@ function getAccountTx(remote, address, options, marker, limit, callback) {
.filter((tx) => tx.validated)
.map(parseAccountTxTransaction)
.filter(_.partial(transactionFilter, address, options))
.filter(_.partial(orderFilter, options))
});
});
}
function getAccountTransactions(address, options, callback) {
validate.address(address);
function getAccountTransactionsInternal(remote, address, options, callback) {
const limit = options.limit || DEFAULT_LIMIT;
const compare = options.earliestFirst ? utils.compareTransactions :
_.rearg(utils.compareTransactions, 1, 0);
const getter = _.partial(getAccountTx, this.remote, address, options);
utils.getRecursive(getter, limit, (error, data) => {
return error ? callback(error) : callback(null, data.sort(compare));
});
const getter = _.partial(getAccountTx, remote, address, options);
utils.getRecursive(getter, limit,
composeAsync((txs) => txs.sort(compare), callback));
}
function getAccountTransactions(address, options, callback) {
validate.address(address);
validate.getAccountTransactionsOptions(options);
const remote = this.remote;
if (options.start) {
getTransaction.bind(this)(options.start, {}, (error, tx) => {
if (error) {
callback(error);
return;
}
const ledgerVersion = tx.outcome.ledgerVersion;
const ledgerOption = options.earliestFirst ?
{minLedgerVersion: ledgerVersion} : {maxLedgerVersion: ledgerVersion};
const newOptions = _.assign({}, options, {startTx: tx}, ledgerOption);
getAccountTransactionsInternal(remote, address, newOptions, callback);
});
} else {
getAccountTransactionsInternal(remote, address, options, callback);
}
}
module.exports = {

View File

@@ -101,6 +101,17 @@ describe('RippleAPI', function() {
_.partial(checkResult, accountTransactionsResponse, done));
});
// TODO: this doesn't test much, just that it doesn't crash
it('getAccountTransactions with start option', function(done) {
const options = {
start: hashes.VALID_TRANSACTION_HASH,
earliestFirst: false,
limit: 2
};
this.api.getAccountTransactions(address, options,
_.partial(checkResult, accountTransactionsResponse, done));
});
it('getTrustlines', function(done) {
const options = {currency: 'USD'};
this.api.getTrustlines(address, options,