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.
- package/CHANGELOG.md +713 -0
- package/LICENSE +29 -0
- package/README.md +63 -0
- package/index.js +3 -0
- package/lib/binance/bnbEngine.js +591 -0
- package/lib/binance/bnbInfo.js +43 -0
- package/lib/binance/bnbPlugin.js +168 -0
- package/lib/binance/bnbSchema.js +83 -0
- package/lib/binance/bnbTypes.js +39 -0
- package/lib/common/engine.js +918 -0
- package/lib/common/plugin.js +152 -0
- package/lib/common/schema.js +108 -0
- package/lib/common/types.js +85 -0
- package/lib/common/utils.js +378 -0
- package/lib/eos/eosEngine.js +1216 -0
- package/lib/eos/eosInfo.js +98 -0
- package/lib/eos/eosPlugin.js +314 -0
- package/lib/eos/eosSchema.js +190 -0
- package/lib/eos/eosTypes.js +88 -0
- package/lib/eos/telosInfo.js +94 -0
- package/lib/eos/waxInfo.js +95 -0
- package/lib/ethereum/etcInfo.js +121 -0
- package/lib/ethereum/ethEngine.js +832 -0
- package/lib/ethereum/ethInfo.js +1300 -0
- package/lib/ethereum/ethMiningFees.js +157 -0
- package/lib/ethereum/ethNetwork.js +2195 -0
- package/lib/ethereum/ethPlugin.js +377 -0
- package/lib/ethereum/ethSchema.js +61 -0
- package/lib/ethereum/ethTypes.js +461 -0
- package/lib/ethereum/ftminfo.js +102 -0
- package/lib/ethereum/rskInfo.js +101 -0
- package/lib/fio/fioConst.js +38 -0
- package/lib/fio/fioEngine.js +1250 -0
- package/lib/fio/fioError.js +38 -0
- package/lib/fio/fioInfo.js +72 -0
- package/lib/fio/fioPlugin.js +486 -0
- package/lib/fio/fioSchema.js +56 -0
- package/lib/index.js +44 -0
- package/lib/pluginError.js +32 -0
- package/lib/react-native/edge-currency-accountbased.js +239635 -0
- package/lib/react-native/edge-currency-accountbased.js.map +1 -0
- package/lib/react-native-io.js +41 -0
- package/lib/stellar/stellarEngine.js +563 -0
- package/lib/stellar/stellarInfo.js +37 -0
- package/lib/stellar/stellarPlugin.js +215 -0
- package/lib/stellar/stellarSchema.js +54 -0
- package/lib/stellar/stellarTypes.js +66 -0
- package/lib/tezos/tezosEngine.js +497 -0
- package/lib/tezos/tezosInfo.js +60 -0
- package/lib/tezos/tezosPlugin.js +174 -0
- package/lib/tezos/tezosTypes.js +110 -0
- package/lib/xrp/xrpEngine.js +583 -0
- package/lib/xrp/xrpInfo.js +47 -0
- package/lib/xrp/xrpPlugin.js +229 -0
- package/lib/xrp/xrpSchema.js +74 -0
- package/lib/xrp/xrpTypes.js +38 -0
- package/package.json +139 -0
- 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
|
+
}
|