@arcblock/did-connect-js 1.21.2

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.
@@ -0,0 +1,768 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ /* eslint-disable indent */
3
+ /* eslint-disable object-curly-newline */
4
+ const qs = require('querystring');
5
+ const pick = require('lodash/pick');
6
+ const random = require('lodash/random');
7
+ const shuffle = require('lodash/shuffle');
8
+ const isEqual = require('lodash/isEqual');
9
+ const Client = require('@ocap/client');
10
+ const Jwt = require('@arcblock/jwt');
11
+ const RSA = require('@ocap/mcrypto/lib/crypter/rsa').default;
12
+ const { toDid, toBase58, fromBase58 } = require('@ocap/util');
13
+ const { fromAddress } = require('@ocap/wallet');
14
+ const { toAddress } = require('@arcblock/did');
15
+
16
+ const BaseAuthenticator = require('./base');
17
+
18
+ // eslint-disable-next-line
19
+ const debug = require('debug')(`${require('../../package.json').name}:authenticator:wallet`);
20
+
21
+ const { DEFAULT_CHAIN_INFO } = BaseAuthenticator;
22
+ const DEFAULT_TIMEOUT = 8000;
23
+ const MFA_CODE_COUNT = 3;
24
+
25
+ const schema = require('../schema');
26
+
27
+ const formatDisplay = (display) => {
28
+ // empty
29
+ if (!display) {
30
+ return '';
31
+ }
32
+
33
+ // object like
34
+ if (display && display.type && display.content) {
35
+ return JSON.stringify(pick(display, ['type', 'content']));
36
+ }
37
+
38
+ // string like
39
+ try {
40
+ const parsed = JSON.parse(display);
41
+ if (parsed && parsed.type && parsed.content) {
42
+ return display;
43
+ }
44
+ return '';
45
+ } catch (err) {
46
+ return '';
47
+ }
48
+ };
49
+
50
+ class WalletAuthenticator extends BaseAuthenticator {
51
+ /**
52
+ * @typedef ApplicationInfo
53
+ * @prop {string} name - application name
54
+ * @prop {string} description - application description
55
+ * @prop {string} icon - application icon/logo url
56
+ * @prop {string} link - application home page, with which user can return application from wallet
57
+ * @prop {string} path - deep link url
58
+ * @prop {string} publisher - application did with `did:abt:` prefix
59
+ */
60
+
61
+ /**
62
+ * @typedef ChainInfo
63
+ * @prop {string} id - application chain id
64
+ * @prop {string} type - application chain type
65
+ * @prop {string} host - graphql endpoint of the application chain
66
+ */
67
+
68
+ /**
69
+ * Creates an instance of DID Authenticator.
70
+ *
71
+ * @class
72
+ * @param {object} config
73
+ * @param {WalletObject|Function} config.wallet - wallet instance {@see @ocap/wallet} or a function that returns wallet instance
74
+ * @param {WalletObject|Function} [config.delegator] - the party that authorizes `wallet` to perform actions on behalf of `wallet`
75
+ * @param {string|Function} [config.delegation] - the jwt token that proves delegation relationship
76
+ * @param {ApplicationInfo|Function} config.appInfo - application basic info or a function that returns application info
77
+ * @param {ChainInfo|Function} config.chainInfo - application chain info or a function that returns chain info
78
+ * @param {Number} [config.timeout=8000] - timeout in milliseconds when generating claim
79
+ * @param {object} [config.baseUrl] - url to assemble wallet request uri, can be inferred from request object
80
+ * @param {string} [config.tokenKey='_t_'] - query param key for `token`
81
+ * @example
82
+ * const { fromRandom } = require('@ocap/wallet');
83
+ *
84
+ * const wallet = fromRandom().toJSON();
85
+ * const chainHost = 'https://beta.abtnetwork.io/api';
86
+ * const chainId = 'beta';
87
+ * const auth = new Authenticator({
88
+ * wallet,
89
+ * baseUrl: 'http://beta.abtnetwork.io/webapp',
90
+ * appInfo: {
91
+ * name: 'DID Wallet Demo',
92
+ * description: 'Demo application to show the potential of DID Wallet',
93
+ * icon: 'https://arcblock.oss-cn-beijing.aliyuncs.com/images/wallet-round.png',
94
+ * },
95
+ * memberAppInfo: null,
96
+ * chainInfo: {
97
+ * host: chainHost,
98
+ * id: chainId,
99
+ * },
100
+ * timeout: 8000,
101
+ * });
102
+ */
103
+ constructor({
104
+ wallet,
105
+ appInfo,
106
+ memberAppInfo,
107
+ delegator,
108
+ delegation,
109
+ timeout = DEFAULT_TIMEOUT,
110
+ chainInfo = DEFAULT_CHAIN_INFO,
111
+ baseUrl = '',
112
+ tokenKey = '_t_',
113
+ }) {
114
+ super();
115
+
116
+ this.wallet = this._validateWallet(wallet);
117
+ this.appInfo = this._validateAppInfo(appInfo);
118
+ this.memberAppInfo = this._validateAppInfo(memberAppInfo, true);
119
+ this.chainInfo = chainInfo;
120
+
121
+ this.delegator = delegator;
122
+ this.delegation = delegation;
123
+
124
+ this.baseUrl = baseUrl;
125
+ this.tokenKey = tokenKey;
126
+ this.timeout = timeout;
127
+
128
+ if (!this.appInfo.link) {
129
+ this.appInfo.link = this.baseUrl;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Generate a deep link url that can be displayed as QRCode for DID Wallet to consume
135
+ *
136
+ * @method
137
+ * @param {object} params
138
+ * @param {string} params.baseUrl - baseUrl inferred from request object
139
+ * @param {string} params.pathname - wallet callback pathname
140
+ * @param {string} params.token - action token
141
+ * @param {object} params.query - params that should be persisted in wallet callback url
142
+ * @returns {string}
143
+ */
144
+ uri({ baseUrl, pathname = '', token = '', query = {} } = {}) {
145
+ const params = { ...query, [this.tokenKey]: token };
146
+ const payload = {
147
+ action: 'requestAuth',
148
+ url: encodeURIComponent(`${this.baseUrl || baseUrl}${pathname}?${qs.stringify(params)}`),
149
+ };
150
+
151
+ const uri = `https://abtwallet.io/i/?${qs.stringify(payload)}`;
152
+ debug('uri', { token, pathname, uri, params, payload });
153
+ return uri;
154
+ }
155
+
156
+ /**
157
+ * Compute public url to return to wallet
158
+ *
159
+ * @method
160
+ * @param {string} pathname
161
+ * @param {object} params
162
+ * @returns {string}
163
+ */
164
+ getPublicUrl(pathname, params = {}, baseUrl = '') {
165
+ return `${this.baseUrl || baseUrl}${pathname}?${qs.stringify(params)}`;
166
+ }
167
+
168
+ /**
169
+ * Sign a plain response, usually on auth success or error
170
+ *
171
+ * @method
172
+ * @param {object} params
173
+ * @param {object} params.response - response
174
+ * @param {string} params.errorMessage - error message, default to empty
175
+ * @param {string} params.successMessage - success message, default to empty
176
+ * @param {string} params.nextWorkflow - https://github.com/ArcBlock/ABT-DID-Protocol#concatenate-multiple-workflow
177
+ * @param {string} params.nextUrl - tell wallet do open this url in webview
178
+ * @param {object} params.cookies - key-value pairs to be set as cookie before open nextUrl
179
+ * @param {object} params.storages - key-value pairs to be set as localStorage before open nextUrl
180
+ * @param {string} baseUrl
181
+ * @param {object} request
182
+ * @returns {Promise<object>} { appPk, agentPk, authInfo }
183
+ */
184
+ async signResponse(
185
+ {
186
+ response = {},
187
+ errorMessage = '',
188
+ successMessage = '',
189
+ nextWorkflow = '',
190
+ nextUrl = '',
191
+ cookies = {},
192
+ storages = {},
193
+ },
194
+ baseUrl,
195
+ request,
196
+ extraParams = {}
197
+ ) {
198
+ const context = request.context || {};
199
+ const infoParams = { baseUrl, request, ...context, extraParams };
200
+ const [wallet, delegator, delegation] = await Promise.all([
201
+ this.getWalletInfo(infoParams),
202
+ this.getDelegator(infoParams),
203
+ this.getDelegation(infoParams),
204
+ ]);
205
+
206
+ const [appInfo, memberAppInfo] = await Promise.all([
207
+ this.getAppInfo({ ...infoParams, wallet, delegator }, 'appInfo'),
208
+ this.getAppInfo({ ...infoParams, wallet, delegator }, 'memberAppInfo'),
209
+ ]);
210
+ const didwallet = request.context.wallet;
211
+
212
+ const payload = {
213
+ appInfo,
214
+ memberAppInfo,
215
+ status: errorMessage ? 'error' : 'ok',
216
+ errorMessage: errorMessage || '',
217
+ successMessage: successMessage || '',
218
+ nextWorkflow: nextWorkflow || '',
219
+ nextUrl: nextUrl || '',
220
+ cookies: cookies || {},
221
+ storages: storages || '',
222
+ response,
223
+ };
224
+
225
+ if (delegator) {
226
+ payload.iss = toDid(delegator.address);
227
+ payload.agentDid = toDid(wallet.address);
228
+ payload.verifiableClaims = [{ type: 'certificate', content: delegation }];
229
+ }
230
+
231
+ const result = {
232
+ appPk: toBase58(wallet.pk),
233
+ authInfo: Jwt.sign(wallet.address, wallet.sk, payload, true, didwallet ? didwallet.jwt : undefined),
234
+ };
235
+
236
+ if (delegator) {
237
+ result.appPk = toBase58(delegator.pk);
238
+ result.agentPk = toBase58(wallet.pk);
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ /**
245
+ * Sign a auth response that returned to wallet: tell the wallet the appInfo/chainInfo
246
+ *
247
+ * @method
248
+ * @param {object} params
249
+ * @param {object} params.claims - info required by application to complete the auth
250
+ * @param {string} params.pathname - pathname to assemble callback url
251
+ * @param {string} params.baseUrl - baseUrl
252
+ * @param {object} params.challenge - random challenge to be included in the body
253
+ * @param {object} params.extraParams - extra query params and locale
254
+ * @param {object} params.request
255
+ * @param {object} params.context
256
+ * @param {string} params.context.token - action token
257
+ * @param {number} params.context.currentStep - current step
258
+ * @param {string} [params.context.sharedKey] - shared key between app and wallet
259
+ * @param {string} [params.context.encryptionKey] - encryption key from wallet
260
+ * @param {Function} [params.context.mfaCode] - function used to generate mfa code
261
+ * @param {string} params.context.userDid - decoded from req.query, base58
262
+ * @param {string} params.context.userPk - decoded from req.query, base58
263
+ * @param {string} params.context.didwallet - DID Wallet os and version
264
+ * @returns {Promise<object>} { appPk, agentPk, sharedKey, authInfo }
265
+ */
266
+ async sign({ context, request, claims, pathname = '', baseUrl = '', challenge = '', extraParams = {} }) {
267
+ // debug('sign.context', context);
268
+ // debug('sign.params', extraParams);
269
+
270
+ const claimsInfo = await this.tryWithTimeout(() =>
271
+ this.genRequestedClaims({
272
+ claims,
273
+ context: { baseUrl, request, ...context },
274
+ extraParams,
275
+ })
276
+ );
277
+
278
+ if (claimsInfo.filter((x) => x.mfaCode && x.mfaCode.length > 0).length > 1) {
279
+ throw new Error('Multiple MFA is not supported when sending more than 1 claim');
280
+ }
281
+
282
+ // FIXME: this maybe buggy if user provided multiple claims
283
+ const tmp = claimsInfo.find((x) => isEqual(this._isValidChainInfo(x.chainInfo), DEFAULT_CHAIN_INFO) === false);
284
+
285
+ const infoParams = { baseUrl, request, ...context, extraParams };
286
+ const [wallet, delegator, delegation, chainInfo] = await Promise.all([
287
+ this.getWalletInfo(infoParams),
288
+ this.getDelegator(infoParams),
289
+ this.getDelegation(infoParams),
290
+ this.getChainInfo(infoParams, tmp?.chainInfo),
291
+ ]);
292
+
293
+ const [appInfo, memberAppInfo] = await Promise.all([
294
+ this.getAppInfo({ ...infoParams, wallet, delegator }, 'appInfo'),
295
+ this.getAppInfo({ ...infoParams, wallet, delegator }, 'memberAppInfo'),
296
+ ]);
297
+
298
+ const payload = {
299
+ action: 'responseAuth',
300
+ challenge,
301
+ appInfo,
302
+ memberAppInfo,
303
+ chainInfo,
304
+ requestedClaims: claimsInfo.map((x) => {
305
+ delete x.chainInfo;
306
+ return x;
307
+ }),
308
+ url: `${this.baseUrl || baseUrl}${pathname}?${qs.stringify({ [this.tokenKey]: context.token })}`,
309
+ };
310
+
311
+ if (delegator) {
312
+ payload.iss = toDid(delegator.address);
313
+ payload.agentDid = toDid(wallet.address);
314
+ payload.verifiableClaims = [{ type: 'certificate', content: delegation }];
315
+ }
316
+
317
+ // debug('sign.payload', payload);
318
+
319
+ const version = context.didwallet ? context.didwallet.jwt : undefined;
320
+ const result = {
321
+ appPk: toBase58(wallet.pk),
322
+ authInfo: Jwt.sign(wallet.address, wallet.sk, payload, true, version),
323
+ sensitive: claimsInfo.every((x) => ['keyPair', 'encryptionKey'].includes(x.type)),
324
+ };
325
+
326
+ // encrypt context.encKey with user pk here
327
+ if (result.sensitive && context.sharedKey && context.encryptionKey) {
328
+ try {
329
+ const pk = fromBase58(context.encryptionKey).toString('utf8');
330
+ result.sharedKey = RSA.encrypt(context.sharedKey, pk, 'base58');
331
+ } catch (err) {
332
+ console.error('Failed to encrypt shared key', err);
333
+ }
334
+ }
335
+
336
+ if (delegator) {
337
+ result.appPk = toBase58(delegator.pk);
338
+ result.agentPk = toBase58(wallet.pk);
339
+ }
340
+
341
+ return result;
342
+ }
343
+
344
+ /**
345
+ * Determine chainInfo on the fly
346
+ *
347
+ * @param {object} params - contains the context of this request
348
+ * @param {object|undefined} [info=undefined] - chain info object or function
349
+ * @returns {Promise<ChainInfo>}
350
+ * @memberof WalletAuthenticator
351
+ */
352
+ async getChainInfo(params, info) {
353
+ if (info && this._isValidChainInfo(info)) {
354
+ return info;
355
+ }
356
+
357
+ if (typeof this.chainInfo === 'function') {
358
+ const result = await this.tryWithTimeout(() => this.chainInfo(params));
359
+ if (this._isValidChainInfo(result)) {
360
+ return result;
361
+ }
362
+ }
363
+
364
+ if (this.chainInfo && this._isValidChainInfo(this.chainInfo)) {
365
+ return this.chainInfo;
366
+ }
367
+
368
+ return DEFAULT_CHAIN_INFO;
369
+ }
370
+
371
+ /**
372
+ * Determine appInfo/memberAppInfo on the fly
373
+ *
374
+ * @param {object} params - contains the context of this request
375
+ * @param {string} key - appInfo | memberAppInfo
376
+ * @returns {Promise<ApplicationInfo>}
377
+ * @memberof WalletAuthenticator
378
+ */
379
+ async getAppInfo(params, key = 'appInfo') {
380
+ if (typeof this[key] === 'function') {
381
+ const info = await this.tryWithTimeout(() => this[key](params));
382
+ if (info) {
383
+ if (!info.link) {
384
+ info.link = params.baseUrl;
385
+ }
386
+ if (!info.publisher) {
387
+ info.publisher = toDid(params.delegator ? params.delegator.address : params.wallet.address);
388
+ }
389
+ }
390
+
391
+ return this._validateAppInfo(info, key === 'memberAppInfo');
392
+ }
393
+
394
+ if (this[key] && !this[key].publisher) {
395
+ this[key].publisher = toDid(params.delegator ? params.delegator.address : params.wallet.address);
396
+ }
397
+
398
+ return this[key];
399
+ }
400
+
401
+ async getWalletInfo(params) {
402
+ if (typeof this.wallet === 'function') {
403
+ const result = await this.tryWithTimeout(() => this.wallet(params));
404
+ return this._validateWallet(result, true);
405
+ }
406
+
407
+ return this.wallet;
408
+ }
409
+
410
+ async getDelegator(params) {
411
+ if (typeof this.delegator === 'function') {
412
+ const result = await this.tryWithTimeout(() => this.delegator(params));
413
+ return result ? this._validateWallet(result, false) : null;
414
+ }
415
+
416
+ return this.delegator;
417
+ }
418
+
419
+ async getDelegation(params) {
420
+ if (typeof this.delegation === 'function') {
421
+ const result = await this.tryWithTimeout(() => this.delegation(params));
422
+ return result;
423
+ }
424
+
425
+ return this.delegation;
426
+ }
427
+
428
+ /**
429
+ * Verify a DID auth response sent from DID Wallet
430
+ *
431
+ * @method
432
+ * @param {object} data
433
+ * @param {string} [locale=en]
434
+ * @param {boolean} [enforceTimestamp=true]
435
+ * @returns Promise<boolean>
436
+ */
437
+ async verify(data, locale = 'en', enforceTimestamp = true) {
438
+ const {
439
+ iss,
440
+ iat,
441
+ challenge = '',
442
+ action = 'responseAuth',
443
+ requestedClaims,
444
+ } = await this._verify(data, 'userPk', 'userInfo', locale, enforceTimestamp);
445
+
446
+ debug('verify.context', { userPk: data.userPk, userDid: toAddress(iss), action, challenge });
447
+ debug('verify.claims', requestedClaims);
448
+ return {
449
+ token: data.token,
450
+ userDid: toAddress(iss),
451
+ userPk: data.userPk,
452
+ claims: requestedClaims,
453
+ action,
454
+ challenge,
455
+ timestamp: iat,
456
+ };
457
+ }
458
+
459
+ // ---------------------------------------
460
+ // Request claim related methods
461
+ // ---------------------------------------
462
+ genRequestedClaims({ claims, context, extraParams }) {
463
+ return Promise.all(
464
+ Object.keys(claims).map(async (x) => {
465
+ let name = x;
466
+ let claim = claims[x];
467
+
468
+ if (Array.isArray(claims[x])) {
469
+ [name, claim] = claims[x];
470
+ }
471
+
472
+ if (!schema.claims[name]) {
473
+ throw new Error(`Unsupported claim type ${name}`);
474
+ }
475
+
476
+ const fn = typeof this[name] === 'function' ? name : 'getClaimInfo';
477
+ const result = await this[fn]({ claim, context, extraParams });
478
+
479
+ if (result.mfa && typeof context.mfaCode === 'function') {
480
+ result.mfaCode = [await context.mfaCode()];
481
+ while (result.mfaCode.length < MFA_CODE_COUNT) {
482
+ const noise = random(10, 99);
483
+ if (result.mfaCode.includes(noise) === false) {
484
+ result.mfaCode.push(noise);
485
+ }
486
+ }
487
+ result.mfaCode = shuffle(result.mfaCode);
488
+ }
489
+
490
+ const { value, error } = schema.claims[name].validate(result);
491
+ if (error) {
492
+ throw new Error(`Invalid ${name} claim: ${error.message}`);
493
+ }
494
+
495
+ return value;
496
+ })
497
+ );
498
+ }
499
+
500
+ async getClaimInfo({ claim, context, extraParams }) {
501
+ const { userDid, userPk, didwallet } = context;
502
+ const result =
503
+ typeof claim === 'function'
504
+ ? await claim({
505
+ userDid: userDid ? toAddress(userDid) : '',
506
+ userPk: userPk || '',
507
+ didwallet,
508
+ extraParams,
509
+ context,
510
+ })
511
+ : claim;
512
+
513
+ const infoParams = { ...context, ...extraParams };
514
+ const chainInfo = await this.getChainInfo(infoParams, result.chainInfo);
515
+
516
+ result.chainInfo = chainInfo;
517
+
518
+ return result;
519
+ }
520
+
521
+ // Request wallet to sign something: transaction/text/html/image
522
+ async signature({ claim, context, extraParams }) {
523
+ const {
524
+ data,
525
+ type = 'mime:text/plain',
526
+ digest = '',
527
+ method = 'sha3', // set this to `none` to instruct wallet not to hash before signing
528
+ wallet,
529
+ sender,
530
+ display,
531
+ description: desc,
532
+ chainInfo,
533
+ meta = {},
534
+ mfa = false,
535
+ nonce = '',
536
+ requirement = { tokens: [], assets: {} },
537
+ } = await this.getClaimInfo({
538
+ claim,
539
+ context,
540
+ extraParams,
541
+ });
542
+
543
+ debug('claim.signature', { data, digest, type, sender, context, nonce, requirement });
544
+
545
+ if (!data && !digest) {
546
+ throw new Error('Signature claim requires either data or digest to be provided');
547
+ }
548
+
549
+ const description = desc || 'Sign this transaction to continue.';
550
+
551
+ // We have to encode the transaction
552
+ if (type.endsWith('Tx')) {
553
+ if (!chainInfo.host) {
554
+ throw new Error('Invalid chainInfo when trying to encoding transaction');
555
+ }
556
+
557
+ const client = new Client(chainInfo.host);
558
+
559
+ if (typeof client[`encode${type}`] !== 'function') {
560
+ throw new Error(`Unsupported transaction type ${type}`);
561
+ }
562
+
563
+ if (!data.pk) {
564
+ data.pk = context.userPk;
565
+ }
566
+
567
+ try {
568
+ const { buffer: txBuffer } = await client[`encode${type}`]({
569
+ tx: data,
570
+ wallet: wallet || fromAddress(sender || context.userDid),
571
+ });
572
+
573
+ return {
574
+ type: 'signature',
575
+ description,
576
+ typeUrl: 'fg:t:transaction',
577
+ origin: toBase58(txBuffer),
578
+ method,
579
+ display: formatDisplay(display),
580
+ digest: '',
581
+ chainInfo,
582
+ meta,
583
+ mfa,
584
+ nonce,
585
+ requirement,
586
+ };
587
+ } catch (err) {
588
+ throw new Error(`Failed to encode transaction: ${err.message}`);
589
+ }
590
+ }
591
+
592
+ // We have en encoded transaction
593
+ if (type === 'fg:t:transaction') {
594
+ return {
595
+ type: 'signature',
596
+ description,
597
+ typeUrl: 'fg:t:transaction',
598
+ origin: toBase58(data),
599
+ display: formatDisplay(display),
600
+ method,
601
+ digest: '',
602
+ chainInfo,
603
+ meta,
604
+ mfa,
605
+ nonce,
606
+ requirement,
607
+ };
608
+ }
609
+
610
+ // If we are ask user to sign anything just pass the data
611
+ // Wallet should not hash the data if `method` is empty
612
+ // If we are asking user to sign a very large piece of data
613
+ // Just hash the data and show him the digest
614
+ return {
615
+ type: 'signature',
616
+ description: desc || 'Sign this message to continue.',
617
+ origin: data ? toBase58(data) : '',
618
+ typeUrl: type,
619
+ display: formatDisplay(display),
620
+ method,
621
+ digest,
622
+ chainInfo,
623
+ meta,
624
+ mfa,
625
+ nonce,
626
+ requirement,
627
+ };
628
+ }
629
+
630
+ // Request wallet to complete and sign a partial tx to broadcasting
631
+ // Usually used in payment scenarios
632
+ // The wallet can leverage multiple input capabilities of the chain
633
+ async prepareTx({ claim, context, extraParams }) {
634
+ const {
635
+ partialTx,
636
+ requirement = { tokens: [], assets: {} },
637
+ type,
638
+ display,
639
+ wallet,
640
+ sender,
641
+ description: desc,
642
+ chainInfo,
643
+ meta = {},
644
+ mfa = false,
645
+ nonce = '',
646
+ } = await this.getClaimInfo({
647
+ claim,
648
+ context,
649
+ extraParams,
650
+ });
651
+
652
+ debug('claim.prepareTx', { partialTx, requirement, type, sender, context });
653
+
654
+ if (!partialTx || !requirement) {
655
+ throw new Error('prepareTx claim requires both partialTx and requirement to be provided');
656
+ }
657
+
658
+ const description = desc || 'Prepare and sign this transaction to continue.';
659
+
660
+ // We have to encode the transaction
661
+ if (type && type.endsWith('Tx')) {
662
+ if (!chainInfo.host) {
663
+ throw new Error('Invalid chainInfo when trying to encoding partial transaction');
664
+ }
665
+
666
+ const client = new Client(chainInfo.host);
667
+
668
+ if (typeof client[`encode${type}`] !== 'function') {
669
+ throw new Error(`Unsupported transaction type ${type} when encoding partial transaction`);
670
+ }
671
+
672
+ if (!partialTx.pk) {
673
+ partialTx.pk = context.userPk;
674
+ }
675
+
676
+ try {
677
+ const { buffer: txBuffer } = await client[`encode${type}`]({
678
+ tx: partialTx,
679
+ wallet: wallet || fromAddress(sender || context.userDid),
680
+ });
681
+
682
+ return {
683
+ type: 'prepareTx',
684
+ description,
685
+ partialTx: toBase58(txBuffer),
686
+ display: formatDisplay(display),
687
+ requirement,
688
+ chainInfo,
689
+ meta,
690
+ mfa,
691
+ nonce,
692
+ };
693
+ } catch (err) {
694
+ throw new Error(`Failed to encode partial transaction: ${err.message}`);
695
+ }
696
+ }
697
+
698
+ // We have en encoded transaction
699
+ return {
700
+ type: 'prepareTx',
701
+ description,
702
+ partialTx: toBase58(partialTx),
703
+ requirement,
704
+ display: formatDisplay(display),
705
+ chainInfo,
706
+ meta,
707
+ mfa,
708
+ nonce,
709
+ };
710
+ }
711
+
712
+ _validateAppInfo(info, allowEmpty = false) {
713
+ if (typeof info === 'function') {
714
+ return info;
715
+ }
716
+
717
+ if (!info) {
718
+ if (allowEmpty === false) {
719
+ throw new Error('Wallet authenticator can not work with invalid appInfo: empty');
720
+ }
721
+
722
+ return null;
723
+ }
724
+
725
+ const { value, error } = schema.appInfo.validate(info);
726
+ if (error) {
727
+ throw new Error(`Wallet authenticator can not work with invalid appInfo: ${error.message}`);
728
+ }
729
+ return value;
730
+ }
731
+
732
+ _isValidChainInfo(x) {
733
+ const { error } = schema.chainInfo.validate(x);
734
+ return !error;
735
+ }
736
+
737
+ tryWithTimeout(asyncFn, label = '') {
738
+ if (typeof asyncFn !== 'function') {
739
+ throw new Error('asyncFn must be a valid function when calling tryWithTimeout');
740
+ }
741
+
742
+ const timeout = Number(this.timeout) || DEFAULT_TIMEOUT;
743
+ const inferredLabel = label || asyncFn.name || asyncFn.toString();
744
+ const invocationStack = new Error(`Timeout at: ${inferredLabel}`).stack;
745
+
746
+ // eslint-disable-next-line no-async-promise-executor
747
+ return new Promise(async (resolve, reject) => {
748
+ const timer = setTimeout(() => {
749
+ const error = new Error(`Async operation (${inferredLabel}) did not complete within ${timeout} ms`);
750
+ error.stack = invocationStack;
751
+ error.name = 'TIMEOUT';
752
+ reject(error);
753
+ }, timeout);
754
+
755
+ try {
756
+ const result = await asyncFn();
757
+ resolve(result);
758
+ } catch (err) {
759
+ reject(err);
760
+ } finally {
761
+ clearTimeout(timer);
762
+ }
763
+ });
764
+ }
765
+ }
766
+
767
+ module.exports = WalletAuthenticator;
768
+ module.exports.formatDisplay = formatDisplay;