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

GoodDollar / GoodDAPP / 9904140208

12 Jul 2024 07:28AM UTC coverage: 40.823% (-0.1%) from 40.961%
9904140208

push

github

L03TJ3
add: configuration for tatum rpc with key

2325 of 6939 branches covered (33.51%)

Branch coverage included in aggregate %.

1 of 10 new or added lines in 2 files covered. (10.0%)

13 existing lines in 3 files now uncovered.

5631 of 12550 relevant lines covered (44.87%)

67.83 hits per line

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

22.92
/src/lib/API/api.js
1
// @flow
2

3
import axios from 'axios'
4
import { find, isArray, shuffle } from 'lodash'
5

6
import type { $AxiosXHR, AxiosInstance, AxiosPromise } from 'axios'
7
import { padLeft } from 'web3-utils'
8

9
import Config, { fuseNetwork } from '../../config/config'
10

11
import { JWT } from '../constants/localStorage'
12
import AsyncStorage from '../utils/asyncStorage'
13

14
import { throttleAdapter } from '../utils/axios'
15
import { delay, fallback } from '../utils/async'
16
import { NETWORK_ID } from '../constants/network'
17
import { log, logNetworkError, requestErrorHandler, responseErrorHandler, responseHandler } from './utils'
18

19
import type { Credentials, UserRecord } from './utils'
20

21
/**
22
 * GoodServer Client.
23
 * This is being initialized with the token retrieved from GoodServer once.
24
 * After init is being used to user operations such add, delete, etc.
25
 */
26
export class APIService {
27
  jwt: string
28

29
  client: AxiosInstance
30

31
  sharedClient: AxiosInstance
32

33
  constructor(jwt = null) {
119✔
34
    const shared = axios.create()
119✔
35

36
    shared.interceptors.response.use(
119✔
37
      ({ data }) => data,
×
38
      error => {
39
        logNetworkError(error)
×
40
        throw error
×
41
      },
42
    )
43

44
    this.sharedClient = shared
119✔
45
    this.init(jwt)
119✔
46
  }
47

48
  /**
49
   * init API with axions client and proper interptors. Needs `GoodDAPP_jwt`to be present in AsyncStorage
50
   */
51
  init(jwtToken = null) {
×
52
    const { serverUrl, apiTimeout } = Config
119✔
53

54
    this.jwt = jwtToken
119✔
55

56
    return (this.ready = (async () => {
119✔
57
      let { jwt } = this
119✔
58

59
      if (!jwt) {
119!
60
        jwt = await AsyncStorage.getItem(JWT)
119✔
61
        this.jwt = jwt
119✔
62
      }
63

64
      log.info('initializing api...', serverUrl, jwt)
119✔
65

66
      let instance: AxiosInstance = axios.create({
119✔
67
        baseURL: serverUrl,
68
        timeout: apiTimeout,
69
        headers: { Authorization: `Bearer ${jwt || ''}` },
238✔
70
        adapter: throttleAdapter(1000),
71
      })
72

73
      const { request, response } = instance.interceptors
119✔
74

75
      request.use(this.verifyJWT, requestErrorHandler)
119✔
76
      response.use(responseHandler, responseErrorHandler)
119✔
77

78
      this.client = instance
119✔
79
      log.info('API ready', jwt)
119✔
80
    })())
81
  }
82

83
  setLoginCallback(callback) {
84
    this.login = callback
492✔
85
  }
86

87
  // using arrow function so it is binded to this, when passed to axios
88
  verifyJWT = async config => {
119✔
89
    if (config.auth !== false) {
3✔
90
      //by default use auth
91
      const loginResult = this.login && (await this.login())
2✔
92
      const { jwt } = loginResult || this
1✔
93
      if (jwt) {
1!
94
        config.headers.Authorization = 'Bearer ' + jwt
×
95
      }
96
    }
97
    return config
2✔
98
  }
99

100
  /**
101
   * `/auth/ping` get api call
102
   */
103
  ping(): AxiosPromise<any> {
104
    return this.client.get('/auth/ping', { throttle: false })
1✔
105
  }
106

107
  isWhitelisted(address): AxiosPromise<any> {
108
    return this.client.get(`/userWhitelisted/${encodeURIComponent(address)}`, { throttle: false })
×
109
  }
110

111
  syncWhitelist(address): AxiosPromise<any> {
112
    return this.client.get(`/syncWhitelist/${encodeURIComponent(address)}`, { throttle: false })
×
113
  }
114

115
  /**
116
   * `/auth/eth` post api call
117
   * @param {Credentials} creds
118
   */
119
  auth(creds: Credentials): AxiosPromise<any> {
120
    return this.client.post('/auth/eth', creds, { auth: false }) // auth false to prevent loop
1✔
121
  }
122

123
  fvAuth(creds: Credentials): AxiosPromise<any> {
124
    return this.client.post('/auth/fv2', creds, { auth: false }) // auth false to prevent loop
×
125
  }
126

127
  /**
128
   * `/user/add` post api call
129
   * @param {UserRecord} user
130
   */
131
  addUser(user: UserRecord): AxiosPromise<any> {
132
    return this.client.post('/user/add', { user })
×
133
  }
134

135
  /**
136
   * `/user/start` post api call
137
   * @param {UserRecord} user
138
   */
139
  addSignupContact(user: UserRecord): AxiosPromise<any> {
140
    return this.client.post('/user/start', { user })
×
141
  }
142

143
  updateClaims(claimData: { claim_counter: number, last_claim: string }): AxiosPromise<any> {
144
    return this.client.post('/user/claim', { ...claimData })
×
145
  }
146

147
  /**
148
   * `/user/delete` post api call
149
   */
150
  deleteAccount(): AxiosPromise<any> {
151
    return this.client.post('/user/delete')
×
152
  }
153

154
  /**
155
   * `/user/exists` get api call
156
   */
157
  userExists(): AxiosPromise<any> {
158
    return this.client.get('/user/exists')
×
159
  }
160

161
  /**
162
   * `/user/exists` get api call
163
   */
164
  userExistsCheck(searchBy: { email: string, mobile: string, identifier: string }): AxiosPromise<any> {
165
    return this.client.post('/userExists', searchBy, { throttle: false })
×
166
  }
167

168
  /**
169
   * `/verify/sendotp` post api call
170
   * @param {UserRecord} user
171
   */
172
  sendOTP(user: UserRecord, onlyCheckAlreadyVerified: boolean = false): AxiosPromise<any> {
×
173
    return this.client.post('/verify/sendotp', { user, onlyCheckAlreadyVerified })
×
174
  }
175

176
  /**
177
   * `/verify/user` post api call
178
   * @param {any} verificationData
179
   */
180
  verifyUser(verificationData: any): AxiosPromise<any> {
181
    return this.client.post('/verify/user', { verificationData })
×
182
  }
183

184
  /**
185
   * `/user/verifyCRM` post api call
186
   * @param {any} profile - user email/mobile/name to be added to CRM if doesnt exists
187
   */
188
  verifyCRM(profile): AxiosPromise<any> {
189
    return this.client.post('/user/verifyCRM', { user: profile })
×
190
  }
191

192
  // eslint-disable-next-line require-await
193
  async verifyCaptcha(payload: any): AxiosPromise<any> {
194
    const { sharedClient } = this
×
195
    const { env, verifyCaptchaUrl } = Config
×
196

197
    return sharedClient.post(`${verifyCaptchaUrl}/verify/recaptcha`, { ...payload, env })
×
198
  }
199

200
  /**
201
   * `ip-api.com/json` get location api call
202
   */
203
  getLocation(): AxiosPromise<any> {
204
    return this.sharedClient.get('https://get.geojs.io/v1/ip/country.json')
×
205
  }
206

207
  /**
208
   * `/verify/mobile` post api call
209
   * @param {any} verificationData
210
   */
211
  verifyMobile(verificationData: any): Promise<$AxiosXHR<any>> {
212
    return this.client.post('/verify/mobile', { verificationData })
×
213
  }
214

215
  /**
216
   * `/verify/topwallet` post api call. Tops users wallet
217
   */
218
  verifyTopWallet(chainId: number): Promise<$AxiosXHR<any>> {
219
    const throttle = { interval: 60000, trailing: false }
×
220

221
    return this.client.post('/verify/topwallet', { chainId }, { throttle })
×
222
  }
223

224
  /**
225
   * `/verify/sendemail` post api call
226
   * @param {UserRecord} user
227
   */
228
  sendVerificationEmail(user: UserRecord): Promise<$AxiosXHR<any>> {
229
    return this.client.post('/verify/sendemail', { user })
×
230
  }
231

232
  /**
233
   * `/verify/email` post api call
234
   * @param {object} verificationData
235
   * @param {string} verificationData.code
236
   */
237
  verifyEmail(verificationData: { code: string }): Promise<$AxiosXHR<any>> {
238
    return this.client.post('/verify/email', { verificationData })
×
239
  }
240

241
  /**
242
   * `/send/recoveryinstructions` post api call
243
   * @param {string} mnemonic
244
   */
245
  sendRecoveryInstructionByEmail(mnemonic: string): Promise<$AxiosXHR<any>> {
246
    return this.client.post('/send/recoveryinstructions', { mnemonic })
×
247
  }
248

249
  /**
250
   * `/send/magiclink` post api call
251
   * @param {string} magiclink
252
   */
253
  sendMagicLinkByEmail(magiclink: string): Promise<$AxiosXHR<any>> {
254
    return this.client.post('/send/magiclink', { magiclink })
×
255
  }
256

257
  /**
258
   * `/send/magiccode` post api call
259
   * @param {string} mobile
260
   * @param {string} magicCode
261
   */
262
  // sendMagicCodeBySms(mobile: string, magicCode: string): Promise<$AxiosXHR<any>> {
263
  //   return this.client.post('/send/magiccode', { to: mobile, magicCode })
264
  // }
265

266
  /** @private */
267
  faceVerificationUrl = '/verify/face'
119✔
268

269
  /** @private */
270
  enrollmentUrl(enrollmentIdentifier) {
271
    const { faceVerificationUrl } = this
1✔
272

273
    return `${faceVerificationUrl}/${encodeURIComponent(enrollmentIdentifier)}`
1✔
274
  }
275

276
  /**
277
   * `/verify/face/license/:licenseType` post api call
278
   */
279
  getLicenseKey(licenseType: string): Promise<$AxiosXHR<any>> {
280
    const { client, faceVerificationUrl } = this
×
281

282
    return client.post(`${faceVerificationUrl}/license/${encodeURIComponent(licenseType)}`, {})
×
283
  }
284

285
  /**
286
   * `/verify/face/session` post api call
287
   */
288
  issueSessionToken(): Promise<$AxiosXHR<any>> {
289
    const { client, faceVerificationUrl } = this
×
290

291
    return client.post(`${faceVerificationUrl}/session`, {})
×
292
  }
293

294
  /**
295
   * `/verify/face/:enrollmentIdentifier` put api call
296
   * @param {any} payload
297
   * @param {string} enrollmentIdentifier
298
   * @param {any} axiosConfig
299
   */
300
  performFaceVerification(enrollmentIdentifier: string, payload: any, axiosConfig: any = {}): Promise<$AxiosXHR<any>> {
×
301
    const { client } = this
×
302
    const endpoint = this.enrollmentUrl(enrollmentIdentifier)
×
303

304
    return client.put(endpoint, payload, axiosConfig)
×
305
  }
306

307
  /**
308
   * `/verify/face/:enrollmentIdentifier` delete api call
309
   * @param {string} enrollmentIdentifier
310
   * @param {string} signature
311
   */
312
  disposeFaceSnapshot(enrollmentIdentifier: string, fvSigner: string): Promise<void> {
313
    const { client } = this
×
314
    const endpoint = this.enrollmentUrl(enrollmentIdentifier)
×
315

316
    return client.delete(endpoint, { params: { fvSigner } })
×
317
  }
318

319
  /**
320
   * `/verify/face/:enrollmentIdentifier` get api call
321
   * @param {string} enrollmentIdentifier
322
   * @param {string} signature
323
   */
324
  checkFaceSnapshotDisposalState(enrollmentIdentifier: string, fvSigner: string): Promise<$AxiosXHR<any>> {
325
    const { client } = this
1✔
326
    const endpoint = this.enrollmentUrl(enrollmentIdentifier)
1✔
327

328
    return client.get(endpoint, { params: { fvSigner } })
1✔
329
  }
330

331
  /**
332
   * `/trust` get api call
333
   */
334
  getTrust() {
335
    return this.client.get('/trust', { throttle: false })
×
336
  }
337

338
  /**
339
   * `/profileBy` get api call
340
   */
341
  getProfileBy(valueHash: string) {
342
    return this.client.get('/profileBy', { params: { valueHash }, throttle: false })
×
343
  }
344

345
  /**
346
   * `/user/enqueue` post api call
347
   * adds user to queue or return queue status
348
   */
349
  checkQueueStatus() {
350
    return this.client.post('/user/enqueue')
×
351
  }
352

353
  // eslint-disable-next-line require-await
354
  async notifyVendor(transactionId, transactionInfo) {
355
    const { callbackUrl, invoiceId, senderEmail, senderName } = transactionInfo || {}
×
356

357
    if (!callbackUrl) {
×
358
      return // or throw error
×
359
    }
360

361
    return this.client.post(callbackUrl, { invoiceId, transactionId, senderEmail, senderName })
×
362
  }
363

364
  // eslint-disable-next-line require-await
365
  async getMessageStrings() {
366
    return this.client.get('/strings')
×
367
  }
368

369
  // eslint-disable-next-line require-await
370
  async invokeCallbackUrl(url, responseObject) {
371
    return this.sharedClient.post(url, responseObject)
×
372
  }
373

374
  // eslint-disable-next-line require-await
375
  async getTokenTxs(token, address, chainId, fromBlock = null, allPages = true) {
4!
376
    if (chainId === NETWORK_ID.FUSE) {
4!
377
      const explorerQuery = { action: 'tokentx', contractaddress: token }
×
378

379
      return this.getExplorerTxs(address, chainId, explorerQuery, fromBlock, allPages)
×
380
    }
381

382
    const tatumQuery = { tokenAddress: token, transactionTypes: 'fungible' }
4✔
383

384
    return this.getTatumTxs(address, chainId, tatumQuery, fromBlock, allPages)
4✔
385
  }
386

387
  // eslint-disable-next-line require-await
388
  async getNativeTxs(address, chainId, fromBlock = null, allPages = true) {
×
389
    if (chainId === NETWORK_ID.FUSE) {
641!
390
      const explorerQuery = { action: 'txlist' }
×
391

392
      return this.getExplorerTxs(address, chainId, explorerQuery, fromBlock, allPages)
×
393
    }
394

395
    const tatumQuery = { transactionTypes: 'native' }
641✔
396

397
    return this.getTatumTxs(address, chainId, tatumQuery, fromBlock, allPages)
641✔
398
  }
399

400
  async getChains(): AxiosPromise<any> {
401
    const { explorer, explorerName, network_id: network } = fuseNetwork
×
402
    const chains = await this.sharedClient.get('/chains.json', {
×
403
      baseURL: Config.chainIdUrl,
404
    })
405

406
    const fuse = find(chains, { chainId: network })
×
407

408
    if (fuse && !fuse.explorers) {
×
409
      fuse.explorers = [
×
410
        {
411
          name: explorerName,
412
          url: explorer,
413
          standard: 'EIP3091',
414
        },
415
      ]
416
    }
417

418
    return chains
×
419
  }
420

421
  async getContractAbi(address, chainId, explorer = null): AxiosPromise<any> {
×
422
    switch (chainId) {
×
423
      case 122:
424
        explorer = fuseNetwork.explorerAPI
×
425
        break
×
426
      case 42220:
427
        explorer = Config.ethereum['42220'].explorerAPI
×
428
        break
×
429
      case 1:
430
        explorer = Config.ethereum['1'].explorerAPI
×
431
        break
×
432
      default:
433
        // eslint-disable-next-line no-lone-blocks
434
        {
435
          if (explorer.startsWith('https://api.') === false) {
×
436
            explorer = explorer.replace('https://', 'https://api.')
×
437
          }
438
        }
439
        break
×
440
    }
441
    if (!explorer) {
×
442
      return undefined
×
443
    }
444
    const params = {
×
445
      module: 'contract',
446
      action: 'getabi',
447
      address,
448
    }
449

450
    const apis = shuffle(explorer.split(',')).map(baseURL => async () => {
×
451
      const { result } = await this.sharedClient.get('/api', {
×
452
        params,
453
        baseURL,
454
      })
455

456
      if (!isArray(result)) {
×
457
        log.warn('Failed to fetch contract ABI', { result, params, chainId, baseURL })
×
458
        throw new Error('Failed to fetch contract ABI')
×
459
      }
460

461
      return result
×
462
    })
463

464
    const result = await fallback(apis)
×
465

466
    return result
×
467
  }
468

469
  async getContractName(address, chainId, explorer = null): AxiosPromise<any> {
4✔
470
    const params = {
4✔
471
      module: 'contract',
472
      action: 'getsourcecode',
473
      address,
474
    }
475

476
    switch (chainId) {
4!
477
      case 122:
478
        explorer = fuseNetwork.explorerAPI
×
479
        break
×
480
      case 42220:
481
        explorer = Config.ethereum['42220'].explorerAPI
×
482
        break
×
483
      case 1:
484
        explorer = Config.ethereum['1'].explorerAPI
×
485
        break
×
486
      default:
487
        break
4✔
488
    }
489

490
    if (!explorer) {
4!
491
      return undefined
4✔
492
    }
493

494
    const apis = shuffle(explorer.split(',')).map(baseURL => async () => {
×
495
      const { result } = await this.sharedClient.get('/api', {
×
496
        params,
497
        baseURL,
498
      })
499

500
      if (!isArray(result)) {
×
501
        log.warn('Failed to fetch contract source', { result, params, chainId, baseURL })
×
502
        throw new Error('Failed to fetch contract source')
×
503
      }
504

505
      return result[0]
×
506
    })
507

508
    const res = await fallback(apis)
×
509

510
    // const { result } = await this.sharedClient.get('/api', {
511
    //   params,
512
    //   baseURL: explorer,
513
    // })
514

515
    const impl = chainId === 42220 ? res?.Implementation : res?.ImplementationAddress
×
516

517
    if (res?.Proxy === '1' || res?.IsProxy === 'true') {
×
518
      return this.getContractName(impl, chainId, explorer)
×
519
    }
520

521
    return res?.ContractName
×
522
  }
523

524
  // eslint-disable-next-line require-await
525
  async graphQuery(query): AxiosPromise<any> {
526
    const payload = { query }
×
527
    const options = { baseURL: Config.graphQlUrl }
×
528

529
    return this.sharedClient.post('', payload, options)
×
530
  }
531

532
  async getOTPLEvents(sender, chainId, address, from, currentBlock, eventHash) {
533
    const txs = []
×
534
    const explorer = Config.ethereum[chainId].explorerAPI
×
535

536
    const sender32 = padLeft(sender, 64)
×
537

538
    const params = {
×
539
      module: 'logs',
540
      action: 'getLogs',
541
      address,
542
      sort: 'desc',
543
      page: 1,
544
      offset: 10000,
545
      topic0: eventHash,
546
      topic1: sender32,
547

548
      // required for fuse explorer, optional for celoscan
549
      topic0_1_opr: 'and',
550
      fromBlock: from,
551
      toBlock: currentBlock,
552
    }
553

554
    for (;;) {
×
555
      const apis = shuffle(explorer.split(',')).map(baseURL => async () => {
×
NEW
556
        const options = { baseURL, params }
×
557

NEW
558
        if (baseURL.includes('tatum')) {
×
NEW
559
          options.headers = {
×
560
            accept: 'application/json',
561
            'x-api-key': Config.tatumApiKey,
562
          }
563
        }
564

NEW
565
        const { result: events } = await this.sharedClient.get('/api', options)
×
566

567
        if (!isArray(events)) {
×
568
          log.warn('Failed to fetch OTP events from explorer', { events, params, chainId, baseURL })
×
569
          throw new Error('Failed to fetch OTP events from explorer')
×
570
        }
571

572
        return events
×
573
      })
574

575
      // eslint-disable-next-line no-await-in-loop
576
      const events = await fallback(apis)
×
577

578
      // const { result: events } = await this.sharedClient.get('/api', {
579
      //   params,
580
      //   baseURL: explorer,
581
      // })
582

583
      params.page += 1
×
584

585
      txs.push(...events)
×
586

587
      if (events.length < params.offset) {
×
588
        // default page size by explorer.fuse.io
589
        break
×
590
      }
591
    }
592

593
    return txs
×
594
  }
595

596
  /**
597
   * @private
598
   */
599
  async getTatumTxs(address, chainId, query = {}, from = null, allPages = true): Promise<any[]> {
×
600
    const url = '/data/transactions'
645✔
601
    const { CELO, MAINNET, GOERLI } = NETWORK_ID
645✔
602

603
    let chain
604
    const txs = []
645✔
605

606
    switch (chainId) {
645!
607
      case MAINNET:
608
        chain = 'ethereum'
×
609
        break
×
610
      case GOERLI:
611
        chain = 'ethereum-goerli'
×
612
        break
×
613
      case CELO:
614
        chain = 'celo'
×
615
        break
×
616

617
      // FUSE not supported on Tatum
618
      default:
619
        throw new Error('Chain not supported')
645✔
620
    }
621

622
    const pageSize = 50 // default page size by Tatum
×
623
    const params = { ...query, chain, addresses: address, offset: 0 }
×
624

NEW
625
    const options = {
×
626
      baseURL: Config.tatumApiUrl,
627
      params,
628
      headers: { accept: 'application/json', 'x-api-key': Config.tatumApiKey },
629
    }
630

631
    if (from) {
×
632
      params.blockFrom = from
×
633
    }
634

635
    for (;;) {
×
636
      const { result } = await this.sharedClient // eslint-disable-line no-await-in-loop
×
637
        .get(url, options)
638

639
      params.offset += 1
×
640
      const chunk = result.filter(({ transactionSubtype }) => transactionSubtype !== 'zero-transfer')
×
641
      txs.push(...chunk)
×
642

643
      if (allPages === false || result.length < pageSize) {
×
644
        break
×
645
      }
646
      // eslint-disable-next-line no-await-in-loop
647
      await delay(500) // wait 500ms before next call to prevent rate limits
×
648
    }
649

650
    return txs
×
651
  }
652

653
  async getExplorerTxs(address, chainId, query, from = null, allPages = true) {
×
654
    const txs = []
×
655
    const url = '/api'
×
656
    const explorer = Config.ethereum[chainId]?.explorerAPI
×
657

658
    const params = { ...query, module: 'account', address, sort: 'asc', page: 1, offset: 10000 }
×
659

660
    if (from) {
×
661
      params.start_block = from
×
662
      params.startblock = from //etherscan
×
663
    }
664

665
    for (;;) {
×
666
      const apis = shuffle(explorer.split(',')).map(baseURL => async () => {
×
667
        const options = { baseURL, params }
×
668

NEW
669
        if (baseURL.includes('tatum')) {
×
NEW
670
          options.headers = {
×
671
            accept: 'application/json',
672
            'x-api-key': Config.tatumApiKey,
673
          }
674
        }
675

676
        const { result } = await this.sharedClient.get(url, options)
×
677
        if (!isArray(result)) {
×
678
          log.warn('Failed to fetch transactions from explorer', { result, params, chainId, baseURL })
×
679
          throw new Error('Failed to fetch transactions from explorer')
×
680
        }
681
        return result
×
682
      })
683

684
      // eslint-disable-next-line no-await-in-loop
685
      const result = await fallback(apis)
×
686

687
      const chunk = result.filter(({ value }) => value !== '0')
×
688

689
      params.page += 1
×
690

691
      txs.push(...chunk)
×
692

693
      if (allPages === false || result.length < params.offset) {
×
694
        // default page size by explorer.fuse.io
695
        break
×
696
      }
697
    }
698

699
    return txs
×
700
  }
701
}
702

703
const api = new APIService()
119✔
704

705
global.api = api
119✔
706
export default api
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