edge-currency-accountbased 0.7.72

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +713 -0
  2. package/LICENSE +29 -0
  3. package/README.md +63 -0
  4. package/index.js +3 -0
  5. package/lib/binance/bnbEngine.js +591 -0
  6. package/lib/binance/bnbInfo.js +43 -0
  7. package/lib/binance/bnbPlugin.js +168 -0
  8. package/lib/binance/bnbSchema.js +83 -0
  9. package/lib/binance/bnbTypes.js +39 -0
  10. package/lib/common/engine.js +918 -0
  11. package/lib/common/plugin.js +152 -0
  12. package/lib/common/schema.js +108 -0
  13. package/lib/common/types.js +85 -0
  14. package/lib/common/utils.js +378 -0
  15. package/lib/eos/eosEngine.js +1216 -0
  16. package/lib/eos/eosInfo.js +98 -0
  17. package/lib/eos/eosPlugin.js +314 -0
  18. package/lib/eos/eosSchema.js +190 -0
  19. package/lib/eos/eosTypes.js +88 -0
  20. package/lib/eos/telosInfo.js +94 -0
  21. package/lib/eos/waxInfo.js +95 -0
  22. package/lib/ethereum/etcInfo.js +121 -0
  23. package/lib/ethereum/ethEngine.js +832 -0
  24. package/lib/ethereum/ethInfo.js +1300 -0
  25. package/lib/ethereum/ethMiningFees.js +157 -0
  26. package/lib/ethereum/ethNetwork.js +2195 -0
  27. package/lib/ethereum/ethPlugin.js +377 -0
  28. package/lib/ethereum/ethSchema.js +61 -0
  29. package/lib/ethereum/ethTypes.js +461 -0
  30. package/lib/ethereum/ftminfo.js +102 -0
  31. package/lib/ethereum/rskInfo.js +101 -0
  32. package/lib/fio/fioConst.js +38 -0
  33. package/lib/fio/fioEngine.js +1250 -0
  34. package/lib/fio/fioError.js +38 -0
  35. package/lib/fio/fioInfo.js +72 -0
  36. package/lib/fio/fioPlugin.js +486 -0
  37. package/lib/fio/fioSchema.js +56 -0
  38. package/lib/index.js +44 -0
  39. package/lib/pluginError.js +32 -0
  40. package/lib/react-native/edge-currency-accountbased.js +239635 -0
  41. package/lib/react-native/edge-currency-accountbased.js.map +1 -0
  42. package/lib/react-native-io.js +41 -0
  43. package/lib/stellar/stellarEngine.js +563 -0
  44. package/lib/stellar/stellarInfo.js +37 -0
  45. package/lib/stellar/stellarPlugin.js +215 -0
  46. package/lib/stellar/stellarSchema.js +54 -0
  47. package/lib/stellar/stellarTypes.js +66 -0
  48. package/lib/tezos/tezosEngine.js +497 -0
  49. package/lib/tezos/tezosInfo.js +60 -0
  50. package/lib/tezos/tezosPlugin.js +174 -0
  51. package/lib/tezos/tezosTypes.js +110 -0
  52. package/lib/xrp/xrpEngine.js +583 -0
  53. package/lib/xrp/xrpInfo.js +47 -0
  54. package/lib/xrp/xrpPlugin.js +229 -0
  55. package/lib/xrp/xrpSchema.js +74 -0
  56. package/lib/xrp/xrpTypes.js +38 -0
  57. package/package.json +139 -0
  58. package/postinstall.sh +7 -0
@@ -0,0 +1,2195 @@
1
+ //
2
+ import { bns } from 'biggystring'
3
+
4
+
5
+
6
+
7
+ import parse from 'url-parse'
8
+
9
+ import {
10
+ asyncWaterfall,
11
+ cleanTxLogs,
12
+ hexToDecimal,
13
+ isHex,
14
+ padHex,
15
+ pickRandom,
16
+ promiseAny,
17
+ removeHexPrefix,
18
+ shuffleArray,
19
+ snooze,
20
+ validateObject
21
+ } from '../common/utils'
22
+ import { EthereumEngine } from './ethEngine'
23
+ import {
24
+ AmberdataRpcSchema,
25
+ BlockChairStatsSchema,
26
+ EtherscanGetAccountNonce,
27
+ EtherscanGetBlockHeight
28
+ } from './ethSchema'
29
+ import {
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ asAlethioAccountsTokenTransfer,
47
+ asAmberdataAccountsFuncs,
48
+ asAmberdataAccountsTx,
49
+ asBlockbookAddress,
50
+ asBlockbookBlockHeight,
51
+ asBlockbookTokenBalance,
52
+ asBlockbookTx,
53
+ asBlockChairAddress,
54
+ asCheckTokenBalBlockchair,
55
+ asCheckTokenBalRpc,
56
+ asEtherscanGetAccountBalance,
57
+ asEtherscanInternalTransaction,
58
+ asEtherscanTokenTransaction,
59
+ asEtherscanTransaction,
60
+ asFetchGetAlethio,
61
+ asFetchGetAmberdataApiResponse
62
+ } from './ethTypes'
63
+
64
+ const BLOCKHEIGHT_POLL_MILLISECONDS = 20000
65
+ const NONCE_POLL_MILLISECONDS = 20000
66
+ const BAL_POLL_MILLISECONDS = 20000
67
+ const TXS_POLL_MILLISECONDS = 20000
68
+
69
+ const ADDRESS_QUERY_LOOKBACK_BLOCKS = 4 * 2 // ~ 2 minutes
70
+ const ADDRESS_QUERY_LOOKBACK_SEC = 2 * 60 // ~ 2 minutes
71
+ const NUM_TRANSACTIONS_TO_QUERY = 50
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
+ async function broadcastWrapper(promise, server) {
122
+ const out = {
123
+ result: await promise,
124
+ server
125
+ }
126
+ return out
127
+ }
128
+
129
+ export class EthereumNetwork {
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+ constructor(ethEngine, currencyInfo) {
152
+ this.ethEngine = ethEngine
153
+ this.ethNeeds = {
154
+ blockHeightLastChecked: 0,
155
+ nonceLastChecked: 0,
156
+ tokenBalLastChecked: {},
157
+ tokenTxsLastChecked: {}
158
+ }
159
+ this.currencyInfo = currencyInfo
160
+ this.fetchGetEtherscan = this.fetchGetEtherscan.bind(this)
161
+ this.multicastServers = this.multicastServers.bind(this)
162
+ this.checkBlockHeightEthscan = this.checkBlockHeightEthscan.bind(this)
163
+ this.checkBlockHeightBlockchair = this.checkBlockHeightBlockchair.bind(this)
164
+ this.checkBlockHeightAmberdata = this.checkBlockHeightAmberdata.bind(this)
165
+ this.checkBlockHeightBlockbook = this.checkBlockHeightBlockbook.bind(this)
166
+ this.checkTxsBlockbook = this.checkTxsBlockbook.bind(this)
167
+ this.checkBlockHeight = this.checkBlockHeight.bind(this)
168
+ this.checkNonceEthscan = this.checkNonceEthscan.bind(this)
169
+ this.checkNonceAmberdata = this.checkNonceAmberdata.bind(this)
170
+ this.checkNonce = this.checkNonce.bind(this)
171
+ this.checkTxs = this.checkTxs.bind(this)
172
+ this.checkTokenBalEthscan = this.checkTokenBalEthscan.bind(this)
173
+ this.checkTokenBalBlockchair = this.checkTokenBalBlockchair.bind(this)
174
+ this.checkTokenBalRpc = this.checkTokenBalRpc.bind(this)
175
+ this.checkTokenBal = this.checkTokenBal.bind(this)
176
+ this.processEthereumNetworkUpdate =
177
+ this.processEthereumNetworkUpdate.bind(this)
178
+ }
179
+
180
+ processEtherscanTransaction(
181
+ tx,
182
+ currencyCode
183
+ ) {
184
+ let netNativeAmount // Amount received into wallet
185
+ const ourReceiveAddresses = []
186
+ let nativeNetworkFee = '0'
187
+
188
+ if (!tx.contractAddress && tx.gasPrice) {
189
+ nativeNetworkFee = bns.mul(tx.gasPrice, tx.gasUsed)
190
+ }
191
+
192
+ if (
193
+ tx.from.toLowerCase() ===
194
+ this.ethEngine.walletLocalData.publicKey.toLowerCase()
195
+ ) {
196
+ // is a spend
197
+ if (tx.from.toLowerCase() === tx.to.toLowerCase()) {
198
+ // Spend to self. netNativeAmount is just the fee
199
+ netNativeAmount = bns.mul(nativeNetworkFee, '-1')
200
+ } else {
201
+ // spend to someone else
202
+ netNativeAmount = bns.sub('0', tx.value)
203
+
204
+ // For spends, include the network fee in the transaction amount
205
+ netNativeAmount = bns.sub(netNativeAmount, nativeNetworkFee)
206
+ }
207
+ } else {
208
+ // Receive transaction
209
+ netNativeAmount = bns.add('0', tx.value)
210
+ ourReceiveAddresses.push(
211
+ this.ethEngine.walletLocalData.publicKey.toLowerCase()
212
+ )
213
+ }
214
+
215
+ const otherParams = {
216
+ from: [tx.from],
217
+ to: [tx.to],
218
+ gas: tx.gas,
219
+ gasPrice: tx.gasPrice || '',
220
+ gasUsed: tx.gasUsed,
221
+ cumulativeGasUsed: tx.cumulativeGasUsed || '',
222
+ errorVal: parseInt(tx.isError),
223
+ tokenRecipientAddress: null
224
+ }
225
+
226
+ let blockHeight = parseInt(tx.blockNumber)
227
+ if (blockHeight < 0) blockHeight = 0
228
+ let txid
229
+ if (tx.hash != null) {
230
+ txid = tx.hash
231
+ } else if (tx.transactionHash != null) {
232
+ txid = tx.transactionHash
233
+ } else {
234
+ throw new Error('Invalid transaction result format')
235
+ }
236
+ const edgeTransaction = {
237
+ txid,
238
+ date: parseInt(tx.timeStamp),
239
+ currencyCode,
240
+ blockHeight,
241
+ nativeAmount: netNativeAmount,
242
+ networkFee: nativeNetworkFee,
243
+ ourReceiveAddresses,
244
+ signedTx: '',
245
+ otherParams
246
+ }
247
+
248
+ return edgeTransaction
249
+ // or should be this.addTransaction(currencyCode, edgeTransaction)?
250
+ }
251
+
252
+ processAlethioTransaction(
253
+ tokenTransfer,
254
+ currencyCode
255
+ ) {
256
+ let netNativeAmount
257
+ const ourReceiveAddresses = []
258
+ let nativeNetworkFee
259
+ let tokenRecipientAddress
260
+
261
+ const value = tokenTransfer.attributes.value
262
+ const fee = tokenTransfer.attributes.fee
263
+ ? tokenTransfer.attributes.fee
264
+ : '0'
265
+ const fromAddress = tokenTransfer.relationships.from.data.id
266
+ const toAddress = tokenTransfer.relationships.to.data.id
267
+
268
+ if (currencyCode === this.currencyInfo.currencyCode) {
269
+ nativeNetworkFee = fee
270
+ tokenRecipientAddress = null
271
+ } else {
272
+ nativeNetworkFee = '0'
273
+ tokenRecipientAddress = toAddress
274
+ }
275
+
276
+ if (
277
+ fromAddress.toLowerCase() ===
278
+ this.ethEngine.walletLocalData.publicKey.toLowerCase()
279
+ ) {
280
+ // is a spend
281
+ if (fromAddress.toLowerCase() === toAddress.toLowerCase()) {
282
+ // Spend to self. netNativeAmount is just the fee
283
+ netNativeAmount = bns.mul(nativeNetworkFee, '-1')
284
+ } else {
285
+ // spend to someone else
286
+ netNativeAmount = bns.sub('0', value)
287
+
288
+ // For spends, include the network fee in the transaction amount
289
+ netNativeAmount = bns.sub(netNativeAmount, nativeNetworkFee)
290
+ }
291
+ } else if (
292
+ toAddress.toLowerCase() ===
293
+ this.ethEngine.walletLocalData.publicKey.toLowerCase()
294
+ ) {
295
+ // Receive transaction
296
+ netNativeAmount = value
297
+ ourReceiveAddresses.push(
298
+ this.ethEngine.walletLocalData.publicKey.toLowerCase()
299
+ )
300
+ } else {
301
+ return null
302
+ }
303
+
304
+ const otherParams = {
305
+ from: [fromAddress],
306
+ to: [toAddress],
307
+ gas: '0',
308
+ gasPrice: '0',
309
+ gasUsed: '0',
310
+ errorVal: 0,
311
+ tokenRecipientAddress
312
+ }
313
+
314
+ let blockHeight = tokenTransfer.attributes.globalRank[0]
315
+ if (blockHeight < 0) blockHeight = 0
316
+ const edgeTransaction = {
317
+ txid: tokenTransfer.relationships.transaction.data.id,
318
+ date: tokenTransfer.attributes.blockCreationTime,
319
+ currencyCode,
320
+ blockHeight,
321
+ nativeAmount: netNativeAmount,
322
+ networkFee: nativeNetworkFee,
323
+ ourReceiveAddresses,
324
+ signedTx: '',
325
+ parentNetworkFee: '',
326
+ otherParams
327
+ }
328
+
329
+ return edgeTransaction
330
+ }
331
+
332
+ processAmberdataTxInternal(
333
+ amberdataTx,
334
+ currencyCode
335
+ ) {
336
+ const walletAddress = this.ethEngine.walletLocalData.publicKey
337
+ let netNativeAmount = bns.add('0', amberdataTx.value)
338
+ const ourReceiveAddresses = []
339
+ let nativeNetworkFee
340
+
341
+ const value = amberdataTx.value
342
+ const fromAddress = amberdataTx.from.address || ''
343
+ const toAddress = amberdataTx.to.length > 0 ? amberdataTx.to[0].address : ''
344
+
345
+ if (fromAddress && toAddress) {
346
+ nativeNetworkFee = '0'
347
+
348
+ if (fromAddress.toLowerCase() === walletAddress.toLowerCase()) {
349
+ // is a spend
350
+ if (fromAddress.toLowerCase() === toAddress.toLowerCase()) {
351
+ // Spend to self. netNativeAmount is just the fee
352
+ netNativeAmount = bns.mul(nativeNetworkFee, '-1')
353
+ } else {
354
+ // spend to someone else
355
+ netNativeAmount = bns.sub('0', value)
356
+
357
+ // For spends, include the network fee in the transaction amount
358
+ netNativeAmount = bns.sub(netNativeAmount, nativeNetworkFee)
359
+ }
360
+ } else if (toAddress.toLowerCase() === walletAddress.toLowerCase()) {
361
+ // Receive transaction
362
+ netNativeAmount = value
363
+ ourReceiveAddresses.push(walletAddress.toLowerCase())
364
+ } else {
365
+ return null
366
+ }
367
+
368
+ const otherParams = {
369
+ from: [fromAddress],
370
+ to: [toAddress],
371
+ gas: '0',
372
+ gasPrice: '0',
373
+ gasUsed: '0',
374
+ errorVal: 0,
375
+ tokenRecipientAddress: null
376
+ }
377
+
378
+ let blockHeight = parseInt(amberdataTx.blockNumber, 10)
379
+ if (blockHeight < 0) blockHeight = 0
380
+ const date = new Date(amberdataTx.timestamp).getTime() / 1000
381
+ const edgeTransaction = {
382
+ txid: amberdataTx.transactionHash,
383
+ date,
384
+ currencyCode,
385
+ blockHeight,
386
+ nativeAmount: netNativeAmount,
387
+ networkFee: nativeNetworkFee,
388
+ ourReceiveAddresses,
389
+ signedTx: '',
390
+ parentNetworkFee: '',
391
+ otherParams
392
+ }
393
+
394
+ return edgeTransaction
395
+ } else {
396
+ return null
397
+ }
398
+ }
399
+
400
+ processAmberdataTxRegular(
401
+ amberdataTx,
402
+ currencyCode
403
+ ) {
404
+ const walletAddress = this.ethEngine.walletLocalData.publicKey
405
+ let netNativeAmount
406
+ const ourReceiveAddresses = []
407
+ let nativeNetworkFee
408
+ let tokenRecipientAddress
409
+
410
+ const value = amberdataTx.value
411
+ const fee = amberdataTx.fee ? amberdataTx.fee : '0'
412
+ const fromAddress =
413
+ amberdataTx.from.length > 0 ? amberdataTx.from[0].address : ''
414
+ const toAddress = amberdataTx.to.length > 0 ? amberdataTx.to[0].address : ''
415
+
416
+ if (fromAddress && toAddress) {
417
+ nativeNetworkFee = fee
418
+ tokenRecipientAddress = null
419
+
420
+ if (fromAddress.toLowerCase() === walletAddress.toLowerCase()) {
421
+ // is a spend
422
+ if (fromAddress.toLowerCase() === toAddress.toLowerCase()) {
423
+ // Spend to self. netNativeAmount is just the fee
424
+ netNativeAmount = bns.mul(nativeNetworkFee, '-1')
425
+ } else {
426
+ // spend to someone else
427
+ netNativeAmount = bns.sub('0', value)
428
+
429
+ // For spends, include the network fee in the transaction amount
430
+ netNativeAmount = bns.sub(netNativeAmount, nativeNetworkFee)
431
+ }
432
+ } else if (toAddress.toLowerCase() === walletAddress.toLowerCase()) {
433
+ // Receive transaction
434
+ netNativeAmount = value
435
+ ourReceiveAddresses.push(walletAddress.toLowerCase())
436
+ } else {
437
+ return null
438
+ }
439
+
440
+ const otherParams = {
441
+ from: [fromAddress],
442
+ to: [toAddress],
443
+ gas: '0',
444
+ gasPrice: '0',
445
+ gasUsed: '0',
446
+ errorVal: 0,
447
+ tokenRecipientAddress
448
+ }
449
+
450
+ let blockHeight = parseInt(amberdataTx.blockNumber, 10)
451
+ if (blockHeight < 0) blockHeight = 0
452
+ const date = new Date(amberdataTx.timestamp).getTime() / 1000
453
+ const edgeTransaction = {
454
+ txid: amberdataTx.hash,
455
+ date,
456
+ currencyCode,
457
+ blockHeight,
458
+ nativeAmount: netNativeAmount,
459
+ networkFee: nativeNetworkFee,
460
+ ourReceiveAddresses,
461
+ signedTx: '',
462
+ parentNetworkFee: '',
463
+ otherParams
464
+ }
465
+
466
+ return edgeTransaction
467
+ } else {
468
+ return null
469
+ }
470
+ }
471
+
472
+ async fetchGet(url, _options = {}) {
473
+ const options = { ..._options }
474
+ options.method = 'GET'
475
+ const response = await this.ethEngine.io.fetch(url, options)
476
+ if (!response.ok) {
477
+ const {
478
+ blockcypherApiKey,
479
+ etherscanApiKey,
480
+ ftmscanApiKey,
481
+ infuraProjectId,
482
+ blockchairApiKey
483
+ } = this.ethEngine.initOptions
484
+ if (typeof etherscanApiKey === 'string')
485
+ url = url.replace(etherscanApiKey, 'private')
486
+ if (Array.isArray(etherscanApiKey)) {
487
+ for (const key of etherscanApiKey) {
488
+ url = url.replace(key, 'private')
489
+ }
490
+ }
491
+ // removes API keys from error messages
492
+ if (blockcypherApiKey) url = url.replace(blockcypherApiKey, 'private')
493
+ if (infuraProjectId) url = url.replace(infuraProjectId, 'private')
494
+ if (blockchairApiKey) url = url.replace(blockchairApiKey, 'private')
495
+ if (ftmscanApiKey) url = url.replace(ftmscanApiKey, 'private')
496
+ throw new Error(
497
+ `The server returned error code ${response.status} for ${url}`
498
+ )
499
+ }
500
+ return response.json()
501
+ }
502
+
503
+ async fetchGetEtherscan(server, cmd) {
504
+ const { etherscanApiKey, ftmscanApiKey } = this.ethEngine.initOptions
505
+ const chosenKey = Array.isArray(etherscanApiKey)
506
+ ? pickRandom(etherscanApiKey, 1)[0]
507
+ : etherscanApiKey
508
+ const apiKey =
509
+ chosenKey && chosenKey.length > 5 && server.includes('etherscan')
510
+ ? '&apikey=' + chosenKey
511
+ : ftmscanApiKey != null && server.includes('ftmscan')
512
+ ? '&apikey=' + ftmscanApiKey
513
+ : ''
514
+
515
+ const url = `${server}/api${cmd}${apiKey}`
516
+ this.ethEngine.log.warn('invalid ftm url ', url)
517
+ return this.fetchGet(url)
518
+ }
519
+
520
+ async fetchGetAmberdata(url, _options = {}) {
521
+ const options = { ..._options }
522
+ options.method = 'GET'
523
+ const response = await this.ethEngine.fetchCors(url, options)
524
+ if (!response.ok) {
525
+ throw new Error(
526
+ `The server returned error code ${response.status} for ${url}`
527
+ )
528
+ }
529
+ return response.json()
530
+ }
531
+
532
+ async fetchPostRPC(
533
+ method,
534
+ params,
535
+ networkId,
536
+ url
537
+ ) {
538
+ const body = {
539
+ id: networkId,
540
+ jsonrpc: '2.0',
541
+ method,
542
+ params
543
+ }
544
+
545
+ let addOnUrl = ''
546
+ if (url.includes('infura')) {
547
+ const { infuraProjectId } = this.ethEngine.initOptions
548
+ if (!infuraProjectId || infuraProjectId.length < 6) {
549
+ throw new Error('Need Infura Project ID')
550
+ }
551
+ addOnUrl = `/${infuraProjectId}`
552
+ } else if (url.includes('alchemyapi')) {
553
+ const { alchemyApiKey } = this.ethEngine.initOptions
554
+ if (!alchemyApiKey || alchemyApiKey.length < 6) {
555
+ throw new Error('Need Alchemy API key')
556
+ }
557
+ addOnUrl = `/v2/-${alchemyApiKey}`
558
+ }
559
+ url += addOnUrl
560
+
561
+ const response = await this.ethEngine.io.fetch(url, {
562
+ headers: {
563
+ Accept: 'application/json',
564
+ 'Content-Type': 'application/json'
565
+ },
566
+ method: 'POST',
567
+ body: JSON.stringify(body)
568
+ })
569
+
570
+ const parsedUrl = parse(url, {}, true)
571
+ if (!response.ok) {
572
+ throw new Error(
573
+ `The server returned error code ${response.status} for ${parsedUrl.hostname}`
574
+ )
575
+ }
576
+ return response.json()
577
+ }
578
+
579
+ async fetchPostBlockcypher(cmd, body, baseUrl) {
580
+ const { blockcypherApiKey } = this.ethEngine.initOptions
581
+ let apiKey = ''
582
+ if (blockcypherApiKey && blockcypherApiKey.length > 5) {
583
+ apiKey = '&token=' + blockcypherApiKey
584
+ }
585
+
586
+ const url = `${baseUrl}/${cmd}${apiKey}`
587
+ const response = await this.ethEngine.io.fetch(url, {
588
+ headers: {
589
+ Accept: 'application/json',
590
+ 'Content-Type': 'application/json'
591
+ },
592
+ method: 'POST',
593
+ body: JSON.stringify(body)
594
+ })
595
+ const parsedUrl = parse(url, {}, true)
596
+ if (!response.ok) {
597
+ throw new Error(
598
+ `The server returned error code ${response.status} for ${parsedUrl.hostname}`
599
+ )
600
+ }
601
+ return response.json()
602
+ }
603
+
604
+ async fetchGetBlockchair(path, includeKey = false) {
605
+ let keyParam = ''
606
+ const { blockchairApiKey } = this.ethEngine.initOptions
607
+ const { blockchairApiServers } =
608
+ this.currencyInfo.defaultSettings.otherSettings
609
+ if (includeKey && blockchairApiKey) {
610
+ keyParam = `&key=${blockchairApiKey}`
611
+ }
612
+ const url = `${blockchairApiServers[0]}${path}${keyParam}`
613
+ return this.fetchGet(url)
614
+ }
615
+
616
+ async fetchPostAmberdataRpc(method, params = []) {
617
+ const { amberdataApiKey } = this.ethEngine.initOptions
618
+ const { amberdataRpcServers } =
619
+ this.currencyInfo.defaultSettings.otherSettings
620
+ if (amberdataRpcServers.length === 0)
621
+ throw new Error(
622
+ `No amberdataRpcServers for ${this.currencyInfo.currencyCode}`
623
+ )
624
+ let apiKey = ''
625
+ if (amberdataApiKey) {
626
+ apiKey = '?x-api-key=' + amberdataApiKey
627
+ }
628
+ const url = `${amberdataRpcServers[0]}${apiKey}`
629
+ const body = {
630
+ jsonrpc: '2.0',
631
+ method: method,
632
+ params: params,
633
+ id: 1
634
+ }
635
+ const response = await this.ethEngine.fetchCors(url, {
636
+ headers: {
637
+ 'x-amberdata-blockchain-id':
638
+ this.currencyInfo.defaultSettings.otherSettings.amberDataBlockchainId
639
+ },
640
+ method: 'POST',
641
+ body: JSON.stringify(body)
642
+ })
643
+ const parsedUrl = parse(url, {}, true)
644
+ if (!response.ok) {
645
+ throw new Error(
646
+ `The server returned error code ${response.status} for ${parsedUrl.hostname}`
647
+ )
648
+ }
649
+ const jsonObj = await response.json()
650
+ return jsonObj
651
+ }
652
+
653
+ async fetchGetAmberdataApi(path) {
654
+ const { amberdataApiKey } = this.ethEngine.initOptions
655
+ const { amberdataApiServers } =
656
+ this.currencyInfo.defaultSettings.otherSettings
657
+ if (amberdataApiServers.length === 0)
658
+ throw new Error(
659
+ `No amberdataApiServers for ${this.currencyInfo.currencyCode}`
660
+ )
661
+ const url = `${amberdataApiServers[0]}${path}`
662
+ return this.fetchGetAmberdata(url, {
663
+ headers: {
664
+ 'x-amberdata-blockchain-id':
665
+ this.currencyInfo.defaultSettings.otherSettings.amberDataBlockchainId,
666
+ 'x-api-key': amberdataApiKey
667
+ }
668
+ })
669
+ }
670
+
671
+ /*
672
+ * @param pathOrLink: A "path" is appended to the alethioServers base URL and
673
+ * a "link" is a full URL that needs no further modification
674
+ * @param isPath: If TRUE then the pathOrLink param is interpretted as a "path"
675
+ * otherwise it is interpretted as a "link"
676
+ *
677
+ * @throws Exception when Alethio throttles with a 429 response code
678
+ */
679
+
680
+ async fetchGetAlethio(
681
+ pathOrLink,
682
+ isPath = true,
683
+ useApiKey
684
+ ) {
685
+ const { alethioApiKey } = this.ethEngine.initOptions
686
+ const { alethioApiServers } =
687
+ this.currencyInfo.defaultSettings.otherSettings
688
+ const url = isPath ? `${alethioApiServers[0]}${pathOrLink}` : pathOrLink
689
+ if (alethioApiKey && useApiKey) {
690
+ return this.fetchGet(url, {
691
+ headers: {
692
+ Authorization: `Bearer ${alethioApiKey}`
693
+ }
694
+ })
695
+ } else {
696
+ return this.fetchGet(url)
697
+ }
698
+ }
699
+
700
+ async broadcastEtherscan(
701
+ edgeTransaction,
702
+ baseUrl
703
+ ) {
704
+ // RSK also uses the "eth_sendRaw" syntax
705
+ const urlSuffix = `?module=proxy&action=eth_sendRawTransaction&hex=${edgeTransaction.signedTx}`
706
+ const jsonObj = await this.fetchGetEtherscan(baseUrl, urlSuffix)
707
+
708
+ if (typeof jsonObj.error !== 'undefined') {
709
+ this.ethEngine.log.error(
710
+ `FAILURE broadcastEtherscan\n${JSON.stringify(
711
+ jsonObj.error
712
+ )}\n${cleanTxLogs(edgeTransaction)}`
713
+ )
714
+ throw jsonObj.error
715
+ } else if (typeof jsonObj.result === 'string') {
716
+ // Success!!
717
+ this.ethEngine.log.warn(
718
+ `SUCCESS broadcastEtherscan\n${cleanTxLogs(edgeTransaction)}`
719
+ )
720
+ return jsonObj
721
+ } else {
722
+ this.ethEngine.log.error(
723
+ `FAILURE broadcastEtherscan invalid return value\n${JSON.stringify(
724
+ jsonObj
725
+ )}\n${cleanTxLogs(edgeTransaction)}`
726
+ )
727
+ throw new Error('Invalid return value on transaction send')
728
+ }
729
+ }
730
+
731
+ async broadcastRPC(
732
+ edgeTransaction,
733
+ networkId,
734
+ baseUrl
735
+ ) {
736
+ const method = 'eth_sendRawTransaction'
737
+ const params = [edgeTransaction.signedTx]
738
+
739
+ const jsonObj = await this.fetchPostRPC(method, params, networkId, baseUrl)
740
+
741
+ const parsedUrl = parse(baseUrl, {}, true)
742
+
743
+ if (typeof jsonObj.error !== 'undefined') {
744
+ this.ethEngine.log.error(
745
+ `FAILURE broadcastRPC ${parsedUrl.host}\n${JSON.stringify(
746
+ jsonObj.error
747
+ )}\n${cleanTxLogs(edgeTransaction)}`
748
+ )
749
+ throw jsonObj.error
750
+ } else if (typeof jsonObj.result === 'string') {
751
+ // Success!!
752
+ this.ethEngine.log.warn(
753
+ `SUCCESS broadcastRPC ${parsedUrl.host}\n${cleanTxLogs(
754
+ edgeTransaction
755
+ )}`
756
+ )
757
+ return jsonObj
758
+ } else {
759
+ this.ethEngine.log.error(
760
+ `FAILURE broadcastRPC ${
761
+ parsedUrl.host
762
+ }\nInvalid return value ${JSON.stringify(jsonObj)}\n${cleanTxLogs(
763
+ edgeTransaction
764
+ )}`
765
+ )
766
+ throw new Error('Invalid return value on transaction send')
767
+ }
768
+ }
769
+
770
+ async broadcastBlockCypher(
771
+ edgeTransaction,
772
+ baseUrl
773
+ ) {
774
+ const urlSuffix = `v1/${this.currencyInfo.currencyCode.toLowerCase()}/main/txs/push`
775
+ const hexTx = edgeTransaction.signedTx.replace('0x', '')
776
+ const jsonObj = await this.fetchPostBlockcypher(
777
+ urlSuffix,
778
+ { tx: hexTx },
779
+ baseUrl
780
+ )
781
+
782
+ if (typeof jsonObj.error !== 'undefined') {
783
+ this.ethEngine.log.error(
784
+ `FAILURE broadcastBlockCypher\n${JSON.stringify(
785
+ jsonObj.error
786
+ )}\n${cleanTxLogs(edgeTransaction)}`
787
+ )
788
+ throw jsonObj.error
789
+ } else if (jsonObj.tx && typeof jsonObj.tx.hash === 'string') {
790
+ this.ethEngine.log.error(
791
+ `SUCCESS broadcastBlockCypher\n${cleanTxLogs(edgeTransaction)}`
792
+ )
793
+ // Success!!
794
+ return jsonObj
795
+ } else {
796
+ this.ethEngine.log.error(
797
+ `FAILURE broadcastBlockCypher\nInvalid return data ${JSON.stringify(
798
+ jsonObj
799
+ )}\n${cleanTxLogs(edgeTransaction)}`
800
+ )
801
+ throw new Error('Invalid return value on transaction send')
802
+ }
803
+ }
804
+
805
+ async multicastServers(func, ...params) {
806
+ const otherSettings =
807
+ this.currencyInfo.defaultSettings.otherSettings
808
+ const {
809
+ rpcServers,
810
+ blockcypherApiServers,
811
+ etherscanApiServers,
812
+ blockbookServers,
813
+ chainParams
814
+ } = otherSettings
815
+ const { chainId } = chainParams
816
+ let out = { result: '', server: 'no server' }
817
+ let funcs, url
818
+ switch (func) {
819
+ case 'broadcastTx': {
820
+ const promises = []
821
+
822
+ rpcServers.forEach(baseUrl => {
823
+ const parsedUrl = parse(baseUrl, {}, true)
824
+ promises.push(
825
+ broadcastWrapper(
826
+ this.broadcastRPC(params[0], chainId, baseUrl),
827
+ parsedUrl.hostname
828
+ )
829
+ )
830
+ })
831
+
832
+ etherscanApiServers.forEach(baseUrl => {
833
+ promises.push(
834
+ broadcastWrapper(
835
+ this.broadcastEtherscan(params[0], baseUrl),
836
+ 'etherscan'
837
+ )
838
+ )
839
+ })
840
+
841
+ blockcypherApiServers.forEach(baseUrl => {
842
+ promises.push(
843
+ broadcastWrapper(
844
+ this.broadcastBlockCypher(params[0], baseUrl),
845
+ 'blockcypher'
846
+ )
847
+ )
848
+ })
849
+
850
+ out = await promiseAny(promises)
851
+
852
+ this.ethEngine.log(
853
+ `${this.currencyInfo.currencyCode} multicastServers ${func} ${out.server} won`
854
+ )
855
+ break
856
+ }
857
+
858
+ case 'eth_blockNumber':
859
+ funcs = etherscanApiServers.map(server => async () => {
860
+ if (!server.includes('etherscan') && !server.includes('blockscout')) {
861
+ throw new Error(`Unsupported command eth_blockNumber in ${server}`)
862
+ }
863
+ let blockNumberUrlSyntax = `?module=proxy&action=eth_blockNumber`
864
+ // special case for blockscout
865
+ if (server.includes('blockscout')) {
866
+ blockNumberUrlSyntax = `?module=block&action=eth_block_number`
867
+ }
868
+
869
+ const result = await this.fetchGetEtherscan(
870
+ server,
871
+ blockNumberUrlSyntax
872
+ )
873
+ if (typeof result.result !== 'string') {
874
+ const msg = `Invalid return value eth_blockNumber in ${server}`
875
+ this.ethEngine.log.error(msg)
876
+ throw new Error(msg)
877
+ }
878
+ return { server, result }
879
+ })
880
+
881
+ funcs.push(
882
+ ...rpcServers.map(baseUrl => async () => {
883
+ const result = await this.fetchPostRPC(
884
+ 'eth_blockNumber',
885
+ [],
886
+ chainId,
887
+ baseUrl
888
+ )
889
+ // Check if successful http response was actually an error
890
+ if (result.error != null) {
891
+ this.ethEngine.log.error(
892
+ `Successful eth_blockNumber response object from ${baseUrl} included an error ${result.error}`
893
+ )
894
+ throw new Error(
895
+ 'Successful eth_blockNumber response object included an error'
896
+ )
897
+ }
898
+ return { server: parse(baseUrl).hostname, result }
899
+ })
900
+ )
901
+
902
+ // Randomize array
903
+ funcs = shuffleArray(funcs)
904
+ out = await asyncWaterfall(funcs)
905
+ break
906
+
907
+ case 'eth_estimateGas':
908
+ funcs = rpcServers.map(baseUrl => async () => {
909
+ const result = await this.fetchPostRPC(
910
+ 'eth_estimateGas',
911
+ params[0],
912
+ chainId,
913
+ baseUrl
914
+ )
915
+ // Check if successful http response was actually an error
916
+ if (result.error != null) {
917
+ this.ethEngine.log.error(
918
+ `Successful eth_estimateGas response object from ${baseUrl} included an error ${result.error}`
919
+ )
920
+ throw new Error(
921
+ 'Successful eth_estimateGas response object included an error'
922
+ )
923
+ }
924
+ return { server: parse(baseUrl).hostname, result }
925
+ })
926
+
927
+ out = await asyncWaterfall(funcs)
928
+ break
929
+
930
+ case 'eth_getCode':
931
+ funcs = rpcServers.map(baseUrl => async () => {
932
+ const result = await this.fetchPostRPC(
933
+ 'eth_getCode',
934
+ params[0],
935
+ chainId,
936
+ baseUrl
937
+ )
938
+ // Check if successful http response was actually an error
939
+ if (result.error != null) {
940
+ this.ethEngine.log.error(
941
+ `Successful eth_getCode response object from ${baseUrl} included an error ${result.error}`
942
+ )
943
+ throw new Error(
944
+ 'Successful eth_getCode response object included an error'
945
+ )
946
+ }
947
+ return { server: parse(baseUrl).hostname, result }
948
+ })
949
+
950
+ out = await asyncWaterfall(funcs)
951
+ break
952
+
953
+ case 'eth_getTransactionCount':
954
+ url = `?module=proxy&action=eth_getTransactionCount&address=${params[0]}&tag=latest`
955
+ funcs = etherscanApiServers.map(server => async () => {
956
+ // if falsy URL then error thrown
957
+ if (!server.includes('etherscan') && !server.includes('blockscout')) {
958
+ throw new Error(
959
+ `Unsupported command eth_getTransactionCount in ${server}`
960
+ )
961
+ }
962
+ const result = await this.fetchGetEtherscan(server, url)
963
+ if (typeof result.result !== 'string') {
964
+ const msg = `Invalid return value eth_getTransactionCount in ${server}`
965
+ this.ethEngine.log.error(msg)
966
+ throw new Error(msg)
967
+ }
968
+ return { server, result }
969
+ })
970
+
971
+ funcs.push(
972
+ ...rpcServers.map(baseUrl => async () => {
973
+ const result = await this.fetchPostRPC(
974
+ 'eth_getTransactionCount',
975
+ [params[0], 'latest'],
976
+ chainId,
977
+ baseUrl
978
+ )
979
+ // Check if successful http response was actually an error
980
+ if (result.error != null) {
981
+ this.ethEngine.log.error(
982
+ `Successful eth_getTransactionCount response object from ${baseUrl} included an error ${result.error}`
983
+ )
984
+ throw new Error(
985
+ 'Successful eth_getTransactionCount response object included an error'
986
+ )
987
+ }
988
+ return { server: parse(baseUrl).hostname, result }
989
+ })
990
+ )
991
+
992
+ // Randomize array
993
+ funcs = shuffleArray(funcs)
994
+ out = await asyncWaterfall(funcs)
995
+ break
996
+
997
+ case 'eth_getBalance':
998
+ url = `?module=account&action=balance&address=${params[0]}&tag=latest`
999
+ funcs = etherscanApiServers.map(server => async () => {
1000
+ const result = await this.fetchGetEtherscan(server, url)
1001
+ if (!result.result || typeof result.result !== 'string') {
1002
+ const msg = `Invalid return value eth_getBalance in ${server}`
1003
+ this.ethEngine.log.error(msg)
1004
+ throw new Error(msg)
1005
+ }
1006
+ return { server, result }
1007
+ })
1008
+
1009
+ funcs.push(
1010
+ ...rpcServers.map(baseUrl => async () => {
1011
+ const result = await this.fetchPostRPC(
1012
+ 'eth_getBalance',
1013
+ [params[0], 'latest'],
1014
+ chainId,
1015
+ baseUrl
1016
+ )
1017
+ // Check if successful http response was actually an error
1018
+ if (result.error != null) {
1019
+ this.ethEngine.log.error(
1020
+ `Successful eth_getBalance response object from ${baseUrl} included an error ${result.error}`
1021
+ )
1022
+ throw new Error(
1023
+ 'Successful eth_getBalance response object included an error'
1024
+ )
1025
+ }
1026
+ // Convert hex
1027
+ if (!isHex(result.result)) {
1028
+ throw new Error(
1029
+ `eth_getBalance not hex for ${parse(baseUrl).hostname}`
1030
+ )
1031
+ }
1032
+ // Convert to decimal
1033
+ result.result = bns.add(result.result, '0')
1034
+ return { server: parse(baseUrl).hostname, result }
1035
+ })
1036
+ )
1037
+
1038
+ // Randomize array
1039
+ funcs = shuffleArray(funcs)
1040
+ out = await asyncWaterfall(funcs)
1041
+ break
1042
+
1043
+ case 'getTokenBalance':
1044
+ url = `?module=account&action=tokenbalance&contractaddress=${params[1]}&address=${params[0]}&tag=latest`
1045
+ funcs = etherscanApiServers.map(server => async () => {
1046
+ const result = await this.fetchGetEtherscan(server, url)
1047
+ if (!result.result || typeof result.result !== 'string') {
1048
+ const msg = `Invalid return value getTokenBalance in ${server}`
1049
+ this.ethEngine.log.error(msg)
1050
+ throw new Error(msg)
1051
+ }
1052
+ return { server, result }
1053
+ })
1054
+ // Randomize array
1055
+ funcs = shuffleArray(funcs)
1056
+ out = await asyncWaterfall(funcs)
1057
+ break
1058
+
1059
+ case 'getTransactions': {
1060
+ const {
1061
+ currencyCode,
1062
+ address,
1063
+ startBlock,
1064
+ page,
1065
+ offset,
1066
+ contractAddress,
1067
+ searchRegularTxs
1068
+ } = params[0]
1069
+ let startUrl
1070
+ if (currencyCode === this.currencyInfo.currencyCode) {
1071
+ startUrl = `?action=${
1072
+ searchRegularTxs ? 'txlist' : 'txlistinternal'
1073
+ }&module=account`
1074
+ } else {
1075
+ startUrl = `?action=tokentx&contractaddress=${contractAddress}&module=account`
1076
+ }
1077
+ url = `${startUrl}&address=${address}&startblock=${startBlock}&endblock=999999999&sort=asc&page=${page}&offset=${offset}`
1078
+ funcs = etherscanApiServers.map(server => async () => {
1079
+ const result = await this.fetchGetEtherscan(server, url)
1080
+ if (
1081
+ typeof result.result !== 'object' ||
1082
+ typeof result.result.length !== 'number'
1083
+ ) {
1084
+ const msg = `Invalid return value getTransactions in ${server}`
1085
+ this.ethEngine.log.error(msg)
1086
+ throw new Error(msg)
1087
+ }
1088
+ return { server, result }
1089
+ })
1090
+ // Randomize array
1091
+ funcs = shuffleArray(funcs)
1092
+ out = await asyncWaterfall(funcs)
1093
+ break
1094
+ }
1095
+
1096
+ case 'blockbookBlockHeight':
1097
+ funcs = blockbookServers.map(server => async () => {
1098
+ const result =
1099
+ server.indexOf('trezor') === -1
1100
+ ? await this.fetchGet(server + '/api/v2')
1101
+ : await this.ethEngine.fetchCors(server + '/api/v2')
1102
+ return { server, result }
1103
+ })
1104
+ // Randomize array
1105
+ funcs = shuffleArray(funcs)
1106
+ out = await asyncWaterfall(funcs)
1107
+ break
1108
+
1109
+ case 'blockbookTxs':
1110
+ funcs = blockbookServers.map(server => async () => {
1111
+ const url = server + params[0]
1112
+ const result =
1113
+ server.indexOf('trezor') === -1
1114
+ ? await this.fetchGet(url)
1115
+ : await this.ethEngine
1116
+ .fetchCors(url)
1117
+ .then(response => response.json())
1118
+ return { server, result }
1119
+ })
1120
+ // Randomize array
1121
+ funcs = shuffleArray(funcs)
1122
+ out = await asyncWaterfall(funcs)
1123
+ break
1124
+ case 'eth_call':
1125
+ funcs = rpcServers.map(baseUrl => async () => {
1126
+ const result = await this.fetchPostRPC(
1127
+ 'eth_call',
1128
+ [params[0], 'latest'],
1129
+ chainId,
1130
+ baseUrl
1131
+ )
1132
+ // Check if successful http response was actually an error
1133
+ if (result.error != null) {
1134
+ this.ethEngine.log.error(
1135
+ `Successful eth_call response object from ${baseUrl} included an error ${result.error}`
1136
+ )
1137
+ throw new Error(
1138
+ 'Successful eth_call response object included an error'
1139
+ )
1140
+ }
1141
+ return { server: parse(baseUrl).hostname, result }
1142
+ })
1143
+
1144
+ out = await asyncWaterfall(funcs)
1145
+ break
1146
+ }
1147
+
1148
+ return out
1149
+ }
1150
+
1151
+ async getBaseFeePerGas() {
1152
+ const { rpcServers, chainId } =
1153
+ this.currencyInfo.defaultSettings.otherSettings
1154
+
1155
+ const funcs = rpcServers.map(
1156
+ baseUrl => async () =>
1157
+ await this.fetchPostRPC(
1158
+ 'eth_getBlockByNumber',
1159
+ ['latest', false],
1160
+ chainId,
1161
+ baseUrl
1162
+ ).then(response => {
1163
+ if (response.error != null) {
1164
+ this.ethEngine.log.error(
1165
+ `multicast get_baseFeePerGas error response from ${baseUrl}: ${response.error}`
1166
+ )
1167
+ throw new Error(
1168
+ `multicast get_baseFeePerGas error response from ${baseUrl}: ${response.error}`
1169
+ )
1170
+ }
1171
+
1172
+ const baseFeePerGas = response.result.baseFeePerGas
1173
+
1174
+ return { baseFeePerGas }
1175
+ })
1176
+ )
1177
+
1178
+ return await asyncWaterfall(funcs)
1179
+ }
1180
+
1181
+ async checkBlockHeightEthscan() {
1182
+ const { result: jsonObj, server } = await this.multicastServers(
1183
+ 'eth_blockNumber'
1184
+ )
1185
+ const valid = validateObject(jsonObj, EtherscanGetBlockHeight)
1186
+ if (valid && /0[xX][0-9a-fA-F]+/.test(jsonObj.result)) {
1187
+ const blockHeight = parseInt(jsonObj.result, 16)
1188
+ return { blockHeight, server }
1189
+ } else {
1190
+ throw new Error('Ethscan returned invalid JSON')
1191
+ }
1192
+ }
1193
+
1194
+ async checkBlockHeightBlockbook() {
1195
+ try {
1196
+ const { result: jsonObj, server } = await this.multicastServers(
1197
+ 'blockbookBlockHeight'
1198
+ )
1199
+
1200
+ const blockHeight = asBlockbookBlockHeight(jsonObj).blockbook.bestHeight
1201
+ return { blockHeight, server }
1202
+ } catch (e) {
1203
+ this.ethEngine.log(`checkBlockHeightBlockbook blockHeight ${e}`)
1204
+ throw new Error(`checkBlockHeightBlockbook returned invalid JSON`)
1205
+ }
1206
+ }
1207
+
1208
+ async checkBlockHeightBlockchair() {
1209
+ const jsonObj = await this.fetchGetBlockchair(
1210
+ `/${this.currencyInfo.pluginId}/stats`,
1211
+ false
1212
+ )
1213
+ const valid = validateObject(jsonObj, BlockChairStatsSchema)
1214
+ if (valid) {
1215
+ const blockHeight = parseInt(jsonObj.data.blocks, 10)
1216
+ return { blockHeight, server: 'blockchair' }
1217
+ } else {
1218
+ throw new Error('Blockchair returned invalid JSON')
1219
+ }
1220
+ }
1221
+
1222
+ async checkBlockHeightAmberdata() {
1223
+ const jsonObj = await this.fetchPostAmberdataRpc('eth_blockNumber', [])
1224
+ const valid = validateObject(jsonObj, AmberdataRpcSchema)
1225
+ if (valid) {
1226
+ const blockHeight = parseInt(jsonObj.result, 16)
1227
+ return { blockHeight, server: 'amberdata' }
1228
+ } else {
1229
+ throw new Error('Amberdata returned invalid JSON')
1230
+ }
1231
+ }
1232
+
1233
+ async checkBlockHeight() {
1234
+ return asyncWaterfall([
1235
+ this.checkBlockHeightEthscan,
1236
+ this.checkBlockHeightAmberdata,
1237
+ this.checkBlockHeightBlockchair,
1238
+ this.checkBlockHeightBlockbook
1239
+ ]).catch(err => {
1240
+ this.ethEngine.log.error('checkBlockHeight failed to update', err)
1241
+ return {}
1242
+ })
1243
+ }
1244
+
1245
+ async checkNonceEthscan() {
1246
+ const address = this.ethEngine.walletLocalData.publicKey
1247
+ const { result: jsonObj, server } = await this.multicastServers(
1248
+ 'eth_getTransactionCount',
1249
+ address
1250
+ )
1251
+ const valid = validateObject(jsonObj, EtherscanGetAccountNonce)
1252
+ if (valid && /0[xX][0-9a-fA-F]+/.test(jsonObj.result)) {
1253
+ const newNonce = bns.add('0', jsonObj.result)
1254
+ return { newNonce, server }
1255
+ } else {
1256
+ throw new Error('Ethscan returned invalid JSON')
1257
+ }
1258
+ }
1259
+
1260
+ async checkNonceAmberdata() {
1261
+ const address = this.ethEngine.walletLocalData.publicKey
1262
+ const jsonObj = await this.fetchPostAmberdataRpc(
1263
+ 'eth_getTransactionCount',
1264
+ [address, 'latest']
1265
+ )
1266
+ const valid = validateObject(jsonObj, AmberdataRpcSchema)
1267
+ if (valid) {
1268
+ const newNonce = `${parseInt(jsonObj.result, 16)}`
1269
+ return { newNonce, server: 'amberdata' }
1270
+ } else {
1271
+ throw new Error('Amberdata returned invalid JSON')
1272
+ }
1273
+ }
1274
+
1275
+ async checkNonce() {
1276
+ return asyncWaterfall([
1277
+ this.checkNonceEthscan,
1278
+ this.checkNonceAmberdata
1279
+ ]).catch(err => {
1280
+ this.ethEngine.log.error('checkNonce failed to update', err)
1281
+ return {}
1282
+ })
1283
+ }
1284
+
1285
+ async getAllTxsEthscan(
1286
+ startBlock,
1287
+ currencyCode,
1288
+ cleanerFunc,
1289
+ options
1290
+ ) {
1291
+ const address = this.ethEngine.walletLocalData.publicKey
1292
+ let page = 1
1293
+
1294
+ const allTransactions = []
1295
+ let server = ''
1296
+ const contractAddress = options.contractAddress
1297
+ const searchRegularTxs = options.searchRegularTxs
1298
+ while (1) {
1299
+ const offset = NUM_TRANSACTIONS_TO_QUERY
1300
+ const response = await this.multicastServers('getTransactions', {
1301
+ currencyCode,
1302
+ address,
1303
+ startBlock,
1304
+ page,
1305
+ offset,
1306
+ contractAddress,
1307
+ searchRegularTxs
1308
+ })
1309
+ server = response.server
1310
+ const transactions = response.result.result
1311
+ for (let i = 0; i < transactions.length; i++) {
1312
+ try {
1313
+ const cleanedTx = cleanerFunc(transactions[i])
1314
+ const tx = this.processEtherscanTransaction(cleanedTx, currencyCode)
1315
+ allTransactions.push(tx)
1316
+ } catch (e) {
1317
+ this.ethEngine.log.error(
1318
+ `getAllTxsEthscan ${cleanerFunc.name}\n${
1319
+ e.message
1320
+ }\n${JSON.stringify(transactions[i])}`
1321
+ )
1322
+ throw new Error(`getAllTxsEthscan ${cleanerFunc.name} is invalid`)
1323
+ }
1324
+ }
1325
+ if (transactions.length === 0) {
1326
+ break
1327
+ }
1328
+ page++
1329
+ }
1330
+
1331
+ return { allTransactions, server }
1332
+ }
1333
+
1334
+ async checkTxsEthscan(
1335
+ startBlock,
1336
+ currencyCode
1337
+ ) {
1338
+ let server
1339
+ let allTransactions
1340
+
1341
+ if (currencyCode === this.currencyInfo.currencyCode) {
1342
+ const txsRegularResp = await this.getAllTxsEthscan(
1343
+ startBlock,
1344
+ currencyCode,
1345
+ asEtherscanTransaction,
1346
+ { searchRegularTxs: true }
1347
+ )
1348
+ const txsInternalResp = await this.getAllTxsEthscan(
1349
+ startBlock,
1350
+ currencyCode,
1351
+ asEtherscanInternalTransaction,
1352
+ { searchRegularTxs: false }
1353
+ )
1354
+ server = txsRegularResp.server || txsInternalResp.server
1355
+ allTransactions = [
1356
+ ...txsRegularResp.allTransactions,
1357
+ ...txsInternalResp.allTransactions
1358
+ ]
1359
+ } else {
1360
+ const tokenInfo = this.ethEngine.getTokenInfo(currencyCode)
1361
+ if (tokenInfo && typeof tokenInfo.contractAddress === 'string') {
1362
+ const contractAddress = tokenInfo.contractAddress
1363
+ const resp = await this.getAllTxsEthscan(
1364
+ startBlock,
1365
+ currencyCode,
1366
+ asEtherscanTokenTransaction,
1367
+ { contractAddress }
1368
+ )
1369
+ server = resp.server
1370
+ allTransactions = resp.allTransactions
1371
+ } else {
1372
+ return {}
1373
+ }
1374
+ }
1375
+
1376
+ const edgeTransactionsBlockHeightTuple = {
1377
+ blockHeight: startBlock,
1378
+ edgeTransactions: allTransactions
1379
+ }
1380
+ return {
1381
+ tokenTxs: { [currencyCode]: edgeTransactionsBlockHeightTuple },
1382
+ server
1383
+ }
1384
+ }
1385
+
1386
+ /*
1387
+ * @returns The currencyCode of the token or undefined if
1388
+ * the token is not enabled for this user.
1389
+ */
1390
+ getTokenCurrencyCode(txnContractAddress) {
1391
+ const address = this.ethEngine.walletLocalData.publicKey
1392
+ if (txnContractAddress.toLowerCase() === address.toLowerCase()) {
1393
+ return this.currencyInfo.currencyCode
1394
+ } else {
1395
+ for (const tk of this.ethEngine.walletLocalData.enabledTokens) {
1396
+ const tokenInfo = this.ethEngine.getTokenInfo(tk)
1397
+ if (tokenInfo) {
1398
+ const tokenContractAddress = tokenInfo.contractAddress
1399
+ if (
1400
+ txnContractAddress &&
1401
+ typeof tokenContractAddress === 'string' &&
1402
+ tokenContractAddress.toLowerCase() ===
1403
+ txnContractAddress.toLowerCase()
1404
+ ) {
1405
+ return tk
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+
1412
+ async checkTxsAlethio(
1413
+ startBlock,
1414
+ currencyCode,
1415
+ useApiKey
1416
+ ) {
1417
+ const address = this.ethEngine.walletLocalData.publicKey
1418
+ const { native, token } =
1419
+ this.currencyInfo.defaultSettings.otherSettings.alethioCurrencies
1420
+ let linkNext
1421
+ let cleanedResponseObj
1422
+ const allTransactions = []
1423
+ while (1) {
1424
+ let jsonObj
1425
+ try {
1426
+ if (linkNext) {
1427
+ jsonObj = await this.fetchGetAlethio(linkNext, false, useApiKey)
1428
+ } else {
1429
+ if (currencyCode === this.currencyInfo.currencyCode) {
1430
+ jsonObj = await this.fetchGetAlethio(
1431
+ `/accounts/${address}/${native}Transfers`,
1432
+ true,
1433
+ useApiKey
1434
+ )
1435
+ } else {
1436
+ jsonObj = await this.fetchGetAlethio(
1437
+ `/accounts/${address}/${token}Transfers`,
1438
+ true,
1439
+ useApiKey
1440
+ )
1441
+ }
1442
+ }
1443
+ cleanedResponseObj = asFetchGetAlethio(jsonObj)
1444
+ } catch (e) {
1445
+ this.ethEngine.log.error(
1446
+ `checkTxsAlethio \n${e.message}\n${linkNext || ''}`
1447
+ )
1448
+ throw new Error('checkTxsAlethio response is invalid')
1449
+ }
1450
+
1451
+ linkNext = cleanedResponseObj.links.next
1452
+ let hasNext = cleanedResponseObj.meta.page.hasNext
1453
+
1454
+ for (const tokenTransfer of cleanedResponseObj.data) {
1455
+ try {
1456
+ const cleanTokenTransfer =
1457
+ asAlethioAccountsTokenTransfer(tokenTransfer)
1458
+ const txBlockheight = cleanTokenTransfer.attributes.globalRank[0]
1459
+ if (txBlockheight > startBlock) {
1460
+ let txCurrencyCode = this.currencyInfo.currencyCode
1461
+ if (currencyCode !== this.currencyInfo.currencyCode) {
1462
+ const contractAddress =
1463
+ cleanTokenTransfer.relationships.token.data.id
1464
+ txCurrencyCode = this.getTokenCurrencyCode(contractAddress)
1465
+ }
1466
+ if (typeof txCurrencyCode === 'string') {
1467
+ const tx = this.processAlethioTransaction(
1468
+ cleanTokenTransfer,
1469
+ txCurrencyCode
1470
+ )
1471
+ if (tx) {
1472
+ allTransactions.push(tx)
1473
+ }
1474
+ }
1475
+ } else {
1476
+ hasNext = false
1477
+ break
1478
+ }
1479
+ } catch (e) {
1480
+ this.ethEngine.log.error(`checkTxsAlethio tokenTransfer ${e.message}`)
1481
+ throw new Error(
1482
+ `checkTxsAlethio tokenTransfer is invalid\n${JSON.stringify(
1483
+ tokenTransfer
1484
+ )}`
1485
+ )
1486
+ }
1487
+ }
1488
+
1489
+ if (!hasNext) {
1490
+ break
1491
+ }
1492
+ }
1493
+
1494
+ // We init txsByCurrency with all tokens (or ETH) in order to
1495
+ // force processEthereumNetworkUpdate to set the lastChecked
1496
+ // timestamp. Otherwise tokens w/out transactions won't get
1497
+ // throttled properly. Remember that Alethio responds with
1498
+ // txs for *all* tokens.
1499
+ const response = { tokenTxs: {}, server: 'alethio' }
1500
+ if (currencyCode !== this.currencyInfo.currencyCode) {
1501
+ for (const tk of this.ethEngine.walletLocalData.enabledTokens) {
1502
+ if (tk !== this.currencyInfo.currencyCode) {
1503
+ response.tokenTxs[tk] = {
1504
+ blockHeight: startBlock,
1505
+ edgeTransactions: []
1506
+ }
1507
+ }
1508
+ }
1509
+ } else {
1510
+ // ETH is singled out here because it is a different (but very
1511
+ // similar) Alethio process
1512
+ response.tokenTxs[this.currencyInfo.currencyCode] = {
1513
+ blockHeight: startBlock,
1514
+ edgeTransactions: []
1515
+ }
1516
+ }
1517
+
1518
+ for (const tx of allTransactions) {
1519
+ response.tokenTxs[tx.currencyCode].edgeTransactions.push(tx)
1520
+ }
1521
+ return response
1522
+ }
1523
+
1524
+ // fine, used in asyncWaterfalls
1525
+ async getAllTxsAmberdata(
1526
+ startBlock,
1527
+ startDate,
1528
+ currencyCode,
1529
+ searchRegularTxs
1530
+ ) {
1531
+ const address = this.ethEngine.walletLocalData.publicKey
1532
+
1533
+ let page = 0
1534
+ const allTransactions = []
1535
+ while (1) {
1536
+ let url = `/addresses/${address}/${
1537
+ searchRegularTxs ? 'transactions' : 'functions'
1538
+ }?page=${page}&size=${NUM_TRANSACTIONS_TO_QUERY}`
1539
+
1540
+ if (searchRegularTxs) {
1541
+ let cleanedResponseObj
1542
+ try {
1543
+ if (startDate) {
1544
+ const newDateObj = new Date(startDate)
1545
+ const now = new Date()
1546
+ if (newDateObj) {
1547
+ url =
1548
+ url +
1549
+ `&startDate=${newDateObj.toISOString()}&endDate=${now.toISOString()}`
1550
+ }
1551
+ }
1552
+
1553
+ const jsonObj = await this.fetchGetAmberdataApi(url)
1554
+ cleanedResponseObj = asFetchGetAmberdataApiResponse(jsonObj)
1555
+ } catch (e) {
1556
+ this.ethEngine.log.error(
1557
+ `checkTxsAmberdata fetch regular ${e.message}\n${url}`
1558
+ )
1559
+ throw new Error('checkTxsAmberdata (regular tx) response is invalid')
1560
+ }
1561
+ const amberdataTxs = cleanedResponseObj.payload.records
1562
+ for (const amberdataTx of amberdataTxs) {
1563
+ try {
1564
+ const cleanAmberdataTx = asAmberdataAccountsTx(amberdataTx)
1565
+
1566
+ const tx = this.processAmberdataTxRegular(
1567
+ cleanAmberdataTx,
1568
+ currencyCode
1569
+ )
1570
+ if (tx) {
1571
+ allTransactions.push(tx)
1572
+ }
1573
+ } catch (e) {
1574
+ this.ethEngine.log.error(
1575
+ `checkTxsAmberdata process regular ${e.message}\n${JSON.stringify(
1576
+ amberdataTx
1577
+ )}`
1578
+ )
1579
+ throw new Error('checkTxsAmberdata regular amberdataTx is invalid')
1580
+ }
1581
+ }
1582
+ if (amberdataTxs.length === 0) {
1583
+ break
1584
+ }
1585
+ page++
1586
+ } else {
1587
+ let cleanedResponseObj
1588
+ try {
1589
+ if (startDate) {
1590
+ url = url + `&startDate=${startDate}&endDate=${Date.now()}`
1591
+ }
1592
+ const jsonObj = await this.fetchGetAmberdataApi(url)
1593
+ cleanedResponseObj = asFetchGetAmberdataApiResponse(jsonObj)
1594
+ } catch (e) {
1595
+ this.ethEngine.log.error(
1596
+ `checkTxsAmberdata fetch internal ${e.message}\n${url}`
1597
+ )
1598
+ throw new Error('checkTxsAmberdata (internal tx) response is invalid')
1599
+ }
1600
+ const amberdataTxs = cleanedResponseObj.payload.records
1601
+ for (const amberdataTx of amberdataTxs) {
1602
+ try {
1603
+ const cleanamberdataTx = asAmberdataAccountsFuncs(amberdataTx)
1604
+ const tx = this.processAmberdataTxInternal(
1605
+ cleanamberdataTx,
1606
+ currencyCode
1607
+ )
1608
+ if (tx) {
1609
+ allTransactions.push(tx)
1610
+ }
1611
+ } catch (e) {
1612
+ this.ethEngine.log.error(
1613
+ `checkTxsAmberdata process internal ${
1614
+ e.message
1615
+ }\n${JSON.stringify(amberdataTx)}`
1616
+ )
1617
+ throw new Error('checkTxsAmberdata internal amberdataTx is invalid')
1618
+ }
1619
+ }
1620
+ if (amberdataTxs.length === 0) {
1621
+ break
1622
+ }
1623
+ page++
1624
+ }
1625
+ }
1626
+
1627
+ return allTransactions
1628
+ }
1629
+
1630
+ async checkTxsAmberdata(
1631
+ startBlock,
1632
+ startDate,
1633
+ currencyCode
1634
+ ) {
1635
+ const allTxsRegular = await this.getAllTxsAmberdata(
1636
+ startBlock,
1637
+ startDate,
1638
+ currencyCode,
1639
+ true
1640
+ )
1641
+
1642
+ const allTxsInternal = await this.getAllTxsAmberdata(
1643
+ startBlock,
1644
+ startDate,
1645
+ currencyCode,
1646
+ false
1647
+ )
1648
+
1649
+ return {
1650
+ tokenTxs: {
1651
+ [`${this.currencyInfo.currencyCode}`]: {
1652
+ blockHeight: startBlock,
1653
+ edgeTransactions: [...allTxsRegular, ...allTxsInternal]
1654
+ }
1655
+ },
1656
+ server: 'amberdata'
1657
+ }
1658
+ }
1659
+
1660
+ async checkTxs(
1661
+ startBlock,
1662
+ startDate,
1663
+ currencyCode
1664
+ ) {
1665
+ let checkTxsFuncs = []
1666
+ // const useApiKey = true
1667
+ if (currencyCode === this.currencyInfo.currencyCode) {
1668
+ checkTxsFuncs = [
1669
+ async () => this.checkTxsAmberdata(startBlock, startDate, currencyCode),
1670
+ // async () => this.checkTxsAlethio(startBlock, currencyCode, useApiKey),
1671
+ // async () => this.checkTxsAlethio(startBlock, currencyCode, !useApiKey),
1672
+ async () => this.checkTxsBlockbook(startBlock),
1673
+ async () => this.checkTxsEthscan(startBlock, currencyCode)
1674
+ ]
1675
+ } else {
1676
+ checkTxsFuncs = [
1677
+ // async () => this.checkTxsAlethio(startBlock, currencyCode, useApiKey),
1678
+ // async () => this.checkTxsAlethio(startBlock, currencyCode, !useApiKey),
1679
+ async () => this.checkTxsBlockbook(startBlock),
1680
+ async () => this.checkTxsEthscan(startBlock, currencyCode)
1681
+ ]
1682
+ }
1683
+ return asyncWaterfall(checkTxsFuncs).catch(err => {
1684
+ this.ethEngine.log.error('checkTxs failed to update', err)
1685
+ return {}
1686
+ })
1687
+ }
1688
+
1689
+ async checkTxsBlockbook(
1690
+ startBlock = 0
1691
+ ) {
1692
+ const address = this.ethEngine.walletLocalData.publicKey.toLowerCase()
1693
+ let page = 1
1694
+ let totalPages = 1
1695
+ const out = {
1696
+ newNonce: '0',
1697
+ tokenBal: {},
1698
+ tokenTxs: {},
1699
+ server: ''
1700
+ }
1701
+ while (page <= totalPages) {
1702
+ const query =
1703
+ '/api/v2/address/' +
1704
+ address +
1705
+ `?from=${startBlock}&page=${page}&details=txs`
1706
+ const { result: jsonObj, server } = await this.multicastServers(
1707
+ 'blockbookTxs',
1708
+ query
1709
+ )
1710
+ let addressInfo
1711
+ try {
1712
+ addressInfo = asBlockbookAddress(jsonObj)
1713
+ } catch (e) {
1714
+ this.ethEngine.log.error(
1715
+ `checkTxsBlockbook ${server} error BlockbookAddress ${JSON.stringify(
1716
+ jsonObj
1717
+ )}`
1718
+ )
1719
+ throw new Error(
1720
+ `checkTxsBlockbook ${server} returned invalid JSON for BlockbookAddress`
1721
+ )
1722
+ }
1723
+ const { nonce, tokens, balance, transactions } = addressInfo
1724
+ out.newNonce = nonce
1725
+ out.tokenBal.ETH = balance
1726
+ out.server = server
1727
+ totalPages = addressInfo.totalPages
1728
+ page++
1729
+
1730
+ // Token balances
1731
+ for (const token of tokens) {
1732
+ try {
1733
+ const { symbol, balance } = asBlockbookTokenBalance(token)
1734
+ out.tokenBal[symbol] = balance
1735
+ } catch (e) {
1736
+ this.ethEngine.log.error(
1737
+ `checkTxsBlockbook ${server} BlockbookTokenBalance ${JSON.stringify(
1738
+ token
1739
+ )}`
1740
+ )
1741
+ throw new Error(
1742
+ `checkTxsBlockbook ${server} returned invalid JSON for BlockbookTokenBalance`
1743
+ )
1744
+ }
1745
+ }
1746
+
1747
+ // Transactions
1748
+ for (const tx of transactions) {
1749
+ const transactionsArray = []
1750
+ try {
1751
+ const cleanTx = asBlockbookTx(tx)
1752
+ if (
1753
+ cleanTx.tokenTransfers !== undefined &&
1754
+ cleanTx.tokenTransfers.length > 0
1755
+ ) {
1756
+ for (const tokenTransfer of cleanTx.tokenTransfers) {
1757
+ if (
1758
+ address === tokenTransfer.to.toLowerCase() ||
1759
+ address === tokenTransfer.from.toLowerCase()
1760
+ ) {
1761
+ try {
1762
+ transactionsArray.push(
1763
+ this.processBlockbookTx(tx, tokenTransfer)
1764
+ )
1765
+ } catch (e) {
1766
+ if (e.message !== 'Unsupported contract address') throw e
1767
+ continue
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ if (
1773
+ address === tx.vout[0].addresses[0].toLowerCase() ||
1774
+ address === tx.vin[0].addresses[0].toLowerCase()
1775
+ )
1776
+ transactionsArray.push(this.processBlockbookTx(tx))
1777
+ } catch (e) {
1778
+ this.ethEngine.log.error(
1779
+ `checkTxsBlockbook ${server} BlockbookTx ${JSON.stringify(tx)}`
1780
+ )
1781
+ throw new Error(
1782
+ `Blockbook ${server} returned invalid JSON for BlockbookTx`
1783
+ )
1784
+ }
1785
+ for (const edgeTransaction of transactionsArray) {
1786
+ if (out.tokenTxs[edgeTransaction.currencyCode] === undefined)
1787
+ out.tokenTxs[edgeTransaction.currencyCode] = {
1788
+ blockHeight: startBlock,
1789
+ edgeTransactions: []
1790
+ }
1791
+ out.tokenTxs[edgeTransaction.currencyCode].edgeTransactions.push(
1792
+ edgeTransaction
1793
+ )
1794
+ }
1795
+ }
1796
+ }
1797
+ return out
1798
+ }
1799
+
1800
+ processBlockbookTx(
1801
+ blockbookTx,
1802
+ tokenTx
1803
+ ) {
1804
+ const {
1805
+ txid,
1806
+ blockHeight,
1807
+ blockTime,
1808
+ value,
1809
+ ethereumSpecific: { gasLimit, status, gasUsed, gasPrice },
1810
+ vin,
1811
+ vout
1812
+ } = blockbookTx
1813
+ const ourAddress = this.ethEngine.walletLocalData.publicKey.toLowerCase()
1814
+ let toAddress = vout[0].addresses[0].toLowerCase()
1815
+ let fromAddress = vin[0].addresses[0].toLowerCase()
1816
+ let currencyCode = 'ETH'
1817
+ let nativeAmount = value
1818
+ let tokenRecipientAddress = null
1819
+ let networkFee = bns.mul(gasPrice, gasUsed.toString())
1820
+ let parentNetworkFee
1821
+ const ourReceiveAddresses = []
1822
+ if (toAddress === fromAddress) {
1823
+ // Send to self
1824
+ nativeAmount = bns.mul('-1', networkFee)
1825
+ } else if (toAddress === ourAddress) {
1826
+ // Receive
1827
+ ourReceiveAddresses.push(ourAddress)
1828
+ } else if (fromAddress === ourAddress) {
1829
+ // Send
1830
+ nativeAmount = bns.mul('-1', bns.add(nativeAmount, networkFee))
1831
+ }
1832
+ if (tokenTx) {
1833
+ const { symbol, value, to, from, token } = tokenTx
1834
+ // Ignore token transaction if the contract address isn't recognized
1835
+ if (
1836
+ !this.ethEngine.allTokens
1837
+ .concat(this.ethEngine.customTokens)
1838
+ .some(
1839
+ metatoken =>
1840
+ metatoken.contractAddress &&
1841
+ metatoken.contractAddress.toLowerCase() === token.toLowerCase()
1842
+ )
1843
+ ) {
1844
+ this.ethEngine.log(`processBlockbookTx unsupported token ${token}`)
1845
+ throw new Error('Unsupported contract address')
1846
+ }
1847
+ // Override currencyCode and nativeAmount if token transaction
1848
+ toAddress = to.toLowerCase()
1849
+ fromAddress = from.toLowerCase()
1850
+ currencyCode = symbol
1851
+ nativeAmount = toAddress === ourAddress ? value : bns.mul('-1', value)
1852
+ tokenRecipientAddress = toAddress
1853
+ networkFee = '0'
1854
+ parentNetworkFee = bns.mul(gasPrice, gasUsed.toString())
1855
+ }
1856
+ const otherParams = {
1857
+ from: [fromAddress],
1858
+ to: [toAddress],
1859
+ gas: gasLimit.toString(),
1860
+ gasPrice,
1861
+ gasUsed: gasUsed.toString(),
1862
+ errorVal: status,
1863
+ tokenRecipientAddress
1864
+ }
1865
+ const edgeTransaction = {
1866
+ txid,
1867
+ date: blockTime,
1868
+ currencyCode,
1869
+ blockHeight,
1870
+ nativeAmount,
1871
+ networkFee,
1872
+ parentNetworkFee,
1873
+ ourReceiveAddresses,
1874
+ signedTx: '',
1875
+ otherParams
1876
+ }
1877
+ return edgeTransaction
1878
+ }
1879
+
1880
+ async checkTokenBalEthscan(tk) {
1881
+ const address = this.ethEngine.walletLocalData.publicKey
1882
+ let response
1883
+ let jsonObj
1884
+ let server
1885
+ let cleanedResponseObj
1886
+ try {
1887
+ if (tk === this.currencyInfo.currencyCode) {
1888
+ response = await this.multicastServers('eth_getBalance', address)
1889
+ jsonObj = response.result
1890
+ server = response.server
1891
+ } else {
1892
+ const tokenInfo = this.ethEngine.getTokenInfo(tk)
1893
+ if (tokenInfo && typeof tokenInfo.contractAddress === 'string') {
1894
+ const contractAddress = tokenInfo.contractAddress
1895
+ const response = await this.multicastServers(
1896
+ 'getTokenBalance',
1897
+ address,
1898
+ contractAddress
1899
+ )
1900
+ jsonObj = response.result
1901
+ server = response.server
1902
+ }
1903
+ }
1904
+ cleanedResponseObj = asEtherscanGetAccountBalance(jsonObj)
1905
+ } catch (e) {
1906
+ this.ethEngine.log.error(
1907
+ `checkTokenBalEthscan token ${tk} response ${response || ''} ${
1908
+ e.message
1909
+ }`
1910
+ )
1911
+ throw new Error(
1912
+ `checkTokenBalEthscan invalid ${tk} response ${JSON.stringify(jsonObj)}`
1913
+ )
1914
+ }
1915
+ if (/^\d+$/.test(cleanedResponseObj.result)) {
1916
+ const balance = cleanedResponseObj.result
1917
+ return { tokenBal: { [tk]: balance }, server }
1918
+ } else {
1919
+ throw new Error(`checkTokenBalEthscan returned invalid JSON for ${tk}`)
1920
+ }
1921
+ }
1922
+
1923
+ async checkTokenBalBlockchair() {
1924
+ let cleanedResponseObj
1925
+ const address = this.ethEngine.walletLocalData.publicKey
1926
+ const url = `/${this.currencyInfo.pluginId}/dashboards/address/${address}?erc_20=true`
1927
+ try {
1928
+ const jsonObj = await this.fetchGetBlockchair(url, true)
1929
+ cleanedResponseObj = asCheckTokenBalBlockchair(jsonObj)
1930
+ } catch (e) {
1931
+ this.ethEngine.log.error(`checkTokenBalBlockchair ${url} ${e.message}`)
1932
+ throw new Error('checkTokenBalBlockchair response is invalid')
1933
+ }
1934
+ const response = {
1935
+ [this.currencyInfo.currencyCode]:
1936
+ cleanedResponseObj.data[address].address.balance
1937
+ }
1938
+ for (const tokenData of cleanedResponseObj.data[address].layer_2.erc_20) {
1939
+ try {
1940
+ const cleanTokenData = asBlockChairAddress(tokenData)
1941
+ const balance = cleanTokenData.balance
1942
+ const tokenAddress = cleanTokenData.token_address
1943
+ const tokenSymbol = cleanTokenData.token_symbol
1944
+ const tokenInfo = this.ethEngine.getTokenInfo(tokenSymbol)
1945
+ if (tokenInfo && tokenInfo.contractAddress === tokenAddress) {
1946
+ response[tokenSymbol] = balance
1947
+ } else {
1948
+ // Do nothing, eg: Old DAI token balance is ignored
1949
+ }
1950
+ } catch (e) {
1951
+ this.ethEngine.log.error(
1952
+ `checkTokenBalBlockchair tokenData ${e.message}\n${JSON.stringify(
1953
+ tokenData
1954
+ )}`
1955
+ )
1956
+ throw new Error('checkTokenBalBlockchair tokenData is invalid')
1957
+ }
1958
+ }
1959
+ return { tokenBal: response, server: 'blockchair' }
1960
+ }
1961
+
1962
+ async checkTokenBalRpc(tk) {
1963
+ if (tk === this.currencyInfo.currencyCode)
1964
+ throw new Error('eth_call cannot be used to query ETH balance')
1965
+ let cleanedResponseObj
1966
+ let response
1967
+ let jsonObj
1968
+ let server
1969
+ const address = this.ethEngine.walletLocalData.publicKey
1970
+ try {
1971
+ const tokenInfo = this.ethEngine.getTokenInfo(tk)
1972
+ if (tokenInfo && typeof tokenInfo.contractAddress === 'string') {
1973
+ const params = {
1974
+ data: `0x70a08231${padHex(removeHexPrefix(address), 32)}`,
1975
+ to: tokenInfo.contractAddress
1976
+ }
1977
+
1978
+ const response = await this.multicastServers('eth_call', params)
1979
+ jsonObj = response.result
1980
+ server = response.server
1981
+ }
1982
+
1983
+ cleanedResponseObj = asCheckTokenBalRpc(jsonObj)
1984
+ } catch (e) {
1985
+ this.ethEngine.log.error(
1986
+ `checkTokenBalRpc token ${tk} response ${response || ''} ${e.message}`
1987
+ )
1988
+ throw new Error(
1989
+ `checkTokenBalRpc invalid ${tk} response ${JSON.stringify(jsonObj)}`
1990
+ )
1991
+ }
1992
+ if (isHex(removeHexPrefix(cleanedResponseObj.result))) {
1993
+ return {
1994
+ tokenBal: { [tk]: hexToDecimal(cleanedResponseObj.result) },
1995
+ server
1996
+ }
1997
+ } else {
1998
+ throw new Error(`checkTokenBalRpc returned invalid JSON for ${tk}`)
1999
+ }
2000
+ }
2001
+
2002
+ async checkTokenBal(tk) {
2003
+ return asyncWaterfall([
2004
+ async () => this.checkTokenBalEthscan(tk),
2005
+ this.checkTokenBalBlockchair,
2006
+ async () => this.checkTokenBalRpc(tk)
2007
+ ]).catch(err => {
2008
+ this.ethEngine.log.error('checkTokenBal failed to update', err.message)
2009
+ return {}
2010
+ })
2011
+ }
2012
+
2013
+ async checkAndUpdate(
2014
+ lastChecked = 0,
2015
+ pollMillisec,
2016
+ preUpdateBlockHeight,
2017
+ checkFunc
2018
+ ) {
2019
+ const now = Date.now()
2020
+ if (now - lastChecked > pollMillisec) {
2021
+ try {
2022
+ const ethUpdate = await checkFunc()
2023
+ this.processEthereumNetworkUpdate(now, ethUpdate, preUpdateBlockHeight)
2024
+ } catch (e) {
2025
+ this.ethEngine.log.error(e)
2026
+ }
2027
+ }
2028
+ }
2029
+
2030
+ getQueryHeightWithLookback(queryHeight) {
2031
+ if (queryHeight > ADDRESS_QUERY_LOOKBACK_BLOCKS) {
2032
+ // Only query for transactions as far back as ADDRESS_QUERY_LOOKBACK_BLOCKS from the last time we queried transactions
2033
+ return queryHeight - ADDRESS_QUERY_LOOKBACK_BLOCKS
2034
+ } else {
2035
+ return 0
2036
+ }
2037
+ }
2038
+
2039
+ getQueryDateWithLookback(date) {
2040
+ if (date > ADDRESS_QUERY_LOOKBACK_SEC) {
2041
+ // Only query for transactions as far back as ADDRESS_QUERY_LOOKBACK_SEC from the last time we queried transactions
2042
+ return date - ADDRESS_QUERY_LOOKBACK_SEC
2043
+ } else {
2044
+ return 0
2045
+ }
2046
+ }
2047
+
2048
+ async needsLoop() {
2049
+ while (this.ethEngine.engineOn) {
2050
+ const preUpdateBlockHeight = this.ethEngine.walletLocalData.blockHeight
2051
+ await this.checkAndUpdate(
2052
+ this.ethNeeds.blockHeightLastChecked,
2053
+ BLOCKHEIGHT_POLL_MILLISECONDS,
2054
+ preUpdateBlockHeight,
2055
+ this.checkBlockHeight
2056
+ )
2057
+
2058
+ await this.checkAndUpdate(
2059
+ this.ethNeeds.nonceLastChecked,
2060
+ NONCE_POLL_MILLISECONDS,
2061
+ preUpdateBlockHeight,
2062
+ this.checkNonce
2063
+ )
2064
+
2065
+ let currencyCodes
2066
+ if (
2067
+ this.ethEngine.walletLocalData.enabledTokens.indexOf(
2068
+ this.currencyInfo.currencyCode
2069
+ ) === -1
2070
+ ) {
2071
+ currencyCodes = [this.currencyInfo.currencyCode].concat(
2072
+ this.ethEngine.walletLocalData.enabledTokens
2073
+ )
2074
+ } else {
2075
+ currencyCodes = this.ethEngine.walletLocalData.enabledTokens
2076
+ }
2077
+ for (const tk of currencyCodes) {
2078
+ await this.checkAndUpdate(
2079
+ this.ethNeeds.tokenBalLastChecked[tk],
2080
+ BAL_POLL_MILLISECONDS,
2081
+ preUpdateBlockHeight,
2082
+ async () => this.checkTokenBal(tk)
2083
+ )
2084
+
2085
+ await this.checkAndUpdate(
2086
+ this.ethNeeds.tokenTxsLastChecked[tk],
2087
+ TXS_POLL_MILLISECONDS,
2088
+ preUpdateBlockHeight,
2089
+ async () =>
2090
+ this.checkTxs(
2091
+ this.getQueryHeightWithLookback(
2092
+ this.ethEngine.walletLocalData.lastTransactionQueryHeight[tk]
2093
+ ),
2094
+ this.getQueryDateWithLookback(
2095
+ this.ethEngine.walletLocalData.lastTransactionDate[tk]
2096
+ ),
2097
+ tk
2098
+ )
2099
+ )
2100
+ }
2101
+
2102
+ await snooze(1000)
2103
+ }
2104
+ }
2105
+
2106
+ processEthereumNetworkUpdate(
2107
+ now,
2108
+ ethereumNetworkUpdate,
2109
+ preUpdateBlockHeight
2110
+ ) {
2111
+ if (!ethereumNetworkUpdate) return
2112
+ if (ethereumNetworkUpdate.blockHeight) {
2113
+ this.ethEngine.log(
2114
+ `${
2115
+ this.currencyInfo.currencyCode
2116
+ } processEthereumNetworkUpdate blockHeight ${
2117
+ ethereumNetworkUpdate.server || 'no server'
2118
+ } won`
2119
+ )
2120
+ const blockHeight = ethereumNetworkUpdate.blockHeight
2121
+ this.ethEngine.log(`Got block height ${blockHeight || 'no blockheight'}`)
2122
+ if (
2123
+ typeof blockHeight === 'number' &&
2124
+ this.ethEngine.walletLocalData.blockHeight !== blockHeight
2125
+ ) {
2126
+ this.ethNeeds.blockHeightLastChecked = now
2127
+ this.ethEngine.checkDroppedTransactionsThrottled()
2128
+ this.ethEngine.walletLocalData.blockHeight = blockHeight // Convert to decimal
2129
+ this.ethEngine.walletLocalDataDirty = true
2130
+ this.ethEngine.currencyEngineCallbacks.onBlockHeightChanged(
2131
+ this.ethEngine.walletLocalData.blockHeight
2132
+ )
2133
+ }
2134
+ }
2135
+
2136
+ if (ethereumNetworkUpdate.newNonce) {
2137
+ this.ethEngine.log(
2138
+ `${this.currencyInfo.currencyCode} processEthereumNetworkUpdate nonce ${
2139
+ ethereumNetworkUpdate.server || 'no server'
2140
+ } won`
2141
+ )
2142
+ this.ethNeeds.nonceLastChecked = now
2143
+ this.ethEngine.walletLocalData.otherData.nextNonce =
2144
+ ethereumNetworkUpdate.newNonce
2145
+ this.ethEngine.walletLocalDataDirty = true
2146
+ }
2147
+
2148
+ if (ethereumNetworkUpdate.tokenBal) {
2149
+ const tokenBal = ethereumNetworkUpdate.tokenBal
2150
+ this.ethEngine.log(
2151
+ `${
2152
+ this.currencyInfo.currencyCode
2153
+ } processEthereumNetworkUpdate tokenBal ${
2154
+ ethereumNetworkUpdate.server || 'no server'
2155
+ } won`
2156
+ )
2157
+ for (const tk of Object.keys(tokenBal)) {
2158
+ this.ethNeeds.tokenBalLastChecked[tk] = now
2159
+ this.ethEngine.updateBalance(tk, tokenBal[tk])
2160
+ }
2161
+ }
2162
+
2163
+ if (ethereumNetworkUpdate.tokenTxs) {
2164
+ const tokenTxs = ethereumNetworkUpdate.tokenTxs
2165
+ this.ethEngine.log(
2166
+ `${
2167
+ this.currencyInfo.currencyCode
2168
+ } processEthereumNetworkUpdate tokenTxs ${
2169
+ ethereumNetworkUpdate.server || 'no server'
2170
+ } won`
2171
+ )
2172
+ for (const tk of Object.keys(tokenTxs)) {
2173
+ this.ethNeeds.tokenTxsLastChecked[tk] = now
2174
+ this.ethEngine.tokenCheckTransactionsStatus[tk] = 1
2175
+ const tuple = tokenTxs[tk]
2176
+ if (tuple.edgeTransactions) {
2177
+ for (const tx of tuple.edgeTransactions) {
2178
+ this.ethEngine.addTransaction(tk, tx)
2179
+ }
2180
+ this.ethEngine.walletLocalData.lastTransactionQueryHeight[tk] =
2181
+ preUpdateBlockHeight
2182
+ this.ethEngine.walletLocalData.lastTransactionDate[tk] = now
2183
+ }
2184
+ }
2185
+ this.ethEngine.updateOnAddressesChecked()
2186
+ }
2187
+
2188
+ if (this.ethEngine.transactionsChangedArray.length > 0) {
2189
+ this.ethEngine.currencyEngineCallbacks.onTransactionsChanged(
2190
+ this.ethEngine.transactionsChangedArray
2191
+ )
2192
+ this.ethEngine.transactionsChangedArray = []
2193
+ }
2194
+ }
2195
+ }