mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-04 21:15:47 +00:00
Merge pull request #369 from clark800/parse-tx
Rewrite transaction parser and add unit test for getTransaction
This commit is contained in:
@@ -1,44 +1,22 @@
|
||||
'use strict';
|
||||
const ripple = require('./core');
|
||||
const Transaction = require('./core').Transaction;
|
||||
const flagIndices = Transaction.set_clear_flags.AccountSet;
|
||||
|
||||
const AccountRootFlags = {
|
||||
PasswordSpent: {
|
||||
name: 'password_spent',
|
||||
value: ripple.Remote.flags.account_root.PasswordSpent
|
||||
},
|
||||
RequireDestTag: {
|
||||
name: 'require_destination_tag',
|
||||
value: ripple.Remote.flags.account_root.RequireDestTag
|
||||
},
|
||||
RequireAuth: {
|
||||
name: 'require_authorization',
|
||||
value: ripple.Remote.flags.account_root.RequireAuth
|
||||
},
|
||||
DisallowXRP: {
|
||||
name: 'disallow_xrp',
|
||||
value: ripple.Remote.flags.account_root.DisallowXRP
|
||||
},
|
||||
DisableMaster: {
|
||||
name: 'disable_master',
|
||||
value: ripple.Remote.flags.account_root.DisableMaster
|
||||
},
|
||||
NoFreeze: {
|
||||
name: 'no_freeze',
|
||||
value: ripple.Remote.flags.account_root.NoFreeze
|
||||
},
|
||||
GlobalFreeze: {
|
||||
name: 'global_freeze',
|
||||
value: ripple.Remote.flags.account_root.GlobalFreeze
|
||||
},
|
||||
DefaultRipple: {
|
||||
name: 'default_ripple',
|
||||
value: ripple.Remote.flags.account_root.DefaultRipple
|
||||
}
|
||||
const AccountFlagIndices = {
|
||||
requireDestinationTag: flagIndices.asfRequireDest,
|
||||
requireAuthorization: flagIndices.asfRequireAuth,
|
||||
disallowIncomingXRP: flagIndices.asfDisallowXRP,
|
||||
disableMasterKey: flagIndices.asfDisableMaster,
|
||||
enableTransactionIDTracking: flagIndices.asfAccountTxnID,
|
||||
noFreeze: flagIndices.asfNoFreeze,
|
||||
globalFreeze: flagIndices.asfGlobalFreeze,
|
||||
defaultRipple: flagIndices.asfDefaultRipple
|
||||
};
|
||||
|
||||
const AccountRootFields = {
|
||||
const AccountFields = {
|
||||
Sequence: {name: 'sequence'},
|
||||
EmailHash: {name: 'emailHash', encoding: 'hex', length: 32, defaults: '0'},
|
||||
EmailHash: {name: 'emailHash', encoding: 'hex',
|
||||
length: 32, defaults: '0'},
|
||||
WalletLocator: {name: 'walletLocator', encoding: 'hex',
|
||||
length: 64, defaults: '0'},
|
||||
WalletSize: {name: 'walletSize', defaults: 0},
|
||||
@@ -48,52 +26,7 @@ const AccountRootFields = {
|
||||
Signers: {name: 'signers'}
|
||||
};
|
||||
|
||||
const AccountSetIntFlags = {
|
||||
noFreeze: ripple.Transaction.set_clear_flags.AccountSet.asfNoFreeze,
|
||||
globalFreeze: ripple.Transaction.set_clear_flags.AccountSet.asfGlobalFreeze,
|
||||
defaultRipple: ripple.Transaction.set_clear_flags.AccountSet.asfDefaultRipple
|
||||
};
|
||||
|
||||
const AccountSetFlags = {
|
||||
requireDestinationTag: {set: 'RequireDestTag', unset: 'OptionalDestTag'},
|
||||
requireAuthorization: {set: 'RequireAuth', unset: 'OptionalAuth'},
|
||||
disallowIncomingXRP: {set: 'DisallowXRP', unset: 'AllowXRP'}
|
||||
};
|
||||
|
||||
const AccountSetResponseFlags = {
|
||||
RequireDestTag: {name: 'require_destination_tag',
|
||||
value: ripple.Transaction.flags.AccountSet.RequireDestTag},
|
||||
RequireAuth: {name: 'require_authorization',
|
||||
value: ripple.Transaction.flags.AccountSet.RequireAuth},
|
||||
DisallowXRP: {name: 'disallow_xrp',
|
||||
value: ripple.Transaction.flags.AccountSet.DisallowXRP}
|
||||
};
|
||||
|
||||
const OfferCreateFlags = {
|
||||
Passive: {name: 'passive',
|
||||
value: ripple.Transaction.flags.OfferCreate.Passive},
|
||||
ImmediateOrCancel: {name: 'immediate_or_cancel',
|
||||
value: ripple.Transaction.flags.OfferCreate.ImmediateOrCancel},
|
||||
FillOrKill: {name: 'fill_or_kill',
|
||||
value: ripple.Transaction.flags.OfferCreate.FillOrKill},
|
||||
Sell: {name: 'sell', value: ripple.Transaction.flags.OfferCreate.Sell}
|
||||
};
|
||||
|
||||
const TrustSetResponseFlags = {
|
||||
NoRipple: {name: 'prevent_rippling',
|
||||
value: ripple.Transaction.flags.TrustSet.NoRipple},
|
||||
SetFreeze: {name: 'account_trustline_frozen',
|
||||
value: ripple.Transaction.flags.TrustSet.SetFreeze},
|
||||
SetAuth: {name: 'authorized',
|
||||
value: ripple.Transaction.flags.TrustSet.SetAuth}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AccountRootFlags: AccountRootFlags,
|
||||
AccountRootFields: AccountRootFields,
|
||||
AccountSetIntFlags: AccountSetIntFlags,
|
||||
AccountSetFlags: AccountSetFlags,
|
||||
AccountSetResponseFlags: AccountSetResponseFlags,
|
||||
OfferCreateFlags: OfferCreateFlags,
|
||||
TrustSetResponseFlags: TrustSetResponseFlags
|
||||
AccountFields,
|
||||
AccountFlagIndices
|
||||
};
|
||||
|
||||
@@ -51,6 +51,12 @@ function NotFoundError(message) {
|
||||
NotFoundError.prototype = new RippleError();
|
||||
NotFoundError.prototype.name = 'NotFoundError';
|
||||
|
||||
function MissingLedgerHistoryError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
MissingLedgerHistoryError.prototype = new RippleError();
|
||||
MissingLedgerHistoryError.prototype.name = 'MissingLedgerHistoryError';
|
||||
|
||||
/**
|
||||
* Request timed out
|
||||
*/
|
||||
@@ -75,6 +81,7 @@ module.exports = {
|
||||
TransactionError: TransactionError,
|
||||
RippledNetworkError: RippledNetworkError,
|
||||
NotFoundError: NotFoundError,
|
||||
MissingLedgerHistoryError: MissingLedgerHistoryError,
|
||||
TimeOutError: TimeOutError,
|
||||
ApiError: ApiError,
|
||||
RippleError: RippleError
|
||||
|
||||
@@ -9,5 +9,6 @@ module.exports = {
|
||||
server: require('./server'),
|
||||
dropsToXrp: utils.dropsToXrp,
|
||||
xrpToDrops: utils.xrpToDrops,
|
||||
convertAmount: utils.convertAmount
|
||||
toRippledAmount: utils.toRippledAmount,
|
||||
wrapCatch: utils.wrapCatch
|
||||
};
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
"messageKey": {"type": "string"},
|
||||
"domain": {"type": "string"},
|
||||
"transferRate": {"type": "integer"},
|
||||
"signers": {"type": "string"}
|
||||
"signers": {"type": "string"},
|
||||
"regularKey": {"$ref": "address"}
|
||||
},
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"qualityIn": {"$ref": "quality"},
|
||||
"qualityOut": {"$ref": "quality"},
|
||||
"allowRippling": {"type": "boolean"},
|
||||
"authorized": {"type": "boolean"},
|
||||
"frozen": {"type": "boolean"}
|
||||
},
|
||||
"required": ["currency", "counterparty", "limit"],
|
||||
|
||||
@@ -9,7 +9,7 @@ function xrpToDrops(xrp) {
|
||||
return (new BigNumber(xrp)).times(1000000.0).floor().toString();
|
||||
}
|
||||
|
||||
function convertAmount(amount) {
|
||||
function toRippledAmount(amount) {
|
||||
if (amount.currency === 'XRP') {
|
||||
return xrpToDrops(amount.value);
|
||||
}
|
||||
@@ -20,8 +20,20 @@ function convertAmount(amount) {
|
||||
};
|
||||
}
|
||||
|
||||
function wrapCatch(asyncFunction: () => void): () => void {
|
||||
return function() {
|
||||
try {
|
||||
asyncFunction.apply(this, arguments);
|
||||
} catch (error) {
|
||||
const callback = arguments[arguments.length - 1];
|
||||
callback(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dropsToXrp: dropsToXrp,
|
||||
xrpToDrops: xrpToDrops,
|
||||
convertAmount: convertAmount
|
||||
dropsToXrp,
|
||||
xrpToDrops,
|
||||
toRippledAmount,
|
||||
wrapCatch
|
||||
};
|
||||
|
||||
@@ -25,9 +25,10 @@ function validateAddressAndSecret(obj) {
|
||||
}
|
||||
|
||||
function validateLedgerRange(options) {
|
||||
if (_.isUndefined(options.minLedger) && _.isUndefined(options.maxLedger)) {
|
||||
if (Number(options.minLedger) > Number(options.maxLedger)) {
|
||||
throw error('minLedger must not be greater than maxLedger');
|
||||
if (!_.isUndefined(options.minLedgerVersion)
|
||||
&& !_.isUndefined(options.maxLedgerVersion)) {
|
||||
if (Number(options.minLedgerVersion) > Number(options.maxLedgerVersion)) {
|
||||
throw error('minLedgerVersion must not be greater than maxLedgerVersion');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const transactions = require('./transactions');
|
||||
const NotificationParser = require('./notification_parser.js');
|
||||
const utils = require('./utils.js');
|
||||
const NotificationParser = require('./parse/notification');
|
||||
const utils = require('./utils');
|
||||
const validate = utils.common.validate;
|
||||
const server = utils.common.server;
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
const _ = require('lodash');
|
||||
const bignum = require('bignumber.js');
|
||||
const asyncify = require('simple-asyncify');
|
||||
const TxToRestConverter = require('./tx-to-rest-converter.js');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const errors = utils.common.errors;
|
||||
const validate = utils.common.validate;
|
||||
const parseTransaction = require('./parse/transaction');
|
||||
|
||||
const DefaultPageLimit = 200;
|
||||
|
||||
@@ -302,18 +302,14 @@ function getOrder(account, identifier, callback) {
|
||||
});
|
||||
|
||||
txRequest.once('error', callback);
|
||||
txRequest.once('transaction', function(response) {
|
||||
if (response.TransactionType !== 'OfferCreate'
|
||||
&& response.TransactionType !== 'OfferCancel') {
|
||||
txRequest.once('transaction', function(tx) {
|
||||
if (tx.TransactionType !== 'OfferCreate'
|
||||
&& tx.TransactionType !== 'OfferCancel') {
|
||||
callback(new errors.InvalidRequestError('Invalid parameter: identifier. '
|
||||
+ 'The transaction corresponding to the given identifier '
|
||||
+ 'is not an order'));
|
||||
} else {
|
||||
const options = {
|
||||
account: account,
|
||||
identifier: identifier
|
||||
};
|
||||
asyncify(TxToRestConverter.parseOrderFromTx)(response, options, callback);
|
||||
asyncify(parseTransaction)(tx, callback);
|
||||
}
|
||||
});
|
||||
txRequest.request();
|
||||
|
||||
18
src/api/ledger/parse/amount.js
Normal file
18
src/api/ledger/parse/amount.js
Normal 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;
|
||||
11
src/api/ledger/parse/cancellation.js
Normal file
11
src/api/ledger/parse/cancellation.js
Normal 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;
|
||||
23
src/api/ledger/parse/fields.js
Normal file
23
src/api/ledger/parse/fields.js
Normal 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;
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const ripple = require('./utils').common.core;
|
||||
const ripple = require('../utils').common.core;
|
||||
|
||||
/**
|
||||
* Convert a Ripple transaction in the JSON format,
|
||||
26
src/api/ledger/parse/order.js
Normal file
26
src/api/ledger/parse/order.js
Normal 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;
|
||||
22
src/api/ledger/parse/pathfind.js
Normal file
22
src/api/ledger/parse/pathfind.js
Normal 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;
|
||||
48
src/api/ledger/parse/payment.js
Normal file
48
src/api/ledger/parse/payment.js
Normal 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;
|
||||
27
src/api/ledger/parse/settings.js
Normal file
27
src/api/ledger/parse/settings.js
Normal 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;
|
||||
42
src/api/ledger/parse/transaction.js
Normal file
42
src/api/ledger/parse/transaction.js
Normal 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;
|
||||
21
src/api/ledger/parse/trustline.js
Normal file
21
src/api/ledger/parse/trustline.js
Normal 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;
|
||||
40
src/api/ledger/parse/utils.js
Normal file
40
src/api/ledger/parse/utils.js
Normal 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
|
||||
};
|
||||
@@ -6,9 +6,10 @@ const async = require('async');
|
||||
const asyncify = require('simple-asyncify');
|
||||
const bignum = require('bignumber.js');
|
||||
const transactions = require('./transactions');
|
||||
const TxToRestConverter = require('./tx-to-rest-converter.js');
|
||||
const utils = require('./utils');
|
||||
const validate = utils.common.validate;
|
||||
const parseTransaction = require('./parse/transaction');
|
||||
const parsePathfind = require('./parse/pathfind');
|
||||
|
||||
const ValidationError = utils.common.errors.ValidationError;
|
||||
const NotFoundError = utils.common.errors.NotFoundError;
|
||||
@@ -32,16 +33,7 @@ function formatPaymentHelper(account, txJSON) {
|
||||
throw new ValidationError('Not a payment. The transaction '
|
||||
+ 'corresponding to the given identifier is not a payment.');
|
||||
}
|
||||
const metadata = {
|
||||
hash: txJSON.hash || '',
|
||||
ledger: String(!_.isUndefined(txJSON.inLedger) ?
|
||||
txJSON.inLedger : txJSON.ledger_index),
|
||||
state: txJSON.validated === true ? 'validated' : 'pending'
|
||||
};
|
||||
const message = {tx_json: txJSON};
|
||||
const meta = txJSON.meta;
|
||||
const parsed = TxToRestConverter.parsePaymentFromTx(account, message, meta);
|
||||
return _.assign({payment: parsed.payment}, metadata);
|
||||
return parseTransaction(txJSON);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +145,7 @@ function getPathFind(pathfind, callback) {
|
||||
const pathfindParams = {
|
||||
src_account: pathfind.source.address,
|
||||
dst_account: pathfind.destination.address,
|
||||
dst_amount: utils.common.convertAmount(pathfind.destination.amount)
|
||||
dst_amount: utils.common.toRippledAmount(pathfind.destination.amount)
|
||||
};
|
||||
if (typeof pathfindParams.dst_amount === 'object'
|
||||
&& !pathfindParams.dst_amount.issuer) {
|
||||
@@ -227,7 +219,7 @@ function getPathFind(pathfind, callback) {
|
||||
function formatPath(pathfindResults) {
|
||||
const alternatives = pathfindResults.alternatives;
|
||||
if (alternatives && alternatives.length > 0) {
|
||||
return TxToRestConverter.parsePaymentsFromPathFind(pathfindResults);
|
||||
return parsePathfind(pathfindResults);
|
||||
}
|
||||
if (pathfindResults.destination_currencies.indexOf(
|
||||
pathfind.destination.amount.currency) === -1) {
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const TxToRestConverter = require('./tx-to-rest-converter.js');
|
||||
const utils = require('./utils');
|
||||
const flags = utils.common.core.Remote.flags.account_root;
|
||||
const validate = utils.common.validate;
|
||||
const constants = utils.common.constants;
|
||||
const parseFields = require('./parse/fields');
|
||||
|
||||
function parseFieldsFromResponse(responseBody, fields) {
|
||||
let parsedBody = {};
|
||||
const AccountFlags = {
|
||||
passwordSpent: flags.PasswordSpent,
|
||||
requireDestinationTag: flags.RequireDestTag,
|
||||
requireAuthorization: flags.RequireAuth,
|
||||
disallowIncomingXRP: flags.DisallowXRP,
|
||||
disableMasterKey: flags.DisableMaster,
|
||||
noFreeze: flags.NoFreeze,
|
||||
globalFreeze: flags.GlobalFreeze,
|
||||
defaultRipple: flags.DefaultRipple
|
||||
};
|
||||
|
||||
for (let fieldName in fields) {
|
||||
const field = fields[fieldName];
|
||||
let value = responseBody[fieldName] || '';
|
||||
if (field.encoding === 'hex' && !field.length) {
|
||||
value = new Buffer(value, 'hex').toString('ascii');
|
||||
function parseFlags(value) {
|
||||
const settings = {};
|
||||
for (const flagName in AccountFlags) {
|
||||
if (value & AccountFlags[flagName]) {
|
||||
settings[flagName] = true;
|
||||
}
|
||||
parsedBody[field.name] = value;
|
||||
}
|
||||
|
||||
return parsedBody;
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves account settings for a given account
|
||||
*
|
||||
* @url
|
||||
* @param {String} request.params.account
|
||||
*
|
||||
*/
|
||||
function getSettings(account, callback) {
|
||||
validate.address(account);
|
||||
|
||||
@@ -35,24 +33,11 @@ function getSettings(account, callback) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
const data = info.account_data;
|
||||
const settings = {
|
||||
account: data.Account,
|
||||
transfer_rate: '0'
|
||||
};
|
||||
|
||||
// Attach account flags
|
||||
_.extend(settings, TxToRestConverter.parseFlagsFromResponse(data.Flags,
|
||||
constants.AccountRootFlags));
|
||||
|
||||
// Attach account fields
|
||||
_.extend(settings, parseFieldsFromResponse(data,
|
||||
constants.AccountRootFields));
|
||||
|
||||
settings.transaction_sequence = String(settings.transaction_sequence);
|
||||
|
||||
callback(null, {settings: settings});
|
||||
const parsedFlags = parseFlags(data.Flags);
|
||||
const parsedFields = parseFields(data);
|
||||
const settings = _.assign({}, parsedFlags, parsedFields);
|
||||
callback(null, settings);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,102 +1,77 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const async = require('async');
|
||||
const utils = require('./utils');
|
||||
const parseTransaction = require('./parse/transaction');
|
||||
const validate = utils.common.validate;
|
||||
const errors = utils.common.errors;
|
||||
|
||||
const DEFAULT_RESULTS_PER_PAGE = 10;
|
||||
const MIN_LEDGER_VERSION = 32570; // earlier versions have been completely lost
|
||||
|
||||
/**
|
||||
* Retrieve a transaction from the Remote based on the account and hash
|
||||
*
|
||||
* Note that if any errors are encountered while executing this function
|
||||
* they will be sent back to the client through the res. If the query is
|
||||
* successful it will be passed to the callback function
|
||||
*
|
||||
* @global
|
||||
* @param {Remote} remote
|
||||
*
|
||||
* @param {RippleAddress} account
|
||||
* @param {Hex-encoded String|ASCII printable character String} identifier
|
||||
* @param {Object} options
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
function getTransaction(api, account, identifier, requestOptions, callback) {
|
||||
try {
|
||||
assert.strictEqual(typeof requestOptions, 'object');
|
||||
validate.address(account);
|
||||
validate.identifier(identifier);
|
||||
validate.options(requestOptions);
|
||||
} catch(err) {
|
||||
return callback(err);
|
||||
function hasCompleteLedgerRange(remote, options) {
|
||||
const minLedgerVersion = options.minLedgerVersion || MIN_LEDGER_VERSION;
|
||||
const maxLedgerVersion = options.maxLedgerVersion
|
||||
|| remote.getLedgerSequence();
|
||||
for (let i = minLedgerVersion; i <= maxLedgerVersion; i++) {
|
||||
if (!remote.getServer().hasLedger(i)) { // TODO: optimize this
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function attachTransactionDate(remote, tx, callback) {
|
||||
if (tx.date) {
|
||||
callback(null, tx);
|
||||
return;
|
||||
}
|
||||
if (!tx.ledger_index) {
|
||||
callback(new errors.ApiError('ledger_index not found in tx'));
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {};
|
||||
options.hash = identifier;
|
||||
remote.requestLedger(tx.ledger_index, (error, data) => {
|
||||
if (error) {
|
||||
callback(new errors.NotFoundError('Transaction ledger not found'));
|
||||
} else if (typeof data.ledger.close_time === 'number') {
|
||||
callback(null, _.assign({date: data.ledger.close_time, tx}));
|
||||
} else {
|
||||
callback(new errors.ApiError('Ledger missing close_time'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isLedgerRangeRequest = !_.isUndefined(requestOptions.min_ledger)
|
||||
&& !_.isUndefined(requestOptions.max_ledger);
|
||||
function isTransactionInRange(tx, options) {
|
||||
return (!options.minLedgerVersion
|
||||
|| tx.ledger_index >= options.minLedgerVersion)
|
||||
&& (!options.maxLedgerVersion
|
||||
|| tx.ledger_index <= options.maxLedgerVersion);
|
||||
}
|
||||
|
||||
if (isLedgerRangeRequest) {
|
||||
const minLedger = Number(options.min_ledger);
|
||||
const maxLedger = Number(options.max_ledger);
|
||||
for (let i = minLedger; i <= maxLedger; i++) {
|
||||
if (!api.remote.getServer().hasLedger(i)) {
|
||||
return callback(new errors.NotFoundError('Ledger not found'));
|
||||
}
|
||||
function getTransaction(identifier, options, callback) {
|
||||
validate.identifier(identifier);
|
||||
validate.options(options);
|
||||
|
||||
const remote = this.remote;
|
||||
|
||||
function callbackWrapper(error, tx) {
|
||||
if (error instanceof errors.NotFoundError
|
||||
&& !hasCompleteLedgerRange(remote, options)) {
|
||||
callback(new errors.MissingLedgerHistoryError('Transaction not found,'
|
||||
+ ' but the server\'s ledger history is incomplete'));
|
||||
} else if (!error && !isTransactionInRange(tx, options)) {
|
||||
callback(new errors.NotFoundError('Transaction not found'));
|
||||
} else {
|
||||
callback(error, parseTransaction(tx));
|
||||
}
|
||||
}
|
||||
|
||||
function queryTransaction(async_callback) {
|
||||
api.remote.requestTx({hash: options.hash}, async_callback);
|
||||
}
|
||||
|
||||
function checkIfRelatedToAccount(transaction, async_callback) {
|
||||
if (options.account) {
|
||||
const transactionString = JSON.stringify(transaction);
|
||||
const account_regex = new RegExp(options.account);
|
||||
if (!account_regex.test(transactionString)) {
|
||||
return async_callback(new errors.InvalidRequestError(
|
||||
'Transaction specified did not affect the given account'));
|
||||
}
|
||||
}
|
||||
async_callback(null, transaction);
|
||||
}
|
||||
|
||||
function attachDate(transaction, async_callback) {
|
||||
if (!transaction || transaction.date || !transaction.ledger_index) {
|
||||
return async_callback(null, transaction);
|
||||
}
|
||||
|
||||
api.remote.requestLedger(transaction.ledger_index,
|
||||
function(error, ledgerRequest) {
|
||||
if (error) {
|
||||
return async_callback(new errors.NotFoundError(
|
||||
'Transaction ledger not found'));
|
||||
}
|
||||
|
||||
if (typeof ledgerRequest.ledger.close_time === 'number') {
|
||||
transaction.date = ledgerRequest.ledger.close_time;
|
||||
}
|
||||
|
||||
async_callback(null, transaction);
|
||||
});
|
||||
}
|
||||
|
||||
const steps = [
|
||||
queryTransaction,
|
||||
checkIfRelatedToAccount,
|
||||
attachDate
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
async.waterfall([
|
||||
_.partial(remote.requestTx.bind(remote), {hash: identifier}),
|
||||
_.partial(attachTransactionDate, remote)
|
||||
], callbackWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +107,7 @@ function getAccountTx(api, options, callback) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
let transactions = [];
|
||||
const transactions = [];
|
||||
account_tx_results.transactions.forEach(function(tx_entry) {
|
||||
if (!tx_entry.validated) {
|
||||
return;
|
||||
@@ -321,6 +296,6 @@ module.exports = {
|
||||
DEFAULT_RESULTS_PER_PAGE: DEFAULT_RESULTS_PER_PAGE,
|
||||
NUM_TRANSACTION_TYPES: 5,
|
||||
DEFAULT_LEDGER_BUFFER: 3,
|
||||
getTransaction: getTransaction,
|
||||
getTransaction: utils.wrapCatch(getTransaction),
|
||||
getAccountTransactions: getAccountTransactions
|
||||
};
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const transactionParser = require('ripple-lib-transactionparser');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const constants = utils.common.constants;
|
||||
const parseBalanceChanges = transactionParser.parseBalanceChanges;
|
||||
const parseOrderBookChanges = transactionParser.parseOrderBookChanges;
|
||||
const dropsToXrp = utils.common.dropsToXrp;
|
||||
|
||||
// This is just to support the legacy naming of "counterparty", this
|
||||
// function should be removed when "issuer" is eliminated
|
||||
function renameCounterpartyToIssuerInOrderChanges(orderChanges) {
|
||||
return _.mapValues(orderChanges, function(changes) {
|
||||
return _.map(changes, function(change) {
|
||||
return utils.renameCounterpartyToIssuerInOrder(change);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renameCounterpartyToIssuerInBalanceChanges(balanceChanges) {
|
||||
return _.mapValues(balanceChanges, function(changes) {
|
||||
return _.map(changes, function(change) {
|
||||
return utils.renameCounterpartyToIssuer(change);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that parses bit flags from ripple response
|
||||
*
|
||||
* @param {Number} responseFlags - Integer flag on the ripple response
|
||||
* @param {Object} flags - Object with parameter name and bit flag value pairs
|
||||
*
|
||||
* @returns {Object} parsedFlags - Object with parameter name and boolean
|
||||
* flags depending on response flag
|
||||
*/
|
||||
function parseFlagsFromResponse(responseFlags, flags) {
|
||||
const parsedFlags = {};
|
||||
|
||||
for (let flagName in flags) {
|
||||
const flag = flags[flagName];
|
||||
parsedFlags[flag.name] = Boolean(responseFlags & flag.value);
|
||||
}
|
||||
|
||||
return parsedFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a transaction in rippled tx format
|
||||
* to a ripple-rest payment
|
||||
*
|
||||
* @param {transaction} tx
|
||||
* @param {Function} callback
|
||||
* @param {Object} options
|
||||
*
|
||||
* @callback
|
||||
* @param {Error} error
|
||||
* @param {Object} payment
|
||||
*/
|
||||
|
||||
function isPartialPayment(tx) {
|
||||
return (tx.Flags & ripple.Transaction.flags.Payment.PartialPayment) !== 0;
|
||||
}
|
||||
|
||||
function isNoDirectRipple(tx) {
|
||||
return (tx.Flags & ripple.Transaction.flags.Payment.NoRippleDirect) !== 0;
|
||||
}
|
||||
|
||||
function convertAmount(amount) {
|
||||
if (typeof amount === 'string') {
|
||||
return {
|
||||
value: dropsToXrp(amount),
|
||||
currency: 'XRP',
|
||||
issuer: ''
|
||||
};
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
function parsePaymentMeta(account, tx, meta) {
|
||||
if (_.isUndefined(meta) || _.isEmpty(meta)) {
|
||||
return {};
|
||||
}
|
||||
if (meta.TransactionResult === 'tejSecretInvalid') {
|
||||
throw new Error('Invalid secret provided.');
|
||||
}
|
||||
|
||||
const balanceChanges = renameCounterpartyToIssuerInBalanceChanges(
|
||||
parseBalanceChanges(meta));
|
||||
|
||||
const order_changes = renameCounterpartyToIssuerInOrderChanges(
|
||||
parseOrderBookChanges(meta))[account];
|
||||
|
||||
const partialPayment = (isPartialPayment(tx) && meta.DeliveredAmount) ? {
|
||||
destination_amount_submitted: convertAmount(tx.Amount),
|
||||
source_amount_submitted: convertAmount(tx.SendMax || tx.Amount)
|
||||
} : {};
|
||||
|
||||
return _.assign({
|
||||
result: meta.TransactionResult,
|
||||
balance_changes: balanceChanges[account] || [],
|
||||
source_balance_changes: balanceChanges[tx.Account] || [],
|
||||
destination_balance_changes: balanceChanges[tx.Destination] || [],
|
||||
order_changes: order_changes || []
|
||||
}, partialPayment);
|
||||
}
|
||||
|
||||
function parsePaymentFromTx(account, message, meta) {
|
||||
if (!account) {
|
||||
throw new Error('Internal Error. must supply options.account');
|
||||
}
|
||||
|
||||
const tx = message.tx_json;
|
||||
if (tx.TransactionType !== 'Payment') {
|
||||
throw new Error('Not a payment. The transaction corresponding to '
|
||||
+ 'the given identifier is not a payment.');
|
||||
}
|
||||
|
||||
let amount;
|
||||
// if there is a DeliveredAmount we should use it over Amount there should
|
||||
// always be a DeliveredAmount if the partial payment flag is set. also
|
||||
// there shouldn't be a DeliveredAmount if there's no partial payment flag
|
||||
if (isPartialPayment(tx) && meta && meta.DeliveredAmount) {
|
||||
amount = meta.DeliveredAmount;
|
||||
} else {
|
||||
amount = tx.Amount;
|
||||
}
|
||||
|
||||
const source_amount = utils.parseCurrencyAmount(tx.SendMax || amount, true);
|
||||
const destination_amount = utils.parseCurrencyAmount(amount, true);
|
||||
|
||||
const payment = {
|
||||
// User supplied
|
||||
source_account: tx.Account,
|
||||
source_tag: (tx.SourceTag ? '' + tx.SourceTag : ''),
|
||||
source_amount: source_amount,
|
||||
source_slippage: '0', // TODO: why is this hard-coded?
|
||||
destination_account: tx.Destination,
|
||||
destination_tag: (tx.DestinationTag ? '' + tx.DestinationTag : ''),
|
||||
destination_amount: destination_amount,
|
||||
// Advanced options
|
||||
invoice_id: tx.InvoiceID || '',
|
||||
paths: JSON.stringify(tx.Paths || []),
|
||||
no_direct_ripple: isNoDirectRipple(tx),
|
||||
partial_payment: isPartialPayment(tx),
|
||||
// TODO: Update to use `unaffected` when perspective account in URI
|
||||
// is not affected
|
||||
direction: (account === tx.Account ? 'outgoing' :
|
||||
(account === tx.Destination ? 'incoming' : 'passthrough')),
|
||||
timestamp: (tx.date
|
||||
? new Date(ripple.utils.toTimestamp(tx.date)).toISOString() : ''),
|
||||
fee: dropsToXrp(tx.Fee) || ''
|
||||
};
|
||||
|
||||
if (Array.isArray(tx.Memos) && tx.Memos.length > 0) {
|
||||
payment.memos = [];
|
||||
for (let m = 0; m < tx.Memos.length; m++) {
|
||||
payment.memos.push(tx.Memos[m].Memo);
|
||||
}
|
||||
}
|
||||
|
||||
const fullPayment = _.assign(payment, parsePaymentMeta(account, tx, meta));
|
||||
return _.assign({payment: fullPayment}, meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an OfferCreate or OfferCancel transaction in rippled tx format
|
||||
* to a ripple-rest order_change
|
||||
*
|
||||
* @param {Object} tx
|
||||
* @param {Object} options
|
||||
* @param {String} options.account - The account to use as perspective when
|
||||
* parsing the transaction.
|
||||
*
|
||||
* @returns {Promise.<Object,Error>} - resolves to a parsed OrderChange
|
||||
* transaction or an Error
|
||||
*/
|
||||
|
||||
function parseOrderFromTx(tx, options) {
|
||||
if (!options.account) {
|
||||
throw new Error('Internal Error. must supply options.account');
|
||||
}
|
||||
if (tx.TransactionType !== 'OfferCreate'
|
||||
&& tx.TransactionType !== 'OfferCancel') {
|
||||
throw new Error('Invalid parameter: identifier. The transaction '
|
||||
+ 'corresponding to the given identifier is not an order');
|
||||
}
|
||||
if (tx.meta !== undefined && tx.meta.TransactionResult !== undefined) {
|
||||
if (tx.meta.TransactionResult === 'tejSecretInvalid') {
|
||||
throw new Error('Invalid secret provided.');
|
||||
}
|
||||
}
|
||||
|
||||
let order;
|
||||
const flags = parseFlagsFromResponse(tx.flags, constants.OfferCreateFlags);
|
||||
const action = tx.TransactionType === 'OfferCreate'
|
||||
? 'order_create' : 'order_cancel';
|
||||
const balance_changes = tx.meta
|
||||
? parseBalanceChanges(tx.meta)[options.account] || [] : [];
|
||||
const timestamp = tx.date
|
||||
? new Date(ripple.utils.toTimestamp(tx.date)).toISOString() : '';
|
||||
const order_changes = tx.meta ?
|
||||
parseOrderBookChanges(tx.meta)[options.account] : [];
|
||||
|
||||
let direction;
|
||||
if (options.account === tx.Account) {
|
||||
direction = 'outgoing';
|
||||
} else if (balance_changes.length && order_changes.length) {
|
||||
direction = 'incoming';
|
||||
} else {
|
||||
direction = 'passthrough';
|
||||
}
|
||||
|
||||
if (action === 'order_create') {
|
||||
order = {
|
||||
account: tx.Account,
|
||||
taker_pays: utils.parseCurrencyAmount(tx.TakerPays),
|
||||
taker_gets: utils.parseCurrencyAmount(tx.TakerGets),
|
||||
passive: flags.passive,
|
||||
immediate_or_cancel: flags.immediate_or_cancel,
|
||||
fill_or_kill: flags.fill_or_kill,
|
||||
type: flags.sell ? 'sell' : 'buy',
|
||||
sequence: tx.Sequence
|
||||
};
|
||||
} else {
|
||||
order = {
|
||||
account: tx.Account,
|
||||
type: 'cancel',
|
||||
sequence: tx.Sequence,
|
||||
cancel_sequence: tx.OfferSequence
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hash: tx.hash,
|
||||
ledger: tx.ledger_index,
|
||||
validated: tx.validated,
|
||||
timestamp: timestamp,
|
||||
fee: dropsToXrp(tx.Fee),
|
||||
action: action,
|
||||
direction: direction,
|
||||
order: order,
|
||||
balance_changes: balance_changes,
|
||||
order_changes: order_changes || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the pathfind results returned from rippled into an
|
||||
* array of payments in the ripple-rest format. The client should be
|
||||
* able to submit any of the payments in the array back to ripple-rest.
|
||||
*
|
||||
* @param {rippled Pathfind results} pathfindResults
|
||||
* @param {Amount} options.destination_amount Since this is not returned by
|
||||
* rippled in the pathfind results it can either be added
|
||||
* to the results or included in the options here
|
||||
* @param {RippleAddress} options.source_account Since this is not returned
|
||||
* by rippled in the pathfind results it can either be added
|
||||
* to the results or included in the options here
|
||||
*
|
||||
* @returns {Array of Payments} payments
|
||||
*/
|
||||
function parsePaymentsFromPathFind(pathfindResults) {
|
||||
return pathfindResults.alternatives.map(function(alternative) {
|
||||
return {
|
||||
source_account: pathfindResults.source_account,
|
||||
source_tag: '',
|
||||
source_amount: (typeof alternative.source_amount === 'string' ?
|
||||
{
|
||||
value: dropsToXrp(alternative.source_amount),
|
||||
currency: 'XRP',
|
||||
issuer: ''
|
||||
} :
|
||||
{
|
||||
value: alternative.source_amount.value,
|
||||
currency: alternative.source_amount.currency,
|
||||
issuer: (typeof alternative.source_amount.issuer !== 'string'
|
||||
|| alternative.source_amount.issuer === pathfindResults.source_account
|
||||
? '' : alternative.source_amount.issuer)
|
||||
}),
|
||||
source_slippage: '0',
|
||||
destination_account: pathfindResults.destination_account,
|
||||
destination_tag: '',
|
||||
destination_amount: (
|
||||
typeof pathfindResults.destination_amount === 'string' ?
|
||||
{
|
||||
value: dropsToXrp(pathfindResults.destination_amount),
|
||||
currency: 'XRP',
|
||||
issuer: ''
|
||||
} :
|
||||
{
|
||||
value: pathfindResults.destination_amount.value,
|
||||
currency: pathfindResults.destination_amount.currency,
|
||||
issuer: pathfindResults.destination_amount.issuer
|
||||
}),
|
||||
invoice_id: '',
|
||||
paths: JSON.stringify(alternative.paths_computed),
|
||||
partial_payment: false,
|
||||
no_direct_ripple: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseOrderCancellationResponse(message, meta) {
|
||||
const order = {
|
||||
account: message.tx_json.Account,
|
||||
fee: dropsToXrp(message.tx_json.Fee),
|
||||
offer_sequence: message.tx_json.OfferSequence,
|
||||
sequence: message.tx_json.Sequence
|
||||
};
|
||||
return _.assign({order: order}, meta);
|
||||
}
|
||||
|
||||
function parseOrderResponse(message, meta) {
|
||||
const order = {
|
||||
account: message.tx_json.Account,
|
||||
taker_gets: utils.parseCurrencyAmount(message.tx_json.TakerGets),
|
||||
taker_pays: utils.parseCurrencyAmount(message.tx_json.TakerPays),
|
||||
fee: dropsToXrp(message.tx_json.Fee),
|
||||
type: (message.tx_json.Flags & ripple.Transaction.flags.OfferCreate.Sell)
|
||||
> 0 ? 'sell' : 'buy',
|
||||
sequence: message.tx_json.Sequence
|
||||
};
|
||||
return _.assign({order: order}, meta);
|
||||
}
|
||||
|
||||
function parseTrustLineResponse(message, meta) {
|
||||
const limit = message.tx_json.LimitAmount;
|
||||
const parsedFlags = parseFlagsFromResponse(message.tx_json.Flags,
|
||||
constants.TrustSetResponseFlags);
|
||||
const trustline = {
|
||||
account: message.tx_json.Account,
|
||||
limit: limit.value,
|
||||
currency: limit.currency,
|
||||
counterparty: limit.issuer,
|
||||
account_allows_rippling: !parsedFlags.prevent_rippling,
|
||||
account_trustline_frozen: parsedFlags.account_trustline_frozen,
|
||||
authorized: parsedFlags.authorized ? parsedFlags.authorized : undefined
|
||||
};
|
||||
return _.assign({trustline: trustline}, meta);
|
||||
}
|
||||
|
||||
function parseSettingsResponse(settings, message, meta) {
|
||||
const _settings = {};
|
||||
for (let flagName in constants.AccountSetIntFlags) {
|
||||
_settings[flagName] = settings[flagName];
|
||||
}
|
||||
|
||||
for (let fieldName in constants.AccountRootFields) {
|
||||
_settings[fieldName] = settings[fieldName];
|
||||
}
|
||||
|
||||
_.assign(_settings, parseFlagsFromResponse(message.tx_json.Flags,
|
||||
constants.AccountSetResponseFlags));
|
||||
return _.assign({settings: _settings}, meta);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parsePaymentFromTx: parsePaymentFromTx,
|
||||
parsePaymentsFromPathFind: parsePaymentsFromPathFind,
|
||||
parseOrderFromTx: parseOrderFromTx,
|
||||
parseCancelOrderFromTx: parseOrderCancellationResponse,
|
||||
parseSubmitOrderFromTx: parseOrderResponse,
|
||||
parseTrustResponseFromTx: parseTrustLineResponse,
|
||||
parseSettingsResponseFromTx: parseSettingsResponse,
|
||||
parseFlagsFromResponse: parseFlagsFromResponse
|
||||
};
|
||||
@@ -43,30 +43,6 @@ function parseLedger(ledger) {
|
||||
return 'validated';
|
||||
}
|
||||
|
||||
function parseCurrencyAmount(rippledAmount, useIssuer) {
|
||||
const amount = {};
|
||||
|
||||
if (typeof rippledAmount === 'string') {
|
||||
amount.currency = 'XRP';
|
||||
amount.value = common.dropsToXrp(rippledAmount);
|
||||
if (useIssuer) {
|
||||
amount.issuer = '';
|
||||
} else {
|
||||
amount.counterparty = '';
|
||||
}
|
||||
} else {
|
||||
amount.currency = rippledAmount.currency;
|
||||
amount.value = rippledAmount.value;
|
||||
if (useIssuer) {
|
||||
amount.issuer = rippledAmount.issuer;
|
||||
} else {
|
||||
amount.counterparty = rippledAmount.issuer;
|
||||
}
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
function signum(num) {
|
||||
return (num === 0) ? 0 : (num > 0 ? 1 : -1);
|
||||
}
|
||||
@@ -121,11 +97,11 @@ function attachDate(api, baseTransactions, callback) {
|
||||
|
||||
module.exports = {
|
||||
parseLedger: parseLedger,
|
||||
parseCurrencyAmount: parseCurrencyAmount,
|
||||
compareTransactions: compareTransactions,
|
||||
renameCounterpartyToIssuer: renameCounterpartyToIssuer,
|
||||
renameCounterpartyToIssuerInOrder: renameCounterpartyToIssuerInOrder,
|
||||
attachDate: attachDate,
|
||||
wrapCatch: common.wrapCatch,
|
||||
common: common
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const BigNumber = require('bignumber.js');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
const convertAmount = utils.common.convertAmount;
|
||||
const toRippledAmount = utils.common.toRippledAmount;
|
||||
|
||||
function isSendMaxAllowed(payment) {
|
||||
const srcAmt = payment.source.amount;
|
||||
@@ -58,7 +58,7 @@ function createPaymentTransaction(account, payment) {
|
||||
const transactionData = {
|
||||
from: payment.source.address,
|
||||
to: payment.destination.address,
|
||||
amount: convertAmount(payment.destination.amount)
|
||||
amount: toRippledAmount(payment.destination.amount)
|
||||
};
|
||||
|
||||
if (payment.invoiceID) {
|
||||
@@ -101,7 +101,7 @@ function createPaymentTransaction(account, payment) {
|
||||
}
|
||||
}
|
||||
|
||||
let flags = [];
|
||||
const flags = [];
|
||||
if (payment.allowPartialPayment) {
|
||||
flags.push('PartialPayment');
|
||||
}
|
||||
|
||||
@@ -2,35 +2,28 @@
|
||||
/* eslint-disable valid-jsdoc */
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const utils = require('./utils');
|
||||
const ripple = utils.common.core;
|
||||
const validate = utils.common.validate;
|
||||
const constants = utils.common.constants;
|
||||
const InvalidRequestError = utils.common.errors.InvalidRequestError;
|
||||
const AccountFlagIndices = utils.common.constants.AccountFlagIndices;
|
||||
const AccountFields = utils.common.constants.AccountFields;
|
||||
const ValidationError = utils.common.errors.ValidationError;
|
||||
|
||||
// Emptry string passed to setting will clear it
|
||||
const CLEAR_SETTING = '';
|
||||
|
||||
/**
|
||||
* Set integer flags on a transaction based on input and a flag map
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Object} input - Object whose properties determine whether
|
||||
* to update the transaction's SetFlag or ClearFlag property
|
||||
* @param {Object} flags - Object that maps property names to transaction
|
||||
* integer flag values
|
||||
*
|
||||
* @returns undefined
|
||||
*/
|
||||
function setTransactionIntFlags(transaction, values, flags) {
|
||||
for (let flagName in flags) {
|
||||
const value = values[flagName];
|
||||
const code = flags[flagName];
|
||||
|
||||
if (value === true) {
|
||||
transaction.tx_json.SetFlag = code;
|
||||
} else if (value === false) {
|
||||
transaction.tx_json.ClearFlag = code;
|
||||
function setTransactionFlags(transaction, values) {
|
||||
const keys = Object.keys(values);
|
||||
assert(keys.length === 1, 'ERROR: can only set one setting per transaction');
|
||||
const flagName = keys[0];
|
||||
const value = values[flagName];
|
||||
const index = AccountFlagIndices[flagName];
|
||||
if (index !== undefined) {
|
||||
if (value) {
|
||||
transaction.tx_json.SetFlag = index;
|
||||
} else {
|
||||
transaction.tx_json.ClearFlag = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +38,9 @@ function setTransactionIntFlags(transaction, values, flags) {
|
||||
*
|
||||
* @returns undefined
|
||||
*/
|
||||
function setTransactionFields(transaction, input, fieldSchema) {
|
||||
for (let fieldName in fieldSchema) {
|
||||
function setTransactionFields(transaction, input) {
|
||||
const fieldSchema = AccountFields;
|
||||
for (const fieldName in fieldSchema) {
|
||||
const field = fieldSchema[fieldName];
|
||||
let value = input[field.name];
|
||||
|
||||
@@ -66,8 +60,7 @@ function setTransactionFields(transaction, input, fieldSchema) {
|
||||
// Field is fixed length, why are we checking here though?
|
||||
// We could move this to validateInputs
|
||||
if (value.length > field.length) {
|
||||
throw new InvalidRequestError(
|
||||
'Parameter length exceeded: ' + fieldName);
|
||||
throw new ValidationError('Parameter length exceeded: ' + fieldName);
|
||||
} else if (value.length < field.length) {
|
||||
value = _.padLeft(value, field.length);
|
||||
}
|
||||
@@ -113,10 +106,8 @@ function createSettingsTransaction(account, settings) {
|
||||
const transaction = new ripple.Transaction();
|
||||
transaction.accountSet(account);
|
||||
|
||||
utils.setTransactionBitFlags(transaction, settings,
|
||||
constants.AccountSetFlags);
|
||||
setTransactionIntFlags(transaction, settings, constants.AccountSetIntFlags);
|
||||
setTransactionFields(transaction, settings, constants.AccountRootFields);
|
||||
setTransactionFlags(transaction, settings);
|
||||
setTransactionFields(transaction, settings);
|
||||
|
||||
if (transaction.tx_json.TransferRate !== undefined) {
|
||||
transaction.tx_json.TransferRate = convertTransferRate(
|
||||
|
||||
@@ -21,7 +21,7 @@ const common = require('../common');
|
||||
*/
|
||||
function setTransactionBitFlags(transaction: any, values: any, flags: any):
|
||||
void {
|
||||
for (let flagName in flags) {
|
||||
for (const flagName in flags) {
|
||||
const flagValue = values[flagName];
|
||||
const flagConversions = flags[flagName];
|
||||
|
||||
@@ -79,20 +79,9 @@ function createTxJSON(transaction: any, remote: any,
|
||||
}
|
||||
}
|
||||
|
||||
function wrapCatch(asyncFunction: any): any {
|
||||
return function() {
|
||||
try {
|
||||
asyncFunction.apply(this, arguments);
|
||||
} catch (error) {
|
||||
const callback = arguments[arguments.length - 1];
|
||||
callback(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setTransactionBitFlags: setTransactionBitFlags,
|
||||
createTxJSON: createTxJSON,
|
||||
wrapCatch: wrapCatch,
|
||||
wrapCatch: common.wrapCatch,
|
||||
common: common
|
||||
};
|
||||
|
||||
@@ -210,9 +210,9 @@ Remote.flags = {
|
||||
RequireAuth: 0x00040000, // require a authorization to hold IOUs
|
||||
DisallowXRP: 0x00080000, // disallow sending XRP
|
||||
DisableMaster: 0x00100000, // force regular key
|
||||
DefaultRipple: 0x00800000,
|
||||
NoFreeze: 0x00200000, // permanently disallowed freezing trustlines
|
||||
GlobalFreeze: 0x00400000 // trustlines globally frozen
|
||||
GlobalFreeze: 0x00400000, // trustlines globally frozen
|
||||
DefaultRipple: 0x00800000
|
||||
},
|
||||
// Offer
|
||||
offer: {
|
||||
@@ -448,7 +448,7 @@ Remote.prototype.disconnect = function(callback_) {
|
||||
throw new Error('No servers available, not disconnecting');
|
||||
}
|
||||
|
||||
let callback = lodash.isFunction(callback_)
|
||||
const callback = lodash.isFunction(callback_)
|
||||
? callback_
|
||||
: function() {};
|
||||
|
||||
@@ -980,7 +980,7 @@ Remote.prototype.requestLedgerEntry = function(type, callback_) {
|
||||
|
||||
const self = this;
|
||||
|
||||
let request = new Request(this, 'ledger_entry');
|
||||
const request = new Request(this, 'ledger_entry');
|
||||
const callback = lodash.isFunction(type) ? type : callback_;
|
||||
|
||||
// Transparent caching. When .request() is invoked, look in the Remote object
|
||||
@@ -1232,7 +1232,7 @@ Remote.accountRequest = function(command, options_, callback_) {
|
||||
};
|
||||
}
|
||||
|
||||
let {account, ledger, peer, limit, marker} = options;
|
||||
const {account, ledger, peer, limit, marker} = options;
|
||||
|
||||
// if a marker is given, we need a ledger
|
||||
// check if a valid ledger_index or ledger_hash is provided
|
||||
@@ -1246,8 +1246,7 @@ Remote.accountRequest = function(command, options_, callback_) {
|
||||
const request = new Request(this, command);
|
||||
|
||||
if (account) {
|
||||
account = UInt160.json_rewrite(account);
|
||||
request.message.account = account;
|
||||
request.message.account = UInt160.json_rewrite(account);
|
||||
}
|
||||
|
||||
request.selectLedger(ledger);
|
||||
@@ -1257,20 +1256,20 @@ Remote.accountRequest = function(command, options_, callback_) {
|
||||
}
|
||||
|
||||
if (!isNaN(limit)) {
|
||||
limit = Number(limit);
|
||||
let _limit = Number(limit);
|
||||
|
||||
// max for 32-bit unsigned int is 4294967295
|
||||
// we'll clamp to 1e9
|
||||
if (limit > 1e9) {
|
||||
limit = 1e9;
|
||||
if (_limit > 1e9) {
|
||||
_limit = 1e9;
|
||||
}
|
||||
// min for 32-bit unsigned int is 0
|
||||
// we'll clamp to 0
|
||||
if (limit < 0) {
|
||||
limit = 0;
|
||||
if (_limit < 0) {
|
||||
_limit = 0;
|
||||
}
|
||||
|
||||
request.message.limit = limit;
|
||||
request.message.limit = _limit;
|
||||
}
|
||||
|
||||
if (marker) {
|
||||
@@ -1551,7 +1550,7 @@ Remote.prototype.requestTxHistory = function(start_, callback_) {
|
||||
|
||||
const request = new Request(this, 'tx_history');
|
||||
let start = start_;
|
||||
let callback = callback_;
|
||||
const callback = callback_;
|
||||
|
||||
if (lodash.isPlainObject(start)) {
|
||||
start = start.start;
|
||||
@@ -1606,7 +1605,7 @@ Remote.prototype.requestBookOffers = function(options_, callback_) {
|
||||
};
|
||||
}
|
||||
|
||||
let {gets, pays, taker, ledger, limit} = options;
|
||||
const {gets, pays, taker, ledger, limit} = options;
|
||||
|
||||
const request = new Request(this, 'book_offers');
|
||||
|
||||
@@ -1630,20 +1629,20 @@ Remote.prototype.requestBookOffers = function(options_, callback_) {
|
||||
request.selectLedger(ledger);
|
||||
|
||||
if (!isNaN(limit)) {
|
||||
limit = Number(limit);
|
||||
let _limit = Number(limit);
|
||||
|
||||
// max for 32-bit unsigned int is 4294967295
|
||||
// we'll clamp to 1e9
|
||||
if (limit > 1e9) {
|
||||
limit = 1e9;
|
||||
if (_limit > 1e9) {
|
||||
_limit = 1e9;
|
||||
}
|
||||
// min for 32-bit unsigned int is 0
|
||||
// we'll clamp to 0
|
||||
if (limit < 0) {
|
||||
limit = 0;
|
||||
if (_limit < 0) {
|
||||
_limit = 0;
|
||||
}
|
||||
|
||||
request.message.limit = limit;
|
||||
request.message.limit = _limit;
|
||||
}
|
||||
|
||||
request.callback(callback);
|
||||
@@ -1661,7 +1660,8 @@ Remote.prototype.requestBookOffers = function(options_, callback_) {
|
||||
Remote.prototype.requestWalletAccounts = function(options_, callback_) {
|
||||
utils.assert(this.trusted); // Don't send secrets.
|
||||
|
||||
let options, callback = callback_;
|
||||
let options;
|
||||
const callback = callback_;
|
||||
|
||||
if (lodash.isPlainObject(options_)) {
|
||||
options = lodash.merge({}, options_);
|
||||
|
||||
@@ -3,6 +3,7 @@ const _ = require('lodash');
|
||||
const assert = require('assert-diff');
|
||||
const setupAPI = require('./setup-api');
|
||||
const address = require('./fixtures/addresses').ACCOUNT;
|
||||
const hashes = require('./fixtures/hashes');
|
||||
const paymentSpecification = require('./fixtures/payment-specification');
|
||||
const paymentResponse = require('./fixtures/payment-response');
|
||||
const orderSpecification = require('./fixtures/order-specification');
|
||||
@@ -20,6 +21,7 @@ const signOutput = require('./fixtures/sign-output');
|
||||
const MockPRNG = require('./mock-prng');
|
||||
const sjcl = require('../src').sjcl;
|
||||
const submitResponse = require('./fixtures/submit-response');
|
||||
const transactionResponse = require('./fixtures/transaction-response');
|
||||
|
||||
function checkResult(expected, done, error, response) {
|
||||
if (error) {
|
||||
@@ -84,4 +86,9 @@ describe('RippleAPI', function() {
|
||||
this.api.getBalances(address, {},
|
||||
_.partial(checkResult, balancesResponse, done));
|
||||
});
|
||||
|
||||
it('getTransaction', function(done) {
|
||||
this.api.getTransaction(hashes.VALID_TRANSACTION_HASH, {},
|
||||
_.partial(checkResult, transactionResponse, done));
|
||||
});
|
||||
});
|
||||
|
||||
3
test/fixtures/mock.js
vendored
3
test/fixtures/mock.js
vendored
@@ -638,7 +638,8 @@ const BINARY_TRANSACTION_SYNTH = module.exports.BINARY_TRANSACTION_SYNTH = {
|
||||
date: 416447810,
|
||||
hash: 'F4AB442A6D4CBB935D66E1DA7309A5FC71C7143ED4049053EC14E3875B0CF9BF',
|
||||
inLedger: 348860,
|
||||
ledger_index: 348860
|
||||
ledger_index: 348860,
|
||||
validated: true
|
||||
};
|
||||
|
||||
module.exports.transactionResponse = function(request) {
|
||||
|
||||
3
test/fixtures/settings-response.json
vendored
3
test/fixtures/settings-response.json
vendored
@@ -2,9 +2,8 @@
|
||||
"Flags": 1048576,
|
||||
"TransactionType": "AccountSet",
|
||||
"Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
|
||||
"SetFlag": 6,
|
||||
"EmailHash": "98B4375E1D753E5B91627516F6D70977",
|
||||
"Domain": "726970706C652E636F6D",
|
||||
"Flags": 0,
|
||||
"LastLedgerSequence": 8820052,
|
||||
"Fee": "12",
|
||||
"Sequence": 23
|
||||
|
||||
5
test/fixtures/settings-specification.json
vendored
5
test/fixtures/settings-specification.json
vendored
@@ -1,6 +1,3 @@
|
||||
{
|
||||
"domain": "ripple.com",
|
||||
"emailHash": "98B4375E1D753E5B91627516F6D70977",
|
||||
"noFreeze": true,
|
||||
"disallowIncomingXRP": true
|
||||
"domain": "ripple.com"
|
||||
}
|
||||
|
||||
89
test/fixtures/transaction-response.json
vendored
Normal file
89
test/fixtures/transaction-response.json
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"type": "payment",
|
||||
"address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
|
||||
"specification": {
|
||||
"source": {
|
||||
"address": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
|
||||
"amount": {
|
||||
"currency": "XRP",
|
||||
"value": "1.112209"
|
||||
}
|
||||
},
|
||||
"destination": {
|
||||
"address": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM",
|
||||
"amount": {
|
||||
"currency": "USD",
|
||||
"value": "0.001",
|
||||
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM"
|
||||
}
|
||||
},
|
||||
"paths": "[[{\"currency\":\"USD\",\"issuer\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"type\":48,\"type_hex\":\"0000000000000030\"},{\"account\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"currency\":\"USD\",\"issuer\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"type\":49,\"type_hex\":\"0000000000000031\"}]]",
|
||||
"allowPartialPayment": false,
|
||||
"noDirectRipple": false
|
||||
},
|
||||
"outcome": {
|
||||
"result": "tesSUCCESS",
|
||||
"timestamp": "2013-03-12T23:56:50.000Z",
|
||||
"fee": "0.00001",
|
||||
"balanceChanges": {
|
||||
"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo": [
|
||||
{
|
||||
"counterparty": "rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM",
|
||||
"currency": "USD",
|
||||
"value": "-0.001"
|
||||
},
|
||||
{
|
||||
"counterparty": "r9tGqzZgKxVFvzKFdUqXAqTzazWBUia8Qr",
|
||||
"currency": "USD",
|
||||
"value": "0.001002"
|
||||
}
|
||||
],
|
||||
"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM": [
|
||||
{
|
||||
"counterparty": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
|
||||
"currency": "USD",
|
||||
"value": "0.001"
|
||||
}
|
||||
],
|
||||
"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59": [
|
||||
{
|
||||
"counterparty": "",
|
||||
"currency": "XRP",
|
||||
"value": "-1.101208"
|
||||
}
|
||||
],
|
||||
"r9tGqzZgKxVFvzKFdUqXAqTzazWBUia8Qr": [
|
||||
{
|
||||
"counterparty": "",
|
||||
"currency": "XRP",
|
||||
"value": "1.101198"
|
||||
},
|
||||
{
|
||||
"counterparty": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
|
||||
"currency": "USD",
|
||||
"value": "-0.001002"
|
||||
}
|
||||
]
|
||||
},
|
||||
"orderbookChanges": {
|
||||
"r9tGqzZgKxVFvzKFdUqXAqTzazWBUia8Qr": [
|
||||
{
|
||||
"taker_pays": {
|
||||
"currency": "XRP",
|
||||
"counterparty": "",
|
||||
"value": "-1.101198"
|
||||
},
|
||||
"taker_gets": {
|
||||
"currency": "USD",
|
||||
"counterparty": "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo",
|
||||
"value": "-0.001002"
|
||||
},
|
||||
"sequence": 58,
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ledgerVersion": 348860,
|
||||
"sequence": 4
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user