Rewrite transaction parser and add unit test for getTransaction

This commit is contained in:
Chris Clark
2015-06-09 17:59:02 -07:00
parent df0cff969c
commit 1b936d2aa2
34 changed files with 570 additions and 707 deletions

View File

@@ -0,0 +1,18 @@
'use strict';
const utils = require('./utils');
function parseAmount(amount) {
if (typeof amount === 'string') {
return {
currency: 'XRP',
value: utils.dropsToXrp(amount)
};
}
return {
currency: amount.currency,
value: amount.value,
counterparty: amount.issuer || amount.counterparty
};
}
module.exports = parseAmount;

View File

@@ -0,0 +1,11 @@
'use strict';
const assert = require('assert');
function parseOrderCancellation(tx) {
assert(tx.TransactionType === 'OfferCancel');
return {
orderSequence: tx.OfferSequence
};
}
module.exports = parseOrderCancellation;

View File

@@ -0,0 +1,23 @@
'use strict';
const AccountFields = require('./utils').constants.AccountFields;
function parseField(info, value) {
if (info.encoding === 'hex' && !info.length) {
return new Buffer(value, 'hex').toString('ascii');
}
return value;
}
function parseFields(data) {
const settings = {};
for (const fieldName in AccountFields) {
const fieldValue = data[fieldName];
if (fieldValue !== undefined) {
const info = AccountFields[fieldName];
settings[info.name] = parseField(info, fieldValue);
}
}
return settings;
}
module.exports = parseFields;

View File

@@ -0,0 +1,103 @@
/* 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

@@ -0,0 +1,26 @@
'use strict';
const assert = require('assert');
const utils = require('./utils');
const parseAmount = require('./amount');
const flags = utils.core.Transaction.flags.OfferCreate;
function parseOrder(tx) {
assert(tx.TransactionType === 'OfferCreate');
const direction = (tx.Flags & flags.Sell) === 0 ? 'buy' : 'sell';
const takerGets = parseAmount(tx.TakerGets);
const takerPays = parseAmount(tx.TakerPays);
const quantity = (direction === 'buy') ? takerPays : takerGets;
const totalPrice = (direction === 'buy') ? takerGets : takerPays;
return {
direction: direction,
quantity: quantity,
totalPrice: totalPrice,
passive: (tx.Flags & flags.Passive) !== 0,
immediateOrCancel: (tx.Flags & flags.ImmediateOrCancel) !== 0,
fillOrKill: (tx.Flags & flags.FillOrKill) !== 0
};
}
module.exports = parseOrder;

View File

@@ -0,0 +1,22 @@
'use strict';
const parseAmount = require('./amount');
function parsePathfind(sourceAddress, destinationAmount, pathfindResult) {
return pathfindResult.alternatives.map(function(alternative) {
return {
source: {
address: sourceAddress,
amount: parseAmount(alternative.source_amount)
},
destination: {
address: pathfindResult.destination_account,
amount: destinationAmount
},
paths: JSON.stringify(alternative.paths_computed),
allowPartialPayment: false,
noDirectRipple: false
};
});
}
module.exports = parsePathfind;

View File

@@ -0,0 +1,48 @@
'use strict';
const assert = require('assert');
const utils = require('./utils');
const parseAmount = require('./amount');
const Transaction = utils.core.Transaction;
function isPartialPayment(tx) {
return (tx.Flags & Transaction.flags.Payment.PartialPayment) !== 0;
}
function isNoDirectRipple(tx) {
return (tx.Flags & Transaction.flags.Payment.NoRippleDirect) !== 0;
}
function parsePaymentMemos(tx) {
if (!Array.isArray(tx.Memos) || tx.Memos.length === 0) {
return undefined;
}
return tx.Memos.map((m) => m.Memo);
}
function parsePayment(tx) {
assert(tx.TransactionType === 'Payment');
const source = {
address: tx.Account,
amount: parseAmount(tx.SendMax || tx.Amount),
tag: tx.SourceTag
};
const destination = {
address: tx.Destination,
amount: parseAmount(tx.Amount),
tag: tx.DestinationTag
};
return {
source: utils.removeUndefined(source),
destination: utils.removeUndefined(destination),
memos: parsePaymentMemos(tx),
invoiceID: tx.InvoiceID,
paths: JSON.stringify(tx.Paths || []),
allowPartialPayment: isPartialPayment(tx),
noDirectRipple: isNoDirectRipple(tx)
};
}
module.exports = parsePayment;

View File

@@ -0,0 +1,27 @@
'use strict';
const _ = require('lodash');
const assert = require('assert');
const AccountSetFlags = require('./utils').constants.AccountSetFlags;
const parseFields = require('./fields');
function getName(flagNumber) {
return _.findKey(AccountSetFlags, (v) => v === flagNumber);
}
function parseSettings(tx) {
const txType = tx.TransactionType;
assert(txType === 'AccountSet' || txType === 'SetRegularKey');
const settings = {};
if (tx.SetFlag) {
settings[getName(tx.SetFlag)] = true;
}
if (tx.ClearFlag) {
settings[getName(tx.ClearFlag)] = false;
}
if (tx.RegularKey) {
settings.regularKey = tx.RegularKey;
}
return _.assign(settings, parseFields(tx));
}
module.exports = parseSettings;

View File

@@ -0,0 +1,42 @@
'use strict';
const assert = require('assert');
const utils = require('./utils');
const parsePayment = require('./payment');
const parseTrustline = require('./trustline');
const parseOrder = require('./order');
const parseOrderCancellation = require('./cancellation');
const parseSettings = require('./settings');
function parseTransactionType(type) {
const mapping = {
Payment: 'payment',
TrustSet: 'trustline',
OfferCreate: 'order',
OfferCancel: 'orderCancellation',
AccountSet: 'settings',
SetRegularKey: 'settings'
};
return mapping[type] || null;
}
function parseTransaction(tx) {
const type = parseTransactionType(tx.TransactionType);
const mapping = {
'payment': parsePayment,
'trustline': parseTrustline,
'order': parseOrder,
'orderCancellation': parseOrderCancellation,
'settings': parseSettings
};
const parser = mapping[type];
assert(parser !== undefined, 'Unrecognized transaction type');
const specification = parser(tx);
return utils.removeUndefined({
type: type,
address: tx.Account,
specification: utils.removeUndefined(specification),
outcome: utils.removeUndefined(utils.parseOutcome(tx))
});
}
module.exports = parseTransaction;

View File

@@ -0,0 +1,21 @@
'use strict';
const assert = require('assert');
const utils = require('./utils');
const flags = utils.core.Transaction.flags.TrustSet;
function parseTrustline(tx) {
assert(tx.TransactionType === 'TrustSet');
return {
limit: tx.LimitAmount.value,
currency: tx.LimitAmount.currency,
counterparty: tx.LimitAmount.issuer,
qualityIn: tx.QualityIn,
qualityOut: tx.QualityOut,
allowRippling: tx.Flags & flags.NoRipple === 0,
frozen: tx.Flags & flags.SetFreeze !== 0,
authorized: tx.Flags & flags.SetAuth !== 0
};
}
module.exports = parseTrustline;

View File

@@ -0,0 +1,40 @@
'use strict';
const _ = require('lodash');
const transactionParser = require('ripple-lib-transactionparser');
const toTimestamp = require('../../../core/utils').toTimestamp;
const utils = require('../utils');
function parseTimestamp(tx) {
return tx.date ? (new Date(toTimestamp(tx.date))).toISOString() : undefined;
}
function removeUndefined(obj) {
return obj ? _.omit(obj, _.isUndefined) : obj;
}
function parseOutcome(tx) {
if (!tx.validated) {
return undefined;
}
const balanceChanges = transactionParser.parseBalanceChanges(tx.meta);
const orderbookChanges = transactionParser.parseOrderBookChanges(tx.meta);
return {
result: tx.meta.TransactionResult,
timestamp: parseTimestamp(tx),
fee: utils.common.dropsToXrp(tx.Fee),
balanceChanges: balanceChanges,
orderbookChanges: orderbookChanges,
ledgerVersion: tx.ledger_index,
sequence: tx.Sequence
};
}
module.exports = {
parseOutcome,
removeUndefined,
dropsToXrp: utils.common.dropsToXrp,
constants: utils.common.constants,
core: utils.common.core
};