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