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

GoodDollar / GoodServer / 14516752085

17 Apr 2025 01:19PM UTC coverage: 49.469%. Remained the same
14516752085

push

github

sirpy
fix: weakset not supporting strings

593 of 1477 branches covered (40.15%)

Branch coverage included in aggregate %.

1 of 6 new or added lines in 3 files covered. (16.67%)

1874 of 3510 relevant lines covered (53.39%)

7.37 hits per line

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

14.87
/src/server/blockchain/stakingModelTasksV2.js
1
import FundManagerABI from '@gooddollar/goodprotocol/artifacts/contracts/staking/GoodFundManager.sol/GoodFundManager.json'
2
import StakingABI from '@gooddollar/goodprotocol/artifacts/contracts/staking/SimpleStaking.sol/SimpleStaking.json'
3
import UBISchemeABI from '@gooddollar/goodprotocol/artifacts/contracts/ubi/UBIScheme.sol/UBIScheme.json'
4
import NameServiceABI from '@gooddollar/goodprotocol/artifacts/contracts/utils/NameService.sol/NameService.json'
5

6
// needed for ropsten allocateTo - mint fake dai
7
import DaiABI from '@gooddollar/goodcontracts/build/contracts/DAIMock.min.json'
8
import cDaiABI from '@gooddollar/goodprotocol/artifacts/contracts/Interfaces.sol/cERC20.json'
9
import ContractsAddress from '@gooddollar/goodprotocol/releases/deployment.json'
10
import fetch from 'cross-fetch'
11
import AdminWallet from './AdminWallet'
12
import { get, chunk, range, flatten, mapValues, once } from 'lodash'
13
import logger from '../../imports/logger'
14
import delay from 'delay'
15
import moment from 'moment'
16
import { toWei } from 'web3-utils'
17
import config from '../server.config'
18
import { sendSlackAlert } from '../../imports/slack'
19
import { retry as retryAttempt } from '../utils/async'
20

21
const BRIDGE_TRANSFER_TIMEOUT = 60 * 1000 * 5 //5 min
4✔
22
const FUSE_DAY_BLOCKS = (60 * 60 * 24) / 5
4✔
23

24
/**
25
 * a manager to make sure we collect and transfer the interest from the staking contract
26
 */
27
export class StakingModelManager {
28
  lastRopstenTopping = moment()
4✔
29
  addresses = get(ContractsAddress, `${AdminWallet.network}-mainnet`) || get(ContractsAddress, `${AdminWallet.network}`)
4!
30
  homeAddresses = get(ContractsAddress, AdminWallet.network)
4✔
31
  managerAddress = this.addresses['GoodFundManager']
4✔
32
  stakingAddresses = this.addresses['StakingContracts']
4✔
33
  daiAddress = this.addresses['DAI']
4✔
34
  cDaiAddress = this.addresses['cDAI']
4✔
35
  bridge = this.addresses['ForeignBridge']
4✔
36
  nameServiceAddress = this.addresses['NameService']
4✔
37

38
  constructor() {
39
    this.log = logger.child({ from: 'StakingModelManagerV2' })
4✔
40
    this.init()
4✔
41
  }
42

43
  init = once(async () => {
4✔
44
    // polling timeout since ethereum has network congestion and we try to pay little gas so it will take a long time to confirm tx
45
    await AdminWallet.ready
4✔
46

47
    this.managerContract = new AdminWallet.mainnetWeb3.eth.Contract(FundManagerABI.abi, this.managerAddress, {
2✔
48
      transactionPollingTimeout: 1000,
49
      from: AdminWallet.address
50
    })
51
    this.stakingContract = new AdminWallet.mainnetWeb3.eth.Contract(StakingABI.abi, this.stakingAddresses[0][0], {
2✔
52
      from: AdminWallet.address
53
    })
54
    this.dai = new AdminWallet.mainnetWeb3.eth.Contract(DaiABI.abi, this.daiAddress, { from: AdminWallet.address })
2✔
55
    this.cDai = new AdminWallet.mainnetWeb3.eth.Contract(cDaiABI.abi, this.cDaiAddress, { from: AdminWallet.address })
2✔
56
    this.nameService = new AdminWallet.mainnetWeb3.eth.Contract(NameServiceABI.abi, this.nameServiceAddress, {
2✔
57
      from: AdminWallet.address
58
    })
59
    this.log.debug('constructor:', {
2✔
60
      fundmanager: this.managerAddress,
61
      staking: this.stakingAddresses,
62
      bridge: this.bridge
63
    })
64
  })
65

66
  canCollectFunds = async () => {
4✔
67
    const result = await this.managerContract.methods.calcSortedContracts().call()
×
68
    this.log.info('canCollectFunds:', result)
×
69
    //collect all contracts that can be run
70
    const contracts = result.filter(_ => _.maxGasLargerOrEqualRequired).map(_ => _.contractAddress)
×
71

72
    return contracts.length > 0 ? contracts : false
×
73
  }
74

75
  getAvailableInterest = async () =>
4✔
76
    this.stakingContract.methods
×
77
      .currentGains(true, true)
78
      .call()
79
      .then(_ => mapValues(_, parseInt))
×
80

81
  transferInterest = async () => {
4✔
82
    let txHash
83
    const stakingContracts = await this.canCollectFunds()
×
84
    if (stakingContracts === false) {
×
85
      this.log.warn('transferInterest no staking contracts')
×
86
      return
×
87
    }
88

89
    try {
×
90
      const fundsTX = await AdminWallet.sendTransactionMainnet(
×
91
        this.managerContract.methods.collectInterest(stakingContracts, false),
92
        { onTransactionHash: h => (txHash = h) },
×
93
        { gas: 2000000 }, //force fixed gas price, tx should take around 450k
94
        AdminWallet.mainnetAddresses[0]
95
      )
96
      const fundsEvent = get(fundsTX, 'events.FundsTransferred')
×
97
      this.log.info('transferInterest result event', { fundsEvent, fundsTX })
×
98
      return fundsEvent
×
99
    } catch (e) {
100
      if (txHash && e.message.toLowerCase().includes('timeout')) {
×
101
        return this.waitForTransferInterest(txHash)
×
102
      } else {
103
        throw e
×
104
      }
105
    }
106
  }
107

108
  waitForTransferInterest = async txHash => {
4✔
109
    const { log } = this
×
110

111
    return retryAttempt(
×
112
      async retry => {
113
        log.info('retrying timedout tx', { txHash, retry })
×
114

115
        const receipt = await AdminWallet.mainnetWeb3.eth.getTransactionReceipt(txHash)
×
116

117
        if (!receipt) {
×
118
          throw new Error('No receipt yet, retrying')
×
119
        }
120

121
        if (receipt.status) {
×
122
          const fundsEvents = await this.managerContract.getPastEvents('FundsTransferred', {
×
123
            fromBlock: receipt.blockNumber,
124
            toBlock: receipt.blockNumber
125
          })
126

127
          const fundsEvent = get(fundsEvents, 0)
×
128

129
          log.info('retrying timedout tx success transferInterest result event', { txHash, fundsEvent })
×
130
          return fundsEvent
×
131
        }
132

133
        // tx eventually failed
134
        throw new Error('retrying timedout tx failed txHash: ' + txHash)
×
135
      },
136
      -1,
137
      1000 * 60 * 10,
138
      e => /no\sreceipt/i.test(get(e, 'message', ''))
×
139
    ) // no receipt yet wait 10 minutes
140
  }
141

142
  getNextCollectionTime = async () => {
4✔
143
    let canCollectFunds = await this.canCollectFunds()
×
144
    const blocksForNextCollection = 4 * 60 * 24 //wait 1 day
×
145
    this.log.info('canRun result:', { canCollectFunds, blocksForNextCollection })
×
146
    if (canCollectFunds === false) {
×
147
      return moment().add(blocksForNextCollection * 15, 'seconds')
×
148
    }
149
    return moment()
×
150
  }
151

152
  mockInterest = async () => {
4✔
153
    this.log.info('mockInterest: start', { mainnetAddresses: AdminWallet.mainnetAddresses })
×
154
    if (config.ethereumMainnet.network_id === 1) {
×
155
      return
×
156
    }
157
    //top ropsten wallet
158
    if (moment().diff(this.lastRopstenTopping, 'days') > 0) {
×
159
      fetch('https://faucet.metamask.io', { method: 'POST', body: AdminWallet.mainnetAddresses[0] }).catch(e => {
×
160
        this.log.error('mockInterest: failed calling ropsten faucet', e.message, e)
×
161
      })
162
      this.lastRopstenTopping = moment()
×
163
    }
164
    await AdminWallet.sendTransactionMainnet(
×
165
      this.dai.methods.approve(this.cDai._address, toWei('1000000000', 'ether')),
166
      {},
167
      {},
168
      AdminWallet.mainnetAddresses[0]
169
    ).catch(() => {
170
      this.log.warn('mockInterest: dai  approve failed')
×
171
      // throw e
172
    })
173
    await AdminWallet.sendTransactionMainnet(
×
174
      this.dai.methods.allocateTo(AdminWallet.mainnetAddresses[0], toWei('2000', 'ether')),
175
      {},
176
      {},
177
      AdminWallet.mainnetAddresses[0]
178
    ).catch(e => {
179
      this.log.warn('mockInterest: dai  allocateTo failed', e.message, e)
×
180
      // throw e
181
    })
182

183
    const balanceBefore = await this.cDai.methods.balanceOf(AdminWallet.mainnetAddresses[0]).call().then(parseInt)
×
184
    this.log.info('mockInterest: approved and allocated dai. minting cDai...', { balanceBefore })
×
185
    await AdminWallet.sendTransactionMainnet(
×
186
      this.cDai.methods.mint(toWei('2000', 'ether')),
187
      {},
188
      {},
189
      AdminWallet.mainnetAddresses[0]
190
    ).catch(e => {
191
      this.log.warn('mockInterest: cdai mint failed', e.message, e)
×
192
    })
193

194
    let ownercDaiBalanceAfter = await this.cDai.methods.balanceOf(AdminWallet.mainnetAddresses[0]).call().then(parseInt)
×
195

196
    let toTransfer = ownercDaiBalanceAfter - balanceBefore
×
197
    this.log.info('mockInterest: minted fake cDai, transferring to staking contract...', {
×
198
      ownercDaiBalanceAfter,
199
      toTransfer,
200
      owner: AdminWallet.mainnetAddresses[0],
201
      stakingContract: this.stakingContract._address
202
    })
203

204
    toTransfer = toTransfer > 0 ? toTransfer : (balanceBefore / 7).toFixed(0)
×
205
    if (toTransfer === 0) {
×
206
      this.log.warn('mockInterest: no mock interest to transfer to staking contract...')
×
207
      return
×
208
    }
209
    await AdminWallet.sendTransactionMainnet(
×
210
      this.cDai.methods.transfer(this.stakingContract._address, toTransfer),
211
      {},
212
      {},
213
      AdminWallet.mainnetAddresses[0]
214
    ).catch(e => {
215
      this.log.warn('mockInterest: transfer interest failed', e.message, e)
×
216
    })
217

218
    let stakingcDaiBalanceAfter = await this.cDai.methods.balanceOf(this.stakingContract._address).call()
×
219

220
    this.log.info('mockInterest: transfered fake cDai to staking contract...', {
×
221
      stakingcDaiBalanceAfter
222
    })
223
  }
224
  run = async () => {
4✔
225
    try {
×
226
      await this.mockInterest().catch(e => {
×
227
        this.log.warn('mockInterest failed, continuing...')
×
228
        sendSlackAlert({ msg: 'failure: mockInterest failed', error: e.message })
×
229
      })
230
      this.log.info('mockInterest done, collecting interest...')
×
231
      const nextCollectionTime = await this.getNextCollectionTime()
×
232
      this.log.info({ nextCollectionTime })
×
233
      if (nextCollectionTime.isAfter()) {
×
234
        this.log.info('waiting for collect interest time', { nextCollectionTime })
×
235
        return { result: 'waiting', cronTime: nextCollectionTime }
×
236
      }
237
      const availableInterest = await this.getAvailableInterest()
×
238
      this.log.info('starting collect interest', {
×
239
        availableInterest,
240
        nextCollectionTime: nextCollectionTime.toString()
241
      })
242

243
      const fundsEvent = await this.transferInterest().catch(e => {
×
244
        this.log.warn('transferInterest failed. stopping.')
×
245
        sendSlackAlert({ msg: 'failure: transferInterest failed', error: e.message })
×
246
        throw e
×
247
      })
248
      const sidechainCurBlock = await AdminWallet.web3.eth.getBlockNumber()
×
249

250
      if (fundsEvent === undefined) {
×
251
        const cronTime = await this.getNextCollectionTime()
×
252
        this.log.warn('No transfered funds event found. (interest was 0?)', { cronTime })
×
253
        sendSlackAlert({ msg: 'warning: no transfer funds event found' })
×
254
        return { result: 'no interest', cronTime }
×
255
      }
256
      const ubiTransfered = fundsEvent.returnValues.gdUBI.toString()
×
257
      if (ubiTransfered === '0') {
×
258
        this.log.warn('No UBI was transfered to bridge')
×
259
      } else {
260
        this.log.info('ubi interest collected. waiting for bridge...', { gdUBI: ubiTransfered })
×
261
        //wait for funds on sidechain to transfer via bridge
262
        const transferEvent = await this.waitForBridgeTransfer(sidechainCurBlock, Date.now(), ubiTransfered)
×
263
        this.log.info('ubi success: bridge transfer event found', {
×
264
          ubiGenerated: transferEvent.returnValues.value
265
        })
266
      }
267
      sendSlackAlert({ msg: 'success: UBI transfered', ubiTransfered })
×
268

269
      const cronTime = await this.getNextCollectionTime()
×
270
      this.log.info('next run:', { cronTime })
×
271
      return { result: true, cronTime }
×
272
    } catch (e) {
273
      const cronTime = await this.getNextCollectionTime()
×
274
      //make sure atleast one hour passes in case of an error
275
      if (cronTime.isBefore(moment().add(1, 'hour'))) cronTime.add(1, 'hour')
×
276

277
      const { message } = e
×
278
      this.log.error('collecting interest failed.', message, e, { cronTime })
×
279
      sendSlackAlert({ msg: 'failure: collecting interest failed.', error: message })
×
280

281
      return { result: false, cronTime }
×
282
    }
283
  }
284

285
  /**
286
   * wait for  bridge on sidechain to transfer the tokens from mainnet
287
   *
288
   * @param {*} fromBlock starting block listen to events
289
   * @param {*} bridge the sender of the tokens
290
   * @param {*} ubiScheme the recipient
291
   * @param {*} start used to calculate timeout
292
   */
293
  waitForBridgeTransfer = async (fromBlock, start, value) => {
4✔
294
    const ubiRecipient = await this.nameService.methods.getAddress('UBI_RECIPIENT').call()
×
295
    const res = await AdminWallet.tokenContract.getPastEvents('Transfer', {
×
296
      fromBlock,
297
      filter: {
298
        to: ubiRecipient,
299
        value
300
      }
301
    })
302
    this.log.info('waitforBirgdeTransfer events:', {
×
303
      fromBlock,
304
      start,
305
      res,
306
      bridge: this.homeBridge,
307
      ubi: ubiRecipient
308
    })
309
    if (res && res.length > 0) {
×
310
      return res[0]
×
311
    }
312
    if (Date.now() - start > BRIDGE_TRANSFER_TIMEOUT) {
×
313
      throw new Error('waiting for bridge transfer timed out')
×
314
    }
315
    //wait 5 sec for retry
316
    await delay(5000)
×
317
    return this.waitForBridgeTransfer(fromBlock, start, value)
×
318
  }
319
}
320

321
export const fundManager = new StakingModelManager()
4✔
322

323
/**
324
 * a manager to make sure we fish inactive users
325
 */
326
class FishingManager {
327
  ubiScheme = get(ContractsAddress, `${AdminWallet.network}.UBIScheme`)
4✔
328

329
  constructor() {
330
    this.log = logger.child({ from: 'FishingManager' })
4✔
331
    this.init()
4✔
332
  }
333

334
  init = once(async () => {
4✔
335
    this.log.info('initializing...')
4✔
336
    await AdminWallet.ready
4✔
337

338
    this.ubiContract = new AdminWallet.web3.eth.Contract(UBISchemeABI.abi, this.ubiScheme, {
2✔
339
      from: AdminWallet.address
340
    })
341
    this.log.info('done initializing')
2✔
342
  })
343

344
  /**
345
   * calculate the next claim epoch
346
   */
347
  getNextDay = async () => {
4✔
348
    const startRef = await this.ubiContract.methods
×
349
      .periodStart()
350
      .call()
351
      .then(_ => moment(parseInt(_) * 1000).startOf('hour'))
×
352
    const blockchainNow = await AdminWallet.web3.eth
×
353
      .getBlock('latest')
354
      .then(_ => moment(_.timestamp * 1000).startOf('hour'))
×
355
    const hoursDiff = blockchainNow.diff(startRef, 'hours')
×
356
    const hoursUntil = 24 - (hoursDiff % 24)
×
357
    this.log.info('fishManager getNextDay', { startRef, blockchainNow, hoursUntil })
×
358
    return blockchainNow.add(hoursUntil, 'hours')
×
359
  }
360

361
  /**
362
   * read events of previous claim epochs
363
   * we get the start block and end block for searching for possible inactive users
364
   */
365
  getUBICalculatedDays = async forceDaysAgo => {
4✔
366
    const dayFuseBlocks = (60 * 60 * 24) / 5
×
367
    const maxInactiveDays = forceDaysAgo || (await this.ubiContract.methods.maxInactiveDays().call().then(parseInt))
×
368

369
    const daysagoBlocks = dayFuseBlocks * (maxInactiveDays + 1)
×
370
    const curBlock = await AdminWallet.web3.eth.getBlockNumber()
×
371
    const blocksAgo = Math.max(curBlock - daysagoBlocks, 0)
×
372
    await AdminWallet.sendTransaction(this.ubiContract.methods.setDay(), {}).catch(() =>
×
373
      this.log.warn('fishManager set day failed')
×
374
    )
375
    const currentUBIDay = await this.ubiContract.methods.currentDay().call().then(parseInt)
×
376
    this.log.info('getInactiveAccounts', { daysagoBlocks, blocksAgo, currentUBIDay, maxInactiveDays })
×
377
    //get claims that were done before inactive period days ago, these accounts has the potential to be inactive
378
    //first we get the starting block
379
    const blockChunks = range(blocksAgo, curBlock, 100000)
×
380
    const ubiEvents = flatten(
×
381
      await Promise.all(
382
        blockChunks.map(startBlock =>
383
          this.ubiContract
×
384
            .getPastEvents('UBICalculated', {
385
              fromBlock: startBlock,
386
              toBlock: Math.min(curBlock, startBlock + 100000)
387
            })
388
            .catch(e => {
389
              this.log.warn('getUBICalculatedDays getPastEvents UBICalculated chunk failed', e.message, {
×
390
                startBlock
391
              })
392
              return []
×
393
            })
394
        )
395
      )
396
    )
397
    // const ubiEvents = await this.ubiContract.getPastEvents('UBICalculated', { fromBlock: blocksAgo }).catch(e => {
398
    //   this.log.warn('fishManager getPastEvents failed')
399
    //   throw e
400
    // })
401
    this.log.info('getUBICalculatedDays ubiEvents:', {
×
402
      ubiEvents: ubiEvents.length,
403
      ubiEventDays: ubiEvents.map(_ => get(_, 'returnValues.day')).map(parseInt)
×
404
    })
405

406
    //find first day older than maxInactiveDays (ubiEvents is sorted from old to new  so we reverse it)
407
    const searchStartDay = ubiEvents
×
408
      .reverse()
409
      .find(e => parseInt(e.returnValues.day) <= currentUBIDay - maxInactiveDays)
×
410

411
    const startDay = parseInt(get(searchStartDay, 'returnValues.day', 0))
×
412
    //find first day newer than searchStartDay
413
    const searchEndDay = ubiEvents.reverse().find(e => parseInt(e.returnValues.day) > startDay)
×
414
    const endDay = parseInt(get(searchEndDay, 'returnValues.day', 0))
×
415

416
    this.log.info('getUBICalculatedDays got UBICalculatedEvents:', {
×
417
      currentUBIDay,
418
      foundEvents: ubiEvents.length,
419
      startDay,
420
      endDay
421
      // searchStartDay: searchStartDay,
422
      // searchEndDay: searchEndDay,
423
    })
424
    return { searchStartDay, searchEndDay, maxInactiveDays }
×
425
  }
426

427
  /**
428
   * users that claimed 14 days(or maxInactiveDays) ago are possible candidates to be inactive
429
   */
430
  getInactiveAccounts = async forceDaysAgo => {
4✔
431
    const { searchStartDay, searchEndDay, maxInactiveDays } = await this.getUBICalculatedDays(forceDaysAgo)
×
432

433
    if (searchStartDay === undefined) {
×
434
      this.log.warn('No UBICalculated event found for inactive interval', { maxInactiveDays })
×
435
    }
436
    //now get accounts that claimed in that day
437
    const claimBlockStart = parseInt(
×
438
      get(
439
        searchStartDay,
440
        'returnValues.blockNumber',
441
        Math.max((await AdminWallet.web3.eth.getBlockNumber()) - maxInactiveDays * FUSE_DAY_BLOCKS, 0)
442
      )
443
    )
444

445
    const claimBlockEnd = parseInt(get(searchEndDay, 'returnValues.blockNumber', claimBlockStart + FUSE_DAY_BLOCKS))
×
446

447
    //get candidates
448
    const chunkSize = 100000
×
449
    const blockChunks = range(claimBlockStart, claimBlockEnd, chunkSize)
×
450
    const claimEvents = flatten(
×
451
      await Promise.all(
452
        blockChunks.map(startBlock =>
453
          this.ubiContract
×
454
            .getPastEvents('UBIClaimed', {
455
              fromBlock: startBlock,
456
              toBlock: Math.min(claimBlockEnd, startBlock + chunkSize)
457
            })
458
            .catch(e => {
459
              this.log.warn('getInactiveAccounts getPastEvents UBIClaimed chunk failed', e.message, {
×
460
                startBlock,
461
                chunkSize
462
              })
463
              return []
×
464
            })
465
        )
466
      )
467
    )
468

469
    this.log.info('getInactiveAccounts got UBIClaimed events', {
×
470
      claimBlockStart,
471
      claimBlockEnd,
472
      total: claimEvents.length
473
    })
474
    //check if they are inactive
475
    let inactiveAccounts = []
×
476
    let inactiveCheckFailed = 0
×
477
    const checkInactive = async e => {
×
478
      const isActive = await this.ubiContract.methods
×
479
        .isActiveUser(e.returnValues.claimer)
480
        .call()
481
        .catch(() => undefined)
×
482
      if (isActive === undefined) {
×
483
        inactiveCheckFailed += 1
×
484
      }
485
      return isActive ? undefined : e.returnValues.claimer
×
486
    }
487
    for (let eventsChunk of chunk(claimEvents, 100)) {
×
488
      const inactive = (await Promise.all(eventsChunk.map(checkInactive))).filter(_ => _)
×
489
      this.log.debug('getInactiveAccounts batch:', { inactiveCheckFailed, inactiveFound: inactive.length })
×
490
      inactiveAccounts = inactiveAccounts.concat(inactive)
×
491
    }
492

493
    this.log.info('getInactiveAccounts found UBIClaimed events', {
×
494
      totalEvents: claimEvents.length,
495
      inactiveFound: inactiveAccounts.length
496
    })
497
    return inactiveAccounts
×
498
  }
499

500
  /**
501
   * perform the fishMulti TX on the ubiContract
502
   */
503
  fishChunk = async tofish => {
4✔
504
    const fishTX = await AdminWallet.fishMulti(tofish, this.log)
×
505
    const fishEvents = await this.ubiContract.getPastEvents('TotalFished', {
×
506
      fromBlock: fishTX.blockNumber,
507
      toBlock: fishTX.blockNumber
508
    })
509
    const fishEvent = fishEvents.find(e => e.transactionHash === fishTX.transactionHash)
×
510
    const totalFished = parseInt(get(fishEvent, 'returnValues.total', 0))
×
511
    this.log.info('Fished accounts', { tofish, totalFished, fisherAccount: fishTX.from, tx: fishTX.transactionHash })
×
512
    return { totalFished, fisherAccount: fishTX.from }
×
513
  }
514

515
  /**
516
   * split fishing into multiple chunks
517
   */
518
  fish = async (accounts, fishers = []) => {
4!
519
    let unfished = []
×
520
    let failed = 0
×
521
    for (let tofish of chunk(accounts, 50)) {
×
522
      try {
×
523
        this.log.info('calling fishChunk', { tofish })
×
524
        const { totalFished, fisherAccount } = await this.fishChunk(tofish)
×
525
        unfished = unfished.concat(tofish.slice(totalFished))
×
526
        fishers.push(fisherAccount)
×
527
      } catch (e) {
528
        failed += tofish.length
×
529
        this.log.error('Failed fishing chunk', e.message, e, { tofish })
×
530
      }
531
    }
532
    if (accounts.length > 0)
×
533
      sendSlackAlert({ msg: 'info: fishing done', unfished: unfished.length, failed, outof: accounts.length })
×
534

535
    if (unfished.length > 0) {
×
536
      this.log.info('Retrying unfished accounts', { unfished: unfished.length })
×
537
      return await this.fish(unfished, fishers)
×
538
    }
539
    return fishers
×
540
  }
541

542
  /**
543
   * transfers recovered funds by fishing back to UBI
544
   * @returns the amount transfered
545
   */
546
  transferFishToUBI = async () => {
4✔
547
    let gdbalance = await AdminWallet.tokenContract.methods
×
548
      .balanceOf(AdminWallet.proxyContract._address)
549
      .call()
550
      .then(parseInt)
551
    if (gdbalance > 0) {
×
NEW
552
      const transferTX = await AdminWallet.transferWalletGoodDollars(this.ubiScheme, gdbalance, this.log)
×
553
      this.log.info('transfered fished funds to ubi', { tx: transferTX.transactionHash, gdbalance })
×
554
    }
555
    return gdbalance
×
556
  }
557

558
  run = async forceDaysAgo => {
4✔
559
    try {
×
560
      const inactive = await this.getInactiveAccounts(forceDaysAgo)
×
561
      const fishers = await this.fish(inactive)
×
562
      const cronTime = await this.getNextDay()
×
563
      await this.transferFishToUBI().catch() //silence exceptions, as they will be error logged in wallet
×
564
      return { result: true, cronTime, fishers, inactive: inactive.length }
×
565
    } catch (exception) {
566
      const { message } = exception
×
567
      this.log.error('fishing task failed:', message, exception)
×
568
      sendSlackAlert({ msg: 'failure: fishing failed', error: message })
×
569

570
      const cronTime = moment().add(1, 'hour')
×
571
      return { result: true, cronTime }
×
572
    }
573
  }
574
}
575

576
export const fishManager = new FishingManager()
4✔
577

578
class StakingModelTask {
579
  // using context allowing us to manipulate task execution
580
  // it's more clear that return some values.
581
  // also, delayed task pattern doesn't generally includes that task should return something
582
  // the task could pass or fail that's all. async function contract allows us to implement those statuses
583
  async execute({ setTime }) {
584
    const { cronTime } = await this.run()
×
585

586
    if (cronTime) {
×
587
      // According to the docs, setTime accepts CronTime only
588
      // CronTime constructor accepts cron string or JS Date.
589
      // there's no info about moment object support.
590
      // probavbly it works due to the .toString or [Symbol.toPrimitive] override
591
      // but let's better convert moment to the JS date to strictly keep node-cron's contracts
592
      setTime(cronTime.toDate())
×
593
    }
594
  }
595

596
  /**
597
   * @abstract
598
   */
599
  async run() {}
600
}
601

602
export class CollectFundsTask extends StakingModelTask {
603
  get schedule() {
604
    return config.stakeTaskCron
×
605
  }
606

607
  get name() {
608
    return 'StakingModel'
×
609
  }
610

611
  async run() {
612
    return fundManager.run()
×
613
  }
614
}
615

616
export class FishInactiveTask extends StakingModelTask {
617
  get schedule() {
618
    return config.fishTaskCron
×
619
  }
620

621
  get name() {
622
    return 'FishInactiveUsers'
×
623
  }
624

625
  async run() {
626
    return fishManager.run()
×
627
  }
628
}
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

© 2026 Coveralls, Inc