@dynamic-labs-wallet/browser 0.0.257 → 0.0.259

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) {
@@ -21,6 +22,33 @@ function _extends() {
21
22
  return _extends.apply(this, arguments);
22
23
  }
23
24
 
25
+ /**
26
+ * Infers the Bitcoin address type from a BIP-44 derivation path
27
+ *
28
+ * @param derivationPathStr - The derivation path string as JSON (e.g. '{"0":84,"1":0,"2":0,"3":0,"4":0}')
29
+ * @returns The inferred BitcoinAddressType, or undefined if it cannot be determined
30
+ */ const getBitcoinAddressTypeFromDerivationPath = (derivationPathStr)=>{
31
+ try {
32
+ const derivationPathObj = JSON.parse(derivationPathStr);
33
+ const path = Object.values(derivationPathObj).map(Number);
34
+ if (path.length < 2) return undefined;
35
+ const purpose = path[0];
36
+ // Handle both raw and hardened purpose values
37
+ // 84 = Native SegWit (BIP-84), 86 = Taproot (BIP-86)
38
+ // Hardened values have 0x80000000 added
39
+ const purposeIndex = purpose >= 0x80000000 ? purpose - 0x80000000 : purpose;
40
+ switch(purposeIndex){
41
+ case 84:
42
+ return core.BitcoinAddressType.NATIVE_SEGWIT;
43
+ case 86:
44
+ return core.BitcoinAddressType.TAPROOT;
45
+ default:
46
+ return undefined;
47
+ }
48
+ } catch (e) {
49
+ return undefined;
50
+ }
51
+ };
24
52
  const getMPCSignatureScheme = ({ signingAlgorithm, baseRelayUrl = core.MPC_RELAY_PROD_API_URL })=>{
25
53
  switch(signingAlgorithm){
26
54
  case core.SigningAlgorithm.ECDSA:
@@ -465,7 +493,6 @@ const SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE = {
465
493
  };
466
494
  const ROOM_EXPIRATION_TIME = 1000 * 60 * 10; // 10 minutes
467
495
  const ROOM_CACHE_COUNT = 5;
468
- const WALLET_BUSY_LOCK_TIMEOUT_MS = 20000; // 20 seconds
469
496
 
470
497
  const ERROR_KEYGEN_FAILED = '[DynamicWaasWalletClient]: Error with keygen';
471
498
  const ERROR_CREATE_WALLET_ACCOUNT = '[DynamicWaasWalletClient]: Error creating wallet account';
@@ -1084,6 +1111,341 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
1084
1111
  }
1085
1112
  };
1086
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
+
1087
1449
  const ALG_LABEL_RSA = 'HYBRID-RSA-AES-256';
1088
1450
  /**
1089
1451
  * Convert base64 to base64url encoding
@@ -1231,99 +1593,6 @@ const localStorageWriteTest = {
1231
1593
  }
1232
1594
  });
1233
1595
 
1234
- class WalletNotReadyError extends Error {
1235
- constructor(accountAddress, walletReadyState){
1236
- super(`Wallet ${accountAddress} is not ready and requires password to be unlocked`);
1237
- this.name = 'WalletNotReadyError';
1238
- this.accountAddress = accountAddress;
1239
- this.walletReadyState = walletReadyState;
1240
- }
1241
- }
1242
- class WalletBusyError extends Error {
1243
- constructor(accountAddress, operation){
1244
- super(`Wallet ${accountAddress} is currently performing a ${operation} operation`);
1245
- this.name = 'WalletBusyError';
1246
- this.operation = operation;
1247
- this.accountAddress = accountAddress;
1248
- }
1249
- }
1250
- /**
1251
- * Creates distribution where shares go to specified cloud providers
1252
- * Last share goes to cloud providers, rest to Dynamic backend
1253
- * @param providers - Array of cloud providers to backup to
1254
- * @param allShares - All key shares to distribute
1255
- */ const createCloudProviderDistribution = ({ providers, allShares })=>{
1256
- const cloudProviderShares = {};
1257
- // Last share goes to cloud providers, rest to Dynamic
1258
- const sharesForCloud = allShares.slice(-1);
1259
- providers.forEach((provider)=>{
1260
- cloudProviderShares[provider] = sharesForCloud;
1261
- });
1262
- return {
1263
- clientShares: allShares.slice(0, -1),
1264
- cloudProviderShares
1265
- };
1266
- };
1267
- /**
1268
- * Creates distribution with delegation + cloud backup
1269
- * Client shares backed up to Dynamic AND cloud providers
1270
- * Delegated share goes to webhook
1271
- */ const createDelegationWithCloudProviderDistribution = ({ providers, existingShares, delegatedShare })=>{
1272
- const cloudProviderShares = {};
1273
- providers.forEach((provider)=>{
1274
- cloudProviderShares[provider] = existingShares;
1275
- });
1276
- return {
1277
- clientShares: existingShares,
1278
- cloudProviderShares,
1279
- delegatedShare
1280
- };
1281
- };
1282
- /**
1283
- * Creates distribution for adding cloud backup to existing delegation
1284
- * Client shares go to both Dynamic AND cloud providers
1285
- * delegatedShare is undefined - we don't re-publish, but preserve the location
1286
- */ const createAddCloudProviderToExistingDelegationDistribution = ({ providers, clientShares })=>{
1287
- const cloudProviderShares = {};
1288
- providers.forEach((provider)=>{
1289
- cloudProviderShares[provider] = clientShares;
1290
- });
1291
- return {
1292
- clientShares,
1293
- cloudProviderShares
1294
- };
1295
- };
1296
- /**
1297
- * Checks if wallet has backup on any of the specified providers
1298
- */ const hasCloudProviderBackup = (backupInfo, providers)=>{
1299
- if (!(backupInfo == null ? void 0 : backupInfo.backups)) return false;
1300
- return providers.some((provider)=>{
1301
- var _backupInfo_backups_provider;
1302
- var _backupInfo_backups_provider_length;
1303
- 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;
1304
- });
1305
- };
1306
- /**
1307
- * Gets all cloud providers that have backups for this wallet
1308
- */ const getActiveCloudProviders = (backupInfo)=>{
1309
- if (!(backupInfo == null ? void 0 : backupInfo.backups)) return [];
1310
- return Object.entries(backupInfo.backups).filter(([location, backups])=>location !== core.BackupLocation.DYNAMIC && location !== core.BackupLocation.DELEGATED && backups.length > 0).map(([location])=>location);
1311
- };
1312
- const createDelegationOnlyDistribution = ({ existingShares, delegatedShare })=>({
1313
- clientShares: existingShares,
1314
- cloudProviderShares: {},
1315
- delegatedShare
1316
- });
1317
- const createDynamicOnlyDistribution = ({ allShares })=>({
1318
- clientShares: allShares,
1319
- cloudProviderShares: {}
1320
- });
1321
- const hasDelegatedBackup = (backupInfo)=>{
1322
- var _backupInfo_backups_BackupLocation_DELEGATED, _backupInfo_backups;
1323
- var _backupInfo_backups_BackupLocation_DELEGATED_length;
1324
- 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;
1325
- };
1326
-
1327
1596
  /**
1328
1597
  * Determines the recovery state of a wallet based on backup info and local shares.
1329
1598
  *
@@ -1376,48 +1645,27 @@ class DynamicWalletClient {
1376
1645
  });
1377
1646
  }
1378
1647
  }
1379
- getWalletBusyOperation(accountAddress) {
1380
- return DynamicWalletClient.walletBusyMap[accountAddress];
1648
+ /**
1649
+ * Check if wallet has heavy operations in progress
1650
+ */ static isHeavyOpInProgress(accountAddress) {
1651
+ return WalletQueueManager.isHeavyOpInProgress(accountAddress);
1381
1652
  }
1382
- isWalletBusy(accountAddress) {
1383
- 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);
1384
1657
  }
1385
- async waitForWalletNotBusy(accountAddress) {
1386
- const busyPromise = DynamicWalletClient.walletBusyPromiseMap[accountAddress];
1387
- if (!busyPromise) {
1388
- return;
1389
- }
1390
- const currentOperation = DynamicWalletClient.walletBusyMap[accountAddress];
1391
- this.logger.debug(`[DynamicWaasWalletClient] Waiting for wallet ${accountAddress} to complete ${currentOperation} operation`);
1392
- try {
1393
- await busyPromise;
1394
- } catch (e) {
1395
- // Intentionally swallowing the error - we proceed with the queued operation regardless of whether the previous one succeeded or failed
1396
- this.logger.debug(`[DynamicWaasWalletClient] Previous ${currentOperation} operation on wallet ${accountAddress} failed, proceeding with queued operation`);
1397
- }
1658
+ /**
1659
+ * Check if recovery is in progress for a wallet
1660
+ */ static isRecoveryInProgress(accountAddress) {
1661
+ return WalletQueueManager.isRecoveryInProgress(accountAddress);
1398
1662
  }
1399
- async withWalletBusyLock({ accountAddress, operation, fn, timeoutMs = WALLET_BUSY_LOCK_TIMEOUT_MS }) {
1400
- const existingOperation = DynamicWalletClient.walletBusyMap[accountAddress];
1401
- if (existingOperation) {
1402
- throw new WalletBusyError(accountAddress, existingOperation);
1403
- }
1404
- DynamicWalletClient.walletBusyMap[accountAddress] = operation;
1405
- const executionPromise = (async ()=>{
1406
- try {
1407
- return await Promise.race([
1408
- fn(),
1409
- timeoutPromise({
1410
- timeInMs: timeoutMs,
1411
- activity: `${operation} operation`
1412
- })
1413
- ]);
1414
- } finally{
1415
- delete DynamicWalletClient.walletBusyMap[accountAddress];
1416
- delete DynamicWalletClient.walletBusyPromiseMap[accountAddress];
1417
- }
1418
- })();
1419
- DynamicWalletClient.walletBusyPromiseMap[accountAddress] = executionPromise.then(()=>undefined, ()=>undefined);
1420
- 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();
1421
1669
  }
1422
1670
  getAuthMode() {
1423
1671
  return this.authMode;
@@ -1938,7 +2186,8 @@ class DynamicWalletClient {
1938
2186
  dynamicRequestId
1939
2187
  }, this.getTraceContext(traceContext)));
1940
2188
  // Recover key shares from backup (don't auto-store, we'll overwrite below)
1941
- 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({
1942
2191
  accountAddress,
1943
2192
  password: password != null ? password : this.environmentId,
1944
2193
  walletOperation,
@@ -1961,24 +2210,23 @@ class DynamicWalletClient {
1961
2210
  }
1962
2211
  //todo: need to modify with imported flag
1963
2212
  async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, context, onError, traceContext, bitcoinConfig }) {
1964
- return this.internalSign({
1965
- accountAddress,
1966
- message,
1967
- chainName,
1968
- password,
1969
- isFormatted,
1970
- signedSessionId,
1971
- mfaToken,
1972
- context,
1973
- onError,
1974
- traceContext,
1975
- bitcoinConfig
1976
- });
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
+ }));
1977
2226
  }
1978
2227
  async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, context, onError, traceContext, bitcoinConfig, hasAttemptedKeyShareRecovery = false }) {
1979
2228
  const dynamicRequestId = uuid.v4();
1980
2229
  try {
1981
- await this.waitForWalletNotBusy(accountAddress);
1982
2230
  await this.verifyPassword({
1983
2231
  accountAddress,
1984
2232
  password,
@@ -2103,18 +2351,29 @@ class DynamicWalletClient {
2103
2351
  }
2104
2352
  }
2105
2353
  async refreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, traceContext }) {
2106
- return this.withWalletBusyLock({
2107
- accountAddress,
2108
- operation: core.WalletOperation.REFRESH,
2109
- fn: ()=>this.internalRefreshWalletAccountShares({
2110
- accountAddress,
2111
- chainName,
2112
- password,
2113
- signedSessionId,
2114
- mfaToken,
2115
- traceContext
2116
- })
2117
- });
2354
+ return WalletQueueManager.queueHeavyOperation(accountAddress, core.WalletOperation.REFRESH, ()=>this.internalRefreshWalletAccountShares({
2355
+ accountAddress,
2356
+ chainName,
2357
+ password,
2358
+ signedSessionId,
2359
+ mfaToken,
2360
+ traceContext
2361
+ }));
2362
+ }
2363
+ /**
2364
+ * Gets the Bitcoin config for MPC operations by looking up addressType
2365
+ * from walletMap, with fallback to deriving it from derivationPath.
2366
+ */ getBitcoinConfigForChain(chainName, accountAddress) {
2367
+ if (chainName !== 'BTC') return undefined;
2368
+ const walletProperties = this.walletMap[accountAddress];
2369
+ let addressType = walletProperties == null ? void 0 : walletProperties.addressType;
2370
+ // Fallback: derive addressType from derivationPath if not explicitly set
2371
+ if (!addressType && (walletProperties == null ? void 0 : walletProperties.derivationPath)) {
2372
+ addressType = getBitcoinAddressTypeFromDerivationPath(walletProperties.derivationPath);
2373
+ }
2374
+ return addressType ? {
2375
+ addressType: addressType
2376
+ } : undefined;
2118
2377
  }
2119
2378
  async internalRefreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, traceContext, hasAttemptedKeyShareRecovery = false }) {
2120
2379
  const dynamicRequestId = uuid.v4();
@@ -2131,13 +2390,7 @@ class DynamicWalletClient {
2131
2390
  password,
2132
2391
  signedSessionId
2133
2392
  });
2134
- const walletProperties = this.walletMap[accountAddress];
2135
- let bitcoinConfig;
2136
- if (chainName === 'BTC' && (walletProperties == null ? void 0 : walletProperties.addressType)) {
2137
- bitcoinConfig = {
2138
- addressType: walletProperties.addressType
2139
- };
2140
- }
2393
+ const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
2141
2394
  const mpcSigner = getMPCSigner({
2142
2395
  chainName,
2143
2396
  baseRelayUrl: this.baseMPCRelayApiUrl,
@@ -2281,22 +2534,18 @@ class DynamicWalletClient {
2281
2534
  };
2282
2535
  }
2283
2536
  async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, revokeDelegation = false }) {
2284
- return this.withWalletBusyLock({
2285
- accountAddress,
2286
- operation: core.WalletOperation.RESHARE,
2287
- fn: ()=>this.internalReshare({
2288
- chainName,
2289
- accountAddress,
2290
- oldThresholdSignatureScheme,
2291
- newThresholdSignatureScheme,
2292
- password,
2293
- signedSessionId,
2294
- cloudProviders,
2295
- delegateToProjectEnvironment,
2296
- mfaToken,
2297
- revokeDelegation
2298
- })
2299
- });
2537
+ return WalletQueueManager.queueHeavyOperation(accountAddress, core.WalletOperation.RESHARE, ()=>this.internalReshare({
2538
+ chainName,
2539
+ accountAddress,
2540
+ oldThresholdSignatureScheme,
2541
+ newThresholdSignatureScheme,
2542
+ password,
2543
+ signedSessionId,
2544
+ cloudProviders,
2545
+ delegateToProjectEnvironment,
2546
+ mfaToken,
2547
+ revokeDelegation
2548
+ }));
2300
2549
  }
2301
2550
  async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false }) {
2302
2551
  const dynamicRequestId = uuid.v4();
@@ -2353,13 +2602,7 @@ class DynamicWalletClient {
2353
2602
  ...serverKeygenIds,
2354
2603
  ...newServerKeygenIds
2355
2604
  ];
2356
- const walletProperties = this.walletMap[accountAddress];
2357
- let bitcoinConfig;
2358
- if (chainName === 'BTC' && (walletProperties == null ? void 0 : walletProperties.addressType)) {
2359
- bitcoinConfig = {
2360
- addressType: walletProperties.addressType
2361
- };
2362
- }
2605
+ const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
2363
2606
  const mpcSigner = getMPCSigner({
2364
2607
  chainName,
2365
2608
  baseRelayUrl: this.baseMPCRelayApiUrl,
@@ -2851,7 +3094,10 @@ class DynamicWalletClient {
2851
3094
  /**
2852
3095
  * Ensures that client key shares exist for the given account address.
2853
3096
  * Throws an error if no shares are found.
2854
- * This method enforces share presence before sensitive operations.
3097
+ *
3098
+ * Note: This method only checks for existing shares in storage.
3099
+ * Auto-recovery logic has been removed in favor of the queue pattern.
3100
+ * Callers should handle recovery explicitly if needed.
2855
3101
  */ async ensureClientShare(accountAddress) {
2856
3102
  const shares = await this.getClientKeySharesFromStorage({
2857
3103
  accountAddress
@@ -3178,6 +3424,17 @@ class DynamicWalletClient {
3178
3424
  };
3179
3425
  }
3180
3426
  async recoverEncryptedBackupByWallet({ accountAddress, password, walletOperation, signedSessionId, shareCount = undefined, storeRecoveredShares = true, mfaToken }) {
3427
+ return WalletQueueManager.queueRecoverOperation(accountAddress, core.WalletOperation.RECOVER, ()=>this.internalRecoverEncryptedBackupByWallet({
3428
+ accountAddress,
3429
+ password,
3430
+ walletOperation,
3431
+ signedSessionId,
3432
+ shareCount,
3433
+ storeRecoveredShares,
3434
+ mfaToken
3435
+ }));
3436
+ }
3437
+ async internalRecoverEncryptedBackupByWallet({ accountAddress, password, walletOperation, signedSessionId, shareCount = undefined, storeRecoveredShares = true, mfaToken }) {
3181
3438
  try {
3182
3439
  const wallet = this.walletMap[accountAddress];
3183
3440
  this.logger.debug(`recoverEncryptedBackupByWallet wallet: ${walletOperation}`, wallet);
@@ -3777,20 +4034,55 @@ class DynamicWalletClient {
3777
4034
  }
3778
4035
  }
3779
4036
  /**
3780
- * Gets the recovery state of a wallet without attempting to decrypt shares.
4037
+ * Gets the recovery state of a wallet, optionally recovering shares if not available.
3781
4038
  * This method is useful for detecting if a wallet is password-encrypted
3782
4039
  * and needs unlocking before use.
3783
4040
  *
4041
+ * If shares are not available locally and recovery parameters are provided,
4042
+ * this method will call getWallet with RECOVER operation to fetch shares.
4043
+ * The underlying recoverEncryptedBackupByWallet handles deduplication via
4044
+ * inFlightRecovery, so concurrent calls won't start multiple recoveries.
4045
+ *
3784
4046
  * @param accountAddress - The account address of the wallet
4047
+ * @param signedSessionId - Optional signed session ID for triggering recovery if shares not available
4048
+ * @param password - Optional password for decrypting recovered shares
3785
4049
  * @returns WalletRecoveryState indicating the wallet's lock state and share availability
3786
- */ async getWalletRecoveryState({ accountAddress }) {
4050
+ * @throws Error if recovery fails or no shares available after recovery
4051
+ */ async getWalletRecoveryState({ accountAddress, signedSessionId, password }) {
3787
4052
  const walletData = this.walletMap[accountAddress];
3788
4053
  if (!walletData) {
3789
4054
  throw new Error(`Wallet not found for address: ${accountAddress}`);
3790
4055
  }
3791
- const clientKeyShares = await this.getClientKeySharesFromStorage({
4056
+ let clientKeyShares = await this.getClientKeySharesFromStorage({
3792
4057
  accountAddress
3793
4058
  });
4059
+ // If no local shares and signedSessionId provided, trigger recovery via getWallet
4060
+ // getWallet -> recoverEncryptedBackupByWallet handles deduplication via inFlightRecovery
4061
+ if (clientKeyShares.length === 0 && signedSessionId) {
4062
+ var _walletData_clientKeySharesBackupInfo;
4063
+ await this.getWallet({
4064
+ accountAddress,
4065
+ walletOperation: core.WalletOperation.RECOVER,
4066
+ signedSessionId,
4067
+ password
4068
+ });
4069
+ // Re-fetch shares after recovery
4070
+ clientKeyShares = await this.getClientKeySharesFromStorage({
4071
+ accountAddress
4072
+ });
4073
+ var _walletData_clientKeySharesBackupInfo_passwordEncrypted;
4074
+ // For password-encrypted wallets, also check for cached encrypted shares
4075
+ const isPasswordEncrypted = (_walletData_clientKeySharesBackupInfo_passwordEncrypted = (_walletData_clientKeySharesBackupInfo = walletData.clientKeySharesBackupInfo) == null ? void 0 : _walletData_clientKeySharesBackupInfo.passwordEncrypted) != null ? _walletData_clientKeySharesBackupInfo_passwordEncrypted : false;
4076
+ let hasEncryptedShares = false;
4077
+ if (isPasswordEncrypted && clientKeyShares.length === 0) {
4078
+ const encryptedStorageKey = `${accountAddress}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
4079
+ const encryptedData = await this.storage.getItem(encryptedStorageKey);
4080
+ hasEncryptedShares = !!encryptedData;
4081
+ }
4082
+ if (clientKeyShares.length === 0 && !hasEncryptedShares) {
4083
+ throw new Error(`No key shares available for wallet ${accountAddress} after recovery`);
4084
+ }
4085
+ }
3794
4086
  return determineWalletRecoveryState({
3795
4087
  clientKeySharesBackupInfo: walletData.clientKeySharesBackupInfo,
3796
4088
  hasLocalShares: clientKeyShares.length > 0
@@ -4086,8 +4378,6 @@ class DynamicWalletClient {
4086
4378
  }
4087
4379
  DynamicWalletClient.rooms = {};
4088
4380
  DynamicWalletClient.roomsInitializing = {};
4089
- DynamicWalletClient.walletBusyMap = {};
4090
- DynamicWalletClient.walletBusyPromiseMap = {};
4091
4381
 
4092
4382
  Object.defineProperty(exports, "BIP340", {
4093
4383
  enumerable: true,
@@ -4152,6 +4442,9 @@ exports.ERROR_SIGN_MESSAGE = ERROR_SIGN_MESSAGE;
4152
4442
  exports.ERROR_SIGN_TYPED_DATA = ERROR_SIGN_TYPED_DATA;
4153
4443
  exports.ERROR_VERIFY_MESSAGE_SIGNATURE = ERROR_VERIFY_MESSAGE_SIGNATURE;
4154
4444
  exports.ERROR_VERIFY_TRANSACTION_SIGNATURE = ERROR_VERIFY_TRANSACTION_SIGNATURE;
4445
+ exports.HEAVY_QUEUE_OPERATIONS = HEAVY_QUEUE_OPERATIONS;
4446
+ exports.RECOVER_QUEUE_OPERATIONS = RECOVER_QUEUE_OPERATIONS;
4447
+ exports.SIGN_QUEUE_OPERATIONS = SIGN_QUEUE_OPERATIONS;
4155
4448
  exports.WalletBusyError = WalletBusyError;
4156
4449
  exports.WalletNotReadyError = WalletNotReadyError;
4157
4450
  exports.cancelICloudAuth = cancelICloudAuth;
@@ -4167,6 +4460,7 @@ exports.extractPubkey = extractPubkey;
4167
4460
  exports.formatEvmMessage = formatEvmMessage;
4168
4461
  exports.formatMessage = formatMessage;
4169
4462
  exports.getActiveCloudProviders = getActiveCloudProviders;
4463
+ exports.getBitcoinAddressTypeFromDerivationPath = getBitcoinAddressTypeFromDerivationPath;
4170
4464
  exports.getClientKeyShareBackupInfo = getClientKeyShareBackupInfo;
4171
4465
  exports.getClientKeyShareExportFileName = getClientKeyShareExportFileName;
4172
4466
  exports.getGoogleOAuthAccountId = getGoogleOAuthAccountId;
@@ -4177,9 +4471,12 @@ exports.hasCloudProviderBackup = hasCloudProviderBackup;
4177
4471
  exports.hasDelegatedBackup = hasDelegatedBackup;
4178
4472
  exports.initializeCloudKit = initializeCloudKit;
4179
4473
  exports.isBrowser = isBrowser;
4474
+ exports.isHeavyQueueOperation = isHeavyQueueOperation;
4180
4475
  exports.isHexString = isHexString;
4181
4476
  exports.isICloudAuthenticated = isICloudAuthenticated;
4182
4477
  exports.isPublicKeyMismatchError = isPublicKeyMismatchError;
4478
+ exports.isRecoverQueueOperation = isRecoverQueueOperation;
4479
+ exports.isSignQueueOperation = isSignQueueOperation;
4183
4480
  exports.listICloudBackups = listICloudBackups;
4184
4481
  exports.mergeUniqueKeyShares = mergeUniqueKeyShares;
4185
4482
  exports.retryPromise = retryPromise;