@dynamic-labs-wallet/browser 1.1.42 → 1.1.44
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.
- package/index.cjs +191 -155
- package/index.esm.js +191 -155
- package/package.json +4 -3
- package/src/backup/password/errors.d.ts.map +1 -1
- package/src/client.d.ts +36 -30
- package/src/client.d.ts.map +1 -1
- package/src/queue.d.ts +10 -5
- package/src/queue.d.ts.map +1 -1
- package/src/services/signedSession.d.ts +5 -3
- package/src/services/signedSession.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -10,6 +10,7 @@ var axios = require('axios');
|
|
|
10
10
|
var gte = require('semver/functions/gte');
|
|
11
11
|
var ProviderEnum = require('@dynamic-labs/sdk-api-core/models/ProviderEnum');
|
|
12
12
|
var PQueue = require('p-queue');
|
|
13
|
+
var pTimeout = require('p-timeout');
|
|
13
14
|
var redaction = require('@dynamic-labs/redaction');
|
|
14
15
|
var ed25519 = require('@noble/curves/ed25519');
|
|
15
16
|
var utils = require('@noble/curves/abstract/utils');
|
|
@@ -587,6 +588,10 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
|
|
|
587
588
|
'invalid_nonce_signature',
|
|
588
589
|
'invalid_session_signature'
|
|
589
590
|
]);
|
|
591
|
+
/** Reasons a fresh nonce fixes, so they may refresh on every attempt, not just once. */ const REFRESHABLE_NONCE_FAILURE_REASONS = new Set([
|
|
592
|
+
'nonce_already_used',
|
|
593
|
+
'nonce_not_found_or_expired'
|
|
594
|
+
]);
|
|
590
595
|
/** Best-effort extraction of the server error `code`/`message` from a 4xx body. */ const readErrorBody = (error)=>{
|
|
591
596
|
var _error_response;
|
|
592
597
|
if (!(error instanceof axios.AxiosError)) {
|
|
@@ -617,6 +622,9 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
|
|
|
617
622
|
};
|
|
618
623
|
/** Maps the server error `code`/`message` onto a {@link NonceFailureReason}. */ const classifyNonceFailure = ({ code, message })=>{
|
|
619
624
|
const haystack = `${code != null ? code : ''} ${message != null ? message : ''}`.toLowerCase();
|
|
625
|
+
if (haystack.includes('not_found_or_expired') || haystack.includes('not found or expired')) {
|
|
626
|
+
return 'nonce_not_found_or_expired';
|
|
627
|
+
}
|
|
620
628
|
if (haystack.includes('already_used') || haystack.includes('already used') || haystack.includes('consumption failed')) {
|
|
621
629
|
return 'nonce_already_used';
|
|
622
630
|
}
|
|
@@ -661,7 +669,8 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
|
|
|
661
669
|
}
|
|
662
670
|
/**
|
|
663
671
|
* Builds a `retryPromise` `shouldRetry` predicate that refreshes a consumed
|
|
664
|
-
* single-use nonce via the reverse channel on a replay 400 and retries once
|
|
672
|
+
* single-use nonce via the reverse channel on a replay 400 and retries once
|
|
673
|
+
* (on every attempt when the body says the nonce was consumed or expired).
|
|
665
674
|
* The signed session carries a single-use replay nonce: it is valid on its
|
|
666
675
|
* first use, but once consumed (e.g. by a preceding operation) the server
|
|
667
676
|
* rejects with a 400, at which point a fresh session is fetched and the call
|
|
@@ -673,18 +682,24 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
|
|
|
673
682
|
// triggered an `alsoRetry`), so the replay 400 can land on any attempt, not
|
|
674
683
|
// just the first. Refresh on the first such 400 seen, whichever attempt
|
|
675
684
|
// it's on, and never again — preserves the single-refresh-per-operation limit.
|
|
685
|
+
// A consumed or expired nonce is the exception: each fresh session may still
|
|
686
|
+
// pop a stale pooled nonce, so it refreshes on every attempt (bounded by the
|
|
687
|
+
// caller's maxAttempts).
|
|
676
688
|
let hasRefreshed = false;
|
|
677
689
|
return async (error, attempt)=>{
|
|
678
690
|
const status = getHttpStatus(error);
|
|
691
|
+
const reason = status === 400 || status === 403 ? classifyNonceFailure(readErrorBody(error)) : undefined;
|
|
679
692
|
// A 400 is a refreshable replay nonce; a 403 is a session-key
|
|
680
693
|
// mismatch the server returns to break the refresh loop (DYNT-1282) — logged only
|
|
681
694
|
// when the body classifies as a genuine session-signature failure so unrelated 403s
|
|
682
695
|
// (e.g. an exhausted add-signer grant) don't pollute the DYNT-1599 metric.
|
|
683
696
|
// Only the 400 can actually refresh.
|
|
684
|
-
const isSessionSignatureFailure = status === 403 && SESSION_SIGNATURE_FAILURE_REASONS.has(
|
|
697
|
+
const isSessionSignatureFailure = status === 403 && reason !== undefined && SESSION_SIGNATURE_FAILURE_REASONS.has(reason);
|
|
698
|
+
const isRefreshableNonceFailure = status === 400 && reason !== undefined && REFRESHABLE_NONCE_FAILURE_REASONS.has(reason);
|
|
699
|
+
const canRefreshNow = isRefreshableNonceFailure || !hasRefreshed;
|
|
685
700
|
const shouldInstrument = status === 400 || isSessionSignatureFailure;
|
|
686
701
|
if (shouldInstrument) {
|
|
687
|
-
const willRefresh = status === 400 &&
|
|
702
|
+
const willRefresh = status === 400 && canRefreshNow && this.canRefresh && this.supportsReverseChannel();
|
|
688
703
|
this.instrumentNonceFailure({
|
|
689
704
|
error,
|
|
690
705
|
attempt,
|
|
@@ -694,7 +709,7 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
|
|
|
694
709
|
logContext
|
|
695
710
|
});
|
|
696
711
|
}
|
|
697
|
-
if (status === 400 &&
|
|
712
|
+
if (status === 400 && canRefreshNow && this.canRefresh) {
|
|
698
713
|
// canRefresh only proves a local callback exists, not that the host answers it.
|
|
699
714
|
if (!this.supportsReverseChannel()) {
|
|
700
715
|
this.logger.warn(`[${operationName}] host SDK version predates the signed-session reverse channel, declining nonce refresh`, _extends({
|
|
@@ -901,6 +916,8 @@ const SESSION_KEY_MISMATCH_REASONS = new Set([
|
|
|
901
916
|
]);
|
|
902
917
|
const NONCE_REASON_TO_BACKUP_REASON = {
|
|
903
918
|
nonce_already_used: 'nonce_already_used',
|
|
919
|
+
// Same dashboard bucket as before CKS split this code out of nonce_already_used.
|
|
920
|
+
nonce_not_found_or_expired: 'nonce_already_used',
|
|
904
921
|
invalid_nonce_signature: 'invalid_nonce_signature',
|
|
905
922
|
invalid_session_signature: 'invalid_session_signature',
|
|
906
923
|
other: 'signed_session'
|
|
@@ -2945,6 +2962,15 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
2945
2962
|
* Queue manager for wallet operations.
|
|
2946
2963
|
* Manages heavy operation queues (refresh/reshare/recover) and sign queues per wallet.
|
|
2947
2964
|
*/ class WalletQueueManager {
|
|
2965
|
+
static releaseActiveSign(accountAddress) {
|
|
2966
|
+
var _this_activeSignCounts_get;
|
|
2967
|
+
const remaining = ((_this_activeSignCounts_get = this.activeSignCounts.get(accountAddress)) != null ? _this_activeSignCounts_get : 1) - 1;
|
|
2968
|
+
if (remaining > 0) {
|
|
2969
|
+
this.activeSignCounts.set(accountAddress, remaining);
|
|
2970
|
+
} else {
|
|
2971
|
+
this.activeSignCounts.delete(accountAddress);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2948
2974
|
/**
|
|
2949
2975
|
* Get or create the heavy operation queue for a wallet.
|
|
2950
2976
|
* Heavy operations (refresh/reshare/recover) run with concurrency=1.
|
|
@@ -3114,7 +3140,8 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3114
3140
|
const holdState = {};
|
|
3115
3141
|
// Add to sign queue - allows concurrent signing
|
|
3116
3142
|
const resultPromise = signQueue.add(()=>{
|
|
3117
|
-
|
|
3143
|
+
var _this_activeSignCounts_get;
|
|
3144
|
+
this.activeSignCounts.set(accountAddress, ((_this_activeSignCounts_get = this.activeSignCounts.get(accountAddress)) != null ? _this_activeSignCounts_get : 0) + 1);
|
|
3118
3145
|
const hooks = {
|
|
3119
3146
|
reportPhase: (phase)=>{
|
|
3120
3147
|
currentPhase = phase;
|
|
@@ -3140,7 +3167,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3140
3167
|
try {
|
|
3141
3168
|
return await callback(hooks);
|
|
3142
3169
|
} finally{
|
|
3143
|
-
this.
|
|
3170
|
+
this.releaseActiveSign(accountAddress);
|
|
3144
3171
|
holdState.release == null ? void 0 : holdState.release.call(holdState);
|
|
3145
3172
|
taskSettled = true;
|
|
3146
3173
|
}
|
|
@@ -3239,7 +3266,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3239
3266
|
const logRecoveryFinished = ()=>core.Logger.info(`[WalletQueueManager] Recovery finished for ${accountAddress} after ${Date.now() - recoveryStartedAt}ms`);
|
|
3240
3267
|
// If we're already inside a heavy or sign operation for this wallet, execute directly
|
|
3241
3268
|
// to avoid deadlock (heavy queue has concurrency=1, sign queue waits for idle)
|
|
3242
|
-
if (this.walletsWithActiveHeavyOp.get(accountAddress) || this.
|
|
3269
|
+
if (this.walletsWithActiveHeavyOp.get(accountAddress) || this.activeSignCounts.get(accountAddress)) {
|
|
3243
3270
|
core.Logger.debug(`[WalletQueueManager] Recovery called from within active op for ${accountAddress}, executing directly`);
|
|
3244
3271
|
const directPromise = (async ()=>{
|
|
3245
3272
|
try {
|
|
@@ -3274,20 +3301,26 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3274
3301
|
return recoveryPromise;
|
|
3275
3302
|
}
|
|
3276
3303
|
/**
|
|
3277
|
-
* Waits for the wallet's in-flight recovery, if any.
|
|
3304
|
+
* Waits for the wallet's in-flight recovery, if any. With `bounded`, for at most `recoveryWaitCapMs`.
|
|
3278
3305
|
* Never rejects: recovery failures are logged where they happen.
|
|
3279
3306
|
*
|
|
3280
|
-
* @returns `none` without a recovery, `settled` once it finished
|
|
3281
|
-
*/ static async waitForPendingRecovery(accountAddress) {
|
|
3307
|
+
* @returns `none` without a recovery, `settled` once it finished, `capped` when the cap hit first
|
|
3308
|
+
*/ static async waitForPendingRecovery(accountAddress, { bounded = false } = {}) {
|
|
3282
3309
|
const pending = this.pendingRecoveryPromises.get(normalizeAddress(accountAddress));
|
|
3283
3310
|
if (!pending) {
|
|
3284
3311
|
return 'none';
|
|
3285
3312
|
}
|
|
3286
|
-
// `pending` may reject; allSettled
|
|
3287
|
-
|
|
3313
|
+
// `pending` may reject; allSettled turns either outcome into "the recovery is over".
|
|
3314
|
+
const recoveryOver = Promise.allSettled([
|
|
3288
3315
|
pending
|
|
3289
|
-
]);
|
|
3290
|
-
|
|
3316
|
+
]).then(()=>'settled');
|
|
3317
|
+
if (!bounded) {
|
|
3318
|
+
return recoveryOver;
|
|
3319
|
+
}
|
|
3320
|
+
return pTimeout(recoveryOver, {
|
|
3321
|
+
milliseconds: this.recoveryWaitCapMs,
|
|
3322
|
+
fallback: ()=>'capped'
|
|
3323
|
+
});
|
|
3291
3324
|
}
|
|
3292
3325
|
static getShareHolderQueue(address) {
|
|
3293
3326
|
const existing = this.shareHolderQueues.get(address);
|
|
@@ -3319,19 +3352,21 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3319
3352
|
this.pendingRecoveryPromises.clear();
|
|
3320
3353
|
this.shareHolderQueues.clear();
|
|
3321
3354
|
this.walletsWithActiveHeavyOp.clear();
|
|
3322
|
-
this.
|
|
3355
|
+
this.activeSignCounts.clear();
|
|
3323
3356
|
this.signQueueTimeoutMs = 20000;
|
|
3357
|
+
this.recoveryWaitCapMs = 10000;
|
|
3324
3358
|
}
|
|
3325
3359
|
}
|
|
3326
3360
|
/** Heavy operation queues per wallet address. These run with concurrency=1. */ WalletQueueManager.heavyOpQueues = new Map();
|
|
3327
3361
|
/** Sign queues per wallet address. These allow concurrent operations. */ WalletQueueManager.signQueues = new Map();
|
|
3328
3362
|
/** Timeout applied to new sign queues. Mutable for tests via resetForTesting. */ WalletQueueManager.signQueueTimeoutMs = 20000;
|
|
3363
|
+
/** Longest a bounded wait on an in-flight recovery lasts before the sign carries on with the shares it has. */ WalletQueueManager.recoveryWaitCapMs = 10000;
|
|
3329
3364
|
/** Track pending recovery promises for deduplication */ WalletQueueManager.pendingRecoveryPromises = new Map();
|
|
3330
3365
|
/** Per wallet, the signs that have loaded their key shares and not finished. */ WalletQueueManager.shareHolderQueues = new Map();
|
|
3331
3366
|
/** Track which wallets are currently executing inside a heavy operation.
|
|
3332
3367
|
* Used to prevent deadlocks when recovery is called from within a heavy op. */ WalletQueueManager.walletsWithActiveHeavyOp = new Map();
|
|
3333
|
-
/**
|
|
3334
|
-
* Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.
|
|
3368
|
+
/** Count of signs currently running per wallet (a boolean is cleared by the first sign to finish).
|
|
3369
|
+
* Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.activeSignCounts = new Map();
|
|
3335
3370
|
|
|
3336
3371
|
const FLAP_WINDOW_MS = 60000;
|
|
3337
3372
|
// Bounds memory on a socket that flaps indefinitely; the count saturates here.
|
|
@@ -4087,13 +4122,7 @@ class DynamicWalletClient {
|
|
|
4087
4122
|
this.restoreWallets()
|
|
4088
4123
|
];
|
|
4089
4124
|
if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] === true) {
|
|
4090
|
-
|
|
4091
|
-
initializePromises.push(this.restoreRooms().then(()=>// assume two of two threshold signature scheme
|
|
4092
|
-
this.createRooms({
|
|
4093
|
-
roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
|
|
4094
|
-
thresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_TWO,
|
|
4095
|
-
roomCount: ROOM_CACHE_COUNT
|
|
4096
|
-
})));
|
|
4125
|
+
initializePromises.push(this.prefetchRoomPool());
|
|
4097
4126
|
}
|
|
4098
4127
|
await Promise.all(initializePromises);
|
|
4099
4128
|
return {
|
|
@@ -4107,6 +4136,25 @@ class DynamicWalletClient {
|
|
|
4107
4136
|
};
|
|
4108
4137
|
}
|
|
4109
4138
|
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Restores the cached rooms, then creates more if the pool is low. Best effort: the pool is only a
|
|
4141
|
+
* prefetch, and takeRooms() falls back and refills it later, so a failure (e.g. a dropped request)
|
|
4142
|
+
* must not fail init. createRooms already logs its own error.
|
|
4143
|
+
*/ async prefetchRoomPool() {
|
|
4144
|
+
try {
|
|
4145
|
+
await this.restoreRooms();
|
|
4146
|
+
// assume two of two threshold signature scheme
|
|
4147
|
+
await this.createRooms({
|
|
4148
|
+
roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
|
|
4149
|
+
thresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_TWO,
|
|
4150
|
+
roomCount: ROOM_CACHE_COUNT
|
|
4151
|
+
});
|
|
4152
|
+
} catch (error) {
|
|
4153
|
+
this.logger.debug('[DynamicWaasWalletClient] Room cache prefetch failed during init', {
|
|
4154
|
+
error
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4110
4158
|
async serverInitializeKeyGen({ chainName, clientKeygenIds, dynamicRequestId, thresholdSignatureScheme, bitcoinConfig, businessAccountId, idempotencyKey, traceContext, onError, onCeremonyComplete }) {
|
|
4111
4159
|
// Initialize keygen, create room, and create the wallet account on the server.
|
|
4112
4160
|
// HTTP transport (flag on + idempotency key present) uses a single JSON
|
|
@@ -4831,14 +4879,14 @@ class DynamicWalletClient {
|
|
|
4831
4879
|
return data;
|
|
4832
4880
|
}
|
|
4833
4881
|
/**
|
|
4834
|
-
* The server half of `signAndBroadcast`: asks Dynamic to open the signing
|
|
4882
|
+
* The server half of `signAndBroadcast`: asks Dynamic to open the signing room and returns the transaction record.
|
|
4835
4883
|
*
|
|
4836
4884
|
* @param params.walletId - The wallet that signs.
|
|
4837
4885
|
* @param params.shareSetId - Which of the wallet's key sets to use. Defaults to its active one.
|
|
4838
|
-
* @param params.
|
|
4839
|
-
* @param params.
|
|
4886
|
+
* @param params.message - The transaction as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
|
|
4887
|
+
* @param params.context - What the message does in readable form (chain, recipient, amount). Dynamic checks it against the wallet and its policies.
|
|
4840
4888
|
* @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
|
|
4841
|
-
* @param params.
|
|
4889
|
+
* @param params.roomId - A signing room you already opened; Dynamic opens one when omitted.
|
|
4842
4890
|
* @param params.requestedCertainty - How sure to be before calling it confirmed: `fast`, `safe` or `final`, or `chain:` plus a chain-specific value. Defaults to `safe`.
|
|
4843
4891
|
* @param params.externalReference - Your own label, returned on every event. Not unique.
|
|
4844
4892
|
* @param params.idempotencyKey - Makes a retry safe: the same key replays the stored answer instead of sending twice.
|
|
@@ -4848,15 +4896,15 @@ class DynamicWalletClient {
|
|
|
4848
4896
|
* @param params.onError - Called with the failure's message, never the raw error.
|
|
4849
4897
|
* @param params.dynamicRequestId - Id of this one request, to match client and server logs.
|
|
4850
4898
|
* @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
|
|
4851
|
-
*/ async serverSignAndBroadcast({ walletId, shareSetId,
|
|
4852
|
-
// Opens
|
|
4899
|
+
*/ async serverSignAndBroadcast({ walletId, shareSetId, context, broadcastOptions, message, roomId, requestedCertainty, externalReference, idempotencyKey, mfaToken, elevatedAccessToken, approvalQuorum, onError, dynamicRequestId, traceContext }) {
|
|
4900
|
+
// Opens a signing room, unless `roomId` names one, and returns the transaction record.
|
|
4853
4901
|
return await this.apiClient.signAndBroadcast({
|
|
4854
4902
|
walletId,
|
|
4855
4903
|
shareSetId,
|
|
4856
|
-
|
|
4904
|
+
context: core.toJsonSafeContext(context),
|
|
4857
4905
|
broadcastOptions,
|
|
4858
|
-
|
|
4859
|
-
|
|
4906
|
+
message,
|
|
4907
|
+
roomId,
|
|
4860
4908
|
isFormatted: false,
|
|
4861
4909
|
requestedCertainty,
|
|
4862
4910
|
externalReference,
|
|
@@ -5314,12 +5362,12 @@ class DynamicWalletClient {
|
|
|
5314
5362
|
}));
|
|
5315
5363
|
}
|
|
5316
5364
|
/**
|
|
5317
|
-
* Signs a transaction for Dynamic to broadcast
|
|
5365
|
+
* Signs a transaction for Dynamic to broadcast. The signature stays on the server.
|
|
5318
5366
|
*
|
|
5319
5367
|
* @param params.accountAddress - The wallet's address.
|
|
5320
5368
|
* @param params.chainName - The wallet's chain, e.g. `EVM`.
|
|
5321
|
-
* @param params.
|
|
5322
|
-
* @param params.
|
|
5369
|
+
* @param params.message - The transaction as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
|
|
5370
|
+
* @param params.context - What the message does in readable form (chain, recipient, amount). Dynamic checks it against the wallet and its policies.
|
|
5323
5371
|
* @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
|
|
5324
5372
|
* @param params.password - The wallet's backup password, if it has one.
|
|
5325
5373
|
* @param params.signedSessionId - The signed-in session, used to unlock the wallet's key share.
|
|
@@ -5332,18 +5380,13 @@ class DynamicWalletClient {
|
|
|
5332
5380
|
* @param params.onError - Called with the failure's message, never the raw error.
|
|
5333
5381
|
* @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
|
|
5334
5382
|
* @returns The transaction record. A finished one comes back without signing again.
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
// Each fee attempt is one message and one context; a mismatch is the caller's, so it is named here, before any ceremony.
|
|
5338
|
-
if (messages.length === 0 || contexts.length !== messages.length) {
|
|
5339
|
-
throw new Error(`Sign and broadcast needs one signing context per message, got ${contexts.length} contexts for ${messages.length} messages`);
|
|
5340
|
-
}
|
|
5341
|
-
return this.queueSign(accountAddress, ({ reportPhase, readingShares })=>this.internalSignAndBroadcast({
|
|
5383
|
+
*/ async signAndBroadcast({ accountAddress, chainName, context, broadcastOptions, message, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey = uuid.v4(), onError, traceContext }) {
|
|
5384
|
+
return this.queueSign(accountAddress, ({ reportPhase, reportDetails, readingShares })=>this.internalSignAndBroadcast({
|
|
5342
5385
|
accountAddress,
|
|
5343
5386
|
chainName,
|
|
5344
|
-
|
|
5387
|
+
context,
|
|
5345
5388
|
broadcastOptions,
|
|
5346
|
-
|
|
5389
|
+
message,
|
|
5347
5390
|
password,
|
|
5348
5391
|
signedSessionId,
|
|
5349
5392
|
mfaToken,
|
|
@@ -5355,17 +5398,21 @@ class DynamicWalletClient {
|
|
|
5355
5398
|
onError,
|
|
5356
5399
|
traceContext,
|
|
5357
5400
|
onSignPhase: reportPhase,
|
|
5401
|
+
onSignDetails: reportDetails,
|
|
5358
5402
|
onReadingShares: readingShares
|
|
5359
5403
|
}));
|
|
5360
5404
|
}
|
|
5361
5405
|
/**
|
|
5362
|
-
* Does the work of `signAndBroadcast` once it has its turn in the wallet queue: loads the wallet, asks Dynamic to open the
|
|
5406
|
+
* Does the work of `signAndBroadcast` once it has its turn in the wallet queue: loads the wallet, asks Dynamic to open the room, then signs in it.
|
|
5363
5407
|
*
|
|
5364
5408
|
* Takes the same fields as `signAndBroadcast`.
|
|
5365
5409
|
*
|
|
5366
5410
|
* @param params.onSignPhase - Reports which step it is on, so the queue can tell which step used up the time.
|
|
5367
|
-
*/ async internalSignAndBroadcast({ accountAddress, chainName,
|
|
5411
|
+
*/ async internalSignAndBroadcast({ accountAddress, chainName, context, broadcastOptions, message, password, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey, onError, traceContext, onSignPhase, onSignDetails, onReadingShares, staleShareRetry = false, roomIdRetry = false }) {
|
|
5368
5412
|
const dynamicRequestId = uuid.v4();
|
|
5413
|
+
onSignDetails == null ? void 0 : onSignDetails({
|
|
5414
|
+
dynamicRequestId
|
|
5415
|
+
});
|
|
5369
5416
|
traceContext = core.withTraceId(traceContext);
|
|
5370
5417
|
let stage = 'prepareWallet';
|
|
5371
5418
|
let roomIdSource;
|
|
@@ -5375,7 +5422,6 @@ class DynamicWalletClient {
|
|
|
5375
5422
|
accountAddress,
|
|
5376
5423
|
chainName,
|
|
5377
5424
|
dynamicRequestId,
|
|
5378
|
-
attemptCount: messages.length,
|
|
5379
5425
|
staleShareRetry,
|
|
5380
5426
|
roomIdRetry
|
|
5381
5427
|
}, this.getTraceContext(traceContext)));
|
|
@@ -5389,8 +5435,15 @@ class DynamicWalletClient {
|
|
|
5389
5435
|
});
|
|
5390
5436
|
stage = 'resolveCachedRoom';
|
|
5391
5437
|
onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
|
|
5392
|
-
const
|
|
5393
|
-
|
|
5438
|
+
const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoom({
|
|
5439
|
+
wallet,
|
|
5440
|
+
accountAddress,
|
|
5441
|
+
chainName,
|
|
5442
|
+
key: 'signAndBroadcast',
|
|
5443
|
+
dynamicRequestId,
|
|
5444
|
+
traceContext
|
|
5445
|
+
});
|
|
5446
|
+
roomIdSource = cachedRoomId ? 'cache' : 'server';
|
|
5394
5447
|
const roomsResolvedAt = Date.now();
|
|
5395
5448
|
this.logSlowSignPreflight({
|
|
5396
5449
|
accountAddress,
|
|
@@ -5408,10 +5461,10 @@ class DynamicWalletClient {
|
|
|
5408
5461
|
const record = await this.serverSignAndBroadcast({
|
|
5409
5462
|
walletId: wallet.walletId,
|
|
5410
5463
|
shareSetId: wallet.shareSetId,
|
|
5411
|
-
|
|
5464
|
+
context,
|
|
5412
5465
|
broadcastOptions,
|
|
5413
|
-
|
|
5414
|
-
|
|
5466
|
+
message,
|
|
5467
|
+
roomId: cachedRoomId,
|
|
5415
5468
|
requestedCertainty,
|
|
5416
5469
|
externalReference,
|
|
5417
5470
|
idempotencyKey,
|
|
@@ -5422,9 +5475,7 @@ class DynamicWalletClient {
|
|
|
5422
5475
|
dynamicRequestId,
|
|
5423
5476
|
traceContext
|
|
5424
5477
|
});
|
|
5425
|
-
|
|
5426
|
-
const rooms = (_record_rooms = record.rooms) != null ? _record_rooms : [];
|
|
5427
|
-
var _roomIds_length;
|
|
5478
|
+
const room = record.room;
|
|
5428
5479
|
this.logger.info('[DynamicWaasWalletClient] Sign and broadcast accepted', _extends({
|
|
5429
5480
|
key: 'signAndBroadcast',
|
|
5430
5481
|
operation: 'accepted',
|
|
@@ -5433,38 +5484,30 @@ class DynamicWalletClient {
|
|
|
5433
5484
|
dynamicRequestId,
|
|
5434
5485
|
transactionId: record.transactionId,
|
|
5435
5486
|
transactionStatus: record.status,
|
|
5436
|
-
|
|
5437
|
-
roomIdSource,
|
|
5438
|
-
roomsSent: (_roomIds_length = roomIds == null ? void 0 : roomIds.length) != null ? _roomIds_length : 0,
|
|
5439
|
-
roomsReturned: rooms.length,
|
|
5440
|
-
// A retry of an existing row names that row's rooms, not the ones just sent.
|
|
5441
|
-
returnedRoomsMatchSent: roomIds ? roomIds.every((roomId, index)=>{
|
|
5442
|
-
var _rooms_index;
|
|
5443
|
-
return ((_rooms_index = rooms[index]) == null ? void 0 : _rooms_index.roomId) === roomId;
|
|
5444
|
-
}) : undefined
|
|
5487
|
+
roomIdSource
|
|
5445
5488
|
}, this.getTraceContext(traceContext)));
|
|
5446
|
-
// A retry of a finished request returns the record without
|
|
5489
|
+
// A retry of a finished request returns the record without a room.
|
|
5447
5490
|
if (record.status !== 'pending_signature') {
|
|
5448
5491
|
return record;
|
|
5449
5492
|
}
|
|
5450
|
-
if (
|
|
5451
|
-
throw new Error(`Expected
|
|
5493
|
+
if (!room) {
|
|
5494
|
+
throw new Error(`Expected a signing room for transaction ${record.transactionId}`);
|
|
5452
5495
|
}
|
|
5453
5496
|
stage = 'ceremony';
|
|
5454
5497
|
onSignPhase == null ? void 0 : onSignPhase('ceremony');
|
|
5455
|
-
const ceremonies =
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5498
|
+
const ceremonies = this.clientSign({
|
|
5499
|
+
chainName,
|
|
5500
|
+
message,
|
|
5501
|
+
roomId: room.roomId,
|
|
5502
|
+
keyShare: clientKeyShares[0],
|
|
5503
|
+
derivationPath,
|
|
5504
|
+
isFormatted: false,
|
|
5505
|
+
dynamicRequestId,
|
|
5506
|
+
traceContext,
|
|
5507
|
+
variant: wallet.variant,
|
|
5508
|
+
// Nothing mints a replacement room for a broadcast.
|
|
5509
|
+
skipForwardMPC: true
|
|
5510
|
+
});
|
|
5468
5511
|
const getClientSignMismatch = this.trackClientSignMismatch(ceremonies, {
|
|
5469
5512
|
accountAddress,
|
|
5470
5513
|
chainName,
|
|
@@ -5495,7 +5538,7 @@ class DynamicWalletClient {
|
|
|
5495
5538
|
this.logger.info('[DynamicWaasWalletClient] Client sign completed', _extends({
|
|
5496
5539
|
accountAddress,
|
|
5497
5540
|
chainName,
|
|
5498
|
-
|
|
5541
|
+
roomId: room.roomId,
|
|
5499
5542
|
dynamicRequestId,
|
|
5500
5543
|
key: 'signAndBroadcast',
|
|
5501
5544
|
operation: 'clientSign'
|
|
@@ -5505,9 +5548,9 @@ class DynamicWalletClient {
|
|
|
5505
5548
|
const retryParams = {
|
|
5506
5549
|
accountAddress,
|
|
5507
5550
|
chainName,
|
|
5508
|
-
|
|
5551
|
+
context,
|
|
5509
5552
|
broadcastOptions,
|
|
5510
|
-
|
|
5553
|
+
message,
|
|
5511
5554
|
password,
|
|
5512
5555
|
signedSessionId,
|
|
5513
5556
|
mfaToken,
|
|
@@ -5519,6 +5562,7 @@ class DynamicWalletClient {
|
|
|
5519
5562
|
onError,
|
|
5520
5563
|
traceContext,
|
|
5521
5564
|
onSignPhase,
|
|
5565
|
+
onSignDetails,
|
|
5522
5566
|
onReadingShares
|
|
5523
5567
|
};
|
|
5524
5568
|
// The server rejected the local shares as stale before opening any room. Heal them
|
|
@@ -5550,9 +5594,9 @@ class DynamicWalletClient {
|
|
|
5550
5594
|
}));
|
|
5551
5595
|
}
|
|
5552
5596
|
// A cached room was already claimed by an earlier ceremony. Retry once with the same
|
|
5553
|
-
// idempotency key and no cached
|
|
5597
|
+
// idempotency key and no cached room, so the server opens a fresh one.
|
|
5554
5598
|
if (!roomIdRetry && isRoomIdAlreadyUsedError(error)) {
|
|
5555
|
-
this.logger.info('[RoomCache] retrying sign and broadcast without cached
|
|
5599
|
+
this.logger.info('[RoomCache] retrying sign and broadcast without a cached room after 409 ROOM_ID_ALREADY_USED', _extends({
|
|
5556
5600
|
key: 'signAndBroadcast',
|
|
5557
5601
|
operation: 'roomIdRetry',
|
|
5558
5602
|
accountAddress,
|
|
@@ -5575,7 +5619,6 @@ class DynamicWalletClient {
|
|
|
5575
5619
|
operation: 'signAndBroadcast',
|
|
5576
5620
|
details: {
|
|
5577
5621
|
stage,
|
|
5578
|
-
attemptCount: messages.length,
|
|
5579
5622
|
roomIdSource,
|
|
5580
5623
|
staleShareRetry,
|
|
5581
5624
|
roomIdRetry
|
|
@@ -5613,95 +5656,67 @@ class DynamicWalletClient {
|
|
|
5613
5656
|
}, this.getTraceContext(traceContext)));
|
|
5614
5657
|
}
|
|
5615
5658
|
/**
|
|
5616
|
-
* Takes one already-opened room
|
|
5659
|
+
* Takes one already-opened room from the local pool for a sign or a sign-and-broadcast, or none when the cache is off or empty, so the server opens one instead.
|
|
5617
5660
|
*
|
|
5618
|
-
* @param wallet - The wallet signing; its key scheme decides which
|
|
5619
|
-
* @param accountAddress - The wallet's address, for logs.
|
|
5620
|
-
* @param chainName - The wallet's chain, for logs.
|
|
5621
|
-
* @param
|
|
5622
|
-
* @param dynamicRequestId - Id of this request, so the pool's logs join the
|
|
5623
|
-
* @param traceContext - Trace ids for the same reason.
|
|
5624
|
-
* @returns The room
|
|
5625
|
-
*/ async
|
|
5661
|
+
* @param params.wallet - The wallet signing; its key scheme decides which room fits.
|
|
5662
|
+
* @param params.accountAddress - The wallet's address, for logs.
|
|
5663
|
+
* @param params.chainName - The wallet's chain, for logs.
|
|
5664
|
+
* @param params.key - Which operation is asking, for logs.
|
|
5665
|
+
* @param params.dynamicRequestId - Id of this request, so the pool's logs join the operation's.
|
|
5666
|
+
* @param params.traceContext - Trace ids for the same reason.
|
|
5667
|
+
* @returns The room id, or `undefined`.
|
|
5668
|
+
*/ async getCachedRoom({ wallet, accountAddress, chainName, key, dynamicRequestId, traceContext }) {
|
|
5626
5669
|
const logContext = _extends({
|
|
5627
5670
|
dynamicRequestId
|
|
5628
5671
|
}, this.getTraceContext(traceContext));
|
|
5629
5672
|
if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
|
|
5630
|
-
this.
|
|
5673
|
+
this.logCachedRoomUnavailable({
|
|
5631
5674
|
reason: 'cache_disabled',
|
|
5675
|
+
key,
|
|
5632
5676
|
accountAddress,
|
|
5633
5677
|
chainName,
|
|
5634
|
-
count,
|
|
5635
5678
|
logContext
|
|
5636
5679
|
});
|
|
5637
5680
|
return undefined;
|
|
5638
5681
|
}
|
|
5639
|
-
const
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
});
|
|
5645
|
-
if (rooms.length === 0) {
|
|
5646
|
-
this.logCachedRoomsUnavailable({
|
|
5647
|
-
reason: 'pool_too_small',
|
|
5682
|
+
const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme, logContext);
|
|
5683
|
+
if (!room) {
|
|
5684
|
+
this.logCachedRoomUnavailable({
|
|
5685
|
+
reason: 'pool_empty',
|
|
5686
|
+
key,
|
|
5648
5687
|
accountAddress,
|
|
5649
5688
|
chainName,
|
|
5650
|
-
count,
|
|
5651
5689
|
logContext
|
|
5652
5690
|
});
|
|
5653
5691
|
return undefined;
|
|
5654
5692
|
}
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
operation: 'cachedRoomsFound',
|
|
5693
|
+
this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
|
|
5694
|
+
key,
|
|
5695
|
+
operation: 'cachedRoomFound',
|
|
5659
5696
|
accountAddress,
|
|
5660
5697
|
chainName,
|
|
5661
5698
|
walletId: wallet.walletId,
|
|
5662
|
-
|
|
5699
|
+
roomId: room.roomId
|
|
5663
5700
|
}, logContext));
|
|
5664
|
-
return
|
|
5701
|
+
return room.roomId;
|
|
5665
5702
|
}
|
|
5666
5703
|
/**
|
|
5667
|
-
* Logs why no cached
|
|
5704
|
+
* Logs why no cached room was sent, so how often the cache covers a request can be measured.
|
|
5668
5705
|
*
|
|
5669
|
-
* @param params.reason - `cache_disabled` or `
|
|
5706
|
+
* @param params.reason - `cache_disabled` or `pool_empty`.
|
|
5707
|
+
* @param params.key - Which operation asked.
|
|
5670
5708
|
* @param params.accountAddress - The wallet's address.
|
|
5671
5709
|
* @param params.chainName - The wallet's chain.
|
|
5672
|
-
* @param params.count - How many rooms were needed.
|
|
5673
5710
|
* @param params.logContext - Request and trace ids to attach.
|
|
5674
|
-
*/
|
|
5675
|
-
this.logger.info('[DynamicWaasWalletClient] Cached
|
|
5676
|
-
key
|
|
5677
|
-
operation: '
|
|
5711
|
+
*/ logCachedRoomUnavailable({ reason, key, accountAddress, chainName, logContext }) {
|
|
5712
|
+
this.logger.info('[DynamicWaasWalletClient] Cached room unavailable; the server opens one', _extends({
|
|
5713
|
+
key,
|
|
5714
|
+
operation: 'cachedRoomUnavailable',
|
|
5678
5715
|
reason,
|
|
5679
5716
|
accountAddress,
|
|
5680
|
-
chainName
|
|
5681
|
-
requestedCount: count
|
|
5717
|
+
chainName
|
|
5682
5718
|
}, logContext));
|
|
5683
5719
|
}
|
|
5684
|
-
async getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext) {
|
|
5685
|
-
if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
|
|
5686
|
-
return undefined;
|
|
5687
|
-
}
|
|
5688
|
-
const logContext = _extends({
|
|
5689
|
-
dynamicRequestId
|
|
5690
|
-
}, this.getTraceContext(traceContext));
|
|
5691
|
-
const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme, logContext);
|
|
5692
|
-
const roomId = room == null ? void 0 : room.roomId;
|
|
5693
|
-
if (roomId) {
|
|
5694
|
-
this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
|
|
5695
|
-
key: 'sign',
|
|
5696
|
-
operation: 'cachedRoomFound',
|
|
5697
|
-
accountAddress,
|
|
5698
|
-
chainName,
|
|
5699
|
-
walletId: wallet.walletId,
|
|
5700
|
-
roomId
|
|
5701
|
-
}, logContext));
|
|
5702
|
-
}
|
|
5703
|
-
return roomId;
|
|
5704
|
-
}
|
|
5705
5720
|
// Without a cached room the client depends on the server reporting one
|
|
5706
5721
|
// before it can join the ceremony.
|
|
5707
5722
|
async resolveSignRoom({ cachedRoomId, serverSignPromise, accountAddress, chainName, walletId, dynamicRequestId, traceContext }) {
|
|
@@ -5785,7 +5800,8 @@ class DynamicWalletClient {
|
|
|
5785
5800
|
const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
|
|
5786
5801
|
onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
|
|
5787
5802
|
const clientKeyShares = await this.ensureClientShare(accountAddress, {
|
|
5788
|
-
onReadingShares
|
|
5803
|
+
onReadingShares,
|
|
5804
|
+
boundRecoveryWait: true
|
|
5789
5805
|
});
|
|
5790
5806
|
const sharesReadyAt = Date.now();
|
|
5791
5807
|
return {
|
|
@@ -5891,7 +5907,14 @@ class DynamicWalletClient {
|
|
|
5891
5907
|
onReadingShares
|
|
5892
5908
|
});
|
|
5893
5909
|
onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
|
|
5894
|
-
const cachedRoomId = roomIdRetry ? undefined : await this.
|
|
5910
|
+
const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoom({
|
|
5911
|
+
wallet,
|
|
5912
|
+
accountAddress,
|
|
5913
|
+
chainName,
|
|
5914
|
+
key: 'sign',
|
|
5915
|
+
dynamicRequestId,
|
|
5916
|
+
traceContext
|
|
5917
|
+
});
|
|
5895
5918
|
const roomResolvedAt = Date.now();
|
|
5896
5919
|
this.logSlowSignPreflight({
|
|
5897
5920
|
accountAddress,
|
|
@@ -8348,22 +8371,26 @@ class DynamicWalletClient {
|
|
|
8348
8371
|
* Note: This method only checks for existing shares in storage.
|
|
8349
8372
|
* Auto-recovery logic has been removed in favor of the queue pattern.
|
|
8350
8373
|
* Callers should handle recovery explicitly if needed.
|
|
8351
|
-
*/ async ensureClientShare(accountAddress, { onReadingShares } = {}) {
|
|
8374
|
+
*/ async ensureClientShare(accountAddress, { onReadingShares, boundRecoveryWait = false } = {}) {
|
|
8352
8375
|
// A recovery may be in flight (e.g. a stale-share heal scheduled when a
|
|
8353
8376
|
// previous sign hit a public-key mismatch). Wait for it so this operation
|
|
8354
8377
|
// reads the recovered shares instead of the superseded ones. Recovery
|
|
8355
8378
|
// failures are logged where they happen; this operation should proceed
|
|
8356
|
-
// with whatever shares exist rather than fail on the recovery.
|
|
8379
|
+
// with whatever shares exist rather than fail on the recovery. A sign bounds the wait so a slow recovery cannot
|
|
8380
|
+
// eat its whole queue budget; after the cap it continues with the stored shares (a stale one is retried).
|
|
8357
8381
|
const waitStartedAt = Date.now();
|
|
8358
|
-
const recoveryWait = await WalletQueueManager.waitForPendingRecovery(accountAddress
|
|
8359
|
-
|
|
8382
|
+
const recoveryWait = await WalletQueueManager.waitForPendingRecovery(accountAddress, {
|
|
8383
|
+
bounded: boundRecoveryWait
|
|
8384
|
+
});
|
|
8385
|
+
if (recoveryWait !== 'none') {
|
|
8360
8386
|
// Every concurrent sign awaits this same promise, so it releases them all
|
|
8361
8387
|
// at once. One line per waiter: the count is the size of the herd and the
|
|
8362
8388
|
// shared finish timestamp is when it was released.
|
|
8363
8389
|
this.logger.info('[SignPreflight] sign waited on in-flight recovery', {
|
|
8364
8390
|
accountAddress,
|
|
8365
8391
|
environmentId: this.environmentId,
|
|
8366
|
-
recoveryWaitMs: Date.now() - waitStartedAt
|
|
8392
|
+
recoveryWaitMs: Date.now() - waitStartedAt,
|
|
8393
|
+
recoveryWaitCapped: recoveryWait === 'capped'
|
|
8367
8394
|
});
|
|
8368
8395
|
}
|
|
8369
8396
|
// From here a recovery waits for this sign to finish, so it cannot replace the shares being read.
|
|
@@ -11208,6 +11235,17 @@ class DynamicWalletClient {
|
|
|
11208
11235
|
}
|
|
11209
11236
|
this.apiClient.syncAuthToken(authToken);
|
|
11210
11237
|
}
|
|
11238
|
+
/** Reads the persisted room pool. A corrupt value counts as empty, so the next write replaces it. */ async readStoredRooms() {
|
|
11239
|
+
const rawData = await this.storage.getItem(`${this.storageKey}-rooms`);
|
|
11240
|
+
if (!rawData) {
|
|
11241
|
+
return {};
|
|
11242
|
+
}
|
|
11243
|
+
try {
|
|
11244
|
+
return JSON.parse(rawData);
|
|
11245
|
+
} catch (e) {
|
|
11246
|
+
return {};
|
|
11247
|
+
}
|
|
11248
|
+
}
|
|
11211
11249
|
async createRooms({ roomType, thresholdSignatureScheme, roomCount = 5 }) {
|
|
11212
11250
|
const numberOfParties = this.getNumberOfParties(roomType, thresholdSignatureScheme);
|
|
11213
11251
|
const generation = DynamicWalletClient.roomsGeneration;
|
|
@@ -11249,8 +11287,7 @@ class DynamicWalletClient {
|
|
|
11249
11287
|
// Re-read from storage under the lock so we don't clobber rooms another
|
|
11250
11288
|
// tab just wrote, and so we don't lose rooms consumed by getRoom() calls
|
|
11251
11289
|
// that ran while our API request was in-flight.
|
|
11252
|
-
const
|
|
11253
|
-
const storedRooms = rawData ? JSON.parse(rawData) : {};
|
|
11290
|
+
const storedRooms = await this.readStoredRooms();
|
|
11254
11291
|
const currentRooms = storedRooms[numberOfParties] || [];
|
|
11255
11292
|
await this.setRooms(numberOfParties, [
|
|
11256
11293
|
...newRooms,
|
|
@@ -11367,8 +11404,7 @@ class DynamicWalletClient {
|
|
|
11367
11404
|
// already consumed by other tabs. Combined with the Web Locks API below, this
|
|
11368
11405
|
// makes room consumption atomic across tabs and prevents two tabs from both
|
|
11369
11406
|
// receiving the same cached room.
|
|
11370
|
-
const
|
|
11371
|
-
const storedRooms = rawData ? JSON.parse(rawData) : {};
|
|
11407
|
+
const storedRooms = await this.readStoredRooms();
|
|
11372
11408
|
const now = Date.now();
|
|
11373
11409
|
const schemeRooms = storedRooms[numberOfParties] || [];
|
|
11374
11410
|
// Filter out expired rooms and remove the next available room.
|