@sphereon/ssi-sdk.ebsi-support 0.26.1-unstable.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +13 -0
  3. package/dist/agent/EbsiSupport.d.ts +12 -0
  4. package/dist/agent/EbsiSupport.d.ts.map +1 -0
  5. package/dist/agent/EbsiSupport.js +202 -0
  6. package/dist/agent/EbsiSupport.js.map +1 -0
  7. package/dist/did/EbsiDidProvider.d.ts +47 -0
  8. package/dist/did/EbsiDidProvider.d.ts.map +1 -0
  9. package/dist/did/EbsiDidProvider.js +172 -0
  10. package/dist/did/EbsiDidProvider.js.map +1 -0
  11. package/dist/did/EbsiDidResolver.d.ts +5 -0
  12. package/dist/did/EbsiDidResolver.d.ts.map +1 -0
  13. package/dist/did/EbsiDidResolver.js +10 -0
  14. package/dist/did/EbsiDidResolver.js.map +1 -0
  15. package/dist/did/functions.d.ts +66 -0
  16. package/dist/did/functions.d.ts.map +1 -0
  17. package/dist/did/functions.js +416 -0
  18. package/dist/did/functions.js.map +1 -0
  19. package/dist/did/index.d.ts +6 -0
  20. package/dist/did/index.d.ts.map +1 -0
  21. package/dist/did/index.js +6 -0
  22. package/dist/did/index.js.map +1 -0
  23. package/dist/did/services/EbsiRPCService.d.ts +13 -0
  24. package/dist/did/services/EbsiRPCService.d.ts.map +1 -0
  25. package/dist/did/services/EbsiRPCService.js +64 -0
  26. package/dist/did/services/EbsiRPCService.js.map +1 -0
  27. package/dist/did/services/EbsiRestService.d.ts +37 -0
  28. package/dist/did/services/EbsiRestService.d.ts.map +1 -0
  29. package/dist/did/services/EbsiRestService.js +90 -0
  30. package/dist/did/services/EbsiRestService.js.map +1 -0
  31. package/dist/did/types.d.ts +386 -0
  32. package/dist/did/types.d.ts.map +1 -0
  33. package/dist/did/types.js +47 -0
  34. package/dist/did/types.js.map +1 -0
  35. package/dist/functions/Attestation.d.ts +32 -0
  36. package/dist/functions/Attestation.d.ts.map +1 -0
  37. package/dist/functions/Attestation.js +182 -0
  38. package/dist/functions/Attestation.js.map +1 -0
  39. package/dist/functions/AttestationHeadlessCallbacks.d.ts +17 -0
  40. package/dist/functions/AttestationHeadlessCallbacks.d.ts.map +1 -0
  41. package/dist/functions/AttestationHeadlessCallbacks.js +194 -0
  42. package/dist/functions/AttestationHeadlessCallbacks.js.map +1 -0
  43. package/dist/functions/index.d.ts +7 -0
  44. package/dist/functions/index.d.ts.map +1 -0
  45. package/dist/functions/index.js +8 -0
  46. package/dist/functions/index.js.map +1 -0
  47. package/dist/index.d.ts +7 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +8 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/types/IEbsiSupport.d.ts +211 -0
  52. package/dist/types/IEbsiSupport.d.ts.map +1 -0
  53. package/dist/types/IEbsiSupport.js +5 -0
  54. package/dist/types/IEbsiSupport.js.map +1 -0
  55. package/package.json +86 -0
  56. package/src/agent/EbsiSupport.ts +250 -0
  57. package/src/did/EbsiDidProvider.ts +269 -0
  58. package/src/did/EbsiDidResolver.ts +16 -0
  59. package/src/did/functions.ts +528 -0
  60. package/src/did/index.ts +5 -0
  61. package/src/did/services/EbsiRPCService.ts +68 -0
  62. package/src/did/services/EbsiRestService.ts +117 -0
  63. package/src/did/types.ts +449 -0
  64. package/src/functions/Attestation.ts +262 -0
  65. package/src/functions/AttestationHeadlessCallbacks.ts +242 -0
  66. package/src/functions/index.ts +15 -0
  67. package/src/index.ts +8 -0
  68. package/src/types/IEbsiSupport.ts +241 -0
@@ -0,0 +1,528 @@
1
+ import { randomBytes } from '@ethersproject/random'
2
+ import { CreateRequestObjectMode } from '@sphereon/oid4vci-common'
3
+ import { getControllerKey, getEthereumAddressFromKey, getKeys } from '@sphereon/ssi-sdk-ext.did-utils'
4
+ import { calculateJwkThumbprint, calculateJwkThumbprintForKey, JwkKeyUse, toJwk } from '@sphereon/ssi-sdk-ext.key-utils'
5
+ import { W3CVerifiableCredential } from '@sphereon/ssi-types'
6
+ import { IAgentContext, IIdentifier, IKey, IKeyManager, MinimalImportableKey, TKeyType } from '@veramo/core'
7
+ import { getBytes, SigningKey, Transaction } from 'ethers'
8
+ import { base58btc } from 'multiformats/bases/base58'
9
+ import * as u8a from 'uint8arrays'
10
+ import { getEbsiApiBaseUrl, wait } from '../functions'
11
+ import { logger } from '../index'
12
+ import { ApiOpts, EbsiApiVersion, EbsiEnvironment, IRequiredContext, WellknownOpts } from '../types/IEbsiSupport'
13
+ import { ebsiWaitTillDocumentAnchored } from './services/EbsiRestService'
14
+ import { callRpcMethod } from './services/EbsiRPCService'
15
+ import {
16
+ BASE_CONTEXT_DOC,
17
+ CreateEbsiDidParams,
18
+ EBSI_DID_SPEC_INFOS,
19
+ EbsiDidRegistryAPIEndpoints,
20
+ EbsiDidSpecInfo,
21
+ EbsiKeyType,
22
+ EbsiPublicKeyPurpose,
23
+ EbsiRpcMethod,
24
+ EbsiRPCResponse,
25
+ IContext,
26
+ IKeyOpts,
27
+ RpcMethodArgs,
28
+ RpcOkResponse,
29
+ } from './types'
30
+
31
+ export function generateEbsiMethodSpecificId(specInfo?: EbsiDidSpecInfo): string {
32
+ const spec = specInfo ?? EBSI_DID_SPEC_INFOS.V1
33
+ const length = spec.didLength ?? 16
34
+
35
+ const result = new Uint8Array(length + (spec.version ? 1 : 0))
36
+ if (spec.version) {
37
+ result.set([spec.version])
38
+ }
39
+ result.set(randomBytes(length), spec.version ? 1 : 0)
40
+ return base58btc.encode(result)
41
+ }
42
+
43
+ export function generateOrUseProvidedEbsiPrivateKeyHex(specInfo?: EbsiDidSpecInfo, privateKeyBytes?: Uint8Array): string {
44
+ const spec = specInfo ?? EBSI_DID_SPEC_INFOS.V1
45
+ const length = spec.didLength ? 2 * spec.didLength : 32
46
+
47
+ if (privateKeyBytes) {
48
+ if (privateKeyBytes.length !== length) {
49
+ throw Error(`Invalid private key length supplied (${privateKeyBytes.length}. Expected ${length} for ${spec.type}`)
50
+ }
51
+ return u8a.toString(privateKeyBytes, 'base16')
52
+ }
53
+ return u8a.toString(randomBytes(length), 'base16')
54
+ }
55
+
56
+ /**
57
+ * Returns the public key in the correct format to be used with the did registry v5
58
+ * - in case of Secp256k1 - returns the uncompressed public key as hex string prefixed with 0x04
59
+ * - in case of Secp256r1 - returns the jwk public key as hex string
60
+ * @param {{ key: IKey, type: EbsiKeyType }} args
61
+ * - key is the cryptographic key containing the public key
62
+ * - type is the type of the key which can be Secp256k1 or Secp256r1
63
+ * @returns {string} The properly formatted public key
64
+ * @throws {Error} If the key type is invalid
65
+ */
66
+ export const formatEbsiPublicKey = (args: { key: IKey; type: TKeyType }): string => {
67
+ const { key, type } = args
68
+ switch (type) {
69
+ case 'Secp256k1': {
70
+ const bytes = getBytes('0x' + key.publicKeyHex, 'key')
71
+ return SigningKey.computePublicKey(bytes, false)
72
+ }
73
+ case 'Secp256r1': {
74
+ /*
75
+ Public key as hex string. For an ES256K key, it must be in uncompressed format prefixed with "0x04".
76
+ For other algorithms, it must be the JWK transformed to string and then to hex format.
77
+ */
78
+ const jwk: JsonWebKey = toJwk(key.publicKeyHex, type, { use: JwkKeyUse.Signature, key })
79
+ /*
80
+ Converting JWK to string and then hex is odd and may lead to errors. Implementing
81
+ it like that because it's how EBSI does it. However, it may be a point of pain
82
+ in the future.
83
+ */
84
+ const jwkString = JSON.stringify(jwk, null, 2)
85
+ return `0x${u8a.toString(u8a.fromString(jwkString), 'base16')}`
86
+ }
87
+ default:
88
+ throw new Error(`Unsupported key type: ${type}`)
89
+ }
90
+ }
91
+
92
+ export const ebsiGetIssuerMock = (args: { environment?: EbsiEnvironment; version?: EbsiApiVersion }): string => {
93
+ const { environment = 'conformance', version = 'v3' } = args
94
+ if (environment === 'pilot') {
95
+ throw Error(`EBSI Pilot network does not have a issuer mock server`)
96
+ }
97
+ return `${getEbsiApiBaseUrl({ environment, version, system: environment })}/issuer-mock`
98
+ }
99
+
100
+ export const ebsiGetAuthorisationServer = (args: { environment?: EbsiEnvironment; version?: EbsiApiVersion }): string => {
101
+ const { environment = 'pilot', version = 'v4' } = args
102
+ return `${getEbsiApiBaseUrl({ environment, version, system: 'authorisation' })}`
103
+ }
104
+
105
+ export const ebsiGetRegistryAPIUrls = (args: { environment?: EbsiEnvironment; version?: EbsiApiVersion }): EbsiDidRegistryAPIEndpoints => {
106
+ const { environment = 'pilot', version = 'v5' } = args
107
+ const baseUrl = `${getEbsiApiBaseUrl({ environment, version, system: 'did-registry' })}`
108
+ return {
109
+ mutate: `${baseUrl}/jsonrpc`,
110
+ query: `${baseUrl}/identifiers`,
111
+ }
112
+ }
113
+
114
+ export const determineWellknownEndpoint = ({ environment, version, type, system = environment, mock }: WellknownOpts): string => {
115
+ const url = `${getEbsiApiBaseUrl({ environment, version, system })}${mock ? `/${mock}` : ''}/.well-known/${type}`
116
+ logger.debug(`wellknown url: ${url}`)
117
+ return url
118
+ }
119
+
120
+ export const ebsiSignAndSendTransaction = async (
121
+ args: {
122
+ rpcRequest: RpcMethodArgs
123
+ previousTxResponse?: EbsiRPCResponse
124
+ kid: string
125
+ accessToken: string
126
+ apiOpts?: ApiOpts
127
+ },
128
+ context: IContext,
129
+ ): Promise<EbsiRPCResponse> => {
130
+ const { rpcRequest, accessToken, kid, apiOpts, previousTxResponse } = args
131
+ const unsignedTxResponse = await callRpcMethod(rpcRequest)
132
+ const nonce = 'result' in unsignedTxResponse ? unsignedTxResponse.result.nonce : undefined
133
+ // We should get a new nonce once the actual previous transaction has been anchored. Thus we retry if the nonce remains the same
134
+ if (
135
+ previousTxResponse &&
136
+ 'result' in unsignedTxResponse &&
137
+ 'nonce' in previousTxResponse &&
138
+ 'nonce' in unsignedTxResponse.result &&
139
+ typeof unsignedTxResponse.result === 'object' &&
140
+ previousTxResponse.nonce === unsignedTxResponse.result.nonce
141
+ ) {
142
+ await wait(1_000)
143
+ return await ebsiSignAndSendTransaction({ ...args, previousTxResponse }, context)
144
+ }
145
+
146
+ if ('error' in unsignedTxResponse && !!unsignedTxResponse.error) {
147
+ logger.error(JSON.stringify(unsignedTxResponse))
148
+ throw new Error(unsignedTxResponse.error.message ?? 'Unknown error occurred')
149
+ }
150
+ const unsignedTx = (unsignedTxResponse as RpcOkResponse).result
151
+
152
+ const agentUnsignedTx = JSON.parse(JSON.stringify(unsignedTx))
153
+ if (unsignedTx && 'chainId' in unsignedTx && typeof unsignedTx.chainId === 'string' && unsignedTx.chainId.toLowerCase().startsWith('0x')) {
154
+ // We expect the chain id to be a regular number and not a hex string
155
+ agentUnsignedTx.chainId = Number.parseInt(unsignedTx.chainId, 16)
156
+ }
157
+ const signedRawTx = await context.agent.keyManagerSignEthTX({
158
+ kid,
159
+ transaction: agentUnsignedTx,
160
+ })
161
+
162
+ const sig = Transaction.from(signedRawTx).signature!
163
+ const { r, s, v } = sig
164
+
165
+ const sTResponse = await callRpcMethod({
166
+ params: [
167
+ {
168
+ protocol: 'eth',
169
+ unsignedTransaction: unsignedTx,
170
+ r,
171
+ s,
172
+ v: `0x${v.toString(16)}`,
173
+ signedRawTransaction: signedRawTx,
174
+ },
175
+ ],
176
+ rpcMethod: EbsiRpcMethod.SEND_SIGNED_TRANSACTION,
177
+ rpcId: unsignedTxResponse.id,
178
+ apiOpts,
179
+ accessToken: accessToken,
180
+ })
181
+
182
+ if ('status' in sTResponse) {
183
+ throw new Error(JSON.stringify(sTResponse, null, 2))
184
+ }
185
+ return { ...sTResponse, nonce }
186
+ }
187
+
188
+ export const ebsiGenerateOrUseKeyPair = async (
189
+ args: {
190
+ keyOpts?: IKeyOpts
191
+ keyType: EbsiKeyType
192
+ kms: string
193
+ controllerKey?: boolean
194
+ },
195
+ context: IAgentContext<IKeyManager>,
196
+ ) => {
197
+ const { keyOpts, keyType, kms, controllerKey = false } = args
198
+ let privateKeyHex = generateOrUseProvidedEbsiPrivateKeyHex(
199
+ EBSI_DID_SPEC_INFOS.V1,
200
+ keyOpts?.privateKeyHex ? u8a.fromString(keyOpts.privateKeyHex, 'base16') : undefined,
201
+ )
202
+ if (privateKeyHex.startsWith('0x')) {
203
+ privateKeyHex = privateKeyHex.substring(2)
204
+ }
205
+ if (!privateKeyHex || privateKeyHex.length !== 64) {
206
+ throw new Error('Private key should be 32 bytes / 64 chars hex')
207
+ }
208
+ const importableKey = await toMinimalImportableKey({ key: { ...keyOpts, privateKeyHex }, type: keyType, kms })
209
+
210
+ if (keyType === 'Secp256k1') {
211
+ importableKey.meta = {
212
+ ...importableKey.meta,
213
+ ebsi: {
214
+ anchored: false,
215
+ controllerKey,
216
+ },
217
+ }
218
+ }
219
+ return importableKey
220
+ }
221
+
222
+ export const toMinimalImportableKey = async (args: { key?: IKeyOpts; type: EbsiKeyType; kms: string }): Promise<MinimalImportableKey> => {
223
+ const { key, kms } = args
224
+ const minimalImportableKey: Partial<MinimalImportableKey> = { ...key }
225
+ const type = args.key?.type ?? args.type
226
+ minimalImportableKey.kms = kms
227
+ minimalImportableKey.type = type
228
+ if (!minimalImportableKey.privateKeyHex) {
229
+ throw Error(`Minimal importable key needs a private key`)
230
+ }
231
+
232
+ minimalImportableKey.meta = {
233
+ purposes: assertedPurposes({ key }) ?? setDefaultPurposes({ key, type }),
234
+ jwkThumbprint: calculateJwkThumbprintForKey({
235
+ key: minimalImportableKey as MinimalImportableKey,
236
+ digestAlgorithm: 'sha256',
237
+ }),
238
+ }
239
+ return minimalImportableKey as MinimalImportableKey
240
+ }
241
+
242
+ export const assertedPurposes = (args: { key?: IKeyOpts }): EbsiPublicKeyPurpose[] | undefined => {
243
+ const { key } = args
244
+ if (key?.purposes && key.purposes.length > 0) {
245
+ switch (key.type) {
246
+ case 'Secp256k1': {
247
+ if (key?.purposes && key.purposes.length > 0 && key.purposes?.includes(EbsiPublicKeyPurpose.CapabilityInvocation)) {
248
+ return key.purposes
249
+ }
250
+ throw new Error(`Secp256k1/ES256K key requires ${EbsiPublicKeyPurpose.CapabilityInvocation} purpose`)
251
+ }
252
+ case 'Secp256r1': {
253
+ if (
254
+ key?.purposes &&
255
+ key.purposes.length > 0 &&
256
+ key.purposes.every((purpose) => [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication].includes(purpose))
257
+ ) {
258
+ return key.purposes
259
+ }
260
+ throw new Error(
261
+ `Secp256r1/ES256 key requires ${[EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication].join(', ')} purposes`,
262
+ )
263
+ }
264
+ default:
265
+ throw new Error(`Unsupported key type: ${key.type}`)
266
+ }
267
+ }
268
+ return key?.purposes
269
+ }
270
+
271
+ export const setDefaultPurposes = (args: { key?: IKeyOpts; type: EbsiKeyType }): EbsiPublicKeyPurpose[] => {
272
+ const { key, type } = args
273
+ if (!key?.purposes || key.purposes.length === 0) {
274
+ switch (type) {
275
+ case 'Secp256k1':
276
+ return [EbsiPublicKeyPurpose.CapabilityInvocation]
277
+ case 'Secp256r1':
278
+ return [EbsiPublicKeyPurpose.AssertionMethod, EbsiPublicKeyPurpose.Authentication]
279
+ default:
280
+ throw new Error(`Unsupported key type: ${key?.type}`)
281
+ }
282
+ }
283
+ return key.purposes
284
+ }
285
+
286
+ export const randomRpcId = (): number => {
287
+ return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
288
+ }
289
+
290
+ export const ebsiCreateDidOnLedger = async (
291
+ args: CreateEbsiDidParams,
292
+ context: IRequiredContext,
293
+ ): Promise<{
294
+ identifier: IIdentifier
295
+ addVerificationMethod: EbsiRPCResponse
296
+ insertDidDoc: EbsiRPCResponse
297
+ addAssertionMethodRelationship: EbsiRPCResponse
298
+ addAuthenticationRelationship: EbsiRPCResponse
299
+ }> => {
300
+ const {
301
+ accessTokenOpts,
302
+ notBefore = Math.floor(Date.now() / 1000 - 60),
303
+ notAfter = Math.floor(Date.now() / 1000 + 10 * 365 * 24 * 60 * 60),
304
+ baseDocument,
305
+ identifier,
306
+ } = args
307
+ const { clientId, redirectUri, environment, credentialIssuer } = accessTokenOpts
308
+ const controllerKey = getControllerKey({ identifier })
309
+ const secp256r1 = getKeys({ identifier, keyType: 'Secp256r1' })?.[0]
310
+ let { attestationToOnboard } = accessTokenOpts
311
+
312
+ if (!controllerKey || !secp256r1) {
313
+ return Promise.reject(`No secp256k1 controller key and/or secp2561r key found for identifier ${identifier}`)
314
+ }
315
+ const from = getEthereumAddressFromKey({ key: controllerKey })
316
+ if (!from) {
317
+ return Promise.reject(Error(`EBSI 'from' address expected for key ${controllerKey.publicKeyHex}`))
318
+ }
319
+ const did = identifier.did
320
+ const kid = controllerKey.kid
321
+ const idOpts = { identifier, kid }
322
+ let rpcId = args.rpcId ?? randomRpcId()
323
+ const apiOpts = {
324
+ environment,
325
+ version: 'v5',
326
+ } satisfies ApiOpts
327
+
328
+ const jwksUri = args.accessTokenOpts.jwksUri ?? `${clientId}/.well-known/jwks/dids/${encodeURIComponent(identifier.did)}.json`
329
+
330
+ if (!attestationToOnboard) {
331
+ const authReqResult = await context.agent.ebsiCreateAttestationAuthRequestURL({
332
+ credentialIssuer,
333
+ idOpts,
334
+ formats: ['jwt_vc'],
335
+ clientId,
336
+ redirectUri,
337
+ credentialType: 'VerifiableAuthorisationToOnboard',
338
+ requestObjectOpts: { iss: clientId, requestObjectMode: CreateRequestObjectMode.REQUEST_OBJECT, jwksUri },
339
+ })
340
+ const attestationResult = await context.agent.ebsiGetAttestation({
341
+ clientId,
342
+ authReqResult,
343
+ opts: { timeout: 120_000 },
344
+ })
345
+ attestationToOnboard = attestationResult.credentials[0].rawVerifiableCredential as W3CVerifiableCredential
346
+ }
347
+
348
+ const insertDidAccessTokenResponse = await context.agent.ebsiAccessTokenGet({
349
+ attestationCredential: attestationToOnboard,
350
+ jwksUri,
351
+ scope: 'didr_invite',
352
+ idOpts,
353
+ redirectUri,
354
+ credentialIssuer,
355
+ clientId,
356
+ environment,
357
+ skipDidResolution: true,
358
+ })
359
+
360
+ const insertDidDocRequest = {
361
+ params: [
362
+ {
363
+ from,
364
+ did,
365
+ baseDocument: baseDocument ?? BASE_CONTEXT_DOC,
366
+ vMethodId: calculateJwkThumbprint({ jwk: toJwk(controllerKey.publicKeyHex, 'Secp256k1') }),
367
+ isSecp256k1: true,
368
+ publicKey: formatEbsiPublicKey({ key: controllerKey, type: 'Secp256k1' }),
369
+ notBefore,
370
+ notAfter,
371
+ },
372
+ ],
373
+ rpcMethod: EbsiRpcMethod.INSERT_DID_DOCUMENT,
374
+ rpcId,
375
+ apiOpts,
376
+ accessToken: insertDidAccessTokenResponse.accessTokenResponse.access_token,
377
+ }
378
+
379
+ const insertDidDocResponse = await ebsiSignAndSendTransaction(
380
+ {
381
+ rpcRequest: insertDidDocRequest,
382
+ kid,
383
+ accessToken: insertDidAccessTokenResponse.accessTokenResponse.access_token,
384
+ apiOpts,
385
+ },
386
+ context,
387
+ )
388
+
389
+ let anchorTime = await ebsiWaitTillDocumentAnchored({
390
+ did,
391
+ ...apiOpts,
392
+ maxWaitTime: 30_000,
393
+ startIntervalMS: 2000,
394
+ minIntervalMS: 500,
395
+ decreaseIntervalMSPerStep: 750,
396
+ })
397
+ if (!anchorTime.didDocument) {
398
+ throw Error(`did ${did} was not registered on EBSI network ${apiOpts.environment} in 45 seconds`)
399
+ }
400
+ logger.debug(`Anchoring did ${did} on network ${apiOpts.environment} took ${anchorTime.totalWaitTime / 1000} seconds in ${anchorTime.count} tries`)
401
+
402
+ // Update to the controller key for the remainder
403
+ idOpts.kid = calculateJwkThumbprintForKey({ key: controllerKey })
404
+
405
+ const addVMAccessTokenResponse = await context.agent.ebsiAccessTokenGet({
406
+ // attestationCredential: attestationToOnboard,
407
+ jwksUri,
408
+ scope: 'didr_write',
409
+ idOpts,
410
+ redirectUri,
411
+ credentialIssuer: undefined,
412
+ clientId,
413
+ environment,
414
+ skipDidResolution: true,
415
+ })
416
+
417
+ const vMethodId = calculateJwkThumbprint({ jwk: toJwk(secp256r1.publicKeyHex, 'Secp256r1') })
418
+ const publicKey = formatEbsiPublicKey({ key: secp256r1, type: 'Secp256r1' })
419
+ const addVerificationMethodRequest = {
420
+ params: [
421
+ {
422
+ from,
423
+ did,
424
+ isSecp256k1: false,
425
+ vMethodId,
426
+ publicKey,
427
+ },
428
+ ],
429
+ rpcMethod: EbsiRpcMethod.ADD_VERIFICATION_METHOD,
430
+ rpcId,
431
+ apiOpts,
432
+ accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
433
+ }
434
+
435
+ const addVerificationMethodResponse = await ebsiSignAndSendTransaction(
436
+ {
437
+ rpcRequest: addVerificationMethodRequest,
438
+ previousTxResponse: insertDidDocResponse,
439
+ kid,
440
+ accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
441
+ apiOpts,
442
+ },
443
+ context,
444
+ )
445
+
446
+ // We need to wait, even after the anchor. The methods below also retry in case the nonce does not get updated.
447
+ // But we simply know that at this point we need to introduce some delay
448
+ await wait(2_000)
449
+
450
+ const addAssertionMethodRelationshipRequest = {
451
+ params: [
452
+ {
453
+ from,
454
+ did,
455
+ vMethodId,
456
+ name: 'assertionMethod',
457
+ notAfter,
458
+ notBefore,
459
+ },
460
+ ],
461
+ rpcMethod: EbsiRpcMethod.ADD_VERIFICATION_RELATIONSHIP,
462
+ rpcId,
463
+ apiOpts,
464
+ accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
465
+ }
466
+
467
+ const addAssertionMethodRelationshipResponse = await ebsiSignAndSendTransaction(
468
+ {
469
+ rpcRequest: addAssertionMethodRelationshipRequest,
470
+ previousTxResponse: addVerificationMethodResponse,
471
+ kid,
472
+ accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
473
+ apiOpts,
474
+ },
475
+ context,
476
+ )
477
+
478
+ anchorTime = await ebsiWaitTillDocumentAnchored({
479
+ did,
480
+ ...apiOpts,
481
+ maxWaitTime: 20_000,
482
+ minIntervalMS: 500,
483
+ decreaseIntervalMSPerStep: 500,
484
+ searchForObject: { assertionMethod: [`${did}#${vMethodId}`] },
485
+ })
486
+ if (!anchorTime.didDocument) {
487
+ throw Error(`did ${did} assertionMethod id ${vMethodId} was not registered on EBSI network ${apiOpts.environment} in 20 seconds`)
488
+ }
489
+ logger.debug(
490
+ `Anchoring assertionMethod ${vMethodId} for DID ${did} on network ${apiOpts.environment} took ${anchorTime.totalWaitTime / 1000} seconds in ${anchorTime.count} tries`,
491
+ )
492
+
493
+ const addAuthenticationRelationshipRequest = {
494
+ params: [
495
+ {
496
+ from,
497
+ did,
498
+ vMethodId,
499
+ name: 'authentication',
500
+ notAfter,
501
+ notBefore,
502
+ },
503
+ ],
504
+ rpcMethod: EbsiRpcMethod.ADD_VERIFICATION_RELATIONSHIP,
505
+ rpcId,
506
+ apiOpts,
507
+ accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
508
+ }
509
+
510
+ const addAuthenticationRelationshipResponse = await ebsiSignAndSendTransaction(
511
+ {
512
+ rpcRequest: addAuthenticationRelationshipRequest,
513
+ previousTxResponse: addAssertionMethodRelationshipResponse,
514
+ kid,
515
+ accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
516
+ apiOpts,
517
+ },
518
+ context,
519
+ )
520
+
521
+ return {
522
+ identifier,
523
+ insertDidDoc: insertDidDocResponse,
524
+ addVerificationMethod: addVerificationMethodResponse,
525
+ addAuthenticationRelationship: addAuthenticationRelationshipResponse,
526
+ addAssertionMethodRelationship: addAssertionMethodRelationshipResponse,
527
+ }
528
+ }
@@ -0,0 +1,5 @@
1
+ export { EbsiDidProvider } from './EbsiDidProvider'
2
+ export { getDidEbsiResolver } from './EbsiDidResolver'
3
+ export { ebsiCreateDidOnLedger, randomRpcId, ebsiSignAndSendTransaction, ebsiGetRegistryAPIUrls } from './functions'
4
+ export * from './services/EbsiRestService'
5
+ export * from './types'
@@ -0,0 +1,68 @@
1
+ import fetch from 'cross-fetch'
2
+ import { wait } from '../../functions'
3
+ import { logger } from '../../index'
4
+ import { ebsiGetRegistryAPIUrls, randomRpcId } from '../functions'
5
+ import { EbsiRPCResponse, JSON_RPC_VERSION, RpcMethodArgs } from '../types'
6
+
7
+ /**
8
+ * Allows to call 5 api methods of the EBSI RPC api
9
+ * - insertDidDocument
10
+ * - updateBaseDocument
11
+ * - addVerificationMethod
12
+ * - addVerificationMethodRelationship
13
+ * - sendSignedTransaction
14
+ * @function callRpcMethod
15
+ * @param {{ params: RPCParams[]; id: number; token: string; method: EbsiRpcMethod; apiOpts? ApiOpts }} args
16
+ */
17
+ export const callRpcMethod = async (args: RpcMethodArgs): Promise<EbsiRPCResponse> => {
18
+ return callRpcMethodImpl({ ...args, retries: 10 })
19
+ }
20
+ const callRpcMethodImpl = async (args: RpcMethodArgs & { retries: number }): Promise<EbsiRPCResponse> => {
21
+ const { params, rpcId, accessToken, rpcMethod, apiOpts, doNotThrowErrors = false, retries } = args
22
+ const options = buildFetchOptions({ accessToken: accessToken, params, rpcId, rpcMethod })
23
+ logger.debug(`RPC call:\r\n ${JSON.stringify(options, null, 2)}`)
24
+ const rpcResponse = await (await fetch(ebsiGetRegistryAPIUrls({ ...apiOpts }).mutate, options)).json()
25
+
26
+ let result = rpcResponse.result
27
+ logger.debug(`RPC RESPONSE:\r\n${JSON.stringify(result ?? rpcResponse.error)}`)
28
+
29
+ if (rpcResponse.error !== undefined && !doNotThrowErrors) {
30
+ logger.error(`RPC ERROR RESPONSE:`, rpcResponse)
31
+ if (rpcResponse.error.message.includes(`replacement fee too low`)) {
32
+ args.rpcId = randomRpcId()
33
+ if (retries <= 0) {
34
+ throw Error(rpcResponse.error.message)
35
+ }
36
+ logger.warning(`Replacement fee too low error. Waiting 1 sec. Retries: ${retries}`)
37
+ await wait(1000)
38
+ return callRpcMethodImpl({ ...args, retries: retries - 1 })
39
+ }
40
+ throw Error(rpcResponse.error.message)
41
+ }
42
+
43
+ return rpcResponse
44
+ }
45
+
46
+ /**
47
+ * Builds the request body of the http request to EBSI RPC api
48
+ * @function buildFetchOptions
49
+ * @param {{ params: RPCParams[]; id: number; token: string; method: EbsiRpcMethod }} args
50
+ */
51
+ const buildFetchOptions = (args: RpcMethodArgs): RequestInit => {
52
+ const { params, rpcId, accessToken, rpcMethod } = args
53
+ const fetchReq = {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ Authorization: `Bearer ${accessToken}`,
58
+ },
59
+ body: JSON.stringify({
60
+ jsonrpc: JSON_RPC_VERSION,
61
+ method: rpcMethod,
62
+ params: params,
63
+ id: rpcId,
64
+ }),
65
+ } satisfies RequestInit
66
+ logger.debug(fetchReq)
67
+ return fetchReq
68
+ }