• 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

85.81
/src/server/goodid/goodid-middleware.js
1
// @flow
2

3
import { Router } from 'express'
4
import passport from 'passport'
5
import requestIp from 'request-ip'
6
import { sha3 } from 'web3-utils'
7
import { get, isEmpty } from 'lodash'
8

9
import { Credential } from './veramo'
10

11
import createEnrollmentProcessor from '../verification/processor/EnrollmentProcessor'
12
import { enrollmentNotFoundMessage } from '../verification/utils/constants'
13
import { normalizeIdentifiers } from '../verification/utils/utils'
14
import { verifyIdentifier } from '../utils/eth.js'
15

16
import MultiWallet from '../blockchain/MultiWallet'
17

18
import { wrapAsync } from '../utils/helpers'
19
import requestRateLimiter from '../utils/requestRateLimiter'
20
import config from '../server.config'
21
import { retry as retryAttempt } from '../utils/async'
22

23
const { Location, Gender, Age, Identity } = Credential
2✔
24

25
export default function addGoodIDMiddleware(app: Router, utils, storage) {
26
  /**
27
   * POST /goodid/certificate/location
28
   * Content-Type: application/json
29
   * {
30
   *   "user": { // optional
31
   *      "mobile": "+380639549357"
32
   *    },
33
   *   "geoposition": { // a GeolocationPosition returned from navigator.geolocation.getCurrentPosition()
34
   *     "timestamp": 1707313563,
35
   *     "coords": {
36
   *       "longitude": 30.394171,
37
   *       "latitude": 50.328899,
38
   *       "accuracy": null,
39
   *       "altitude": null,
40
   *       "altitudeAccuracy": null,
41
   *       "heading": null,
42
   *       "speed": null,
43
   *     }
44
   *   }
45
   * }
46
   *
47
   * HTTP/1.1 200 OK
48
   * Content-Type: application/json
49
   * {
50
   *   "success": true,
51
   *   "certificate": {
52
   *     "credential": {
53
   *       "credentialSubject": {
54
   *         "id": 'did:ethr:<g$ wallet address>',
55
   *         "countryCode": "<2-chars upercased>"
56
   *       },
57
   *       "issuer": {
58
   *         "id": 'did:key:<GoodServer's DID>',
59
   *       },
60
   *       "type": ["VerifiableCredential", "VerifiableLocationCredential"],
61
   *       "@context": ["https://www.w3.org/2018/credentials/v1"],
62
   *       "issuanceDate": "2022-10-28T11:54:22.000Z",
63
   *       "proof": {
64
   *         "type": "JwtProof2020",
65
   *         "jwt": 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InlvdSI6IlJvY2sifX0sInN1YiI6ImRpZDp3ZWI6ZXhhbXBsZS5jb20iLCJuYmYiOjE2NjY5NTgwNjIsImlzcyI6ImRpZDpldGhyOmdvZXJsaToweDAzNTBlZWVlYTE0MTBjNWIxNTJmMWE4OGUwZmZlOGJiOGEwYmMzZGY4NjhiNzQwZWIyMzUyYjFkYmY5M2I1OWMxNiJ9.EPeuQBpkK13V9wu66SLg7u8ebY2OS8b2Biah2Vw-RI-Atui2rtujQkVc2t9m1Eqm4XQFECfysgQBdWwnSDvIjw',
66
   *       },
67
   *     },
68
   *   }
69
   * }
70
   */
71
  app.post(
2✔
72
    '/goodid/certificate/location',
73
    requestRateLimiter(10, 1),
74
    passport.authenticate('jwt', { session: false }),
75
    wrapAsync(async (req, res) => {
76
      const { user = {}, body, log } = req
7!
77
      const { mobile } = get(body, 'user', {})
7✔
78
      const { mobile: mobileHash, smsValidated, gdAddress } = user
7✔
79
      const { longitude, latitude } = get(body, 'geoposition.coords', {})
7✔
80

81
      log.debug('Location certificate request', { longitude, latitude, user })
7✔
82
      const issueCertificate = async countryCode => {
7✔
83
        const certificate = await utils.issueCertificate(gdAddress, Location, { countryCode })
5✔
84

85
        res.json({ success: true, certificate })
5✔
86
      }
87

88
      try {
7✔
89
        if (mobile) {
7✔
90
          const countryCodeFromMobile = utils.getCountryCodeFromMobile(mobile)
4✔
91
          const isPhoneMatchesAndVerified = smsValidated && mobileHash === sha3(mobile)
4✔
92
          log.debug('Got country code from mobile:', { countryCodeFromMobile, isPhoneMatchesAndVerified })
4✔
93
          if (isPhoneMatchesAndVerified) {
4✔
94
            await issueCertificate(countryCodeFromMobile)
2✔
95
            return
2✔
96
          }
97
        }
98

99
        if (!longitude && !latitude) {
5✔
100
          throw new Error('Failed to verify location: missing geolocation data')
1✔
101
        }
102

103
        const clientIp = requestIp.getClientIp(req)
4✔
104

105
        log.debug('Getting country data', { clientIp, longitude, latitude, gdAddress })
4✔
106

107
        const [countryCodeFromIP, countryCodeFromLocation] = await Promise.all([
4✔
108
          utils.getCountryCodeFromIPAddress(clientIp),
109
          retryAttempt(() => utils.getCountryCodeFromGeoLocation(latitude, longitude), 3, 1500)
4✔
110
        ])
111

112
        log.debug('Got country data', { countryCodeFromIP, countryCodeFromLocation })
4✔
113
        if (countryCodeFromIP !== countryCodeFromLocation) {
4✔
114
          log.warn('ip doesnt match geolocation', { clientIp, longitude, latitude, gdAddress })
1✔
115
          return res.status(400).json({ success: false, error: 'location could not be verified' })
1✔
116
        }
117

118
        await issueCertificate(countryCodeFromIP)
3✔
119
      } catch (exception) {
120
        const { message } = exception
1✔
121

122
        log.error('Failed to issue location ceritifate:', message, exception, { mobile, longitude, latitude })
1✔
123
        res.status(400).json({ success: false, error: message })
1✔
124
      }
125
    })
126
  )
127

128
  /**
129
   * POST /goodid/certificate/identity
130
   * Content-Type: application/json
131
   * {
132
   *   "enrollmentIdentifier": "<v2 identifier string>",
133
   *   "fvSigner": "<v1 identifier string>", // optional
134
   * }
135
   *
136
   * HTTP/1.1 200 OK
137
   * Content-Type: application/json
138
   * {
139
   *   "success": true,
140
   *   "certificate": {
141
   *     "credentialSubject": {
142
   *       "id": 'did:ethr:<g$ wallet address>',
143
   *       "gender": "<Male | Female>" // yep, AWS doesn't supports LGBT,
144
   *       "age": {
145
   *         "min": <years>, // "open" ranges also allowed, e.g. { to: 7 } or { from: 30 }
146
   *         "max": <years>,   // this value includes to the range, "from 30" means 30 and older, if < 30 you will get "from 25 to 29"
147
   *       }
148
   *     },
149
   *     "issuer": {
150
   *       "id": 'did:key:<GoodServer's DID>',
151
   *     },
152
   *     "type": ["VerifiableCredential", "VerifiableIdentityCredential", "VerifiableAgeCredential", "VerifiableGenderCredential"],
153
   *     "@context": ["https://www.w3.org/2018/credentials/v1"],
154
   *     "issuanceDate": "2022-10-28T11:54:22.000Z",
155
   *     "proof": {
156
   *       "type": "JwtProof2020",
157
   *       "jwt": 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InlvdSI6IlJvY2sifX0sInN1YiI6ImRpZDp3ZWI6ZXhhbXBsZS5jb20iLCJuYmYiOjE2NjY5NTgwNjIsImlzcyI6ImRpZDpldGhyOmdvZXJsaToweDAzNTBlZWVlYTE0MTBjNWIxNTJmMWE4OGUwZmZlOGJiOGEwYmMzZGY4NjhiNzQwZWIyMzUyYjFkYmY5M2I1OWMxNiJ9.EPeuQBpkK13V9wu66SLg7u8ebY2OS8b2Biah2Vw-RI-Atui2rtujQkVc2t9m1Eqm4XQFECfysgQBdWwnSDvIjw',
158
   *     },
159
   *   }
160
   * }
161
   */
162
  app.post(
2✔
163
    '/goodid/certificate/identity',
164
    requestRateLimiter(10, 1),
165
    passport.authenticate('jwt', { session: false }),
166
    wrapAsync(async (req, res) => {
167
      const { user, body, log } = req
4✔
168
      // Currently certificates for goodid are only supported on celo
169
      const { enrollmentIdentifier, fvSigner, chainId = 42220 } = body
4✔
170
      const { gdAddress } = user
4✔
171

172
      log.info('identity certificate request:', { user, enrollmentIdentifier })
4✔
173
      try {
4✔
174
        const processor = createEnrollmentProcessor(storage, log)
4✔
175

176
        if (!enrollmentIdentifier) {
4✔
177
          throw new Error('Failed to verify identify: missing face verification ID')
1✔
178
        }
179

180
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
3✔
181
        // Currently certificates for goodid are only supported on celo
182
        await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
3✔
183

184
        // here we check if wallet was registered using v1 of v2 identifier
185
        const [isV2, isV1] = await Promise.all([
2✔
186
          processor.isIdentifierExists(v2Identifier),
187
          v1Identifier && processor.isIdentifierExists(v1Identifier)
2!
188
        ])
189

190
        const faceIdentifier = isV2 ? v2Identifier : isV1 ? v1Identifier : null
2!
191

192
        if (!faceIdentifier) {
2✔
193
          throw new Error(enrollmentNotFoundMessage)
1✔
194
        }
195

196
        const { auditTrailBase64 } = await processor.getEnrollment(faceIdentifier, log)
1✔
197
        const estimation = await utils.ageGenderCheck(auditTrailBase64)
1✔
198
        log.info('identity certificat request estimation:', { estimation })
1✔
199
        const certificate = await utils.issueCertificate(gdAddress, [Identity, Gender, Age], {
1✔
200
          unique: true,
201
          ...estimation
202
        })
203

204
        res.json({ success: true, certificate })
1✔
205
      } catch (exception) {
206
        const { message } = exception
3✔
207

208
        log.error('Failed to issue identity ceritifate:', message, exception, {
3✔
209
          enrollmentIdentifier,
210
          fvSigner
211
        })
212

213
        res.status(400).json({ success: false, error: message })
3✔
214
      }
215
    })
216
  )
217

218
  /**
219
   * POST /goodid/certificate/verify
220
   * Content-Type: application/json
221
   * {
222
   *   "certificate": {
223
   *     "credentialSubject": {
224
   *       "id": 'did:ethr:<g$ wallet address>',
225
   *       "countryCode": "<2-chars upercased>"
226
   *     },
227
   *     "issuer": {
228
   *       "id": 'did:key:<GoodServer's DID>',
229
   *     },
230
   *     "type": ["VerifiableCredential", <set of VerifiableLocationCredential | VerifiableIdentityCredential | VerifiableGenderCredential | VerifiableAgeCredential items>],
231
   *     "@context": ["https://www.w3.org/2018/credentials/v1"],
232
   *     "issuanceDate": "2022-10-28T11:54:22.000Z",
233
   *     "proof": {
234
   *       "type": "JwtProof2020",
235
   *       "jwt": 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InlvdSI6IlJvY2sifX0sInN1YiI6ImRpZDp3ZWI6ZXhhbXBsZS5jb20iLCJuYmYiOjE2NjY5NTgwNjIsImlzcyI6ImRpZDpldGhyOmdvZXJsaToweDAzNTBlZWVlYTE0MTBjNWIxNTJmMWE4OGUwZmZlOGJiOGEwYmMzZGY4NjhiNzQwZWIyMzUyYjFkYmY5M2I1OWMxNiJ9.EPeuQBpkK13V9wu66SLg7u8ebY2OS8b2Biah2Vw-RI-Atui2rtujQkVc2t9m1Eqm4XQFECfysgQBdWwnSDvIjw',
236
   *     },
237
   *   }
238
   * }
239
   *
240
   * HTTP/1.1 200 OK
241
   * Content-Type: application/json
242
   * {
243
   *   "success": true
244
   * }
245
   */
246
  app.post(
2✔
247
    '/goodid/certificate/verify',
248
    requestRateLimiter(10, 1),
249
    wrapAsync(async (req, res) => {
250
      const { body, log } = req
2✔
251
      const { certificate } = body ?? {}
2!
252

253
      log.info('certificate verification request:', { certificate })
2✔
254
      try {
2✔
255
        if (!certificate) {
2✔
256
          throw new Error('Failed to verify credential: missing certificate data')
1✔
257
        }
258

259
        const success = await utils.verifyCertificate(certificate)
1✔
260

261
        res.status(200).json({ success })
1✔
262
      } catch (exception) {
263
        const { message } = exception
1✔
264

265
        log.error('Failed to verify ceriticate:', message, exception, { certificate })
1✔
266
        res.status(400).json({ success: false, error: message })
1✔
267
      }
268
    })
269
  )
270

271
  /**
272
   * POST /goodid/redtent
273
   * Content-Type: application/json
274
   * {
275
   *   "videoFilename": "<wallet address>.<ext>",
276
   *   "certificates": [{ // both location + identity certs
277
   *     "credentialSubject": {
278
   *       "id": 'did:ethr:<g$ wallet address>',
279
   *       "gender": "<Male | Female>" // yep, AWS doesn't supports LGBT,
280
   *       "age": {
281
   *         "min": <years>, // "open" ranges also allowed, e.g. { to: 7 } or { from: 30 }
282
   *         "max": <years>,   // this value includes to the range, "from 30" means 30 and older, if < 30 you will get "from 25 to 29"
283
   *       }
284
   *     },
285
   *     "issuer": {
286
   *       "id": 'did:key:<GoodServer's DID>',
287
   *     },
288
   *     "type": ["VerifiableCredential", "VerifiableIdentityCredential", "VerifiableAgeCredential", "VerifiableGenderCredential"],
289
   *     "@context": ["https://www.w3.org/2018/credentials/v1"],
290
   *     "issuanceDate": "2022-10-28T11:54:22.000Z",
291
   *     "proof": {
292
   *       "type": "JwtProof2020",
293
   *       "jwt": 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InlvdSI6IlJvY2sifX0sInN1YiI6ImRpZDp3ZWI6ZXhhbXBsZS5jb20iLCJuYmYiOjE2NjY5NTgwNjIsImlzcyI6ImRpZDpldGhyOmdvZXJsaToweDAzNTBlZWVlYTE0MTBjNWIxNTJmMWE4OGUwZmZlOGJiOGEwYmMzZGY4NjhiNzQwZWIyMzUyYjFkYmY5M2I1OWMxNiJ9.EPeuQBpkK13V9wu66SLg7u8ebY2OS8b2Biah2Vw-RI-Atui2rtujQkVc2t9m1Eqm4XQFECfysgQBdWwnSDvIjw',
294
   *     },
295
   *   }, {
296
   *     "credentialSubject": {
297
   *       "id": 'did:ethr:<g$ wallet address>',
298
   *       "countryCode": "<2-chars upercased>"
299
   *     },
300
   *     "issuer": {
301
   *       "id": 'did:key:<GoodServer's DID>',
302
   *     },
303
   *     "type": ["VerifiableCredential", "VerifiableLocationCredential"],
304
   *     "@context": ["https://www.w3.org/2018/credentials/v1"],
305
   *     "issuanceDate": "2022-10-28T11:54:22.000Z",
306
   *     "proof": {
307
   *       "type": "JwtProof2020",
308
   *       "jwt": 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InlvdSI6IlJvY2sifX0sInN1YiI6ImRpZDp3ZWI6ZXhhbXBsZS5jb20iLCJuYmYiOjE2NjY5NTgwNjIsImlzcyI6ImRpZDpldGhyOmdvZXJsaToweDAzNTBlZWVlYTE0MTBjNWIxNTJmMWE4OGUwZmZlOGJiOGEwYmMzZGY4NjhiNzQwZWIyMzUyYjFkYmY5M2I1OWMxNiJ9.EPeuQBpkK13V9wu66SLg7u8ebY2OS8b2Biah2Vw-RI-Atui2rtujQkVc2t9m1Eqm4XQFECfysgQBdWwnSDvIjw',
309
   *     },
310
   *   }]
311
   * }
312
   *
313
   * HTTP/1.1 200 OK
314
   * Content-Type: application/json
315
   * {
316
   *   "success": true
317
   * }
318
   */
319
  app.post(
2✔
320
    '/goodid/redtent',
321
    requestRateLimiter(10, 1),
322
    wrapAsync(async (req, res) => {
323
      const { body, log } = req
7✔
324
      const { certificates, videoFilename } = body ?? {}
7!
325

326
      log.info('redtent request:', { certificates, videoFilename })
7✔
327
      try {
7✔
328
        if (isEmpty(certificates)) {
7✔
329
          throw new Error('Failed to verify: missing certificate data')
1✔
330
        }
331

332
        if (!videoFilename) {
6✔
333
          throw new Error('Failed to verify: missing file name of the video uploaded to the bucket')
1✔
334
        }
335

336
        const { unique, gender, countryCode, account } = await utils.aggregateCredentials(certificates)
5✔
337
        log.debug('aggregating credentials result', { unique, gender, countryCode, account })
4✔
338

339
        if (!unique) {
4✔
340
          throw new Error('Failed to verify: certificates are missing uniqueness credential')
1✔
341
        }
342

343
        let registerToPool = countryCode
3✔
344
        if (['development', 'staging'].includes(config.env)) {
3!
345
          //           -Men - Japan, Ukraine, Israel, Brazil, Nigeria
346
          // --Women - US, Israel, Spain, Colombia
UNCOV
347
          if (gender === 'Male' && ['JP', 'UA', 'IL', 'BR', 'NG', 'DN', 'NL'].includes(countryCode) === false) {
×
348
            throw new Error("Failed to verify: allowed 'JP','UA','IL','BR','NG', 'DN', 'NL' for male only")
×
349
          }
UNCOV
350
          if (gender === 'Female' && ['US', 'IL', 'ES', 'CO'].includes(countryCode) === false) {
×
351
            throw new Error("Failed to verify: allowed 'US','IL','ES','CO' for female only")
×
352
          }
UNCOV
353
          registerToPool = gender === 'Female' ? 'NG' : 'CO'
×
354
        } else if ((countryCode !== 'NG' && countryCode !== 'CO') || gender !== 'Female') {
3✔
355
          throw new Error('Failed to verify: allowed for the Nigerian/Colombian accounts owned by women only')
1✔
356
        }
357

358
        await utils.checkS3AccountVideo(videoFilename)
2✔
359
        await MultiWallet.registerRedtent(account, registerToPool, log)
1✔
360

361
        res.status(200).json({ success: true })
1✔
362
      } catch (exception) {
363
        const { message } = exception
6✔
364

365
        log.error('Failed to register at RedTent:', message, exception, { certificates, videoFilename })
6✔
366
        res.status(400).json({ success: false, error: message })
6✔
367
      }
368
    })
369
  )
370
}
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