1 | import BigNumber from 'bignumber.js'
|
2 | import Accounts from './accounts'
|
3 | import RoutingTable from './routing-table'
|
4 | import RateBackend from './rate-backend'
|
5 | import Config from './config'
|
6 | import reduct = require('reduct')
|
7 | import * as IlpPacket from 'ilp-packet'
|
8 | import { create as createLogger } from '../common/log'
|
9 | const log = createLogger('route-builder')
|
10 | const {
|
11 | InsufficientTimeoutError,
|
12 | InvalidPacketError,
|
13 | PeerUnreachableError,
|
14 | UnreachableError
|
15 | } = IlpPacket.Errors
|
16 |
|
17 | export 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 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
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 |
|
121 |
|
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 | }
|