@stellar-broker/client 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.js CHANGED
@@ -1,357 +1,360 @@
1
- import {Networks, StrKey} from '@stellar/stellar-sdk'
2
- import errors from './errors.js'
3
- import {buildEvent} from './events.js'
4
- import {processTxRequest} from './tx-processor.js'
5
- import {validateQuoteRequest} from './quote-request.js'
6
- import {QuoteResult} from './quote-result.js'
7
- import {AuthorizationWrapper} from './authorization.js'
8
-
9
- /**
10
- * Client for StellarBroker service
11
- */
12
- export class StellarBrokerClient {
13
- /**
14
- * @param {ClientInitializationParams} params
15
- */
16
- constructor(params) {
17
- this.partnerKey = params.partnerKey
18
- this.emitter = new EventTarget()
19
- this.network = Networks.PUBLIC
20
- this.trader = params.account
21
- if (params.authorization) {
22
- this.authorization = new AuthorizationWrapper(params.authorization)
23
- }
24
- }
25
-
26
- /**
27
- * @type {string}
28
- * @private
29
- */
30
- uid
31
- /**
32
- * @type {ClientSessionStatus}
33
- * @readonly
34
- */
35
- status = 'disconnected'
36
- /**
37
- * @type {WebSocket}
38
- * @private
39
- */
40
- socket
41
- /**
42
- * @type {EventTarget}
43
- * @private
44
- */
45
- emitter
46
- /**
47
- * @type {string}
48
- * @private
49
- */
50
- origin = 'https://api.stellar.broker'
51
- /**
52
- * Stellar network passphrase
53
- * @type {string}
54
- * @readonly
55
- */
56
- network = ''
57
- /**
58
- * Last quote request submitted by the client
59
- * @type {QuoteParams}
60
- * @readonly
61
- */
62
- quoteRequest
63
- /**
64
- * Last quote received from the sever
65
- * @type {QuoteResult}
66
- * @readonly
67
- */
68
- lastQuote
69
- /**
70
- * Current quote accepted for the trading
71
- * @private
72
- */
73
- tradeQuote
74
- /**
75
- * API key of the partner
76
- * @type {string}
77
- * @readonly
78
- */
79
- partnerKey
80
- /**
81
- * Account secret key or authorization callback
82
- * @type {AuthorizationWrapper}
83
- * @private
84
- */
85
- authorization
86
- /**
87
- * Trader account public key
88
- * @type {string}
89
- * @readonly
90
- */
91
- trader
92
-
93
- /**
94
- * Connect to the StellarBroker server
95
- * @return {Promise<StellarBrokerClient>}
96
- */
97
- connect() {
98
- if (this.socket?.readyState === WebSocket.OPEN)
99
- return Promise.resolve(this) //already opened
100
- this.socket = new WebSocket(this.origin + '/ws?partner=' + encodeURIComponent(this.partnerKey))
101
- this.socket.onmessage = this.processMessage.bind(this)
102
- this.socket.onclose = () => {
103
- if (this.status !== 'disconnected') {
104
- this.status = 'disconnected'
105
- this.notifyError(errors.notConnected())
106
- }
107
- console.log('Connection closed')
108
- }
109
-
110
- this.socket.onerror = e => console.error(e)
111
-
112
- return new Promise((confirm, reject) => { //TODO: use 'once'
113
- const expirationTimeout = setTimeout(() => reject(this), 2000) // 2s timeout
114
- this.onSocketOpen = () => {
115
- this.status = 'ready'
116
- this.heartbeat()
117
- this.onSocketOpen = undefined
118
- clearTimeout(expirationTimeout)
119
- confirm(this)
120
- }
121
- })
122
- }
123
-
124
- /**
125
- * Process incoming message
126
- * @param message
127
- * @private
128
- */
129
- processMessage(message) {
130
- const raw = JSON.parse(message.data)
131
- switch (raw.type) {
132
- case 'connected':
133
- this.uid = raw.uid
134
- if (this.onSocketOpen) {
135
- this.onSocketOpen()
136
- }
137
- if (this.status === 'disconnected') {
138
- this.status = 'ready'
139
- }
140
- break
141
- case 'quote':
142
- this.lastQuote = new QuoteResult(raw.quote)
143
- //send event to the client app
144
- this.emitter.dispatchEvent(buildEvent('quote', this.lastQuote))
145
- break
146
- case 'paused':
147
- //quotation paused due to inactivity
148
- this.emitter.dispatchEvent(buildEvent('paused', {}))
149
- break
150
- case 'tx':
151
- if (this.status !== 'trade') {
152
- console.log('Received tx in a non-trading state', this.status, raw)
153
- return //skip unless trading is in progress
154
- }
155
- processTxRequest(this, raw)
156
- .then(xdr => {
157
- this.send({
158
- type: 'tx',
159
- hash: raw.hash,
160
- xdr
161
- })
162
- })
163
- .catch(e => this.notifyError(e))
164
- break
165
- case 'stop':
166
- this.emitter.dispatchEvent(buildEvent('finished', {
167
- status: raw.status,
168
- sold: raw.sold,
169
- bought: raw.bought
170
- }, 'result'))
171
- this.status = 'ready'
172
- this.tradeQuote = undefined
173
- break
174
- case 'progress':
175
- this.emitter.dispatchEvent(buildEvent('progress', {
176
- sold: raw.sold,
177
- bought: raw.bought
178
- }, 'status'))
179
- break
180
- case 'ping':
181
- if (raw.uid === this.uid) {
182
- this.heartbeat()
183
- this.send({type: 'pong', uid: this.uid})
184
- }
185
- break
186
- case 'error':
187
- this.stop()
188
- this.notifyError(raw.error)
189
- break
190
- default:
191
- console.log('Unknown message type: ' + raw.type)
192
- break
193
- }
194
- }
195
-
196
- /**
197
- * Request swap quote
198
- * @param {QuoteParams} params - Quote parameters
199
- */
200
- quote(params) {
201
- if (this.status === 'trade')
202
- throw errors.tradeInProgress()
203
- this.quoteRequest = validateQuoteRequest(params)
204
-
205
- this.status = 'quote'
206
- this.connect()
207
- .then(() => {
208
- this.status = 'quote'
209
- this.send({
210
- type: 'quote',
211
- ...this.quoteRequest
212
- })
213
- })
214
- }
215
-
216
- /**
217
- * Confirm current quote and start trading
218
- * @param {string} [account] - Trader account address (overrides value provided in the constructor)
219
- * @param {ClientAuthorizationParams} [authorization] - Authorization params (overrides value provided in the constructor)
220
- */
221
- confirmQuote(account, authorization) {
222
- if (this.status === 'disconnected')
223
- throw errors.notConnected()
224
- if (this.status === 'trade')
225
- throw errors.tradeInProgress()
226
- if (this.status !== 'quote' || !this.lastQuote)
227
- throw errors.quoteNotSet()
228
- if ((new Date() - this.lastQuote.ts) > 10_000) //do not allow stale quotes quoted more than 10s ago
229
- throw errors.quoteExpired()
230
- if (this.lastQuote.status !== 'success')
231
- throw errors.quoteError(this.lastQuote.error || 'quote not available')
232
- if (account) {
233
- this.trader = account
234
- }
235
- if (authorization) {
236
- this.authorization = new AuthorizationWrapper(authorization)
237
- }
238
- const {trader} = this
239
- if (!trader || !StrKey.isValidEd25519PublicKey(trader)) //ensure that a trader account address provided
240
- throw errors.invalidQuoteParam('account', 'Invalid trader account address: ' + (!trader ? 'missing' : trader))
241
- if (!this.authorization)
242
- throw errors.invalidQuoteParam('authorization', 'Client authorization not provided')
243
- this.tradeQuote = this.lastQuote
244
- this.send({
245
- type: 'trade',
246
- account: trader
247
- })
248
- this.status = 'trade'
249
- }
250
-
251
- /**
252
- * Stop quotation/trading
253
- */
254
- stop() {
255
- if (this.status !== 'trade' && this.status !== 'quote')
256
- return
257
- this.send({type: 'stop'})
258
- this.tradeQuote = undefined
259
- this.status = 'ready'
260
- }
261
-
262
- /**
263
- * @param {{}} data
264
- * @private
265
- */
266
- send(data) {
267
- this.socket.send(JSON.stringify(data))
268
- }
269
-
270
- /**
271
- * @private
272
- */
273
- heartbeat() {
274
- clearTimeout(this.pingHandler)
275
- this.pingHandler = setTimeout(() => {
276
- console.warn('Lost connection with the server')
277
- this.socket.close()
278
- }, 7_000) // 7 seconds heartbeat timeout
279
- }
280
-
281
- /**
282
- * Add event listener
283
- * @param {StellarBrokerClientEvent} type
284
- * @param {function} callback
285
- */
286
- on(type, callback) {
287
- validateEventType(type)
288
- this.emitter.addEventListener(type, callback)
289
- }
290
-
291
- /**
292
- * Add event listener that will be executed once
293
- * @param {StellarBrokerClientEvent} type
294
- * @param {function} callback
295
- */
296
- once(type, callback) {
297
- validateEventType(type)
298
- this.emitter.addEventListener(type, callback, {once: true})
299
- }
300
-
301
- /**
302
- * Remove event listener
303
- * @param {StellarBrokerClientEvent} type
304
- * @param {function} callback
305
- */
306
- off(type, callback) {
307
- validateEventType(type)
308
- this.emitter.removeEventListener(type, callback)
309
- }
310
-
311
- /**
312
- * Close underlying connection and finalize the client
313
- */
314
- close() {
315
- try {
316
- //TODO: remove all attached event listeners
317
- this.status = 'disconnected'
318
- this.socket.close()
319
- } catch (e) {
320
- }
321
- }
322
-
323
- /**
324
- * @param {Error} e
325
- * @private
326
- */
327
- notifyError(e) {
328
- console.error(e)
329
- try {
330
- this.emitter.dispatchEvent(buildEvent('error', e instanceof Error ? e.message : e))
331
- } catch (e) {
332
- console.error(e)
333
- }
334
- }
335
- }
336
-
337
- const stellarBrokerEvents = new Set(['quote', 'paused', 'progress', 'finished', 'error'])
338
-
339
- function validateEventType(type) {
340
- if (!stellarBrokerEvents.has(type))
341
- throw errors.unsupportedEventType(type)
342
- }
343
-
344
- /**
345
- * @typedef {object} ClientInitializationParams
346
- * @property {string} [partnerKey] - Partner key
347
- * @property {string} [account] - Trader account address
348
- * @property {ClientAuthorizationParams} [authorization] - Authorization method, either account secret key or an authorization callback
349
- */
350
-
351
- /**
352
- * @typedef {'disconnected'|'ready'|'quote'|'trade'} ClientSessionStatus - Current client session status
353
- */
354
-
355
- /**
356
- * @typedef {'quote'|'paused'|'progress'|'finished'|'error'} StellarBrokerClientEvent - Event type generated by the client
357
- */
1
+ import {Networks, StrKey} from '@stellar/stellar-sdk'
2
+ import errors from './errors.js'
3
+ import {buildEvent} from './events.js'
4
+ import {processTxRequest} from './tx-processor.js'
5
+ import {validateQuoteRequest} from './quote-request.js'
6
+ import {QuoteResult} from './quote-result.js'
7
+ import {AuthorizationWrapper} from './authorization.js'
8
+
9
+ /**
10
+ * Client for StellarBroker service
11
+ */
12
+ export class StellarBrokerClient {
13
+ /**
14
+ * @param {ClientInitializationParams} params
15
+ */
16
+ constructor(params) {
17
+ this.partnerKey = params.partnerKey
18
+ this.emitter = new EventTarget()
19
+ this.network = params.network || Networks.PUBLIC
20
+ if (params.origin) {
21
+ this.origin = params.origin
22
+ }
23
+ this.trader = params.account
24
+ if (params.authorization) {
25
+ this.authorization = new AuthorizationWrapper(params.authorization)
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @type {string}
31
+ * @private
32
+ */
33
+ uid
34
+ /**
35
+ * @type {ClientSessionStatus}
36
+ * @readonly
37
+ */
38
+ status = 'disconnected'
39
+ /**
40
+ * @type {WebSocket}
41
+ * @private
42
+ */
43
+ socket
44
+ /**
45
+ * @type {EventTarget}
46
+ * @private
47
+ */
48
+ emitter
49
+ /**
50
+ * @type {string}
51
+ * @private
52
+ */
53
+ origin = 'https://api.stellar.broker'
54
+ /**
55
+ * Stellar network passphrase
56
+ * @type {string}
57
+ * @readonly
58
+ */
59
+ network = ''
60
+ /**
61
+ * Last quote request submitted by the client
62
+ * @type {QuoteParams}
63
+ * @readonly
64
+ */
65
+ quoteRequest
66
+ /**
67
+ * Last quote received from the sever
68
+ * @type {QuoteResult}
69
+ * @readonly
70
+ */
71
+ lastQuote
72
+ /**
73
+ * Current quote accepted for the trading
74
+ * @private
75
+ */
76
+ tradeQuote
77
+ /**
78
+ * API key of the partner
79
+ * @type {string}
80
+ * @readonly
81
+ */
82
+ partnerKey
83
+ /**
84
+ * Account secret key or authorization callback
85
+ * @type {AuthorizationWrapper}
86
+ * @private
87
+ */
88
+ authorization
89
+ /**
90
+ * Trader account public key
91
+ * @type {string}
92
+ * @readonly
93
+ */
94
+ trader
95
+
96
+ /**
97
+ * Connect to the StellarBroker server
98
+ * @return {Promise<StellarBrokerClient>}
99
+ */
100
+ connect() {
101
+ if (this.socket?.readyState === WebSocket.OPEN)
102
+ return Promise.resolve(this) //already opened
103
+ this.socket = new WebSocket(this.origin + '/ws?partner=' + encodeURIComponent(this.partnerKey))
104
+ this.socket.onmessage = this.processMessage.bind(this)
105
+ this.socket.onclose = () => {
106
+ if (this.status !== 'disconnected') {
107
+ this.status = 'disconnected'
108
+ this.notifyError(errors.notConnected())
109
+ }
110
+ console.log('Connection closed')
111
+ }
112
+
113
+ this.socket.onerror = e => console.error(e)
114
+
115
+ return new Promise((confirm, reject) => { //TODO: use 'once'
116
+ const expirationTimeout = setTimeout(() => reject(this), 2000) // 2s timeout
117
+ this.onSocketOpen = () => {
118
+ this.status = 'ready'
119
+ this.heartbeat()
120
+ this.onSocketOpen = undefined
121
+ clearTimeout(expirationTimeout)
122
+ confirm(this)
123
+ }
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Process incoming message
129
+ * @param message
130
+ * @private
131
+ */
132
+ processMessage(message) {
133
+ const raw = JSON.parse(message.data)
134
+ switch (raw.type) {
135
+ case 'connected':
136
+ this.uid = raw.uid
137
+ if (this.onSocketOpen) {
138
+ this.onSocketOpen()
139
+ }
140
+ if (this.status === 'disconnected') {
141
+ this.status = 'ready'
142
+ }
143
+ break
144
+ case 'quote':
145
+ this.lastQuote = new QuoteResult(raw.quote)
146
+ //send event to the client app
147
+ this.emitter.dispatchEvent(buildEvent('quote', this.lastQuote))
148
+ break
149
+ case 'paused':
150
+ //quotation paused due to inactivity
151
+ this.emitter.dispatchEvent(buildEvent('paused', {}))
152
+ break
153
+ case 'tx':
154
+ if (this.status !== 'trade') {
155
+ console.log('Received tx in a non-trading state', this.status, raw)
156
+ return //skip unless trading is in progress
157
+ }
158
+ processTxRequest(this, raw)
159
+ .then(xdr => {
160
+ this.send({
161
+ type: 'tx',
162
+ hash: raw.hash,
163
+ xdr
164
+ })
165
+ })
166
+ .catch(e => this.notifyError(e))
167
+ break
168
+ case 'stop':
169
+ this.emitter.dispatchEvent(buildEvent('finished', {
170
+ status: raw.status,
171
+ sold: raw.sold,
172
+ bought: raw.bought
173
+ }, 'result'))
174
+ this.status = 'ready'
175
+ this.tradeQuote = undefined
176
+ break
177
+ case 'progress':
178
+ this.emitter.dispatchEvent(buildEvent('progress', {
179
+ sold: raw.sold,
180
+ bought: raw.bought
181
+ }, 'status'))
182
+ break
183
+ case 'ping':
184
+ if (raw.uid === this.uid) {
185
+ this.heartbeat()
186
+ this.send({type: 'pong', uid: this.uid})
187
+ }
188
+ break
189
+ case 'error':
190
+ this.stop()
191
+ this.notifyError(raw.error)
192
+ break
193
+ default:
194
+ console.log('Unknown message type: ' + raw.type)
195
+ break
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Request swap quote
201
+ * @param {QuoteParams} params - Quote parameters
202
+ */
203
+ quote(params) {
204
+ if (this.status === 'trade')
205
+ throw errors.tradeInProgress()
206
+ this.quoteRequest = validateQuoteRequest(params)
207
+
208
+ this.status = 'quote'
209
+ this.connect()
210
+ .then(() => {
211
+ this.status = 'quote'
212
+ this.send({
213
+ type: 'quote',
214
+ ...this.quoteRequest
215
+ })
216
+ })
217
+ }
218
+
219
+ /**
220
+ * Confirm current quote and start trading
221
+ * @param {string} [account] - Trader account address (overrides value provided in the constructor)
222
+ * @param {ClientAuthorizationParams} [authorization] - Authorization params (overrides value provided in the constructor)
223
+ */
224
+ confirmQuote(account, authorization) {
225
+ if (this.status === 'disconnected')
226
+ throw errors.notConnected()
227
+ if (this.status === 'trade')
228
+ throw errors.tradeInProgress()
229
+ if (this.status !== 'quote' || !this.lastQuote)
230
+ throw errors.quoteNotSet()
231
+ if ((new Date() - this.lastQuote.ts) > 10_000) //do not allow stale quotes quoted more than 10s ago
232
+ throw errors.quoteExpired()
233
+ if (this.lastQuote.status !== 'success')
234
+ throw errors.quoteError(this.lastQuote.error || 'quote not available')
235
+ if (account) {
236
+ this.trader = account
237
+ }
238
+ if (authorization) {
239
+ this.authorization = new AuthorizationWrapper(authorization)
240
+ }
241
+ const {trader} = this
242
+ if (!trader || !StrKey.isValidEd25519PublicKey(trader)) //ensure that a trader account address provided
243
+ throw errors.invalidQuoteParam('account', 'Invalid trader account address: ' + (!trader ? 'missing' : trader))
244
+ if (!this.authorization)
245
+ throw errors.invalidQuoteParam('authorization', 'Client authorization not provided')
246
+ this.tradeQuote = this.lastQuote
247
+ this.send({
248
+ type: 'trade',
249
+ account: trader
250
+ })
251
+ this.status = 'trade'
252
+ }
253
+
254
+ /**
255
+ * Stop quotation/trading
256
+ */
257
+ stop() {
258
+ if (this.status !== 'trade' && this.status !== 'quote')
259
+ return
260
+ this.send({type: 'stop'})
261
+ this.tradeQuote = undefined
262
+ this.status = 'ready'
263
+ }
264
+
265
+ /**
266
+ * @param {{}} data
267
+ * @private
268
+ */
269
+ send(data) {
270
+ this.socket.send(JSON.stringify(data))
271
+ }
272
+
273
+ /**
274
+ * @private
275
+ */
276
+ heartbeat() {
277
+ clearTimeout(this.pingHandler)
278
+ this.pingHandler = setTimeout(() => {
279
+ console.warn('Lost connection with the server')
280
+ this.socket.close()
281
+ }, 7_000) // 7 seconds heartbeat timeout
282
+ }
283
+
284
+ /**
285
+ * Add event listener
286
+ * @param {StellarBrokerClientEvent} type
287
+ * @param {function} callback
288
+ */
289
+ on(type, callback) {
290
+ validateEventType(type)
291
+ this.emitter.addEventListener(type, callback)
292
+ }
293
+
294
+ /**
295
+ * Add event listener that will be executed once
296
+ * @param {StellarBrokerClientEvent} type
297
+ * @param {function} callback
298
+ */
299
+ once(type, callback) {
300
+ validateEventType(type)
301
+ this.emitter.addEventListener(type, callback, {once: true})
302
+ }
303
+
304
+ /**
305
+ * Remove event listener
306
+ * @param {StellarBrokerClientEvent} type
307
+ * @param {function} callback
308
+ */
309
+ off(type, callback) {
310
+ validateEventType(type)
311
+ this.emitter.removeEventListener(type, callback)
312
+ }
313
+
314
+ /**
315
+ * Close underlying connection and finalize the client
316
+ */
317
+ close() {
318
+ try {
319
+ //TODO: remove all attached event listeners
320
+ this.status = 'disconnected'
321
+ this.socket.close()
322
+ } catch (e) {
323
+ }
324
+ }
325
+
326
+ /**
327
+ * @param {Error} e
328
+ * @private
329
+ */
330
+ notifyError(e) {
331
+ console.error(e)
332
+ try {
333
+ this.emitter.dispatchEvent(buildEvent('error', e instanceof Error ? e.message : e))
334
+ } catch (e) {
335
+ console.error(e)
336
+ }
337
+ }
338
+ }
339
+
340
+ const stellarBrokerEvents = new Set(['quote', 'paused', 'progress', 'finished', 'error'])
341
+
342
+ function validateEventType(type) {
343
+ if (!stellarBrokerEvents.has(type))
344
+ throw errors.unsupportedEventType(type)
345
+ }
346
+
347
+ /**
348
+ * @typedef {object} ClientInitializationParams
349
+ * @property {string} [partnerKey] - Partner key
350
+ * @property {string} [account] - Trader account address
351
+ * @property {ClientAuthorizationParams} [authorization] - Authorization method, either account secret key or an authorization callback
352
+ */
353
+
354
+ /**
355
+ * @typedef {'disconnected'|'ready'|'quote'|'trade'} ClientSessionStatus - Current client session status
356
+ */
357
+
358
+ /**
359
+ * @typedef {'quote'|'paused'|'progress'|'finished'|'error'} StellarBrokerClientEvent - Event type generated by the client
360
+ */