-
Notifications
You must be signed in to change notification settings - Fork 22
/
ilqp.js
202 lines (180 loc) · 6.92 KB
/
ilqp.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
'use strict'
const co = require('co')
const IlpPacket = require('ilp-packet')
const Packet = require('../utils/packet')
const debug = require('debug')('ilp:ilqp')
const moment = require('moment')
const BigNumber = require('bignumber.js')
const { safeConnect, startsWith, xor, omitUndefined } =
require('../utils')
const DEFAULT_MESSAGE_TIMEOUT = 5000
const DEFAULT_EXPIRY_DURATION = 10
function * _handleConnectorResponses (connectors, promises) {
if (connectors.length === 0) {
throw new Error('no connectors specified')
}
const quotes = []
const errors = []
for (let c = 0; c < connectors.length; ++c) {
try {
const quote = yield promises[c]
if (quote.responseType === IlpPacket.Type.TYPE_ILP_ERROR) {
throw new Error('remote quote error: ' + quote.name)
} else if (quote) {
quotes.push(Object.assign({connector: connectors[c]}, quote))
} else {
throw new Error('got empty quote response: ' + quote)
}
} catch (err) {
errors.push(connectors[c] + ': ' + (err.message || err))
}
}
if (quotes.length === 0) {
throw new Error('Errors occurred during quoting: ' +
errors.join(', '))
}
return quotes
}
function _serializeQuoteRequest (requestParams) {
if (requestParams.sourceAmount) {
return IlpPacket.serializeIlqpBySourceRequest(requestParams)
}
if (requestParams.destinationAmount) {
return IlpPacket.serializeIlqpByDestinationRequest(requestParams)
}
return IlpPacket.serializeIlqpLiquidityRequest(requestParams)
}
/**
* @param {Object} params
* @param {Object} params.plugin The LedgerPlugin used to send quote request
* @param {String} params.connector The ILP address of the connector to quote from
* @param {Object} params.quoteQuery ILQP request packet parameters
* @param {Integer} [params.timeout] Milliseconds
* @returns {Object} Ilqp{Liquidity,BySourceAmount,ByDestinationAmount}Response or IlpError
*/
function quoteByConnector ({
plugin,
connector,
quoteQuery,
timeout
}) {
const prefix = plugin.getInfo().prefix
const requestPacket = _serializeQuoteRequest(quoteQuery)
const requestType = requestPacket[0]
debug('remote quote connector=' + connector, 'query=' + JSON.stringify(quoteQuery))
return plugin.sendRequest({
ledger: prefix,
from: plugin.getAccount(),
to: connector,
ilp: requestPacket.toString('base64'),
timeout: timeout || DEFAULT_MESSAGE_TIMEOUT
}).then((response) => {
if (!response.ilp) throw new Error('Quote response has no packet')
const responsePacket = Buffer.from(response.ilp, 'base64')
const responseType = responsePacket[0]
const packetData = IlpPacket.deserializeIlpPacket(responsePacket).data
const isErrorPacket = responseType === IlpPacket.Type.TYPE_ILP_ERROR
if (isErrorPacket) {
debug('remote quote error connector=' + connector, 'ilpError=' + JSON.stringify(packetData))
}
if (isErrorPacket || responseType === requestType + 1) {
return Object.assign({responseType}, packetData)
}
throw new Error('Quote response packet has incorrect type')
})
}
function _getCheaperQuote (quote1, quote2) {
if (quote1.sourceAmount) {
const source1 = new BigNumber(quote1.sourceAmount)
if (source1.lessThan(quote2.sourceAmount)) return quote1
} else {
const dest1 = new BigNumber(quote1.destinationAmount)
if (dest1.greaterThan(quote2.destinationAmount)) return quote1
}
return quote2
}
/**
* @module ILQP
*/
/**
* @param {Object} plugin The LedgerPlugin used to send quote request
* @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. This value is a string representation of an integer, expressed in the lowest indivisible unit supported by the ledger.
* @param {String} [query.destinationAmount] Either the sourceAmount or destinationAmount must be specified. This value is a string representation of an integer, expressed in the lowest indivisible unit supported by the ledger.
* @param {String|Number} [query.destinationExpiryDuration] Number of seconds between when the destination transfer is proposed and when it expires.
* @param {Array} [query.connectors] List of ILP addresses of connectors to use for this quote.
* @returns {Promise<Quote>}
*/
function * quote (plugin, {
sourceAddress,
destinationAddress,
sourceAmount,
destinationAmount,
destinationExpiryDuration,
connectors,
timeout
}) {
if (!xor(sourceAmount, destinationAmount)) {
throw new Error('should provide source or destination amount but not both' +
' ' + JSON.stringify({ sourceAmount, destinationAmount }))
}
yield safeConnect(plugin)
const prefix = plugin.getInfo().prefix
const amount = sourceAmount || destinationAmount
const destinationHoldDuration = +(destinationExpiryDuration || DEFAULT_EXPIRY_DURATION)
if (startsWith(prefix, destinationAddress)) {
debug('returning a local transfer to', destinationAddress, 'for', amount)
return omitUndefined({
// send directly to the destination
connectorAccount: destinationAddress,
sourceAmount: amount,
destinationAmount: amount,
sourceExpiryDuration: destinationHoldDuration.toString()
})
}
const quoteQuery = omitUndefined({
destinationAccount: destinationAddress,
destinationHoldDuration: destinationHoldDuration * 1000,
sourceAmount,
destinationAmount
})
const quoteConnectors = connectors || plugin.getInfo().connectors || []
debug('quoting', amount,
(sourceAmount ? '(source amount)' : '(destination amount)'),
'to', destinationAddress, 'via', quoteConnectors)
// handle connector responses will return all successful quotes, or
// throw all errors if there were none.
const quotes = yield _handleConnectorResponses(
quoteConnectors,
quoteConnectors.map((connector) => {
return quoteByConnector({ plugin, connector, quoteQuery, timeout })
}))
const bestQuote = quotes.reduce(_getCheaperQuote)
const sourceHoldDuration = bestQuote.sourceHoldDuration / 1000
debug('got best quote from connector:', bestQuote.connector, 'quote:', JSON.stringify(bestQuote))
return omitUndefined({
sourceAmount: sourceAmount || bestQuote.sourceAmount,
destinationAmount: destinationAmount || bestQuote.destinationAmount,
connectorAccount: bestQuote.connector,
sourceExpiryDuration: sourceHoldDuration.toString(),
// current time plus sourceExpiryDuration, for convenience
expiresAt: moment()
.add(sourceHoldDuration, 'seconds')
.toISOString()
})
}
function * quoteByPacket (plugin, packet) {
const { account, amount } = Packet.parse(packet)
return yield quote(plugin, {
destinationAmount: amount,
destinationAddress: account
})
}
module.exports = {
_getCheaperQuote,
quoteByConnector,
quote: co.wrap(quote),
quoteByPacket: co.wrap(quoteByPacket)
}