UNPKG

five-bells-routing

Version:
254 lines (227 loc) 9.12 kB
'use strict' const debug = require('debug')('five-bells-routing:routing-tables') const PrefixMap = require('./prefix-map') const Route = require('./route') const RoutingTable = require('./routing-table') // A next hop of PAIR distinguishes a local pair A→B from a complex route // that just happens to be local, i.e. when A→C & C→B are local pairs. const PAIR = 'PAIR' class RoutingTables { /** * @param {String} baseURI * @param {Object[]} localRoutes * @param {Integer} expiryDuration milliseconds */ constructor (baseURI, localRoutes, expiryDuration) { this.baseURI = baseURI this.expiryDuration = expiryDuration this.sources = new PrefixMap() // { "sourceLedger" => RoutingTable } this.accounts = {} // { "connector;ledger" => accountURI } this.addLocalRoutes(localRoutes) } /** * @param {RouteData[]|Route[]} localRoutes - Each local route should include the optional * `destinationAccount` parameter. `connector` should always be `baseURI`. */ addLocalRoutes (_localRoutes) { const localRoutes = _localRoutes.map(Route.fromData) for (const localRoute of localRoutes) { const table = this.sources.get(localRoute.sourceLedger) || this.sources.insert(localRoute.sourceLedger, new RoutingTable()) table.addRoute(localRoute.destinationLedger, PAIR, localRoute) } localRoutes.forEach((route) => this.addRoute(route)) } /** * Given a `route` B→C, create a route A→C for each source ledger A with a * local route to B. * * @param {Route|RouteData} _route from ledger B→C * @returns {Boolean} whether or not a new route was added */ addRoute (_route) { const route = Route.fromData(_route) this.accounts[route.connector + ';' + route.sourceLedger] = route.sourceAccount if (route.destinationAccount) { this.accounts[route.connector + ';' + route.destinationLedger] = route.destinationAccount } let added = false this.eachSource((tableFromA, ledgerA) => { added = this._addRouteFromSource(tableFromA, ledgerA, route) || added }) return added } _addRouteFromSource (tableFromA, ledgerA, routeFromBToC) { const ledgerB = routeFromBToC.sourceLedger const ledgerC = routeFromBToC.destinationLedger const connectorFromBToC = routeFromBToC.connector let added = false // Don't create local route A→B→C if local route A→C already exists. if (this.baseURI === connectorFromBToC && this._getLocalPairRoute(ledgerA, ledgerC)) return // Don't create A→B→C when A→B is not a local pair. const routeFromAToB = this._getLocalPairRoute(ledgerA, ledgerB) if (!routeFromAToB) return // Make sure the routes can be joined. const routeFromAToC = routeFromAToB.join(routeFromBToC, this.expiryDuration) if (!routeFromAToC) return if (!this._getRoute(ledgerA, ledgerC, connectorFromBToC)) added = true tableFromA.addRoute(ledgerC, connectorFromBToC, routeFromAToC) // Given pairs A↔B,B→C; on addRoute(C→D) create A→D after creating B→D. if (added) added = this.addRoute(routeFromAToC) || added return added } _removeRoute (ledgerB, ledgerC, connectorFromBToC) { this.eachSource((tableFromA, ledgerA) => { tableFromA.removeRoute(ledgerC, connectorFromBToC) }) } removeExpiredRoutes () { this.eachRoute((routeFromAToB, ledgerA, ledgerB, nextHop) => { if (routeFromAToB.isExpired()) { this._removeRoute(ledgerA, ledgerB, nextHop) } }) } /** * @param {function(tableFromA, ledgerA)} fn */ eachSource (fn) { this.sources.each(fn) } /** * @param {function(routeFromAToB, ledgerA, ledgerB, nextHop)} fn */ eachRoute (fn) { this.eachSource((tableFromA, ledgerA) => { tableFromA.destinations.each((routesFromAToB, ledgerB) => { for (const nextHop of routesFromAToB.keys()) { const routeFromAToB = routesFromAToB.get(nextHop) fn(routeFromAToB, ledgerA, ledgerB, nextHop) } }) }) } /** * @param {Integer} maxPoints * @returns {Routes} */ toJSON (maxPoints) { const routes = [] this.eachSource((table, sourceLedger) => { table.destinations.each((routesByConnector, destinationLedger) => { const combinedRoute = combineRoutesByConnector(routesByConnector, maxPoints) const combinedRouteData = combinedRoute.toJSON() combinedRouteData.connector = this.baseURI combinedRouteData.source_account = this._getAccount(this.baseURI, sourceLedger) routes.push(combinedRouteData) }) }) return routes } _getAccount (connector, ledger) { return this.accounts[connector + ';' + ledger] } _getLocalPairRoute (ledgerA, ledgerB) { return this._getRoute(ledgerA, ledgerB, PAIR) } _getRoute (ledgerA, ledgerB, connector) { const routesFromAToB = this.sources.get(ledgerA).destinations.get(ledgerB) if (!routesFromAToB) return return routesFromAToB.get(connector) } /** * Find the best intermediate ledger (`nextLedger`) to use after `sourceLedger` on * the way to `finalLedger`. * This connector must have `[sourceLedger, nextLedger]` as a pair. * * @param {IlpAddress} sourceAddress * @param {IlpAddress} finalAddress * @param {String} finalAmount * @returns {Object} */ findBestHopForDestinationAmount (sourceAddress, finalAddress, finalAmount) { const nextHop = this._findBestHopForDestinationAmount(sourceAddress, finalAddress, +finalAmount) if (!nextHop) return // sourceLedger is the longest known prefix of sourceAddress (likewise for // finalLedger/finalAddress). const sourceLedger = nextHop.bestRoute.sourceLedger const finalLedger = nextHop.bestRoute.destinationLedger const nextLedger = nextHop.bestRoute.nextLedger const routeFromAToB = this._getLocalPairRoute(sourceLedger, nextLedger) const isFinal = nextLedger === finalLedger return { isFinal: isFinal, connector: nextHop.bestHop, sourceLedger: sourceLedger, sourceAmount: nextHop.bestCost.toString(), destinationLedger: nextLedger, destinationAmount: routeFromAToB.amountAt(nextHop.bestCost).toString(), destinationCreditAccount: isFinal ? null : this._getAccount(nextHop.bestHop, nextLedger), finalLedger: finalLedger, finalAmount: finalAmount, minMessageWindow: nextHop.bestRoute.minMessageWindow, additionalInfo: isFinal ? nextHop.bestRoute.additionalInfo : undefined } } /** * @param {IlpAddress} sourceAddress * @param {IlpAddress} finalAddress * @param {String} sourceAmount * @returns {Object} */ findBestHopForSourceAmount (sourceAddress, finalAddress, sourceAmount) { const nextHop = this._findBestHopForSourceAmount(sourceAddress, finalAddress, +sourceAmount) if (!nextHop) return const sourceLedger = nextHop.bestRoute.sourceLedger const finalLedger = nextHop.bestRoute.destinationLedger const nextLedger = nextHop.bestRoute.nextLedger const routeFromAToB = this._getLocalPairRoute(sourceLedger, nextLedger) const isFinal = nextLedger === finalLedger return { isFinal: isFinal, connector: nextHop.bestHop, sourceLedger: sourceLedger, sourceAmount: sourceAmount, destinationLedger: nextLedger, destinationAmount: routeFromAToB.amountAt(+sourceAmount).toString(), destinationCreditAccount: isFinal ? null : this._getAccount(nextHop.bestHop, nextLedger), finalLedger: finalLedger, finalAmount: nextHop.bestValue.toString(), minMessageWindow: nextHop.bestRoute.minMessageWindow, additionalInfo: isFinal ? nextHop.bestRoute.additionalInfo : undefined } } _findBestHopForSourceAmount (source, destination, amount) { debug('searching best hop from %s to %s for %s (by src amount)', source, destination, amount) const table = this.sources.resolve(source) if (!table) { debug('source %s is not in known sources: %s', source, Object.keys(this.sources.prefixes)) return undefined } return this._rewriteLocalHop( table.findBestHopForSourceAmount(destination, amount)) } _findBestHopForDestinationAmount (source, destination, amount) { debug('searching best hop from %s to %s for %s (by dst amount)', source, destination, amount) const table = this.sources.resolve(source) if (!table) { debug('source %s is not in known sources: %s', source, Object.keys(this.sources.prefixes)) return undefined } return this._rewriteLocalHop( table.findBestHopForDestinationAmount(destination, amount)) } _rewriteLocalHop (hop) { if (hop && hop.bestHop === PAIR) hop.bestHop = this.baseURI return hop } } function combineRoutesByConnector (routesByConnector, maxPoints) { const routes = routesByConnector.values() let totalRoute = routes.next().value for (const subRoute of routes) { totalRoute = totalRoute.combine(subRoute) } return totalRoute.simplify(maxPoints) } module.exports = RoutingTables