• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GoodDollar / GoodServer / 17411668733

01 Sep 2025 09:03AM UTC coverage: 49.555%. Remained the same
17411668733

push

github

web-flow
Xdc update (#503)

* add: xdc wallet support

* add: make verifyIdentifier uses correct chainid to support smartwallets

* add: xdc bridge fees, small chains refactoring

* fix: missing network configs

---------

Co-authored-by: LewisB <lewis@gooddollar.org>

617 of 1518 branches covered (40.65%)

Branch coverage included in aggregate %.

22 of 51 new or added lines in 7 files covered. (43.14%)

392 existing lines in 5 files now uncovered.

1888 of 3537 relevant lines covered (53.38%)

7.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.71
/src/server/blockchain/Web3Wallet.js
1
// @flow
2
import Crypto from 'crypto'
3
import Web3 from 'web3'
4
import HDKey from 'hdkey'
5
import bip39 from 'bip39-light'
6
import get from 'lodash/get'
7
import assign from 'lodash/assign'
8
import chunk from 'lodash/chunk'
9
import * as web3Utils from 'web3-utils'
10
import IdentityABI from '@gooddollar/goodprotocol/artifacts/contracts/identity/IdentityV2.sol/IdentityV2.json'
11
import GoodDollarABI from '@gooddollar/goodcontracts/build/contracts/GoodDollar.min.json'
12
import UBIABI from '@gooddollar/goodprotocol/artifacts/contracts/ubi/UBIScheme.sol/UBIScheme.json'
13
import ProxyContractABI from '@gooddollar/goodprotocol/artifacts/contracts/utils/AdminWalletFuse.sol/AdminWalletFuse.json'
14
import ContractsAddress from '@gooddollar/goodprotocol/releases/deployment.json'
15
import FaucetABI from '@gooddollar/goodprotocol/artifacts/contracts/fuseFaucet/FuseFaucetV2.sol/FuseFaucetV2.json'
16
import BuyGDFactoryABI from '@gooddollar/goodprotocol/artifacts/abis/BuyGDCloneFactory.min.json'
17
import BuyGDABI from '@gooddollar/goodprotocol/artifacts/abis/BuyGDClone.min.json'
18

19
import conf from '../server.config'
20
import logger from '../../imports/logger'
21
import { isNonceError, isFundsError } from '../utils/eth'
22
import { retry as retryAsync, withTimeout } from '../utils/async'
23
import { type TransactionReceipt } from './blockchain-types'
24

25
import { getManager } from '../utils/tx-manager'
26
import { sendSlackAlert } from '../../imports/slack'
27
// import { HttpProviderFactory, WebsocketProvider } from './transport'
28

9✔
29
const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000'
9✔
30
const FUSE_TX_TIMEOUT = 25000 // should be confirmed after max 5 blocks (25sec)
31

9✔
32
//extend admin abi with genericcallbatch
33
const AdminWalletABI = [
9✔
34
  ...ProxyContractABI.abi,
35
  {
9✔
36
    inputs: [
37
      {
9✔
38
        internalType: 'address[]',
39
        name: '_contracts',
40
        type: 'address[]'
41
      },
42
      {
43
        internalType: 'bytes[]',
44
        name: '_datas',
45
        type: 'bytes[]'
46
      },
47
      {
48
        internalType: 'uint256[]',
49
        name: '_values',
50
        type: 'uint256[]'
51
      }
14✔
52
    ],
53
    name: 'genericCallBatch',
54
    outputs: [
×
55
      {
56
        internalType: 'bool',
8✔
57
        name: 'success',
8✔
58
        type: 'bool'
1✔
59
      },
1✔
60
      {
8✔
61
        internalType: 'bytes',
9✔
62
        name: 'returnValue',
9!
63
        type: 'bytes'
9✔
64
      }
9✔
65
    ],
66
    stateMutability: 'nonpayable',
9✔
67
    type: 'function'
9✔
68
  }
9✔
69
]
9✔
70
export const adminMinBalance = conf.adminMinBalance
9✔
71

9✔
72
export const forceUserToUseFaucet = conf.forceFaucetCall
9✔
73

9✔
74
export const defaultGas = 500000
9✔
75

9✔
76
export const web3Default = {
9!
77
  defaultBlock: 'latest',
9✔
78
  transactionBlockTimeout: 5,
9✔
79
  transactionConfirmationBlocks: 1,
9✔
80
  transactionPollingTimeout: 30
9✔
81
}
82

9✔
83
/**
84
 * Exported as AdminWallet
85
 * Interface with blockchain contracts via web3 using HDWalletProvider
86
 */
23✔
87
export class Web3Wallet {
88
  // defining vars here breaks "inheritance"
23✔
89
  get ready() {
9✔
90
    return this.initialize()
9✔
91
  }
92

93
  constructor(name, conf, options = null) {
23✔
94
    const {
95
      ethereum = null,
96
      network = null,
97
      maxFeePerGas = undefined,
9✔
98
      maxPriorityFeePerGas = undefined,
99
      faucetTxCost = 150000,
100
      gasPrice = undefined
9✔
101
    } = options || {}
9!
102
    const ethOpts = ethereum || conf.fuse
UNCOV
103
    const { network_id: networkId } = ethOpts
×
UNCOV
104

×
105
    this.faucetTxCost = faucetTxCost
×
106
    this.name = name
107
    this.addresses = []
108
    this.filledAddresses = []
109
    this.wallets = {}
9✔
110
    this.conf = conf
9✔
111
    this.mnemonic = conf.mnemonic
112
    this.network = network || conf.network
113
    this.ethereum = ethOpts
9✔
114
    this.networkId = networkId
115
    this.numberOfAdminWalletAccounts = conf.privateKey ? 1 : conf.numberOfAdminWalletAccounts
9✔
116
    this.maxFeePerGas = maxFeePerGas
117
    this.maxPriorityFeePerGas = maxPriorityFeePerGas
9✔
118
    this.gasPrice = gasPrice
119
    this.log = logger.child({ from: `${name}/${this.networkId}` })
120

121
    this.initialize()
122
  }
123

124
  async initialize() {
9✔
125
    let { _readyPromise } = this
126

127
    if (!_readyPromise) {
128
      _readyPromise = this.init()
129
      assign(this, { _readyPromise })
130
    }
131

132
    return _readyPromise
133
  }
134

135
  getWeb3TransportProvider(): HttpProvider | WebSocketProvider {
136
    const { log } = this
137
    let provider
138
    let web3Provider
139
    let transport = this.ethereum.web3Transport
140
    switch (transport) {
141
      case 'WebSocket':
142
        provider = this.ethereum.websocketWeb3Provider
143
        web3Provider = new Web3.providers.WebsocketProvider(provider)
144
        break
145

146
      case 'HttpProvider':
147
      default:
148
        provider = this.ethereum.httpWeb3Provider.split(',')[0]
149
        web3Provider = new Web3.providers.HttpProvider(provider, {
150
          timeout: FUSE_TX_TIMEOUT
151
        })
152
        break
153
    }
154

85✔
155
    log.debug({ conf, web3Provider, provider })
156

85✔
157
    log.debug('getWeb3TransportProvider', {
85✔
158
      conf: this.conf,
159
      web3Provider,
160
      provider,
161
      wallet: this.name,
45✔
162
      network: this.networkId
163
    })
45✔
164
    return web3Provider
45✔
165
  }
45✔
166

167
  // getWeb3TransportProvider(): HttpProvider | WebSocketProvider {
168
  //   let provider
169
  //   let web3Provider
9✔
170
  //   const { web3Transport, websocketWeb3Provider, httpWeb3Provider } = this.ethereum
171
  //   const { log } = this
9✔
172

9✔
173
  //   switch (web3Transport) {
9!
UNCOV
174
  //     case 'WebSocket':
×
175
  //       provider = websocketWeb3Provider
176
  //       web3Provider = new WebsocketProvider(provider)
177
  //       break
178

UNCOV
179
  //     case 'HttpProvider':
×
180
  //     default: {
181
  //       provider = httpWeb3Provider
182
  //       web3Provider = HttpProviderFactory.create(provider, {
9✔
183
  //         timeout: FUSE_TX_TIMEOUT
9✔
184
  //       })
185
  //       break
9✔
186
  //     }
187
  //   }
9!
UNCOV
188

×
189
  //   log.debug({ conf: this.conf, web3Provider, provider, wallet: this.name, network: this.networkId })
190
  //   return web3Provider
×
UNCOV
191
  // }
×
192

193
  addWalletAccount(web3, account) {
×
194
    const { eth } = web3
9!
195

9✔
196
    eth.accounts.wallet.add(account)
197
    eth.defaultAccount = account.address
9✔
198
  }
45✔
199

45✔
200
  addWallet(account) {
45✔
201
    const { address } = account
202

45✔
203
    this.addWalletAccount(this.web3, account)
204
    this.addresses.push(address)
205
    this.wallets[address] = account
9✔
206
  }
207

9✔
208
  async init() {
209
    const { log } = this
9✔
210

9✔
211
    const adminWalletAddress = get(ContractsAddress, `${this.network}.AdminWallet`)
212
    log.debug('WalletInit: Initializing wallet:', { conf: this.ethereum, adminWalletAddress, name: this.name })
9✔
213
    if (!adminWalletAddress) {
5✔
214
      log.debug('WalletInit: missing adminwallet address skipping initialization', {
215
        conf: this.ethereum,
5✔
216
        adminWalletAddress,
217
        name: this.name
5✔
218
      })
1✔
219
      return false
220
    }
1!
UNCOV
221

×
UNCOV
222
    this.txManager = getManager(this.ethereum.network_id)
×
223
    this.web3 = new Web3(this.getWeb3TransportProvider(), null, web3Default)
224

225
    assign(this.web3.eth, web3Default)
226

227
    if (this.conf.privateKey) {
228
      let account = this.web3.eth.accounts.privateKeyToAccount(this.conf.privateKey)
229

1✔
230
      this.address = account.address
1✔
231
      this.addWallet(account)
232

1✔
233
      log.info('WalletInit: Initialized by private key:', { address: account.address })
234
    } else if (this.mnemonic) {
1!
UNCOV
235
      let root = HDKey.fromMasterSeed(bip39.mnemonicToSeed(this.mnemonic, this.conf.adminWalletPassword))
×
UNCOV
236

×
237
      for (let i = 0; i < this.numberOfAdminWalletAccounts; i++) {
×
238
        const path = "m/44'/60'/0'/0/" + i
239
        let addrNode = root.derive(path)
240
        let account = this.web3.eth.accounts.privateKeyToAccount('0x' + addrNode._privateKey.toString('hex'))
241

1✔
242
        this.addWallet(account)
243
      }
1✔
244

245
      this.address = this.addresses[0]
5✔
246

5✔
247
      log.info('WalletInit: Initialized by mnemonic:', { address: this.addresses })
248
    }
5✔
249
    try {
250
      log.info('WalletInit: Obtained AdminWallet address', { adminWalletAddress, network: this.network })
5!
251

5✔
252
      const adminWalletContractBalance = await this.web3.eth.getBalance(adminWalletAddress)
5✔
253
      log.info(`WalletInit: AdminWallet contract balance`, { adminWalletContractBalance, adminWalletAddress })
254

255
      this.proxyContract = new this.web3.eth.Contract(AdminWalletABI, adminWalletAddress, { from: this.address })
256

257
      const maxAdminBalance = await this.proxyContract.methods.adminToppingAmount().call()
1✔
258
      const minAdminBalance = parseInt(web3Utils.fromWei(maxAdminBalance, 'gwei')) / 2
259

1!
UNCOV
260
      if (web3Utils.fromWei(adminWalletContractBalance, 'gwei') < minAdminBalance * this.addresses.length) {
×
261
        log.error('AdminWallet contract low funds')
262
        await sendSlackAlert({
×
263
          msg: `AdminWallet contract low funds ${this.name}`,
264
          adminWalletAddress,
265
          adminWalletContractBalance
266
        })
267
      }
1✔
268

269
      this.txManager.getTransactionCount = this.web3.eth.getTransactionCount
1✔
270
      await this.txManager.createListIfNotExists(this.addresses)
271

272
      log.info('WalletInit: Initialized wallet queue manager')
273

274
      if (this.conf.topAdminsOnStartup) {
275
        log.info('WalletInit: calling topAdmins...')
1✔
276
        await this.topAdmins(this.conf.numberOfAdminWalletAccounts).catch(e => {
1!
UNCOV
277
          log.warn('WalletInit: topAdmins failed', { e, errMessage: e.message })
×
278
        })
279
      }
280

1✔
281
      log.info('Initializing adminwallet addresses', { addresses: this.addresses })
282

283
      await Promise.all(
284
        this.addresses.map(async addr => {
285
          const balance = await this.web3.eth.getBalance(addr)
286
          const isAdminWallet = await this.isVerifiedAdmin(addr)
1✔
287

288
          log.info(`WalletInit: try address ${addr}:`, { balance, isAdminWallet, minAdminBalance })
289

290
          if (isAdminWallet && parseFloat(web3Utils.fromWei(balance, 'gwei')) > minAdminBalance) {
291
            log.info(`WalletInit: admin wallet ${addr} balance ${balance}`)
292
            this.filledAddresses.push(addr)
293
          }
294
        })
1✔
295
      )
296

297
      log.info('WalletInit: Initialized adminwallet addresses', { filled: this.filledAddresses })
298

299
      if (this.filledAddresses.length === 0) {
300
        log.error('WalletInit: no admin wallet with funds')
301

302
        await sendSlackAlert({
303
          msg: `critical: no fuse admin wallet with funds ${this.name}`
304
        })
305
      }
306

307
      this.address = this.filledAddresses[0]
1✔
308

309
      this.identityContract = new this.web3.eth.Contract(
310
        IdentityABI.abi,
311
        get(ContractsAddress, `${this.network}.Identity`, ADDRESS_ZERO),
312
        { from: this.address }
1!
UNCOV
313
      )
×
314

315
      const oldIdentity = get(ContractsAddress, `${this.network}.IdentityOld`)
316
      if (oldIdentity) {
317
        this.oldIdentityContract = new this.web3.eth.Contract(IdentityABI.abi, oldIdentity, { from: this.address })
318
      }
1✔
319

1✔
320
      this.tokenContract = new this.web3.eth.Contract(
321
        GoodDollarABI.abi,
1✔
322
        get(ContractsAddress, `${this.network}.GoodDollar`, ADDRESS_ZERO),
323
        { from: this.address }
324
      )
325

326
      this.UBIContract = new this.web3.eth.Contract(
327
        UBIABI.abi,
328
        get(ContractsAddress, `${this.network}.UBIScheme`, ADDRESS_ZERO),
329
        {
330
          from: this.address
331
        }
4✔
332
      )
333

4!
UNCOV
334
      this.faucetContract = new this.web3.eth.Contract(
×
335
        FaucetABI.abi,
336
        get(
337
          ContractsAddress,
338
          `${this.network}.FuseFaucet`,
5✔
339
          get(ContractsAddress, `${this.network}.Faucet`),
340
          ADDRESS_ZERO
341
        ),
342
        {
343
          from: this.address
344
        }
345
      )
346

UNCOV
347
      const buygdAddress = get(
×
348
        ContractsAddress,
349
        `${this.network}.BuyGDFactoryV2`,
×
UNCOV
350
        get(ContractsAddress, `${this.network}.BuyGDFactory`)
×
351
      )
352
      if (buygdAddress) {
×
UNCOV
353
        this.buygdFactoryContract = new this.web3.eth.Contract(BuyGDFactoryABI.abi, buygdAddress, {
×
354
          from: this.address
×
355
        })
×
356
      }
×
357

×
358
      let nativebalance = await this.web3.eth.getBalance(this.address)
359
      this.nonce = parseInt(await this.web3.eth.getTransactionCount(this.address))
×
UNCOV
360

×
361
      log.debug('WalletInit: AdminWallet Ready:', {
×
362
        activeWallets: this.filledAddresses.length,
363
        account: this.address,
364
        nativebalance,
365
        networkId: this.networkId,
366
        network: this.network,
367
        nonce: this.nonce,
UNCOV
368
        ContractsAddress: ContractsAddress[this.network]
×
369
      })
370
    } catch (e) {
UNCOV
371
      log.error('WalletInit: Error initializing wallet', e.message, e)
×
372

373
      if (this.conf.env !== 'test' && this.conf.env !== 'development') {
×
UNCOV
374
        process.exit(-1)
×
375
      }
376
    }
UNCOV
377

×
378
    return true
379
  }
380

381
  /**
382
   * top admin wallet accounts
383
   * @param {object} event callbacks
384
   * @returns {Promise<String>}
385
   */
386
  async topAdmins(numAdmins: number): Promise<any> {
387
    const { log } = this
388

389
    try {
390
      const { nonce, release, fail, address } = await this.txManager.lock(this.addresses[0], 500) // timeout of 1 sec, so all "workers" fail except for the first
3✔
391

4✔
392
      try {
4✔
393
        log.debug('topAdmins:', { numAdmins, address, nonce })
394
        for (let i = 0; i < numAdmins; i += 50) {
4✔
395
          log.debug('topAdmins sending tx', { address, nonce, adminIdx: i })
396
          const tx = this.proxyContract.methods.topAdmins(i, i + 50)
397
          const gas = await tx
398
            .estimateGas()
4✔
399
            .then(gas => parseInt(gas) + 200000) //buffer for proxy contract, reimburseGas?
4✔
400
            .catch(() => 1000000)
401
          await this.proxyContract.methods.topAdmins(i, i + 50).send({
4!
402
            gas,
403
            maxFeePerGas: this.maxFeePerGas,
4!
UNCOV
404
            maxPriorityFeePerGas: this.maxPriorityFeePerGas,
×
405
            from: address,
406
            nonce
407
          })
4✔
408
          log.debug('topAdmins success', { adminIdx: i })
409
        }
410

411
        release()
412
      } catch (e) {
4!
413
        log.error('topAdmins failed', e)
UNCOV
414
        fail()
×
415
      }
416
    } catch (e) {
417
      log.error('topAdmins failed', e)
4✔
418
    }
4✔
419
  }
4✔
420

421
  /**
422
   * whitelist an user in the `Identity` contract
423
   * @param {string} address
424
   * @param {string} did
425
   * @returns {Promise<TransactionReceipt>}
4✔
426
   */
427
  async whitelistUser(
4✔
428
    address: string,
429
    did: string,
430
    chainId: number = null,
431
    lastAuthenticated: number = 0,
432
    customLogger = null
433
  ): Promise<TransactionReceipt | boolean> {
434
    const log = customLogger || this.log
435

436
    let txHash
437

4✔
438
    try {
439
      const isWhitelisted = await this.isVerified(address)
4✔
440
      // if lastAuthenticated is 0, then we force reauthentication, otherwise assume this is just syncing whitelisting between chains
4✔
441
      const isVerified = lastAuthenticated > 0 && isWhitelisted
UNCOV
442

×
443
      if (isVerified) {
444
        return { status: true }
×
445
      }
446

447
      const [identityRecord, lastAuth] = await Promise.all([
448
        this.identityContract.methods.identities(address).call(),
449
        this.identityContract.methods.lastAuthenticated(address).call().then(parseInt)
450
      ])
451

UNCOV
452
      if (parseInt(identityRecord.status) === 1) {
×
453
        // user was already whitelisted in the past, just needs re-authentication
454
        return this.authenticateUser(address, log)
455
      }
456

1✔
457
      const onTransactionHash = hash => {
1✔
458
        log.debug('Whitelisting user got txhash:', { hash, address, did, wallet: this.name })
459
        txHash = hash
1✔
460
      }
1✔
461

462
      // we add a check for lastAuth, since on fuse the identityRecord can be empty for OLD whitelisted accounts and we don't want
463
      // to mark them as whitelisted on a chain other than fuse
464
      const txExtraArgs =
465
        conf.enableWhitelistAtChain && chainId !== null && lastAuth === 0 ? [chainId, lastAuthenticated] : []
466

467
      const txPromise = this.sendTransaction(
468
        this.proxyContract.methods.whitelist(address, did, ...txExtraArgs),
469
        {
470
          onTransactionHash
471
        },
472
        undefined,
473
        true,
474
        log
1✔
475
      )
1✔
476

477
      const tx = await txPromise
1✔
478

1✔
479
      log.info('Whitelisting user success:', { txHash, address, did, chainId, lastAuthenticated, wallet: this.name })
UNCOV
480
      return tx
×
481
    } catch (exception) {
482
      const { message } = exception
×
UNCOV
483

×
484
      log.warn('Whitelisting user failed:', message, exception, {
485
        txHash,
486
        address,
487
        did,
488
        chainId,
1✔
489
        lastAuthenticated,
490
        wallet: this.name
1✔
491
      })
1✔
492
      throw exception
493
    }
1✔
494
  }
UNCOV
495

×
496
  async authenticateUser(address: string, customLogger = null): Promise<TransactionReceipt> {
497
    const log = customLogger || this.log
×
UNCOV
498

×
499
    try {
500
      let encodedCall = this.web3.eth.abi.encodeFunctionCall(
501
        {
502
          name: 'authenticate',
UNCOV
503
          type: 'function',
×
504
          inputs: [
505
            {
×
UNCOV
506
              type: 'address',
×
507
              name: 'account'
508
            }
×
509
          ]
510
        },
×
511
        [address]
512
      )
×
UNCOV
513

×
514
      const transaction = await this.proxyContract.methods.genericCall(this.identityContract._address, encodedCall, 0)
515
      const tx = await this.sendTransaction(transaction, {})
516

517
      log.info('authenticating user success:', { address, tx, wallet: this.name })
UNCOV
518
      return tx
×
519
    } catch (exception) {
520
      const { message } = exception
×
UNCOV
521

×
522
      log.warn('authenticating user failed:', message, exception, { address })
523
      throw exception
524
    }
525
  }
UNCOV
526

×
527
  async getAuthenticationPeriod(): Promise<number> {
×
528
    const { log } = this
529

530
    try {
531
      const result = await this.identityContract.methods.authenticationPeriod().call().then(parseInt)
UNCOV
532

×
533
      return result
534
    } catch (exception) {
535
      const { message } = exception
UNCOV
536

×
537
      log.warn('Error getAuthenticationPeriod', message, exception)
538
      throw exception
×
539
    }
540
  }
×
UNCOV
541

×
542
  async getWhitelistedOnChainId(account): Promise<number> {
543
    const { log } = this
544

545
    try {
546
      const result = await this.identityContract.methods.getWhitelistedOnChainId(account).call().then(parseInt)
547

548
      return result
549
    } catch (exception) {
550
      const { message } = exception
UNCOV
551

×
552
      log.warn('Error getWhitelistedOnChainId', message, exception)
553
      throw exception
×
554
    }
555
  }
×
UNCOV
556

×
557
  async getLastAuthenticated(account): Promise<number> {
558
    const { log } = this
559

UNCOV
560
    try {
×
561
      const [newResult, oldResult] = await Promise.all([
562
        this.identityContract.methods
563
          .lastAuthenticated(account)
564
          .call()
565
          .then(parseInt)
566
          .catch(() => 0),
567
        this.oldIdentityContract
568
          ? this.oldIdentityContract.methods
569
              .lastAuthenticated(account)
1✔
570
              .call()
571
              .then(parseInt)
1✔
572
              .catch(() => 0)
573
          : Promise.resolve(0)
UNCOV
574
      ])
×
UNCOV
575

×
576
      return newResult || oldResult
577
    } catch (exception) {
578
      const { message } = exception
1✔
579

580
      log.warn('Error getLastAuthenticated', message, exception)
581
      throw exception
582
    }
583
  }
584

585
  /**
586
   * blacklist an user in the `Identity` contract
587
   * @param {string} address
7✔
588
   * @returns {Promise<TransactionReceipt>}
589
   */
7✔
590
  async blacklistUser(address: string): Promise<TransactionReceipt> {
591
    const { log } = this
592

UNCOV
593
    const tx: TransactionReceipt = await this.sendTransaction(this.proxyContract.methods.blacklist(address)).catch(
×
UNCOV
594
      e => {
×
595
        log.error('Error blackListUser', e.message, e, { address })
596
        throw e
597
      }
7✔
598
    )
599

600
    return tx
UNCOV
601
  }
×
602

603
  /**
×
604
   * remove a user in the `Identity` contract
605
   * @param {string} address
606
   * @returns {Promise<TransactionReceipt>}
UNCOV
607
   */
×
UNCOV
608
  async removeWhitelisted(address: string): Promise<TransactionReceipt> {
×
609
    const { log } = this
610

UNCOV
611
    const tx: TransactionReceipt = await this.sendTransaction(
×
612
      this.proxyContract.methods.removeWhitelist(address)
613
    ).catch(e => {
614
      log.error('Error removeWhitelisted', e.message, e, { address })
615
      throw e
616
    })
617

618
    return tx
619
  }
5✔
620

621
  /**
5✔
622
   * verify if an user is verified in the `Identity` contract
623
   * @param {string} address
624
   * @returns {Promise<boolean>}
UNCOV
625
   */
×
UNCOV
626
  async isVerified(address: string): Promise<boolean> {
×
627
    const { log } = this
628

629
    const tx: boolean = await this.identityContract.methods
5✔
630
      .isWhitelisted(address)
631
      .call()
632
      .catch(e => {
633
        log.error('Error isVerified', e.message, e)
634
        throw e
635
      })
636

637
    return tx
8✔
638
  }
8✔
639

8✔
640
  async getDID(address: string): Promise<string> {
641
    const { log } = this
8!
642

8✔
643
    const tx: boolean = await this.identityContract.methods
644
      .addrToDID(address)
645
      .call()
UNCOV
646
      .catch(e => {
×
647
        log.error('Error getDID', e.message, e)
648
        throw e
UNCOV
649
      })
×
650

651
    return tx
×
652
  }
653
  /**
UNCOV
654
   *
×
655
   * @param {string} address
656
   * @returns {Promise<boolean>}
×
UNCOV
657
   */
×
658
  async isVerifiedAdmin(address: string): Promise<boolean> {
659
    const { log } = this
×
UNCOV
660

×
661
    const tx: boolean = await this.proxyContract.methods
662
      .isAdmin(address)
663
      .call()
664
      .catch(e => {
665
        log.error('Error isAdmin', e.message, e)
UNCOV
666
        throw e
×
UNCOV
667
      })
×
668

669
    return tx
UNCOV
670
  }
×
UNCOV
671

×
672
  /**
×
673
   * top wallet if needed
×
674
   * @param {string} address
675
   * @returns {PromiEvent<TransactionReceipt>}
UNCOV
676
   */
×
UNCOV
677
  async topWallet(address: string, customLogger = null): PromiEvent<TransactionReceipt> {
×
678
    const logger = customLogger || this.log
×
679
    const faucetRes = await this.topWalletFaucet(address, logger).catch(() => false)
×
680

681
    if (faucetRes) {
UNCOV
682
      return faucetRes
×
683
    }
684

685
    // if we reached here, either we used the faucet or user should call faucet on its own.
686
    let txHash = ''
687

688
    // simulate tx to detect revert
689
    const canTopOrError = await retryAsync(
UNCOV
690
      () =>
×
UNCOV
691
        this.proxyContract.methods
×
692
          .topWallet(address)
693
          .call()
×
UNCOV
694
          .then(() => true)
×
695
          .catch(e => {
696
            if (e.message.search(/VM execution|reverted/i) >= 0) {
697
              return false
698
            } else {
×
699
              logger.debug('retrying canTopOrError', e.message, { chainId: this.networkId, data: e.data })
8!
700
              throw e
8✔
701
            }
8✔
702
          }),
703
      3,
8✔
704
      500
705
    ).catch(e => {
8!
UNCOV
706
      logger.warn('canTopOrError failed after retries', e.message, e, { chainId: this.networkId })
×
707
      throw e
708
    })
709

8✔
710
    if (canTopOrError === false) {
8✔
711
      let userBalance = web3Utils.toBN(await this.web3.eth.getBalance(address))
8✔
712
      logger.debug('Topwallet will revert, skipping', { address, canTopOrError, wallet: this.name, userBalance })
713
      return false
8✔
714
    }
715

716
    try {
717
      const onTransactionHash = hash => {
718
        logger.debug('Topwallet got txhash:', { hash, address, wallet: this.name })
719
        txHash = hash
720
      }
721

8!
UNCOV
722
      const res = await this.sendTransaction(
×
UNCOV
723
        this.proxyContract.methods.topWallet(address),
×
724
        { onTransactionHash },
725
        undefined,
726
        true,
8✔
727
        logger
728
      )
729

730
      logger.debug('Topwallet result:', { txHash, address, res, wallet: this.name })
731
      return res
732
    } catch (e) {
733
      logger.error('Error topWallet', e.message, e, { txHash, address, wallet: this.name })
734
      throw e
735
    }
736
  }
737

738
  async banInFaucet(toBan, customLogger = null) {
739
    const logger = customLogger || this.log
740
    const chunks = chunk(toBan, 25)
8✔
741
    logger.debug('banInFaucet:', {
8✔
742
      toBan,
8✔
743
      chunks: chunks.length
8✔
744
    })
745
    for (const idx in chunks) {
8✔
746
      const addresses = chunks[idx]
8✔
747
      logger.debug('banInFaucet chunk:', {
UNCOV
748
        addresses,
×
UNCOV
749
        idx
×
750
      })
751
      try {
752
        const datas = addresses.map(address => {
753
          let encodedCall = this.web3.eth.abi.encodeFunctionCall(
×
UNCOV
754
            {
×
755
              name: 'banAddress',
756
              type: 'function',
×
UNCOV
757
              inputs: [
×
758
                {
759
                  type: 'address',
760
                  name: 'account'
761
                }
762
              ]
763
            },
764
            [address]
765
          )
766
          return encodedCall
767
        })
768
        const contracts = addresses.map(() => this.faucetContract._address)
769
        const values = addresses.map(() => 0)
770
        const transaction = this.proxyContract.methods.genericCallBatch(contracts, datas, values)
UNCOV
771
        const onTransactionHash = hash =>
×
772
          void logger.debug('banInFaucet got txhash:', { hash, addresses, wallet: this.name })
773
        const res = await this.sendTransaction(transaction, { onTransactionHash }, undefined, true, logger)
×
UNCOV
774

×
775
        logger.debug('banInFaucet result:', { addresses, res, wallet: this.name })
776
        return res
×
UNCOV
777
      } catch (e) {
×
778
        logger.error('Error banInFaucet', e.message, e, { addresses, wallet: this.name })
779
        throw e
×
780
      }
781
    }
×
UNCOV
782
  }
×
783

784
  async topWalletFaucet(address, customLogger = null) {
785
    const logger = customLogger || this.log
786
    try {
×
UNCOV
787
      const canTop = await this.faucetContract.methods.canTop(address).call()
×
UNCOV
788

×
789
      logger.debug('topWalletFaucet canTop result:', { address, canTop, wallet: this.name })
×
790

791
      if (canTop === false) {
×
792
        return false //we try to top from admin wallet
793
      }
×
UNCOV
794

×
795
      let userBalance = web3Utils.toBN(await this.web3.eth.getBalance(address))
796
      const gasPrice = await this.web3.eth.getGasPrice()
×
797
      let faucetTxCost = web3Utils.toBN(this.faucetTxCost).mul(web3Utils.toBN(gasPrice))
798

UNCOV
799
      logger.debug('topWalletFaucet:', {
×
800
        address,
801
        userBalance: userBalance.toString(),
×
802
        faucetTxCost: faucetTxCost.toString(),
803
        wallet: this.name
804
      })
805

806
      // user can't call faucet directly
807
      if (forceUserToUseFaucet && userBalance.gte(faucetTxCost)) {
808
        logger.debug('User has enough gas to call faucet', { address, wallet: this.name })
809
        return true //return true so we don't call AdminWallet to topwallet
810
      }
811

812
      let encodedCall = this.web3.eth.abi.encodeFunctionCall(
813
        {
814
          name: 'topWallet',
815
          type: 'function',
816
          inputs: [
817
            {
818
              type: 'address',
UNCOV
819
              name: 'account'
×
UNCOV
820
            }
×
821
          ]
×
822
        },
×
823
        [address]
824
      )
UNCOV
825

×
826
      const transaction = this.proxyContract.methods.genericCall(this.faucetContract._address, encodedCall, 0)
827
      const onTransactionHash = hash =>
UNCOV
828
        void logger.debug('topWalletFaucet got txhash:', { hash, address, wallet: this.name })
×
UNCOV
829
      const res = await this.sendTransaction(transaction, { onTransactionHash }, undefined, true, logger)
×
830

831
      logger.debug('topWalletFaucet result:', { address, res, wallet: this.name })
832
      return res
833
    } catch (e) {
834
      logger.error('Error topWalletFaucet', e.message, e, { address, wallet: this.name })
835
      throw e
836
    }
837
  }
838

839
  async fishMulti(toFish: Array<string>, customLogger = null): Promise<TransactionReceipt> {
840
    const logger = customLogger || this.log
841

842
    try {
843
      let encodedCall = this.web3.eth.abi.encodeFunctionCall(
844
        {
845
          name: 'fishMulti',
846
          type: 'function',
UNCOV
847
          inputs: [
×
UNCOV
848
            {
×
849
              type: 'address[]',
×
850
              name: '_accounts'
×
851
            }
852
          ]
UNCOV
853
        },
×
854
        [toFish]
855
      )
×
856

857
      logger.info('fishMulti sending tx', { encodedCall, toFish, ubischeme: this.UBIContract._address })
×
UNCOV
858

×
859
      const transaction = await this.proxyContract.methods.genericCall(this.UBIContract._address, encodedCall, 0)
860
      const tx = await this.sendTransaction(transaction, {}, { gas: 2000000 }, false, logger)
861

862
      logger.info('fishMulti success', { toFish, tx: tx.transactionHash })
863
      return tx
864
    } catch (exception) {
865
      const { message } = exception
866

867
      logger.error('fishMulti failed', message, exception, { toFish })
868
      throw exception
869
    }
×
UNCOV
870
  }
×
871

872
  async swaphelper(address, customLogger = null) {
×
UNCOV
873
    const logger = customLogger || this.log
×
874
    const predictedAddress = await this.buygdFactoryContract.methods.predict(address).call()
875
    const isHelperDeployed = await this.web3.eth.getCode(predictedAddress).then(code => code !== '0x')
876

877
    try {
878
      let swapResult
879
      if (isHelperDeployed) {
880
        const buygdContract = new this.web3.eth.Contract(BuyGDABI.abi, predictedAddress)
881
        //simulate tx
882
        const estimatedGas = await buygdContract.methods
883
          .swap(0, this.proxyContract._address)
884
          .estimateGas()
885
          .then(_ => parseInt(_) + 200000)
886

887
        let encodedCall = this.web3.eth.abi.encodeFunctionCall(
888
          {
889
            name: 'swap',
UNCOV
890
            type: 'function',
×
891
            inputs: [
892
              {
×
UNCOV
893
                type: 'uint256',
×
894
                name: 'minAmount'
895
              },
×
UNCOV
896
              {
×
897
                type: 'address',
898
                name: 'gasRefund'
×
899
              }
900
            ]
×
UNCOV
901
          },
×
902
          [0, this.proxyContract._address]
903
        )
904

905
        const transaction = this.proxyContract.methods.genericCall(predictedAddress, encodedCall, 0)
906
        const onTransactionHash = hash =>
1✔
907
          void logger.debug('swaphelper swap got txhash:', { estimatedGas, hash, address, wallet: this.name })
908
        swapResult = await this.sendTransaction(transaction, { onTransactionHash }, { gas: estimatedGas }, true, logger)
909
      } else {
910
        //simulate tx
911
        const estimatedGas = await this.buygdFactoryContract.methods
912
          .createAndSwap(address, 0)
913
          .estimateGas()
914
          .then(_ => parseInt(_) + 200000)
1✔
915
        let encodedCall = this.web3.eth.abi.encodeFunctionCall(
916
          {
1✔
917
            name: 'createAndSwap',
1✔
918
            type: 'function',
UNCOV
919
            inputs: [
×
UNCOV
920
              {
×
921
                type: 'address',
922
                name: 'account'
923
              },
924
              {
×
925
                type: 'uint256',
1!
926
                name: 'minAmount'
927
              }
1!
928
            ]
1✔
929
          },
1✔
930
          [address, 0]
UNCOV
931
        )
×
932

933
        const transaction = this.proxyContract.methods.genericCall(this.buygdFactoryContract._address, encodedCall, 0)
×
UNCOV
934
        const onTransactionHash = hash =>
×
935
          void logger.debug('swaphelper createAndSwap got txhash:', { estimatedGas, hash, address, wallet: this.name })
936
        swapResult = await this.sendTransaction(transaction, { onTransactionHash }, { gas: estimatedGas }, true, logger)
937
      }
938

939
      logger.debug('swaphelper tx result:', { address, swapResult, wallet: this.name })
940

941
      return swapResult
942
    } catch (e) {
943
      logger.error('Error swaphelper', e.message, e, { address, predictedAddress, isHelperDeployed, wallet: this.name })
944
      throw e
945
    }
946
  }
947

948
  /**
949
   * transfer G$s locked in adminWallet contract to recipient
950
   * @param {*} to recipient
951
   * @param {*} value amount to transfer
UNCOV
952
   * @param {*} logger
×
UNCOV
953
   * @returns
×
954
   */
955
  async transferWalletGoodDollars(to, value, customLogger = null): Promise<TransactionReceipt> {
×
UNCOV
956
    const logger = customLogger || this.log
×
957

958
    try {
×
959
      let encodedCall = this.web3.eth.abi.encodeFunctionCall(
960
        {
×
UNCOV
961
          name: 'transfer',
×
962
          type: 'function',
963
          inputs: [
964
            {
965
              type: 'address',
UNCOV
966
              name: 'to'
×
967
            },
968
            {
×
UNCOV
969
              type: 'uint256',
×
970
              name: 'value'
971
            }
×
UNCOV
972
          ]
×
973
        },
974
        [to, value]
×
975
      )
976
      logger.info('transferWalletGoodDollars sending tx', { encodedCall, to, value })
977

978
      const transaction = await this.proxyContract.methods.genericCall(this.tokenContract._address, encodedCall, 0)
979
      const tx = await this.sendTransaction(transaction, {}, undefined, false, logger)
980

981
      logger.info('transferWalletGoodDollars success', { to, value, tx: tx.transactionHash })
982
      return tx
983
    } catch (exception) {
984
      const { message } = exception
985

986
      logger.error('transferWalletGoodDollars failed', message, exception, { to, value })
987
      throw exception
988
    }
989
  }
990

991
  async getAddressBalance(address: string): Promise<string> {
992
    return this.web3.eth.getBalance(address)
993
  }
994

995
  /**
996
   * get balance for admin wallet
1✔
997
   * @returns {Promise<number>}
14✔
998
   */
999
  async getBalance(): Promise<number> {
1000
    const { log } = this
1001

1002
    return this.getAddressBalance(this.address)
2✔
1003
      .then(b => parseFloat(web3Utils.fromWei(b)))
2✔
1004
      .catch(e => {
1005
        log.error('Error getBalance', e.message, e)
1006
        throw e
14✔
1007
      })
14✔
1008
  }
1009

14✔
1010
  async registerRedtent(account: string, countryCode: string, customLogger = null): Promise<TransactionReceipt> {
14✔
1011
    const logger = customLogger || this.log
1012

14✔
1013
    if (this.networkId != 42220) {
28✔
1014
      logger.info(`skipping registerRedtent for non Celo: ${this.networkId}`)
1015
      return
1016
    }
14✔
1017
    const poolAddress = conf.redtentPools[countryCode]
UNCOV
1018

×
UNCOV
1019
    try {
×
1020
      let encodedCall = this.web3.eth.abi.encodeFunctionCall(
×
1021
        {
1022
          name: 'addMember',
1023
          type: 'function',
1024
          inputs: [
14!
UNCOV
1025
            {
×
1026
              name: 'member',
1027
              type: 'address'
1028
            },
14✔
1029
            {
14✔
1030
              name: 'extraData',
14✔
1031
              type: 'bytes'
1032
            }
14!
NEW
1033
          ]
×
1034
        },
1035
        [account, '0x']
14✔
1036
      )
1037

14!
UNCOV
1038
      const transaction = await this.proxyContract.methods.genericCall(poolAddress, encodedCall, 0)
×
UNCOV
1039
      const tx = await this.sendTransaction(transaction, {}, undefined, false, logger)
×
UNCOV
1040

×
1041
      logger.info('registerRedtent success', { account, countryCode, tx: tx.transactionHash, poolAddress })
1042
      return tx
14✔
1043
    } catch (exception) {
1044
      const { message } = exception
14✔
1045

1046
      logger.error('registerRedtent failed', message, exception, { account, poolAddress, countryCode })
14✔
1047
      throw exception
1048
    }
14✔
1049
  }
1050

14!
UNCOV
1051
  async getFeeEstimates() {
×
1052
    const result = await this.web3.eth.getFeeHistory('0x5', 'latest', [10])
1053

1054
    const baseFees = result.baseFeePerGas.map(hex => parseInt(hex, 16))
14✔
1055
    const rewards = result.reward.map(r => parseInt(r[0], 16)) // 10th percentile
14✔
1056

14✔
1057
    const latestBaseFee = baseFees[baseFees.length - 1]
1058
    const minPriorityFee = Math.min(...rewards)
1059

1060
    return {
1061
      baseFee: Math.floor(latestBaseFee * 1.1), // in wei
1062
      priorityFee: minPriorityFee // in wei
1063
    }
1064
  }
1065

1066
  /**
1067
   * Helper function to handle a tx Send call
14✔
1068
   * @param tx
14✔
1069
   * @param {object} promiEvents
1070
   * @param {function} promiEvents.onTransactionHash
14✔
1071
   * @param {function} promiEvents.onReceipt
14✔
1072
   * @param {function} promiEvents.onConfirmation
1073
   * @param {function} promiEvents.onError
14✔
1074
   * @param {object} gasValues
12✔
1075
   * @param {number} gasValues.gas
1076
   * @param {number} gasValues.maxFeePerGas
1077
   * @param {number} gasValues.maxPriorityFeePerGas
1078
   * @returns {Promise<Promise|Q.Promise<any>|Promise<*>|Promise<*>|Promise<*>|*>}
14✔
1079
   */
14✔
1080
  async sendTransaction(
1081
    tx: any,
1082
    txCallbacks: PromiEvents = {},
14✔
1083
    { gas, maxPriorityFeePerGas, maxFeePerGas, gasPrice }: GasValues = {
1084
      gas: undefined,
14!
UNCOV
1085
      maxFeePerGas: undefined,
×
1086
      maxPriorityFeePerGas: undefined
1087
    },
1088
    retry = true,
14✔
1089
    customLogger = null
1090
  ) {
1091
    let currentAddress, txHash, currentNonce
7!
UNCOV
1092
    const txuuid = Crypto.randomBytes(5).toString('base64')
×
1093
    const logger = customLogger || this.log
1094

1095
    try {
UNCOV
1096
      const { onTransactionHash, onReceipt, onConfirmation, onError } = txCallbacks
×
UNCOV
1097

×
1098
      gas =
UNCOV
1099
        gas ||
×
1100
        (await tx
1101
          .estimateGas()
1102
          .then(gas => parseInt(gas) + 200000) //buffer for proxy contract, reimburseGas?
1103
          .catch(e => {
1104
            logger.warn('Failed to estimate gas for tx', e.message, e, { wallet: this.name, network: this.networkId })
1105
            if (e.message.toLowerCase().includes('reverted')) throw e
1106
            return defaultGas
1107
          }))
1108

1109
      // adminwallet contract might give wrong gas estimates, so if its more than block gas limit reduce it to default
1110
      if (gas > 8000000) {
1111
        gas = defaultGas
1112
      }
1113

1114
      gasPrice = gasPrice || this.gasPrice
UNCOV
1115
      maxFeePerGas = maxFeePerGas || this.maxFeePerGas
×
UNCOV
1116
      maxPriorityFeePerGas = maxPriorityFeePerGas || this.maxPriorityFeePerGas
×
1117

1118
      if (gasPrice && !maxFeePerGas && !maxPriorityFeePerGas) {
UNCOV
1119
        logger.info('using legacy gasPrice tx')
×
1120
      } else {
1121
        gasPrice = undefined
1122
      }
1123
      if (!gasPrice && (!maxFeePerGas || !maxPriorityFeePerGas)) {
14✔
1124
        const { baseFee, priorityFee } = await this.getFeeEstimates()
1125
        maxFeePerGas = maxFeePerGas || baseFee
14✔
1126
        maxPriorityFeePerGas = maxPriorityFeePerGas || priorityFee
1127
      }
UNCOV
1128
      logger.trace('getting tx lock:', { txuuid })
×
UNCOV
1129

×
1130
      const { nonce, release, address } = await this.txManager.lock(this.filledAddresses)
1131

UNCOV
1132
      logger.trace('got tx lock:', { txuuid, address })
×
1133

1134
      let balance = NaN
UNCOV
1135

×
1136
      if (this.conf.env === 'development') {
1137
        balance = await this.web3.eth.getBalance(address)
×
1138
      }
UNCOV
1139

×
1140
      currentAddress = address
×
1141
      currentNonce = nonce
1142
      logger.debug(`sending tx from:`, {
1143
        address,
1144
        nonce,
1145
        txuuid,
1146
        balance,
1147
        gas,
1148
        maxFeePerGas,
1149
        maxPriorityFeePerGas,
1150
        wallet: this.name
UNCOV
1151
      })
×
UNCOV
1152

×
UNCOV
1153
      let txPromise = new Promise((res, rej) => {
×
1154
        tx.send({ gas, maxFeePerGas, maxPriorityFeePerGas, gasPrice, chainId: this.networkId, nonce, from: address })
1155
          .on('transactionHash', h => {
1156
            txHash = h
1157
            logger.trace('got tx hash:', { txuuid, txHash, wallet: this.name })
1158

1159
            if (onTransactionHash) {
1160
              onTransactionHash(h)
1161
            }
1162
          })
UNCOV
1163
          .on('sent', payload => {
×
1164
            release()
UNCOV
1165
            logger.debug('tx sent:', { txHash, payload, txuuid, wallet: this.name })
×
UNCOV
1166
          })
×
1167
          .on('receipt', r => {
1168
            logger.debug('got tx receipt:', { txuuid, txHash, wallet: this.name })
1169

1170
            if (onReceipt) {
1171
              onReceipt(r)
1172
            }
1173

1174
            res(r)
1175
          })
1176
          .on('confirmation', c => {
UNCOV
1177
            if (onConfirmation) {
×
1178
              onConfirmation(c)
1179
            }
1180
          })
1181
          .on('error', async e => {
UNCOV
1182
            if (isFundsError(e)) {
×
UNCOV
1183
              balance = await this.web3.eth.getBalance(address)
×
1184

1185
              logger.warn('sendTransaciton funds issue retry', {
UNCOV
1186
                errMessage: e.message,
×
UNCOV
1187
                nonce,
×
UNCOV
1188
                gas,
×
1189
                maxFeePerGas,
1190
                maxPriorityFeePerGas,
1191
                address,
1192
                balance,
1193
                wallet: this.name,
1194
                network: this.networkId
1195
              })
1196
            }
1197

1198
            //we maually unlock in catch
1199
            //fail()
1200

UNCOV
1201
            if (onError) {
×
1202
              onError(e)
NEW
1203
            }
×
1204

1205
            rej(e)
1206
          })
1207
      })
1208

1209
      const response = await withTimeout(txPromise, FUSE_TX_TIMEOUT, `${this.name} tx timeout`)
UNCOV
1210

×
UNCOV
1211
      return response
×
1212
    } catch (e) {
1213
      // error before executing a tx
1214
      if (!currentAddress) {
1215
        throw e
1216
      }
1217
      //reset nonce on every error, on celo we dont get nonce errors
1218
      let netNonce = parseInt(await this.web3.eth.getTransactionCount(currentAddress))
1219

1220
      //check if tx did go through after timeout or not
1221
      if (txHash && e.message.toLowerCase().includes('timeout')) {
UNCOV
1222
        // keeping address locked for another 30 seconds
×
NEW
1223
        retryAsync(
×
1224
          async attempt => {
1225
            const receipt = await this.web3.eth.getTransactionReceipt(txHash).catch()
1226
            logger.debug('retrying for timedout tx', {
1227
              currentAddress,
1228
              currentNonce,
1229
              netNonce,
1230
              attempt,
1231
              txuuid,
UNCOV
1232
              txHash,
×
UNCOV
1233
              receipt,
×
1234
              wallet: this.name,
1235
              network: this.networkId
1236
            })
1237
            if (receipt) {
1238
              await this.txManager.unlock(currentAddress, currentNonce + 1)
1239
              logger.info('receipt found for timedout tx attempts', {
1240
                currentAddress,
1241
                currentNonce,
1242
                attempt,
1243
                txuuid,
1244
                txHash,
UNCOV
1245
                receipt,
×
1246
                wallet: this.name,
1247
                network: this.networkId
1248
              })
1249
            } else if (attempt === 4) {
1250
              //increase nonce assuming tx went through
1251
              await this.txManager.unlock(currentAddress, currentNonce + 1)
1252
              logger.info('stopped retrying for timedout tx attempts', {
1253
                currentAddress,
1254
                currentNonce,
1255
                netNonce,
1256
                attempt,
1257
                txuuid,
1258
                txHash,
1259
                receipt,
1260
                wallet: this.name,
1261
                network: this.networkId
1262
              })
1263
            } else throw new Error('receipt not found') //trigger retry
1264
          },
1265
          3,
1✔
1266
          10000
1✔
1267
        ).catch(e => {
1268
          this.txManager.unlock(currentAddress, netNonce)
1269
          logger.error('retryAsync for timeout tx failed', e.message, e, { txHash })
1270
        })
1271
        // return assuming tx will mine
1272
        return
1273
      } else if (retry && (e.message.includes('FeeTooLowToCompete') || e.message.includes('underpriced'))) {
1274
        logger.warn('sendTransaction assuming duplicate nonce:', {
1✔
1275
          error: e.message,
1276
          maxFeePerGas,
1✔
1277
          maxPriorityFeePerGas,
1✔
1278
          currentAddress,
1279
          currentNonce,
1✔
1280
          netNonce,
1✔
1281
          txuuid,
1✔
1282
          txHash,
1✔
1283
          wallet: this.name,
1!
NEW
1284
          network: this.networkId
×
1285
        })
1286
        // increase nonce, since we assume therre's a tx pending with same nonce
1✔
1287
        await this.txManager.unlock(currentAddress, currentNonce + 1)
1288

1!
NEW
1289
        return this.sendTransaction(
×
NEW
1290
          tx,
×
NEW
1291
          txCallbacks,
×
1292
          { gas, gasPrice, maxFeePerGas, maxPriorityFeePerGas },
1293
          false,
1✔
1294
          logger
1295
        )
1✔
1296
      } else if (retry && e.message.toLowerCase().includes('revert') === false) {
1✔
1297
        logger.warn('sendTransaction retrying non reverted error:', {
1298
          error: e.message,
1✔
1299
          currentAddress,
1✔
1300
          currentNonce,
1301
          netNonce,
1302
          txuuid,
1303
          txHash,
1304
          wallet: this.name,
1305
          network: this.networkId
1306
        })
1307

1308
        await this.txManager.unlock(currentAddress, netNonce)
1309
        return this.sendTransaction(
1310
          tx,
UNCOV
1311
          txCallbacks,
×
UNCOV
1312
          { gas, gasPrice, maxFeePerGas, maxPriorityFeePerGas },
×
1313
          false,
1314
          logger
UNCOV
1315
        )
×
1316
      }
1317

UNCOV
1318
      await this.txManager.unlock(currentAddress, netNonce)
×
UNCOV
1319
      logger.error('sendTransaction error:', e.message, e, {
×
1320
        from: currentAddress,
1321
        currentNonce,
1322
        maxFeePerGas,
×
1323
        maxPriorityFeePerGas,
1324
        netNonce,
UNCOV
1325
        txuuid,
×
UNCOV
1326
        txHash,
×
1327
        retry,
1328
        wallet: this.name,
1329
        network: this.networkId
1330
      })
1✔
1331
      throw e
1332
    }
1!
UNCOV
1333
  }
×
1334

UNCOV
1335
  /**
×
1336
   * Helper function to handle a tx Send call
1337
   * @param tx
1338
   * @param {object} promiEvents
1339
   * @param {function} promiEvents.onTransactionHash
1340
   * @param {function} promiEvents.onReceipt
1341
   * @param {function} promiEvents.onConfirmation
1342
   * @param {function} promiEvents.onError
1343
   * @param {object} gasValues
1344
   * @param {number} gasValues.gas
1345
   * @param {number} gasValues.maxFeePerGas
1346
   * @param {number} gasValues.maxPriorityFeePerGas
1347
   * @returns {Promise<Promise|Q.Promise<any>|Promise<*>|Promise<*>|Promise<*>|*>}
×
1348
   */
UNCOV
1349
  async sendNative(
×
NEW
1350
    params: { from: string, to: string, value: string },
×
1351
    txCallbacks: PromiEvents = {},
1352
    { gas, maxFeePerGas, maxPriorityFeePerGas, gasPrice }: GasValues = {
1353
      gas: undefined,
UNCOV
1354
      maxFeePerGas: undefined,
×
1355
      maxPriorityFeePerGas: undefined,
1356
      gasPrice: undefined
1357
    }
1✔
1358
  ) {
1359
    let currentAddress
1!
1360
    const { log } = this
×
1361

1362
    try {
1363
      const { onTransactionHash, onReceipt, onConfirmation, onError } = txCallbacks
1✔
1364

1✔
1365
      gas = gas || defaultGas
1366
      gasPrice = gasPrice || this.gasPrice
1367
      maxFeePerGas = maxFeePerGas || this.maxFeePerGas
1368
      maxPriorityFeePerGas = maxPriorityFeePerGas || this.maxPriorityFeePerGas
1369
      if (gasPrice && !maxFeePerGas && !maxPriorityFeePerGas) {
×
UNCOV
1370
        logger.info('using legacy gasPrice tx')
×
UNCOV
1371
      } else {
×
1372
        gasPrice = undefined
1373
      }
1374
      if (!gasPrice && (!maxFeePerGas || !maxPriorityFeePerGas)) {
1375
        const { baseFee, priorityFee } = await this.getFeeEstimates()
1376
        maxFeePerGas = maxFeePerGas || baseFee
1377
        maxPriorityFeePerGas = maxPriorityFeePerGas || priorityFee
1378
      }
1379
      const { nonce, release, fail, address } = await this.txManager.lock(this.filledAddresses)
1380

1381
      log.debug('sendNative', { nonce, gas, maxFeePerGas, maxPriorityFeePerGas })
1382
      currentAddress = address
1383

1384
      return new Promise((res, rej) => {
1385
        this.web3.eth
1386
          .sendTransaction({
1387
            gas,
1388
            gasPrice,
1389
            maxFeePerGas,
1390
            maxPriorityFeePerGas,
1391
            chainId: this.networkId,
1392
            nonce,
1393
            ...params,
1394
            from: address
1395
          })
1396
          .on('transactionHash', h => {
1397
            if (onTransactionHash) {
1398
              onTransactionHash(h)
1399
            }
1400

1401
            release()
1402
          })
1403
          .on('receipt', r => {
1404
            if (onReceipt) {
1405
              onReceipt(r)
1406
            }
1407

1408
            res(r)
1409
          })
1410
          .on('confirmation', c => {
1411
            if (onConfirmation) {
1412
              onConfirmation(c)
1413
            }
1414
          })
1415
          .on('error', async e => {
1416
            const { message } = e
1417

1418
            if (isNonceError(e)) {
1419
              let netNonce = parseInt(await this.web3.eth.getTransactionCount(address))
1420

1421
              log.warn('sendNative nonce failure retry', message, e, {
1422
                params,
1423
                nonce,
1424
                gas,
1425
                maxFeePerGas,
1426
                maxPriorityFeePerGas,
1427
                address,
1428
                newNonce: netNonce,
1429
                wallet: this.name,
1430
                network: this.networkId
1431
              })
1432

1433
              await this.txManager.unlock(address, netNonce)
1434

1435
              try {
1436
                await this.sendNative(params, txCallbacks, { gas, gasPrice, maxFeePerGas, maxPriorityFeePerGas }).then(
1437
                  res
1438
                )
1439
              } catch (e) {
1440
                rej(e)
1441
              }
1442
            } else {
1443
              fail()
1444

1445
              if (onError) {
1446
                onError(e)
1447
              }
1448

1449
              log.error('sendNative failed', message, e, { wallet: this.name, network: this.networkId })
1450
              rej(e)
1451
            }
1452
          })
1453
      })
1454
    } catch (e) {
1455
      let netNonce = parseInt(await this.web3.eth.getTransactionCount(currentAddress))
1456
      await this.txManager.unlock(currentAddress, netNonce)
1457
      throw new Error(e)
1458
    }
1459
  }
1460
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc