@dynamic-labs-wallet/browser 0.0.258 → 0.0.260

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.js CHANGED
@@ -9,6 +9,7 @@ var sdkApiCore = require('@dynamic-labs/sdk-api-core');
9
9
  var loadArgon2idWasm = require('argon2id');
10
10
  var axios = require('axios');
11
11
  var createHttpError = require('http-errors');
12
+ var PQueue = require('p-queue');
12
13
 
13
14
  function _extends() {
14
15
  _extends = Object.assign || function assign(target) {
@@ -492,7 +493,6 @@ const SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE = {
492
493
  };
493
494
  const ROOM_EXPIRATION_TIME = 1000 * 60 * 10; // 10 minutes
494
495
  const ROOM_CACHE_COUNT = 5;
495
- const WALLET_BUSY_LOCK_TIMEOUT_MS = 20000; // 20 seconds
496
496
 
497
497
  const ERROR_KEYGEN_FAILED = '[DynamicWaasWalletClient]: Error with keygen';
498
498
  const ERROR_CREATE_WALLET_ACCOUNT = '[DynamicWaasWalletClient]: Error creating wallet account';
@@ -1111,6 +1111,341 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
1111
1111
  }
1112
1112
  };
1113
1113
 
1114
+ class WalletNotReadyError extends Error {
1115
+ constructor(accountAddress, walletReadyState){
1116
+ super(`Wallet ${accountAddress} is not ready and requires password to be unlocked`);
1117
+ this.name = 'WalletNotReadyError';
1118
+ this.accountAddress = accountAddress;
1119
+ this.walletReadyState = walletReadyState;
1120
+ }
1121
+ }
1122
+ /**
1123
+ * Set of allowed heavy queue operations for runtime validation.
1124
+ */ const HEAVY_QUEUE_OPERATIONS = new Set([
1125
+ core.WalletOperation.REFRESH,
1126
+ core.WalletOperation.RESHARE,
1127
+ core.WalletOperation.RECOVER
1128
+ ]);
1129
+ /**
1130
+ * Type guard to validate that an operation is a valid heavy queue operation.
1131
+ */ const isHeavyQueueOperation = (operation)=>HEAVY_QUEUE_OPERATIONS.has(operation);
1132
+ /**
1133
+ * Set of allowed sign queue operations for runtime validation.
1134
+ */ const SIGN_QUEUE_OPERATIONS = new Set([
1135
+ core.WalletOperation.SIGN_MESSAGE,
1136
+ core.WalletOperation.SIGN_TRANSACTION
1137
+ ]);
1138
+ /**
1139
+ * Type guard to validate that an operation is a valid sign queue operation.
1140
+ */ const isSignQueueOperation = (operation)=>SIGN_QUEUE_OPERATIONS.has(operation);
1141
+ /**
1142
+ * Set of allowed recover queue operations for runtime validation.
1143
+ */ const RECOVER_QUEUE_OPERATIONS = new Set([
1144
+ core.WalletOperation.RECOVER
1145
+ ]);
1146
+ /**
1147
+ * Type guard to validate that an operation is a valid recover queue operation.
1148
+ */ const isRecoverQueueOperation = (operation)=>RECOVER_QUEUE_OPERATIONS.has(operation);
1149
+ class WalletBusyError extends Error {
1150
+ constructor(accountAddress, operation){
1151
+ super(`Wallet ${accountAddress} is currently performing a ${operation} operation`);
1152
+ this.name = 'WalletBusyError';
1153
+ this.operation = operation;
1154
+ this.accountAddress = accountAddress;
1155
+ }
1156
+ }
1157
+ /**
1158
+ * Creates distribution where shares go to specified cloud providers
1159
+ * Last share goes to cloud providers, rest to Dynamic backend
1160
+ * @param providers - Array of cloud providers to backup to
1161
+ * @param allShares - All key shares to distribute
1162
+ */ const createCloudProviderDistribution = ({ providers, allShares })=>{
1163
+ const cloudProviderShares = {};
1164
+ // Last share goes to cloud providers, rest to Dynamic
1165
+ const sharesForCloud = allShares.slice(-1);
1166
+ providers.forEach((provider)=>{
1167
+ cloudProviderShares[provider] = sharesForCloud;
1168
+ });
1169
+ return {
1170
+ clientShares: allShares.slice(0, -1),
1171
+ cloudProviderShares
1172
+ };
1173
+ };
1174
+ /**
1175
+ * Creates distribution with delegation + cloud backup
1176
+ * Client shares backed up to Dynamic AND cloud providers
1177
+ * Delegated share goes to webhook
1178
+ */ const createDelegationWithCloudProviderDistribution = ({ providers, existingShares, delegatedShare })=>{
1179
+ const cloudProviderShares = {};
1180
+ providers.forEach((provider)=>{
1181
+ cloudProviderShares[provider] = existingShares;
1182
+ });
1183
+ return {
1184
+ clientShares: existingShares,
1185
+ cloudProviderShares,
1186
+ delegatedShare
1187
+ };
1188
+ };
1189
+ /**
1190
+ * Creates distribution for adding cloud backup to existing delegation
1191
+ * Client shares go to both Dynamic AND cloud providers
1192
+ * delegatedShare is undefined - we don't re-publish, but preserve the location
1193
+ */ const createAddCloudProviderToExistingDelegationDistribution = ({ providers, clientShares })=>{
1194
+ const cloudProviderShares = {};
1195
+ providers.forEach((provider)=>{
1196
+ cloudProviderShares[provider] = clientShares;
1197
+ });
1198
+ return {
1199
+ clientShares,
1200
+ cloudProviderShares
1201
+ };
1202
+ };
1203
+ /**
1204
+ * Checks if wallet has backup on any of the specified providers
1205
+ */ const hasCloudProviderBackup = (backupInfo, providers)=>{
1206
+ if (!(backupInfo == null ? void 0 : backupInfo.backups)) return false;
1207
+ return providers.some((provider)=>{
1208
+ var _backupInfo_backups_provider;
1209
+ var _backupInfo_backups_provider_length;
1210
+ return ((_backupInfo_backups_provider_length = (_backupInfo_backups_provider = backupInfo.backups[provider]) == null ? void 0 : _backupInfo_backups_provider.length) != null ? _backupInfo_backups_provider_length : 0) > 0;
1211
+ });
1212
+ };
1213
+ /**
1214
+ * Gets all cloud providers that have backups for this wallet
1215
+ */ const getActiveCloudProviders = (backupInfo)=>{
1216
+ if (!(backupInfo == null ? void 0 : backupInfo.backups)) return [];
1217
+ return Object.entries(backupInfo.backups).filter(([location, backups])=>location !== core.BackupLocation.DYNAMIC && location !== core.BackupLocation.DELEGATED && backups.length > 0).map(([location])=>location);
1218
+ };
1219
+ const createDelegationOnlyDistribution = ({ existingShares, delegatedShare })=>({
1220
+ clientShares: existingShares,
1221
+ cloudProviderShares: {},
1222
+ delegatedShare
1223
+ });
1224
+ const createDynamicOnlyDistribution = ({ allShares })=>({
1225
+ clientShares: allShares,
1226
+ cloudProviderShares: {}
1227
+ });
1228
+ const hasDelegatedBackup = (backupInfo)=>{
1229
+ var _backupInfo_backups_BackupLocation_DELEGATED, _backupInfo_backups;
1230
+ var _backupInfo_backups_BackupLocation_DELEGATED_length;
1231
+ return ((_backupInfo_backups_BackupLocation_DELEGATED_length = backupInfo == null ? void 0 : (_backupInfo_backups = backupInfo.backups) == null ? void 0 : (_backupInfo_backups_BackupLocation_DELEGATED = _backupInfo_backups[core.BackupLocation.DELEGATED]) == null ? void 0 : _backupInfo_backups_BackupLocation_DELEGATED.length) != null ? _backupInfo_backups_BackupLocation_DELEGATED_length : 0) > 0;
1232
+ };
1233
+
1234
+ /**
1235
+ * Queue manager for wallet operations.
1236
+ * Manages heavy operation queues (refresh/reshare/recover) and sign queues per wallet.
1237
+ */ class WalletQueueManager {
1238
+ /**
1239
+ * Get or create the heavy operation queue for a wallet.
1240
+ * Heavy operations (refresh/reshare/recover) run with concurrency=1.
1241
+ */ static getHeavyOpQueue(accountAddress) {
1242
+ if (!this.heavyOpQueues.has(accountAddress)) {
1243
+ const queue = new PQueue({
1244
+ concurrency: 1,
1245
+ timeout: 20000
1246
+ });
1247
+ queue.on('error', (error)=>{
1248
+ logger.error('[WalletQueueManager] Heavy operation queue error', {
1249
+ accountAddress,
1250
+ error: error instanceof Error ? error.message : error
1251
+ });
1252
+ });
1253
+ this.heavyOpQueues.set(accountAddress, queue);
1254
+ }
1255
+ return this.heavyOpQueues.get(accountAddress);
1256
+ }
1257
+ /**
1258
+ * Get or create the sign queue for a wallet.
1259
+ * Sign operations run with unlimited concurrency (they're read-only on key shares).
1260
+ */ static getSignQueue(accountAddress) {
1261
+ if (!this.signQueues.has(accountAddress)) {
1262
+ const queue = new PQueue({
1263
+ concurrency: Infinity,
1264
+ timeout: 20000
1265
+ });
1266
+ queue.on('error', (error)=>{
1267
+ logger.error('[WalletQueueManager] Sign queue error', {
1268
+ accountAddress,
1269
+ error: error instanceof Error ? error.message : error
1270
+ });
1271
+ });
1272
+ this.signQueues.set(accountAddress, queue);
1273
+ }
1274
+ return this.signQueues.get(accountAddress);
1275
+ }
1276
+ /**
1277
+ * Check if wallet has heavy operations in progress
1278
+ */ static isHeavyOpInProgress(accountAddress) {
1279
+ const queue = this.heavyOpQueues.get(accountAddress);
1280
+ return queue ? queue.size > 0 || queue.pending > 0 : false;
1281
+ }
1282
+ /**
1283
+ * Check if wallet has operations in any queue (heavy or sign)
1284
+ */ static isWalletBusy(accountAddress) {
1285
+ const heavyQueue = this.heavyOpQueues.get(accountAddress);
1286
+ const signQueue = this.signQueues.get(accountAddress);
1287
+ const heavyBusy = heavyQueue ? heavyQueue.size > 0 || heavyQueue.pending > 0 : false;
1288
+ const signBusy = signQueue ? signQueue.size > 0 || signQueue.pending > 0 : false;
1289
+ return heavyBusy || signBusy;
1290
+ }
1291
+ /**
1292
+ * Check if recovery is in progress for a wallet
1293
+ */ static isRecoveryInProgress(accountAddress) {
1294
+ return this.pendingRecoveryPromises.has(accountAddress);
1295
+ }
1296
+ /**
1297
+ * Get existing pending recovery promise if one exists
1298
+ */ static getPendingRecoveryPromise(accountAddress) {
1299
+ return this.pendingRecoveryPromises.get(accountAddress);
1300
+ }
1301
+ /**
1302
+ * Track a pending recovery promise
1303
+ */ static setPendingRecoveryPromise(accountAddress, promise) {
1304
+ this.pendingRecoveryPromises.set(accountAddress, promise);
1305
+ }
1306
+ /**
1307
+ * Clear pending recovery promise for a wallet
1308
+ */ static clearPendingRecoveryPromise(accountAddress) {
1309
+ this.pendingRecoveryPromises.delete(accountAddress);
1310
+ }
1311
+ /**
1312
+ * Queue a heavy operation with type validation.
1313
+ * Ensures only valid heavy operations (REFRESH, RESHARE, RECOVER) can be queued.
1314
+ * Waits for sign operations to complete before executing.
1315
+ *
1316
+ * @param accountAddress - The wallet address
1317
+ * @param operation - The operation type (must be a valid HeavyQueueOperation)
1318
+ * @param callback - The operation to execute
1319
+ * @throws Error if operation is not a valid heavy queue operation
1320
+ */ static async queueHeavyOperation(accountAddress, operation, callback) {
1321
+ // Runtime validation to catch any type assertion bypasses
1322
+ if (!isHeavyQueueOperation(operation)) {
1323
+ throw new Error(`Invalid heavy queue operation: ${operation}. Must be REFRESH, RESHARE, or RECOVER.`);
1324
+ }
1325
+ const heavyQueue = this.getHeavyOpQueue(accountAddress);
1326
+ const signQueue = this.getSignQueue(accountAddress);
1327
+ // Wait for any in-progress sign operations to complete
1328
+ await signQueue.onIdle();
1329
+ // Add to heavy queue - will wait if other heavy operations are in progress
1330
+ return heavyQueue.add(async ()=>{
1331
+ this.walletsWithActiveHeavyOp.set(accountAddress, true);
1332
+ try {
1333
+ return await callback();
1334
+ } finally{
1335
+ this.walletsWithActiveHeavyOp.delete(accountAddress);
1336
+ }
1337
+ });
1338
+ }
1339
+ /**
1340
+ * Queue a sign operation with type validation.
1341
+ * Ensures only valid sign operations (SIGN_MESSAGE, SIGN_TRANSACTION) can be queued.
1342
+ * Waits for heavy operations to complete before executing.
1343
+ * Allows concurrent sign operations.
1344
+ *
1345
+ * @param accountAddress - The wallet address
1346
+ * @param operation - The operation type (must be a valid SignQueueOperation)
1347
+ * @param callback - The sign operation to execute
1348
+ * @throws Error if operation is not a valid sign queue operation
1349
+ * @returns Promise resolving to the sign result
1350
+ */ static async queueSignOperation(accountAddress, operation, callback) {
1351
+ // Runtime validation to catch any type assertion bypasses
1352
+ if (!isSignQueueOperation(operation)) {
1353
+ throw new Error(`Invalid sign queue operation: ${operation}. Must be SIGN_MESSAGE or SIGN_TRANSACTION.`);
1354
+ }
1355
+ const heavyQueue = this.getHeavyOpQueue(accountAddress);
1356
+ const signQueue = this.getSignQueue(accountAddress);
1357
+ // Wait for any running heavy operations to complete before signing.
1358
+ // This prevents signing with stale key shares during refresh/reshare/recover.
1359
+ // Note: There's a small race window between this check and adding to signQueue,
1360
+ // but if a collision occurs, the MPC protocol will detect the public key mismatch
1361
+ // and our recovery handler will automatically retry with fresh key shares.
1362
+ if (heavyQueue.pending > 0 || heavyQueue.size > 0) {
1363
+ await heavyQueue.onIdle();
1364
+ }
1365
+ // Add to sign queue - allows concurrent signing
1366
+ return signQueue.add(async ()=>{
1367
+ this.walletsWithActiveSignOp.set(accountAddress, true);
1368
+ try {
1369
+ return await callback();
1370
+ } finally{
1371
+ this.walletsWithActiveSignOp.delete(accountAddress);
1372
+ }
1373
+ });
1374
+ }
1375
+ /**
1376
+ * Queue a recovery operation with deduplication and type validation.
1377
+ * If a recovery is already in progress for the wallet, returns that promise.
1378
+ * If called from within a heavy operation, executes directly to avoid deadlock.
1379
+ *
1380
+ * @param accountAddress - The wallet address
1381
+ * @param operation - The operation type (must be RECOVER)
1382
+ * @param callback - The recovery operation to execute
1383
+ * @throws Error if operation is not a valid recover queue operation
1384
+ * @returns Promise resolving to the recovery result
1385
+ */ static async queueRecoverOperation(accountAddress, operation, callback) {
1386
+ // Runtime validation to catch any type assertion bypasses
1387
+ if (!isRecoverQueueOperation(operation)) {
1388
+ throw new Error(`Invalid recover queue operation: ${operation}. Must be RECOVER.`);
1389
+ }
1390
+ // Deduplication: if recovery already in progress, return that promise
1391
+ const existing = this.getPendingRecoveryPromise(accountAddress);
1392
+ if (existing) {
1393
+ logger.debug(`[WalletQueueManager] Recovery already in progress for ${accountAddress}, returning existing promise`);
1394
+ return existing;
1395
+ }
1396
+ // If we're already inside a heavy or sign operation for this wallet, execute directly
1397
+ // to avoid deadlock (heavy queue has concurrency=1, sign queue waits for idle)
1398
+ if (this.walletsWithActiveHeavyOp.get(accountAddress) || this.walletsWithActiveSignOp.get(accountAddress)) {
1399
+ logger.debug(`[WalletQueueManager] Recovery called from within active op for ${accountAddress}, executing directly`);
1400
+ const directPromise = (async ()=>{
1401
+ try {
1402
+ return await callback();
1403
+ } finally{
1404
+ this.clearPendingRecoveryPromise(accountAddress);
1405
+ }
1406
+ })();
1407
+ this.setPendingRecoveryPromise(accountAddress, directPromise);
1408
+ return directPromise;
1409
+ }
1410
+ const heavyQueue = this.getHeavyOpQueue(accountAddress);
1411
+ const signQueue = this.getSignQueue(accountAddress);
1412
+ // Create promise and track it
1413
+ const recoveryPromise = (async ()=>{
1414
+ // Wait for any in-progress sign operations to complete
1415
+ await signQueue.onIdle();
1416
+ // Add to heavy queue - will wait if other heavy operations are in progress
1417
+ return heavyQueue.add(async ()=>{
1418
+ try {
1419
+ return await callback();
1420
+ } finally{
1421
+ // Clean up tracking when done
1422
+ this.clearPendingRecoveryPromise(accountAddress);
1423
+ }
1424
+ });
1425
+ })();
1426
+ // Track this recovery
1427
+ this.setPendingRecoveryPromise(accountAddress, recoveryPromise);
1428
+ return recoveryPromise;
1429
+ }
1430
+ /**
1431
+ * Reset static state for testing purposes.
1432
+ * This clears all wallet queues and pending recovery promises.
1433
+ */ static resetForTesting() {
1434
+ this.heavyOpQueues.clear();
1435
+ this.signQueues.clear();
1436
+ this.pendingRecoveryPromises.clear();
1437
+ this.walletsWithActiveHeavyOp.clear();
1438
+ this.walletsWithActiveSignOp.clear();
1439
+ }
1440
+ }
1441
+ /** Heavy operation queues per wallet address. These run with concurrency=1. */ WalletQueueManager.heavyOpQueues = new Map();
1442
+ /** Sign queues per wallet address. These allow concurrent operations. */ WalletQueueManager.signQueues = new Map();
1443
+ /** Track pending recovery promises for deduplication */ WalletQueueManager.pendingRecoveryPromises = new Map();
1444
+ /** Track which wallets are currently executing inside a heavy operation.
1445
+ * Used to prevent deadlocks when recovery is called from within a heavy op. */ WalletQueueManager.walletsWithActiveHeavyOp = new Map();
1446
+ /** Track which wallets are currently executing inside a sign operation.
1447
+ * Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
1448
+
1114
1449
  const ALG_LABEL_RSA = 'HYBRID-RSA-AES-256';
1115
1450
  /**
1116
1451
  * Convert base64 to base64url encoding
@@ -1258,99 +1593,6 @@ const localStorageWriteTest = {
1258
1593
  }
1259
1594
  });
1260
1595
 
1261
- class WalletNotReadyError extends Error {
1262
- constructor(accountAddress, walletReadyState){
1263
- super(`Wallet ${accountAddress} is not ready and requires password to be unlocked`);
1264
- this.name = 'WalletNotReadyError';
1265
- this.accountAddress = accountAddress;
1266
- this.walletReadyState = walletReadyState;
1267
- }
1268
- }
1269
- class WalletBusyError extends Error {
1270
- constructor(accountAddress, operation){
1271
- super(`Wallet ${accountAddress} is currently performing a ${operation} operation`);
1272
- this.name = 'WalletBusyError';
1273
- this.operation = operation;
1274
- this.accountAddress = accountAddress;
1275
- }
1276
- }
1277
- /**
1278
- * Creates distribution where shares go to specified cloud providers
1279
- * Last share goes to cloud providers, rest to Dynamic backend
1280
- * @param providers - Array of cloud providers to backup to
1281
- * @param allShares - All key shares to distribute
1282
- */ const createCloudProviderDistribution = ({ providers, allShares })=>{
1283
- const cloudProviderShares = {};
1284
- // Last share goes to cloud providers, rest to Dynamic
1285
- const sharesForCloud = allShares.slice(-1);
1286
- providers.forEach((provider)=>{
1287
- cloudProviderShares[provider] = sharesForCloud;
1288
- });
1289
- return {
1290
- clientShares: allShares.slice(0, -1),
1291
- cloudProviderShares
1292
- };
1293
- };
1294
- /**
1295
- * Creates distribution with delegation + cloud backup
1296
- * Client shares backed up to Dynamic AND cloud providers
1297
- * Delegated share goes to webhook
1298
- */ const createDelegationWithCloudProviderDistribution = ({ providers, existingShares, delegatedShare })=>{
1299
- const cloudProviderShares = {};
1300
- providers.forEach((provider)=>{
1301
- cloudProviderShares[provider] = existingShares;
1302
- });
1303
- return {
1304
- clientShares: existingShares,
1305
- cloudProviderShares,
1306
- delegatedShare
1307
- };
1308
- };
1309
- /**
1310
- * Creates distribution for adding cloud backup to existing delegation
1311
- * Client shares go to both Dynamic AND cloud providers
1312
- * delegatedShare is undefined - we don't re-publish, but preserve the location
1313
- */ const createAddCloudProviderToExistingDelegationDistribution = ({ providers, clientShares })=>{
1314
- const cloudProviderShares = {};
1315
- providers.forEach((provider)=>{
1316
- cloudProviderShares[provider] = clientShares;
1317
- });
1318
- return {
1319
- clientShares,
1320
- cloudProviderShares
1321
- };
1322
- };
1323
- /**
1324
- * Checks if wallet has backup on any of the specified providers
1325
- */ const hasCloudProviderBackup = (backupInfo, providers)=>{
1326
- if (!(backupInfo == null ? void 0 : backupInfo.backups)) return false;
1327
- return providers.some((provider)=>{
1328
- var _backupInfo_backups_provider;
1329
- var _backupInfo_backups_provider_length;
1330
- return ((_backupInfo_backups_provider_length = (_backupInfo_backups_provider = backupInfo.backups[provider]) == null ? void 0 : _backupInfo_backups_provider.length) != null ? _backupInfo_backups_provider_length : 0) > 0;
1331
- });
1332
- };
1333
- /**
1334
- * Gets all cloud providers that have backups for this wallet
1335
- */ const getActiveCloudProviders = (backupInfo)=>{
1336
- if (!(backupInfo == null ? void 0 : backupInfo.backups)) return [];
1337
- return Object.entries(backupInfo.backups).filter(([location, backups])=>location !== core.BackupLocation.DYNAMIC && location !== core.BackupLocation.DELEGATED && backups.length > 0).map(([location])=>location);
1338
- };
1339
- const createDelegationOnlyDistribution = ({ existingShares, delegatedShare })=>({
1340
- clientShares: existingShares,
1341
- cloudProviderShares: {},
1342
- delegatedShare
1343
- });
1344
- const createDynamicOnlyDistribution = ({ allShares })=>({
1345
- clientShares: allShares,
1346
- cloudProviderShares: {}
1347
- });
1348
- const hasDelegatedBackup = (backupInfo)=>{
1349
- var _backupInfo_backups_BackupLocation_DELEGATED, _backupInfo_backups;
1350
- var _backupInfo_backups_BackupLocation_DELEGATED_length;
1351
- return ((_backupInfo_backups_BackupLocation_DELEGATED_length = backupInfo == null ? void 0 : (_backupInfo_backups = backupInfo.backups) == null ? void 0 : (_backupInfo_backups_BackupLocation_DELEGATED = _backupInfo_backups[core.BackupLocation.DELEGATED]) == null ? void 0 : _backupInfo_backups_BackupLocation_DELEGATED.length) != null ? _backupInfo_backups_BackupLocation_DELEGATED_length : 0) > 0;
1352
- };
1353
-
1354
1596
  /**
1355
1597
  * Determines the recovery state of a wallet based on backup info and local shares.
1356
1598
  *
@@ -1403,48 +1645,27 @@ class DynamicWalletClient {
1403
1645
  });
1404
1646
  }
1405
1647
  }
1406
- getWalletBusyOperation(accountAddress) {
1407
- return DynamicWalletClient.walletBusyMap[accountAddress];
1648
+ /**
1649
+ * Check if wallet has heavy operations in progress
1650
+ */ static isHeavyOpInProgress(accountAddress) {
1651
+ return WalletQueueManager.isHeavyOpInProgress(accountAddress);
1408
1652
  }
1409
- isWalletBusy(accountAddress) {
1410
- return DynamicWalletClient.walletBusyMap[accountAddress] !== undefined;
1653
+ /**
1654
+ * Check if wallet has operations in any queue (heavy or sign)
1655
+ */ static isWalletBusy(accountAddress) {
1656
+ return WalletQueueManager.isWalletBusy(accountAddress);
1411
1657
  }
1412
- async waitForWalletNotBusy(accountAddress) {
1413
- const busyPromise = DynamicWalletClient.walletBusyPromiseMap[accountAddress];
1414
- if (!busyPromise) {
1415
- return;
1416
- }
1417
- const currentOperation = DynamicWalletClient.walletBusyMap[accountAddress];
1418
- this.logger.debug(`[DynamicWaasWalletClient] Waiting for wallet ${accountAddress} to complete ${currentOperation} operation`);
1419
- try {
1420
- await busyPromise;
1421
- } catch (e) {
1422
- // Intentionally swallowing the error - we proceed with the queued operation regardless of whether the previous one succeeded or failed
1423
- this.logger.debug(`[DynamicWaasWalletClient] Previous ${currentOperation} operation on wallet ${accountAddress} failed, proceeding with queued operation`);
1424
- }
1658
+ /**
1659
+ * Check if recovery is in progress for a wallet
1660
+ */ static isRecoveryInProgress(accountAddress) {
1661
+ return WalletQueueManager.isRecoveryInProgress(accountAddress);
1425
1662
  }
1426
- async withWalletBusyLock({ accountAddress, operation, fn, timeoutMs = WALLET_BUSY_LOCK_TIMEOUT_MS }) {
1427
- const existingOperation = DynamicWalletClient.walletBusyMap[accountAddress];
1428
- if (existingOperation) {
1429
- throw new WalletBusyError(accountAddress, existingOperation);
1430
- }
1431
- DynamicWalletClient.walletBusyMap[accountAddress] = operation;
1432
- const executionPromise = (async ()=>{
1433
- try {
1434
- return await Promise.race([
1435
- fn(),
1436
- timeoutPromise({
1437
- timeInMs: timeoutMs,
1438
- activity: `${operation} operation`
1439
- })
1440
- ]);
1441
- } finally{
1442
- delete DynamicWalletClient.walletBusyMap[accountAddress];
1443
- delete DynamicWalletClient.walletBusyPromiseMap[accountAddress];
1444
- }
1445
- })();
1446
- DynamicWalletClient.walletBusyPromiseMap[accountAddress] = executionPromise.then(()=>undefined, ()=>undefined);
1447
- return executionPromise;
1663
+ /**
1664
+ * Reset static state for testing purposes.
1665
+ * This clears all wallet queues and in-flight recovery tracking.
1666
+ * @internal For testing only
1667
+ */ static resetStaticState() {
1668
+ WalletQueueManager.resetForTesting();
1448
1669
  }
1449
1670
  getAuthMode() {
1450
1671
  return this.authMode;
@@ -1965,7 +2186,8 @@ class DynamicWalletClient {
1965
2186
  dynamicRequestId
1966
2187
  }, this.getTraceContext(traceContext)));
1967
2188
  // Recover key shares from backup (don't auto-store, we'll overwrite below)
1968
- const recoveredKeyShares = await this.recoverEncryptedBackupByWallet({
2189
+ // Use internal method directly to bypass queueing - we're already inside a queued operation
2190
+ const recoveredKeyShares = await this.internalRecoverEncryptedBackupByWallet({
1969
2191
  accountAddress,
1970
2192
  password: password != null ? password : this.environmentId,
1971
2193
  walletOperation,
@@ -1988,24 +2210,23 @@ class DynamicWalletClient {
1988
2210
  }
1989
2211
  //todo: need to modify with imported flag
1990
2212
  async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, context, onError, traceContext, bitcoinConfig }) {
1991
- return this.internalSign({
1992
- accountAddress,
1993
- message,
1994
- chainName,
1995
- password,
1996
- isFormatted,
1997
- signedSessionId,
1998
- mfaToken,
1999
- context,
2000
- onError,
2001
- traceContext,
2002
- bitcoinConfig
2003
- });
2213
+ return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, ()=>this.internalSign({
2214
+ accountAddress,
2215
+ message,
2216
+ chainName,
2217
+ password,
2218
+ isFormatted,
2219
+ signedSessionId,
2220
+ mfaToken,
2221
+ context,
2222
+ onError,
2223
+ traceContext,
2224
+ bitcoinConfig
2225
+ }));
2004
2226
  }
2005
2227
  async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, context, onError, traceContext, bitcoinConfig, hasAttemptedKeyShareRecovery = false }) {
2006
2228
  const dynamicRequestId = uuid.v4();
2007
2229
  try {
2008
- await this.waitForWalletNotBusy(accountAddress);
2009
2230
  await this.verifyPassword({
2010
2231
  accountAddress,
2011
2232
  password,
@@ -2130,18 +2351,14 @@ class DynamicWalletClient {
2130
2351
  }
2131
2352
  }
2132
2353
  async refreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, traceContext }) {
2133
- return this.withWalletBusyLock({
2134
- accountAddress,
2135
- operation: core.WalletOperation.REFRESH,
2136
- fn: ()=>this.internalRefreshWalletAccountShares({
2137
- accountAddress,
2138
- chainName,
2139
- password,
2140
- signedSessionId,
2141
- mfaToken,
2142
- traceContext
2143
- })
2144
- });
2354
+ return WalletQueueManager.queueHeavyOperation(accountAddress, core.WalletOperation.REFRESH, ()=>this.internalRefreshWalletAccountShares({
2355
+ accountAddress,
2356
+ chainName,
2357
+ password,
2358
+ signedSessionId,
2359
+ mfaToken,
2360
+ traceContext
2361
+ }));
2145
2362
  }
2146
2363
  /**
2147
2364
  * Gets the Bitcoin config for MPC operations by looking up addressType
@@ -2287,9 +2504,11 @@ class DynamicWalletClient {
2287
2504
  * }>} Object containing new and existing client keygen results, IDs and shares
2288
2505
  * @todo Support higher to lower reshare strategies
2289
2506
  */ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme }) {
2507
+ const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
2290
2508
  const mpcSigner = getMPCSigner({
2291
2509
  chainName,
2292
- baseRelayUrl: this.baseMPCRelayApiUrl
2510
+ baseRelayUrl: this.baseMPCRelayApiUrl,
2511
+ bitcoinConfig
2293
2512
  });
2294
2513
  // Determine share counts based on threshold signature schemes
2295
2514
  const { newClientShareCount, existingClientShareCount } = core.getReshareConfig({
@@ -2307,7 +2526,8 @@ class DynamicWalletClient {
2307
2526
  })).slice(0, existingClientShareCount);
2308
2527
  const existingClientKeygenIds = await Promise.all(existingClientKeyShares.map(async (keyShare)=>await this.getExportId({
2309
2528
  chainName,
2310
- clientKeyShare: keyShare
2529
+ clientKeyShare: keyShare,
2530
+ bitcoinConfig
2311
2531
  })));
2312
2532
  return {
2313
2533
  newClientInitKeygenResults,
@@ -2317,22 +2537,18 @@ class DynamicWalletClient {
2317
2537
  };
2318
2538
  }
2319
2539
  async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, revokeDelegation = false }) {
2320
- return this.withWalletBusyLock({
2321
- accountAddress,
2322
- operation: core.WalletOperation.RESHARE,
2323
- fn: ()=>this.internalReshare({
2324
- chainName,
2325
- accountAddress,
2326
- oldThresholdSignatureScheme,
2327
- newThresholdSignatureScheme,
2328
- password,
2329
- signedSessionId,
2330
- cloudProviders,
2331
- delegateToProjectEnvironment,
2332
- mfaToken,
2333
- revokeDelegation
2334
- })
2335
- });
2540
+ return WalletQueueManager.queueHeavyOperation(accountAddress, core.WalletOperation.RESHARE, ()=>this.internalReshare({
2541
+ chainName,
2542
+ accountAddress,
2543
+ oldThresholdSignatureScheme,
2544
+ newThresholdSignatureScheme,
2545
+ password,
2546
+ signedSessionId,
2547
+ cloudProviders,
2548
+ delegateToProjectEnvironment,
2549
+ mfaToken,
2550
+ revokeDelegation
2551
+ }));
2336
2552
  }
2337
2553
  async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false }) {
2338
2554
  const dynamicRequestId = uuid.v4();
@@ -2369,6 +2585,7 @@ class DynamicWalletClient {
2369
2585
  ...newClientKeygenIds,
2370
2586
  ...existingClientKeygenIds
2371
2587
  ];
2588
+ const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
2372
2589
  // Server to create the room and complete the server reshare logics
2373
2590
  const data = await this.apiClient.reshare({
2374
2591
  walletId: wallet.walletId,
@@ -2389,7 +2606,6 @@ class DynamicWalletClient {
2389
2606
  ...serverKeygenIds,
2390
2607
  ...newServerKeygenIds
2391
2608
  ];
2392
- const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
2393
2609
  const mpcSigner = getMPCSigner({
2394
2610
  chainName,
2395
2611
  baseRelayUrl: this.baseMPCRelayApiUrl,
@@ -2881,7 +3097,10 @@ class DynamicWalletClient {
2881
3097
  /**
2882
3098
  * Ensures that client key shares exist for the given account address.
2883
3099
  * Throws an error if no shares are found.
2884
- * This method enforces share presence before sensitive operations.
3100
+ *
3101
+ * Note: This method only checks for existing shares in storage.
3102
+ * Auto-recovery logic has been removed in favor of the queue pattern.
3103
+ * Callers should handle recovery explicitly if needed.
2885
3104
  */ async ensureClientShare(accountAddress) {
2886
3105
  const shares = await this.getClientKeySharesFromStorage({
2887
3106
  accountAddress
@@ -3208,6 +3427,17 @@ class DynamicWalletClient {
3208
3427
  };
3209
3428
  }
3210
3429
  async recoverEncryptedBackupByWallet({ accountAddress, password, walletOperation, signedSessionId, shareCount = undefined, storeRecoveredShares = true, mfaToken }) {
3430
+ return WalletQueueManager.queueRecoverOperation(accountAddress, core.WalletOperation.RECOVER, ()=>this.internalRecoverEncryptedBackupByWallet({
3431
+ accountAddress,
3432
+ password,
3433
+ walletOperation,
3434
+ signedSessionId,
3435
+ shareCount,
3436
+ storeRecoveredShares,
3437
+ mfaToken
3438
+ }));
3439
+ }
3440
+ async internalRecoverEncryptedBackupByWallet({ accountAddress, password, walletOperation, signedSessionId, shareCount = undefined, storeRecoveredShares = true, mfaToken }) {
3211
3441
  try {
3212
3442
  const wallet = this.walletMap[accountAddress];
3213
3443
  this.logger.debug(`recoverEncryptedBackupByWallet wallet: ${walletOperation}`, wallet);
@@ -3807,20 +4037,55 @@ class DynamicWalletClient {
3807
4037
  }
3808
4038
  }
3809
4039
  /**
3810
- * Gets the recovery state of a wallet without attempting to decrypt shares.
4040
+ * Gets the recovery state of a wallet, optionally recovering shares if not available.
3811
4041
  * This method is useful for detecting if a wallet is password-encrypted
3812
4042
  * and needs unlocking before use.
3813
4043
  *
4044
+ * If shares are not available locally and recovery parameters are provided,
4045
+ * this method will call getWallet with RECOVER operation to fetch shares.
4046
+ * The underlying recoverEncryptedBackupByWallet handles deduplication via
4047
+ * inFlightRecovery, so concurrent calls won't start multiple recoveries.
4048
+ *
3814
4049
  * @param accountAddress - The account address of the wallet
4050
+ * @param signedSessionId - Optional signed session ID for triggering recovery if shares not available
4051
+ * @param password - Optional password for decrypting recovered shares
3815
4052
  * @returns WalletRecoveryState indicating the wallet's lock state and share availability
3816
- */ async getWalletRecoveryState({ accountAddress }) {
4053
+ * @throws Error if recovery fails or no shares available after recovery
4054
+ */ async getWalletRecoveryState({ accountAddress, signedSessionId, password }) {
3817
4055
  const walletData = this.walletMap[accountAddress];
3818
4056
  if (!walletData) {
3819
4057
  throw new Error(`Wallet not found for address: ${accountAddress}`);
3820
4058
  }
3821
- const clientKeyShares = await this.getClientKeySharesFromStorage({
4059
+ let clientKeyShares = await this.getClientKeySharesFromStorage({
3822
4060
  accountAddress
3823
4061
  });
4062
+ // If no local shares and signedSessionId provided, trigger recovery via getWallet
4063
+ // getWallet -> recoverEncryptedBackupByWallet handles deduplication via inFlightRecovery
4064
+ if (clientKeyShares.length === 0 && signedSessionId) {
4065
+ var _walletData_clientKeySharesBackupInfo;
4066
+ await this.getWallet({
4067
+ accountAddress,
4068
+ walletOperation: core.WalletOperation.RECOVER,
4069
+ signedSessionId,
4070
+ password
4071
+ });
4072
+ // Re-fetch shares after recovery
4073
+ clientKeyShares = await this.getClientKeySharesFromStorage({
4074
+ accountAddress
4075
+ });
4076
+ var _walletData_clientKeySharesBackupInfo_passwordEncrypted;
4077
+ // For password-encrypted wallets, also check for cached encrypted shares
4078
+ const isPasswordEncrypted = (_walletData_clientKeySharesBackupInfo_passwordEncrypted = (_walletData_clientKeySharesBackupInfo = walletData.clientKeySharesBackupInfo) == null ? void 0 : _walletData_clientKeySharesBackupInfo.passwordEncrypted) != null ? _walletData_clientKeySharesBackupInfo_passwordEncrypted : false;
4079
+ let hasEncryptedShares = false;
4080
+ if (isPasswordEncrypted && clientKeyShares.length === 0) {
4081
+ const encryptedStorageKey = `${accountAddress}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
4082
+ const encryptedData = await this.storage.getItem(encryptedStorageKey);
4083
+ hasEncryptedShares = !!encryptedData;
4084
+ }
4085
+ if (clientKeyShares.length === 0 && !hasEncryptedShares) {
4086
+ throw new Error(`No key shares available for wallet ${accountAddress} after recovery`);
4087
+ }
4088
+ }
3824
4089
  return determineWalletRecoveryState({
3825
4090
  clientKeySharesBackupInfo: walletData.clientKeySharesBackupInfo,
3826
4091
  hasLocalShares: clientKeyShares.length > 0
@@ -4116,8 +4381,6 @@ class DynamicWalletClient {
4116
4381
  }
4117
4382
  DynamicWalletClient.rooms = {};
4118
4383
  DynamicWalletClient.roomsInitializing = {};
4119
- DynamicWalletClient.walletBusyMap = {};
4120
- DynamicWalletClient.walletBusyPromiseMap = {};
4121
4384
 
4122
4385
  Object.defineProperty(exports, "BIP340", {
4123
4386
  enumerable: true,
@@ -4182,6 +4445,9 @@ exports.ERROR_SIGN_MESSAGE = ERROR_SIGN_MESSAGE;
4182
4445
  exports.ERROR_SIGN_TYPED_DATA = ERROR_SIGN_TYPED_DATA;
4183
4446
  exports.ERROR_VERIFY_MESSAGE_SIGNATURE = ERROR_VERIFY_MESSAGE_SIGNATURE;
4184
4447
  exports.ERROR_VERIFY_TRANSACTION_SIGNATURE = ERROR_VERIFY_TRANSACTION_SIGNATURE;
4448
+ exports.HEAVY_QUEUE_OPERATIONS = HEAVY_QUEUE_OPERATIONS;
4449
+ exports.RECOVER_QUEUE_OPERATIONS = RECOVER_QUEUE_OPERATIONS;
4450
+ exports.SIGN_QUEUE_OPERATIONS = SIGN_QUEUE_OPERATIONS;
4185
4451
  exports.WalletBusyError = WalletBusyError;
4186
4452
  exports.WalletNotReadyError = WalletNotReadyError;
4187
4453
  exports.cancelICloudAuth = cancelICloudAuth;
@@ -4208,9 +4474,12 @@ exports.hasCloudProviderBackup = hasCloudProviderBackup;
4208
4474
  exports.hasDelegatedBackup = hasDelegatedBackup;
4209
4475
  exports.initializeCloudKit = initializeCloudKit;
4210
4476
  exports.isBrowser = isBrowser;
4477
+ exports.isHeavyQueueOperation = isHeavyQueueOperation;
4211
4478
  exports.isHexString = isHexString;
4212
4479
  exports.isICloudAuthenticated = isICloudAuthenticated;
4213
4480
  exports.isPublicKeyMismatchError = isPublicKeyMismatchError;
4481
+ exports.isRecoverQueueOperation = isRecoverQueueOperation;
4482
+ exports.isSignQueueOperation = isSignQueueOperation;
4214
4483
  exports.listICloudBackups = listICloudBackups;
4215
4484
  exports.mergeUniqueKeyShares = mergeUniqueKeyShares;
4216
4485
  exports.retryPromise = retryPromise;