UNPKG

4.67 kBPlain TextView Raw
1import BigNumber from 'bignumber.js'
2import Accounts from './accounts'
3import RoutingTable from './routing-table'
4import RateBackend from './rate-backend'
5import Config from './config'
6import reduct = require('reduct')
7import * as IlpPacket from 'ilp-packet'
8import { create as createLogger } from '../common/log'
9const log = createLogger('route-builder')
10const {
11 InsufficientTimeoutError,
12 InvalidPacketError,
13 PeerUnreachableError,
14 UnreachableError
15} = IlpPacket.Errors
16
17export default class RouteBuilder {
18 protected accounts: Accounts
19 protected routingTable: RoutingTable
20 protected backend: RateBackend
21 protected config: Config
22
23 protected isTrivialRate: boolean
24
25 constructor (deps: reduct.Injector) {
26 this.accounts = deps(Accounts)
27 this.routingTable = deps(RoutingTable)
28 this.backend = deps(RateBackend)
29 this.config = deps(Config)
30
31 this.isTrivialRate =
32 this.config.backend === 'one-to-one' &&
33 this.config.spread === 0
34 }
35
36 getNextHop (sourceAccount: string, destinationAccount: string) {
37 const route = this.routingTable.resolve(destinationAccount)
38
39 if (!route) {
40 log.debug('no route found. destinationAccount=' + destinationAccount)
41 throw new UnreachableError('no route found. source=' + sourceAccount + ' destination=' + destinationAccount)
42 }
43
44 if (!this.config.reflectPayments && sourceAccount === route.nextHop) {
45 log.debug('refusing to route payments back to sender. sourceAccount=%s destinationAccount=%s', sourceAccount, destinationAccount)
46 throw new UnreachableError('refusing to route payments back to sender. sourceAccount=' + sourceAccount + ' destinationAccount=' + destinationAccount)
47 }
48
49 return route.nextHop
50 }
51
52 /**
53 * @typedef {Object} NextHopPacketInfo
54 * @property {string} nextHop Address of the next peer to forward the packet to
55 * @property {Buffer} nextHopPacket Outgoing packet
56 */
57
58 /**
59 * Get next ILP prepare packet.
60 *
61 * Given a previous ILP prepare packet, returns the next ILP prepare packet in
62 * the chain.
63 *
64 * @param {string} sourceAccount ILP address of our peer who sent us the packet
65 * @param {IlpPrepare} sourcePacket (Parsed packet that we received
66 * @returns {NextHopPacketInfo} Account and packet for next hop
67 */
68 async getNextHopPacket (sourceAccount: string, sourcePacket: IlpPacket.IlpPrepare) {
69 const {
70 amount,
71 executionCondition,
72 expiresAt,
73 destination,
74 data
75 } = sourcePacket
76
77 log.trace(
78 'constructing next hop packet. sourceAccount=%s sourceAmount=%s destination=%s',
79 sourceAccount, amount, destination
80 )
81
82 if (destination.length < 1) {
83 throw new InvalidPacketError('missing destination.')
84 }
85
86 const nextHop = this.getNextHop(sourceAccount, destination)
87
88 log.trace('determined next hop. nextHop=%s', nextHop)
89
90 const rate = await this.backend.getRate(sourceAccount, nextHop)
91
92 log.trace('determined local rate. rate=%s', rate)
93
94 this._verifyPluginIsConnected(nextHop)
95
96 const nextAmount = new BigNumber(amount).times(rate).integerValue(BigNumber.ROUND_FLOOR)
97
98 return {
99 nextHop,
100 nextHopPacket: {
101 amount: nextAmount.toString(),
102 expiresAt: this._getDestinationExpiry(expiresAt),
103 executionCondition,
104 destination,
105 data
106 }
107 }
108 }
109
110 _getDestinationExpiry (sourceExpiry: Date) {
111 if (!sourceExpiry) {
112 throw new TypeError('source expiry must be a Date')
113 }
114 const sourceExpiryTime = sourceExpiry.getTime()
115
116 if (sourceExpiryTime < Date.now()) {
117 throw new InsufficientTimeoutError('source transfer has already expired. sourceExpiry=' + sourceExpiry.toISOString() + ' currentTime=' + (new Date().toISOString()))
118 }
119
120 // We will set the next transfer's expiry based on the source expiry and our
121 // minMessageWindow, but cap it at our maxHoldTime.
122 const destinationExpiryTime = Math.min(sourceExpiryTime - this.config.minMessageWindow, Date.now() + this.config.maxHoldTime)
123
124 if ((destinationExpiryTime - Date.now()) < this.config.minMessageWindow) {
125 throw new InsufficientTimeoutError('source transfer expires too soon to complete payment. actualSourceExpiry=' + sourceExpiry.toISOString() + ' requiredSourceExpiry=' + (new Date(Date.now() + 2 * this.config.minMessageWindow).toISOString()) + ' currentTime=' + (new Date().toISOString()))
126 }
127
128 return new Date(destinationExpiryTime)
129 }
130
131 _verifyPluginIsConnected (account: string) {
132 if (!this.accounts.getPlugin(account).isConnected()) {
133 throw new PeerUnreachableError('no connection to account. account=' + account)
134 }
135 }
136}