UNPKG

divvy-rest

Version:

A RESTful API for submitting payments and monitoring accounts on the Divvy network.

476 lines (419 loc) 14.9 kB
/* eslint-disable valid-jsdoc */ 'use strict'; var divvy = require('divvy-lib'); var utils = require('./utils'); var _ = require('lodash'); var Promise = require('bluebird'); var parseBalanceChanges = require('divvy-lib-transactionparser') .parseBalanceChanges; var parseOrderBookChanges = require('divvy-lib-transactionparser') .parseOrderBookChanges; function TxToRestConverter() {} // This is just to support the legacy naming of "counterparty", this // function should be removed when "issuer" is eliminated function renameCounterpartyToIssuer(orderChanges) { return _.mapValues(orderChanges, function(changes) { return _.map(changes, function(change) { var converted; if (change.taker_pays) { converted = _.omit(change, ['taker_pays', 'taker_gets']); converted.taker_pays = { issuer: change.taker_pays.counterparty, value: change.taker_pays.value, currency: change.taker_pays.currency }; converted.taker_gets = { issuer: change.taker_gets.counterparty, value: change.taker_gets.value, currency: change.taker_gets.currency }; } else { converted = _.omit(change, 'counterparty'); converted.issuer = change.counterparty; } return converted; }); }); } // Orders var OfferCreateFlags = { Passive: {name: 'passive', value: divvy.Transaction.flags.OfferCreate.Passive}, ImmediateOrCancel: {name: 'immediate_or_cancel', value: divvy.Transaction.flags.OfferCreate.ImmediateOrCancel}, FillOrKill: {name: 'fill_or_kill', value: divvy.Transaction.flags.OfferCreate.FillOrKill}, Sell: {name: 'sell', value: divvy.Transaction.flags.OfferCreate.Sell} }; // Paths /** * Convert a transaction in divvyd tx format * to a divvy-rest payment * * @param {transaction} tx * @param {Function} callback * @param {Object} options * * @callback * @param {Error} error * @param {Object} payment */ TxToRestConverter.prototype.parsePaymentFromTx = function(tx, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } if (!options.account) { if (callback !== undefined) { callback(new Error('Internal Error. must supply options.account')); } return; } if (tx.TransactionType !== 'Payment') { if (callback !== undefined) { callback(new Error('Not a payment. The transaction corresponding to ' + 'the given identifier is not a payment.')); } return; } if (tx.meta !== undefined && tx.meta.TransactionResult !== undefined) { if (tx.meta.TransactionResult === 'tejSecretInvalid') { if (callback !== undefined) { callback(new Error('Invalid secret provided.')); } return; } } var Amount; var isPartialPayment = tx.Flags & 0x00020000 ? true : false; // 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 && tx.meta.DeliveredAmount) { Amount = tx.meta.DeliveredAmount; } else { Amount = tx.Amount; } var balanceChanges = tx.meta ? renameCounterpartyToIssuer(parseBalanceChanges(tx.meta)) : []; var order_changes = tx.meta ? renameCounterpartyToIssuer( parseOrderBookChanges(tx.meta))[options.account] : []; var source_amount = tx.SendMax ? utils.parseCurrencyAmount(tx.SendMax, true) : utils.parseCurrencyAmount(Amount, true); var destination_amount = utils.parseCurrencyAmount(Amount, true); var payment = { // User supplied source_account: tx.Account, source_tag: (tx.SourceTag ? '' + tx.SourceTag : ''), source_amount: source_amount, source_slippage: '0', 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_divvy: (tx.Flags & 0x00010000 ? true : false), partial_payment: isPartialPayment, // Generated after validation // TODO: Update to use `unaffected` when perspective account in URI // is not affected direction: (options.account ? (options.account === tx.Account ? 'outgoing' : (options.account === tx.Destination ? 'incoming' : 'passthrough')) : ''), result: tx.meta ? tx.meta.TransactionResult : '', timestamp: (tx.date ? new Date(divvy.utils.toTimestamp(tx.date)).toISOString() : ''), fee: utils.dropsToXdv(tx.Fee) || '', balance_changes: balanceChanges[options.account] || [], source_balance_changes: balanceChanges[tx.Account] || [], destination_balance_changes: balanceChanges[tx.Destination] || [], order_changes: order_changes || [] }; if (Array.isArray(tx.Memos) && tx.Memos.length > 0) { payment.memos = []; for (var m = 0; m < tx.Memos.length; m++) { payment.memos.push(tx.Memos[m].Memo); } } if (isPartialPayment && tx.meta && tx.meta.DeliveredAmount) { payment.destination_amount_submitted = (typeof tx.Amount === 'object' ? tx.Amount : { value: utils.dropsToXdv(tx.Amount), currency: 'XDV', issuer: '' }); payment.source_amount_submitted = (tx.SendMax ? (typeof tx.SendMax === 'object' ? tx.SendMax : { value: utils.dropsToXdv(tx.SendMax), currency: 'XDV', issuer: '' }) : (typeof tx.Amount === 'string' ? { value: utils.dropsToXdv(tx.Amount), currency: 'XDV', issuer: '' } : tx.Amount)); } callback(null, payment); }; /** * Convert an OfferCreate or OfferCancel transaction in divvyd tx format * to a divvy-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 */ TxToRestConverter.prototype.parseOrderFromTx = function(tx, options) { return new Promise(function(resolve, reject) { if (!options.account) { reject(new Error('Internal Error. must supply options.account')); } if (tx.TransactionType !== 'OfferCreate' && tx.TransactionType !== 'OfferCancel') { reject(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') { reject(new Error('Invalid secret provided.')); } } var order; var flags = TxToRestConverter.prototype.parseFlagsFromResponse(tx.flags, OfferCreateFlags); var action = tx.TransactionType === 'OfferCreate' ? 'order_create' : 'order_cancel'; var balance_changes = tx.meta ? parseBalanceChanges(tx.meta)[options.account] || [] : []; var timestamp = tx.date ? new Date(divvy.utils.toTimestamp(tx.date)).toISOString() : ''; var order_changes = tx.meta ? parseOrderBookChanges(tx.meta)[options.account] : []; var 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 }; } resolve({ hash: tx.hash, ledger: tx.ledger_index, validated: tx.validated, timestamp: timestamp, fee: utils.dropsToXdv(tx.Fee), action: action, direction: direction, order: order, balance_changes: balance_changes, order_changes: order_changes || [] }); }); }; // Paths /** * Convert the pathfind results returned from divvyd into an * array of payments in the divvy-rest format. The client should be * able to submit any of the payments in the array back to divvy-rest. * * @param {divvyd Pathfind results} pathfindResults * @param {Amount} options.destination_amount Since this is not returned by * divvyd in the pathfind results it can either be added * to the results or included in the options here * @param {DivvyAddress} options.source_account Since this is not returned * by divvyd in the pathfind results it can either be added * to the results or included in the options here * * @returns {Array of Payments} payments */ TxToRestConverter.prototype.parsePaymentsFromPathFind = function(pathfindResults, callback) { var payments = []; pathfindResults.alternatives.forEach(function(alternative) { var payment = { source_account: pathfindResults.source_account, source_tag: '', source_amount: (typeof alternative.source_amount === 'string' ? { value: utils.dropsToXdv(alternative.source_amount), currency: 'XDV', 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: utils.dropsToXdv(pathfindResults.destination_amount), currency: 'XDV', 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_divvy: false }; payments.push(payment); }); callback(null, payments); }; TxToRestConverter.prototype.parseCancelOrderFromTx = function(message, meta, callback) { var result = { order: {} }; _.extend(result.order, { account: message.tx_json.Account, fee: utils.dropsToXdv(message.tx_json.Fee), offer_sequence: message.tx_json.OfferSequence, sequence: message.tx_json.Sequence }); _.extend(result, meta); callback(null, result); }; TxToRestConverter.prototype.parseSubmitOrderFromTx = function(message, meta, callback) { var result = { order: {} }; _.extend(result.order, { account: message.tx_json.Account, taker_gets: utils.parseCurrencyAmount(message.tx_json.TakerGets), taker_pays: utils.parseCurrencyAmount(message.tx_json.TakerPays), fee: utils.dropsToXdv(message.tx_json.Fee), type: (message.tx_json.Flags & divvy.Transaction.flags.OfferCreate.Sell) > 0 ? 'sell' : 'buy', sequence: message.tx_json.Sequence }); _.extend(result, meta); callback(null, result); }; // Trustlines var TrustSetResponseFlags = { NoDivvy: {name: 'prevent_rippling', value: divvy.Transaction.flags.TrustSet.NoDivvy}, SetFreeze: {name: 'account_trustline_frozen', value: divvy.Transaction.flags.TrustSet.SetFreeze}, SetAuth: {name: 'authorized', value: divvy.Transaction.flags.TrustSet.SetAuth} }; TxToRestConverter.prototype.parseTrustResponseFromTx = function(message, meta, callback) { var result = { trustline: {} }; var line = message.tx_json.LimitAmount; var parsedFlags = TxToRestConverter.prototype.parseFlagsFromResponse( message.tx_json.Flags, TrustSetResponseFlags); _.extend(result.trustline, { account: message.tx_json.Account, limit: line.value, currency: line.currency, counterparty: line.issuer, account_allows_rippling: !parsedFlags.prevent_rippling, account_trustline_frozen: parsedFlags.account_trustline_frozen, authorized: parsedFlags.authorized ? parsedFlags.authorized : undefined }); _.extend(result, meta); callback(null, result); }; // Settings var AccountSetResponseFlags = { RequireDestTag: {name: 'require_destination_tag', value: divvy.Transaction.flags.AccountSet.RequireDestTag}, RequireAuth: {name: 'require_authorization', value: divvy.Transaction.flags.AccountSet.RequireAuth}, DisallowXDV: {name: 'disallow_xdv', value: divvy.Transaction.flags.AccountSet.DisallowXDV} }; TxToRestConverter.prototype.parseSettingResponseFromTx = function(settings, message, meta, callback) { var result = { settings: {} }; // cannot require at the top due to circular dependency var settingsModule = require('../settings'); for (var flagName in settingsModule.AccountSetIntFlags) { var flag = settingsModule.AccountSetIntFlags[flagName]; result.settings[flag.name] = settings[flag.name]; } for (var fieldName in settingsModule.AccountRootFields) { var field = settingsModule.AccountRootFields[fieldName]; result.settings[field.name] = settings[field.name]; } _.extend(result.settings, TxToRestConverter.prototype.parseFlagsFromResponse( message.tx_json.Flags, AccountSetResponseFlags)); _.extend(result, meta); callback(null, result); }; // Utilities /** * Helper that parses bit flags from divvy response * * @param {Number} responseFlags - Integer flag on the divvy 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 */ TxToRestConverter.prototype.parseFlagsFromResponse = function(responseFlags, flags) { var parsedFlags = {}; for (var flagName in flags) { var flag = flags[flagName]; parsedFlags[flag.name] = Boolean(responseFlags & flag.value); } return parsedFlags; }; module.exports = new TxToRestConverter();