UNPKG

ilp-core

Version:

ILP core module managing ledger abstraction

228 lines (200 loc) 8 kB
'use strict' const co = require('co') const EventEmitter = require('eventemitter2') const BigNumber = require('bignumber.js') const isUndefined = require('lodash/fp/isUndefined') const omitUndefined = require('lodash/fp/omitBy')(isUndefined) const routing = require('ilp-routing') const debug = require('debug')('ilp-core') class Core extends EventEmitter { /** * @param {Object} options * @param {ilp-routing.RoutingTables} options.routingTables */ constructor (options) { if (!options) options = {} super() this.clientList = [] // Client[] this.clients = {} // { prefix ⇒ Client } this.tables = options.routingTables || new routing.RoutingTables([], null) const core = this this._relayEvent = function () { const event = arguments[0] const args = Array.prototype.slice.call(arguments, 1) return core.emitAsync.apply(core, [event, this].concat(args)) } } /** * `getClient`/`getPlugin` will find the Client/Plugin corresponding * to either a local ledger address (e.g. "us.fed.wf."). * * @param {IlpAddress} address * @returns {Client|null} */ getClient (ledger) { if (ledger.slice(-1) !== '.') { throw new Error('prefix must end with "."') } return this.clients[ledger] || null } /** * @param {IlpAddress} address * @returns {LedgerPlugin|null} */ getPlugin (ledger) { const client = this.getClient(ledger) return client && client.getPlugin() } /** * @returns {Client[]} */ getClients () { return this.clientList.slice() } /** * @param {IlpAddress} prefix * @param {Client} client */ addClient (prefix, client) { if (prefix.slice(-1) !== '.') { throw new Error('prefix must end with "."') } client.onAny(this._relayEvent) this.clientList.push(client) this.clients[prefix] = client } /** * @param {IlpAddress} prefix * @returns {Client} */ removeClient (prefix) { const client = this.getClient(prefix) if (!client) return client.offAny(this._relayEvent) this.clientList.splice(this.clientList.indexOf(client), 1) delete this.clients[prefix] return client } connect (options) { return Promise.all(this.clientList.map((client) => client.connect(options))) } disconnect () { return Promise.all(this.clientList.map((client) => client.disconnect())) } /** * @param {Object} query * @param {String} query.sourceAddress Sender's address * @param {String} query.destinationAddress Recipient's address * @param {String} [query.sourceAmount] Either the sourceAmount or destinationAmount must be specified * @param {String} [query.destinationAmount] Either the sourceAmount or destinationAmount must be specified * @param {String|Number} [query.sourceExpiryDuration] Number of seconds between when the source transfer is proposed and when it expires. * @param {String|Number} [query.destinationExpiryDuration] Number of seconds between when the destination transfer is proposed and when it expires. * @returns {Promise<Quote>} */ quote (query) { return co(this._quote.bind(this), query) } * _quote (query) { const hop = this._findBestHopForAmount( query.sourceAddress, query.destinationAddress, query.sourceAmount, query.destinationAmount) if (!hop) return null const sourceLedger = hop.sourceLedger const connectorAccount = this.getPlugin(sourceLedger).getAccount() const sourceExpiryDuration = parseDuration(query.sourceExpiryDuration) const destinationExpiryDuration = (sourceExpiryDuration || query.destinationExpiryDuration) ? parseDuration(query.destinationExpiryDuration) : 5 const quote = {connectorAccount, sourceLedger} const localQuote = Object.assign( getExpiryDurations(sourceExpiryDuration, destinationExpiryDuration, hop.minMessageWindow), hopToQuote(hop), quote) const isLocalPath = hop.isLocal const isDirectPath = getLedgerPrefix(query.destinationAddress) === hop.finalLedger // If we know a local route to the destinationAddress, use the local route. // If the route's destination is exactly the prefix being targeted, use the local route. // Otherwise, ask a connector closer to the destination. // if (isLocalPath || isDirectPath) return localQuote if (isLocalPath) return localQuote if (isDirectPath) debug('quote is direct, but multi-hop-direct is disabled') let headHop // Quote by source amount if (query.sourceAmount) { headHop = this.tables.findBestHopForSourceAmount( sourceLedger, hop.destinationCreditAccount, query.sourceAmount) } const sourceClient = this.getClient(hop.destinationLedger) const intermediateConnector = hop.destinationCreditAccount const tailQuote = yield sourceClient._getQuote(intermediateConnector, omitUndefined({ source_address: intermediateConnector, source_amount: (!query.sourceAmount) ? undefined : (new BigNumber(headHop.destinationAmount)).toFixed(0, BigNumber.ROUND_DOWN), destination_address: query.destinationAddress, destination_amount: (!query.sourceAmount) ? hop.finalAmount : undefined, source_expiry_duration: (sourceExpiryDuration && headHop) ? (sourceExpiryDuration - headHop.minMessageWindow) : undefined, destination_expiry_duration: destinationExpiryDuration, slippage: '0' // Slippage will be applied at the first connector, not an intermediate one. })) // If no remote quote can be found, just use the local one. // if (!tailQuote) return localQuote if (!tailQuote) { debug('_quote no tailQuote - returning null!') return null } // Quote by destination amount if (query.destinationAmount) { headHop = this.tables.findBestHopForDestinationAmount( sourceLedger, intermediateConnector, tailQuote.source_amount) } const minMessageWindow = headHop.minMessageWindow + (parseFloat(tailQuote.source_expiry_duration) - parseFloat(tailQuote.destination_expiry_duration)) const curve = tailQuote.liquidity_curve && (new routing.LiquidityCurve(headHop.liquidityCurve)).join( new routing.LiquidityCurve(tailQuote.liquidity_curve)).getPoints() return Object.assign({ nextLedger: headHop.destinationLedger, destinationLedger: tailQuote.destination_ledger, sourceAmount: headHop.sourceAmount, destinationAmount: tailQuote.destination_amount, minMessageWindow: minMessageWindow, liquidityCurve: curve }, quote, getExpiryDurations(sourceExpiryDuration, destinationExpiryDuration, minMessageWindow)) } _findBestHopForAmount (sourceLedger, destinationAddress, sourceAmount, destinationAmount) { return (!sourceAmount) ? this.tables.findBestHopForDestinationAmount( sourceLedger, destinationAddress, destinationAmount) : this.tables.findBestHopForSourceAmount( sourceLedger, destinationAddress, sourceAmount) } } function hopToQuote (hop) { return omitUndefined({ nextLedger: hop.destinationLedger, destinationLedger: hop.finalLedger, sourceAmount: hop.sourceAmount, destinationAmount: hop.finalAmount, minMessageWindow: hop.minMessageWindow, liquidityCurve: hop.liquidityCurve, additionalInfo: hop.additionalInfo }) } /** * @param {IlpAddress} address * @returns {IlpAddress} prefix */ function getLedgerPrefix (address) { return address.split('.').slice(0, -1).join('.') + '.' } function parseDuration (expiryDuration) { return expiryDuration ? parseFloat(expiryDuration) : undefined } function getExpiryDurations (sourceExpiryDuration, destinationExpiryDuration, minMessageWindow) { return { sourceExpiryDuration: sourceExpiryDuration || (destinationExpiryDuration + minMessageWindow), destinationExpiryDuration: destinationExpiryDuration || (sourceExpiryDuration - minMessageWindow) } } module.exports = Core