@dynamic-labs-wallet/browser 1.0.42 → 1.0.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 +542 -74
- package/index.esm.js +542 -75
- package/package.json +3 -3
- package/src/client.d.ts +56 -17
- package/src/client.d.ts.map +1 -1
- package/src/services/logger.d.ts +6 -0
- package/src/services/logger.d.ts.map +1 -1
- package/src/utils.d.ts +12 -0
- package/src/utils.d.ts.map +1 -1
package/index.esm.js
CHANGED
|
@@ -1266,6 +1266,22 @@ const downloadStringAsFile = ({ filename, content, mimeType = 'application/json'
|
|
|
1266
1266
|
}
|
|
1267
1267
|
return false;
|
|
1268
1268
|
};
|
|
1269
|
+
/**
|
|
1270
|
+
* Checks for the server's stale-share preflight rejection (HTTP 409 sent as
|
|
1271
|
+
* an SSE error event with `code: 'STALE_CLIENT_SHARES'`). The server compares
|
|
1272
|
+
* the `shareSetId` the SDK sends on sign requests against the wallet's active
|
|
1273
|
+
* share set and rejects BEFORE any room/ceremony work when they diverge
|
|
1274
|
+
* (e.g. the shares were rotated by a reshare/refresh on another device).
|
|
1275
|
+
* The SDK heals (re-syncs local shares from backup) and retries internally.
|
|
1276
|
+
*
|
|
1277
|
+
* The SSE event-stream layer copies the event's data fields onto the Error
|
|
1278
|
+
* object (see createErrorFromEventData), so `code` is available here.
|
|
1279
|
+
*/ const isStaleClientSharesError = (error)=>{
|
|
1280
|
+
if (!error || typeof error !== 'object') {
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
return error.code === 'STALE_CLIENT_SHARES';
|
|
1284
|
+
};
|
|
1269
1285
|
/**
|
|
1270
1286
|
* Determines whether a reshare operation should reuse the wallet's existing backup locations.
|
|
1271
1287
|
*
|
|
@@ -1786,12 +1802,40 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
|
|
|
1786
1802
|
* `WalletApiError` and caught again (e.g. by queue error handlers).
|
|
1787
1803
|
* Logging these at `warn` prevents double-counting them as real errors.
|
|
1788
1804
|
*/ const isTransientApiError = (error)=>error instanceof WalletApiError && error.status === 429;
|
|
1805
|
+
/**
|
|
1806
|
+
* Returns true for p-queue TimeoutError. These are transient and
|
|
1807
|
+
* retryable client-side, so logging at `warn` prevents them from
|
|
1808
|
+
* firing error-level monitors (e.g. the WaaS task timeout alert).
|
|
1809
|
+
*/ const isQueueTimeoutError = (error)=>error instanceof Error && error.name === 'TimeoutError';
|
|
1810
|
+
const CLIENT_NETWORK_ERROR_MESSAGES = new Set([
|
|
1811
|
+
'network error',
|
|
1812
|
+
'failed to fetch',
|
|
1813
|
+
'load failed'
|
|
1814
|
+
]);
|
|
1815
|
+
/**
|
|
1816
|
+
* Returns true for errors caused by transient client-side network failures
|
|
1817
|
+
* (e.g. user lost connectivity mid-operation). These are not actionable as
|
|
1818
|
+
* errors — the client will retry or the user will reload.
|
|
1819
|
+
*/ const isClientNetworkError = (error)=>{
|
|
1820
|
+
if (!(error instanceof Error)) return false;
|
|
1821
|
+
const msg = error.message.toLowerCase();
|
|
1822
|
+
return CLIENT_NETWORK_ERROR_MESSAGES.has(msg) || msg.startsWith('webassembly compilation aborted: network error');
|
|
1823
|
+
};
|
|
1824
|
+
const resolveLogLevel = (error, fallback)=>{
|
|
1825
|
+
if (isExpectedUserError(error)) {
|
|
1826
|
+
return 'info';
|
|
1827
|
+
}
|
|
1828
|
+
if (isTransientApiError(error) || isQueueTimeoutError(error) || isClientNetworkError(error)) {
|
|
1829
|
+
return 'warn';
|
|
1830
|
+
}
|
|
1831
|
+
return fallback;
|
|
1832
|
+
};
|
|
1789
1833
|
const logError = ({ message, error, context, level = 'error' })=>{
|
|
1790
1834
|
if (error instanceof AxiosError) {
|
|
1791
1835
|
handleAxiosError(error, message, context);
|
|
1792
1836
|
return;
|
|
1793
1837
|
}
|
|
1794
|
-
const resolvedLevel =
|
|
1838
|
+
const resolvedLevel = resolveLogLevel(error, level);
|
|
1795
1839
|
// Surface the unwrapped `cause` (name only — never its raw fields, which may
|
|
1796
1840
|
// carry key material). Errors like EncryptionSelfTestFailedError wrap the real
|
|
1797
1841
|
// failure (e.g. KeyShareDecryptionError) as `cause`; without this the original
|
|
@@ -2538,6 +2582,15 @@ const KNOWN_SHARE_SET_TYPES = new Set([
|
|
|
2538
2582
|
'delegated',
|
|
2539
2583
|
'server'
|
|
2540
2584
|
]);
|
|
2585
|
+
/**
|
|
2586
|
+
* keygenIds the server records for the wallet's Dynamic-backed-up client
|
|
2587
|
+
* shares — the reference generation that local shares are compared against.
|
|
2588
|
+
* Empty for first-time backups or legacy data without keygenIds.
|
|
2589
|
+
*/ const recordedDynamicKeygenIds = (backupInfo)=>{
|
|
2590
|
+
var _backupInfo_backups;
|
|
2591
|
+
var _backupInfo_backups_BackupLocation_DYNAMIC;
|
|
2592
|
+
return ((_backupInfo_backups_BackupLocation_DYNAMIC = backupInfo == null ? void 0 : (_backupInfo_backups = backupInfo.backups) == null ? void 0 : _backupInfo_backups[BackupLocation.DYNAMIC]) != null ? _backupInfo_backups_BackupLocation_DYNAMIC : []).map((backup)=>backup.keygenId).filter((keygenId)=>typeof keygenId === 'string' && keygenId.length > 0);
|
|
2593
|
+
};
|
|
2541
2594
|
class DynamicWalletClient {
|
|
2542
2595
|
/**
|
|
2543
2596
|
* Check if wallet has heavy operations in progress
|
|
@@ -2823,7 +2876,7 @@ class DynamicWalletClient {
|
|
|
2823
2876
|
operationSource: OperationSource.FORWARD_MPC_SERVICE
|
|
2824
2877
|
};
|
|
2825
2878
|
} catch (error) {
|
|
2826
|
-
this.logger.
|
|
2879
|
+
this.logger.warn('Error during forward MPC client keygen', {
|
|
2827
2880
|
error,
|
|
2828
2881
|
chainName,
|
|
2829
2882
|
environmentId: this.environmentId,
|
|
@@ -3263,7 +3316,7 @@ class DynamicWalletClient {
|
|
|
3263
3316
|
}
|
|
3264
3317
|
return this.processForwardMPCSignature(signatureBytes, signingAlgo);
|
|
3265
3318
|
} catch (error) {
|
|
3266
|
-
this.logger.
|
|
3319
|
+
this.logger.warn('Error signing message with forward MPC client', {
|
|
3267
3320
|
error,
|
|
3268
3321
|
environmentId: this.environmentId,
|
|
3269
3322
|
userId: this.userId,
|
|
@@ -3372,22 +3425,114 @@ class DynamicWalletClient {
|
|
|
3372
3425
|
}
|
|
3373
3426
|
}
|
|
3374
3427
|
/**
|
|
3375
|
-
*
|
|
3376
|
-
* the
|
|
3377
|
-
*
|
|
3378
|
-
*
|
|
3379
|
-
*
|
|
3380
|
-
*/ async
|
|
3381
|
-
|
|
3382
|
-
|
|
3428
|
+
* Best-effort heal for stale local shares. Runs verifyAndRecoverStaleShare
|
|
3429
|
+
* through the shared recovery queue: concurrent triggers join the in-flight
|
|
3430
|
+
* recovery (WalletQueueManager dedup), and execution is deadlock-safe when
|
|
3431
|
+
* called from inside a queued sign/heavy operation. Never throws — callers
|
|
3432
|
+
* are already on a failing path and rethrow their original error.
|
|
3433
|
+
*/ async healStaleShares(params) {
|
|
3434
|
+
const runCheck = ()=>WalletQueueManager.queueRecoverOperation(params.accountAddress, WalletOperation.RECOVER, ()=>this.verifyAndRecoverStaleShare(params));
|
|
3435
|
+
try {
|
|
3436
|
+
const result = await runCheck();
|
|
3437
|
+
// The queue dedup returns the in-flight recovery's promise, dropping
|
|
3438
|
+
// this caller's params. If that run skipped recovery but we carry a
|
|
3439
|
+
// confirmed mismatch, re-run once with our params. (A run we started
|
|
3440
|
+
// ourselves with confirmedMismatch=true always recovers, so this only
|
|
3441
|
+
// fires after joining a weaker concurrent check.)
|
|
3442
|
+
if (params.confirmedMismatch && (result == null ? void 0 : result.recovered) === false) {
|
|
3443
|
+
await runCheck();
|
|
3444
|
+
}
|
|
3445
|
+
} catch (error) {
|
|
3446
|
+
this.logger.error('[StaleShareCheck] share recovery failed', {
|
|
3447
|
+
accountAddress: params.accountAddress,
|
|
3448
|
+
dynamicRequestId: params.dynamicRequestId,
|
|
3449
|
+
trigger: params.trigger,
|
|
3450
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3451
|
+
});
|
|
3383
3452
|
}
|
|
3384
|
-
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Ground-truth staleness check: refreshes the wallet from the API, compares
|
|
3456
|
+
* local share keygenIds against the server's recorded Dynamic-backup
|
|
3457
|
+
* keygenIds, and restores from backup when the local generation is stale
|
|
3458
|
+
* (a reshare/refresh on another device/browser rotates the shares without
|
|
3459
|
+
* this client noticing). No-ops when shares are fresh, so callers may run
|
|
3460
|
+
* it on any ceremony failure regardless of the error message.
|
|
3461
|
+
*/ async verifyAndRecoverStaleShare({ accountAddress, chainName, password, signedSessionId, walletOperation, mfaToken, dynamicRequestId, traceContext, bitcoinConfig, trigger, confirmedMismatch }) {
|
|
3462
|
+
this.logger.info('[StaleShareCheck] verifying local share generation against server', _extends({
|
|
3385
3463
|
accountAddress,
|
|
3386
|
-
|
|
3464
|
+
chainName,
|
|
3465
|
+
trigger,
|
|
3466
|
+
confirmedMismatch,
|
|
3387
3467
|
dynamicRequestId
|
|
3388
3468
|
}, this.getTraceContext(traceContext)));
|
|
3389
|
-
// Refresh walletMap from API
|
|
3469
|
+
// Refresh walletMap from the API: the cached entry may predate a rotation
|
|
3470
|
+
// performed on another device, which would make this comparison a no-op.
|
|
3471
|
+
// Full getWallets (not just backup info) so shareSetId etc. are also
|
|
3472
|
+
// current for the caller's retry.
|
|
3390
3473
|
await this.getWallets();
|
|
3474
|
+
const wallet = this.getWalletFromMap(accountAddress);
|
|
3475
|
+
const recordedKeygenIds = recordedDynamicKeygenIds(wallet == null ? void 0 : wallet.clientKeySharesBackupInfo);
|
|
3476
|
+
let staleness;
|
|
3477
|
+
let localKeygenIds = [];
|
|
3478
|
+
if (recordedKeygenIds.length === 0) {
|
|
3479
|
+
// First-time backup or legacy data (server didn't populate keygenId) —
|
|
3480
|
+
// generation can't be compared. A confirmed mismatch still recovers
|
|
3481
|
+
// below (legacy recovery behavior); an unclassified
|
|
3482
|
+
// failure does not.
|
|
3483
|
+
staleness = 'unverifiable';
|
|
3484
|
+
} else {
|
|
3485
|
+
try {
|
|
3486
|
+
const localShares = await this.getClientKeySharesFromStorage({
|
|
3487
|
+
accountAddress
|
|
3488
|
+
});
|
|
3489
|
+
const comparison = await this.compareShareGenerations({
|
|
3490
|
+
accountAddress,
|
|
3491
|
+
chainName,
|
|
3492
|
+
localShares,
|
|
3493
|
+
recordedKeygenIds,
|
|
3494
|
+
bitcoinConfig
|
|
3495
|
+
});
|
|
3496
|
+
localKeygenIds = comparison.localKeygenIds;
|
|
3497
|
+
staleness = comparison.fresh ? 'fresh' : 'stale';
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
// Unreadable/corrupted local share — treat as stale and recover.
|
|
3500
|
+
this.logger.warn('[StaleShareCheck] could not derive keygenId from local shares — treating as stale', {
|
|
3501
|
+
accountAddress,
|
|
3502
|
+
trigger,
|
|
3503
|
+
dynamicRequestId,
|
|
3504
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3505
|
+
});
|
|
3506
|
+
staleness = 'stale';
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
// Stale generation always recovers. A confirmed mismatch also recovers
|
|
3510
|
+
// when the comparison is inconclusive or even reports fresh (possible
|
|
3511
|
+
// server-side inconsistency) — matching the legacy recovery behavior.
|
|
3512
|
+
const shouldRecover = staleness === 'stale' || confirmedMismatch;
|
|
3513
|
+
if (!shouldRecover) {
|
|
3514
|
+
this.logger.info('[StaleShareCheck] no recovery needed', {
|
|
3515
|
+
accountAddress,
|
|
3516
|
+
trigger,
|
|
3517
|
+
staleness,
|
|
3518
|
+
localKeygenIds,
|
|
3519
|
+
dynamicRequestId
|
|
3520
|
+
});
|
|
3521
|
+
return {
|
|
3522
|
+
recovered: false,
|
|
3523
|
+
localKeygenIds,
|
|
3524
|
+
recordedKeygenIds
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
this.logger.warn('[StaleShareCheck] recovering current shares from backup', _extends({
|
|
3528
|
+
accountAddress,
|
|
3529
|
+
trigger,
|
|
3530
|
+
confirmedMismatch,
|
|
3531
|
+
staleness,
|
|
3532
|
+
localKeygenIds,
|
|
3533
|
+
recordedKeygenIds,
|
|
3534
|
+
dynamicRequestId
|
|
3535
|
+
}, this.getTraceContext(traceContext)));
|
|
3391
3536
|
await this.internalRecoverEncryptedBackupByWallet({
|
|
3392
3537
|
accountAddress,
|
|
3393
3538
|
password,
|
|
@@ -3396,12 +3541,35 @@ class DynamicWalletClient {
|
|
|
3396
3541
|
mfaToken,
|
|
3397
3542
|
storeRecoveredShares: true
|
|
3398
3543
|
});
|
|
3399
|
-
this.logger.info('[
|
|
3544
|
+
this.logger.info('[StaleShareCheck] recovery completed — next operation will use the recovered shares', {
|
|
3400
3545
|
accountAddress,
|
|
3401
|
-
|
|
3546
|
+
trigger,
|
|
3402
3547
|
dynamicRequestId
|
|
3403
|
-
}
|
|
3404
|
-
|
|
3548
|
+
});
|
|
3549
|
+
return {
|
|
3550
|
+
recovered: true,
|
|
3551
|
+
localKeygenIds,
|
|
3552
|
+
recordedKeygenIds
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
/**
|
|
3556
|
+
* Set-compare local share keygenIds (MPC exportID) against the keygenIds
|
|
3557
|
+
* the server recorded for the wallet's Dynamic backups. Shared by the
|
|
3558
|
+
* stale-share heal (verifyAndRecoverStaleShare) and the backup preflight
|
|
3559
|
+
* (ensureLocalSharesAreFresh).
|
|
3560
|
+
*/ async compareShareGenerations({ accountAddress, chainName, localShares, recordedKeygenIds, bitcoinConfig }) {
|
|
3561
|
+
const mpcSigner = getMPCSigner({
|
|
3562
|
+
chainName,
|
|
3563
|
+
baseRelayUrl: this.baseMPCRelayApiUrl,
|
|
3564
|
+
bitcoinConfig: bitcoinConfig != null ? bitcoinConfig : this.getBitcoinConfigForChain(chainName, accountAddress)
|
|
3565
|
+
});
|
|
3566
|
+
const localKeygenIds = await Promise.all(localShares.map((share)=>this.getKeygenIdForShare(mpcSigner, share)));
|
|
3567
|
+
const recordedSet = new Set(recordedKeygenIds);
|
|
3568
|
+
const fresh = localKeygenIds.length === recordedSet.size && localKeygenIds.every((id)=>recordedSet.has(id));
|
|
3569
|
+
return {
|
|
3570
|
+
fresh,
|
|
3571
|
+
localKeygenIds
|
|
3572
|
+
};
|
|
3405
3573
|
}
|
|
3406
3574
|
//todo: need to modify with imported flag
|
|
3407
3575
|
async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, onError, traceContext, bitcoinConfig }) {
|
|
@@ -3420,7 +3588,7 @@ class DynamicWalletClient {
|
|
|
3420
3588
|
bitcoinConfig
|
|
3421
3589
|
}));
|
|
3422
3590
|
}
|
|
3423
|
-
async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, onError, traceContext, bitcoinConfig }) {
|
|
3591
|
+
async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, onError, traceContext, bitcoinConfig, staleShareRetry = false }) {
|
|
3424
3592
|
const dynamicRequestId = v4();
|
|
3425
3593
|
try {
|
|
3426
3594
|
await this.verifyPassword({
|
|
@@ -3463,6 +3631,17 @@ class DynamicWalletClient {
|
|
|
3463
3631
|
// (sseErrorPromise would never reach Promise.race in that path)
|
|
3464
3632
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
3465
3633
|
sseErrorPromise.catch(()=>{});
|
|
3634
|
+
// Dedicated channel for the server's 409 stale-shares preflight: it must
|
|
3635
|
+
// abort the ceremony into the heal-and-retry path WITHOUT settling the
|
|
3636
|
+
// host request (so the transparent retry's outcome is what the host
|
|
3637
|
+
// sees). Needed as a race arm because in the cached-room flow
|
|
3638
|
+
// serverSignPromise is never awaited, so its rejection alone would float.
|
|
3639
|
+
let rejectOnStaleShares;
|
|
3640
|
+
const staleSharesPromise = new Promise((_, reject)=>{
|
|
3641
|
+
rejectOnStaleShares = reject;
|
|
3642
|
+
});
|
|
3643
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
3644
|
+
staleSharesPromise.catch(()=>{});
|
|
3466
3645
|
// Perform the server sign
|
|
3467
3646
|
const serverSignPromise = this.serverSign({
|
|
3468
3647
|
walletId: wallet.walletId,
|
|
@@ -3474,6 +3653,13 @@ class DynamicWalletClient {
|
|
|
3474
3653
|
roomId,
|
|
3475
3654
|
context,
|
|
3476
3655
|
onError: (error)=>{
|
|
3656
|
+
// The server's 409 stale-shares preflight rejection is healed and
|
|
3657
|
+
// retried internally (see the catch below) — don't settle the host
|
|
3658
|
+
// request or poison the SSE race with it.
|
|
3659
|
+
if (isStaleClientSharesError(error)) {
|
|
3660
|
+
rejectOnStaleShares(error);
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3477
3663
|
onError == null ? void 0 : onError(error);
|
|
3478
3664
|
rejectOnSSEError(error);
|
|
3479
3665
|
},
|
|
@@ -3481,6 +3667,11 @@ class DynamicWalletClient {
|
|
|
3481
3667
|
traceContext,
|
|
3482
3668
|
bitcoinConfig
|
|
3483
3669
|
});
|
|
3670
|
+
// In the cached-room flow the promise below is never awaited — attach a
|
|
3671
|
+
// handler so a rejection (e.g. the 409 preflight, which also arrives via
|
|
3672
|
+
// staleSharesPromise above) doesn't surface as an unhandled rejection.
|
|
3673
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
3674
|
+
serverSignPromise.catch(()=>{});
|
|
3484
3675
|
// Only await if roomId is not provided
|
|
3485
3676
|
roomId = roomId != null ? roomId : (await serverSignPromise).roomId;
|
|
3486
3677
|
this.logger.info('[DynamicWaasWalletClient] Server sign completed', _extends({
|
|
@@ -3493,6 +3684,9 @@ class DynamicWalletClient {
|
|
|
3493
3684
|
operation: 'serverSign'
|
|
3494
3685
|
}, this.getTraceContext(traceContext)));
|
|
3495
3686
|
let signature;
|
|
3687
|
+
// Set by the clientSign rejection handler below; read in the catch to
|
|
3688
|
+
// detect a client-side mismatch masked by a generic SSE race winner.
|
|
3689
|
+
let clientSignMismatchError;
|
|
3496
3690
|
try {
|
|
3497
3691
|
// Race clientSign against SSE errors so that server-side failures
|
|
3498
3692
|
// (e.g. policy violations) immediately reject instead of hanging
|
|
@@ -3520,6 +3714,12 @@ class DynamicWalletClient {
|
|
|
3520
3714
|
roomId: undefined,
|
|
3521
3715
|
context,
|
|
3522
3716
|
onError: (error)=>{
|
|
3717
|
+
// See the initial serverSign onError: 409 stale-shares is
|
|
3718
|
+
// healed + retried internally, never surfaced to the host.
|
|
3719
|
+
if (isStaleClientSharesError(error)) {
|
|
3720
|
+
rejectOnStaleShares(error);
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3523
3723
|
onError == null ? void 0 : onError(error);
|
|
3524
3724
|
rejectOnSSEError(error);
|
|
3525
3725
|
},
|
|
@@ -3530,26 +3730,63 @@ class DynamicWalletClient {
|
|
|
3530
3730
|
return freshRoomId;
|
|
3531
3731
|
}
|
|
3532
3732
|
});
|
|
3533
|
-
//
|
|
3534
|
-
//
|
|
3535
|
-
//
|
|
3536
|
-
//
|
|
3537
|
-
|
|
3733
|
+
// Handle the losing race promise (prevents an unhandled rejection) and
|
|
3734
|
+
// capture a client-side ERROR_PUBLIC_KEY_MISMATCH here — the only place
|
|
3735
|
+
// a stale-local-share failure surfaces, since the server's generic SSE
|
|
3736
|
+
// error wins the race and masks it. Schedule recovery regardless of
|
|
3737
|
+
// which promise wins or when this rejection settles.
|
|
3738
|
+
clientSignPromise.catch((error)=>{
|
|
3739
|
+
if (isPublicKeyMismatchError(error)) {
|
|
3740
|
+
clientSignMismatchError = error;
|
|
3741
|
+
this.logger.warn('[StaleShareCheck] clientSign rejected with public-key mismatch — scheduling share recovery', _extends({
|
|
3742
|
+
accountAddress,
|
|
3743
|
+
chainName,
|
|
3744
|
+
dynamicRequestId
|
|
3745
|
+
}, this.getTraceContext(traceContext)));
|
|
3746
|
+
void this.healStaleShares({
|
|
3747
|
+
accountAddress,
|
|
3748
|
+
chainName,
|
|
3749
|
+
password,
|
|
3750
|
+
signedSessionId,
|
|
3751
|
+
walletOperation: WalletOperation.SIGN_MESSAGE,
|
|
3752
|
+
mfaToken,
|
|
3753
|
+
dynamicRequestId,
|
|
3754
|
+
traceContext,
|
|
3755
|
+
bitcoinConfig,
|
|
3756
|
+
trigger: 'clientSign rejection',
|
|
3757
|
+
confirmedMismatch: true
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3538
3761
|
signature = await Promise.race([
|
|
3539
3762
|
clientSignPromise,
|
|
3540
|
-
sseErrorPromise
|
|
3763
|
+
sseErrorPromise,
|
|
3764
|
+
staleSharesPromise
|
|
3541
3765
|
]);
|
|
3542
3766
|
} catch (signError) {
|
|
3543
|
-
//
|
|
3544
|
-
|
|
3545
|
-
|
|
3767
|
+
// The 409 stale-shares preflight is handled exclusively by the outer
|
|
3768
|
+
// catch (heal + transparent retry) — skip the generic heal here so the
|
|
3769
|
+
// ground-truth check runs once, right before the retry decision.
|
|
3770
|
+
if (isStaleClientSharesError(signError)) {
|
|
3771
|
+
throw signError;
|
|
3772
|
+
}
|
|
3773
|
+
// Heal before rethrowing so the caller's retry succeeds. Run the
|
|
3774
|
+
// ground-truth check whatever the raced error was: a generic SSE
|
|
3775
|
+
// error frequently wins the race while the client-side mismatch
|
|
3776
|
+
// settles later, so error-string inspection alone is timing-dependent.
|
|
3777
|
+
// The check no-ops when shares are fresh.
|
|
3778
|
+
await this.healStaleShares({
|
|
3546
3779
|
accountAddress,
|
|
3780
|
+
chainName,
|
|
3547
3781
|
password,
|
|
3548
3782
|
signedSessionId,
|
|
3549
3783
|
walletOperation: WalletOperation.SIGN_MESSAGE,
|
|
3550
3784
|
mfaToken,
|
|
3785
|
+
dynamicRequestId,
|
|
3551
3786
|
traceContext,
|
|
3552
|
-
|
|
3787
|
+
bitcoinConfig,
|
|
3788
|
+
trigger: 'sign failure',
|
|
3789
|
+
confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatchError !== undefined
|
|
3553
3790
|
});
|
|
3554
3791
|
throw signError;
|
|
3555
3792
|
}
|
|
@@ -3565,6 +3802,51 @@ class DynamicWalletClient {
|
|
|
3565
3802
|
}, this.getTraceContext(traceContext)));
|
|
3566
3803
|
return signature;
|
|
3567
3804
|
} catch (error) {
|
|
3805
|
+
// Server-side stale-share preflight (409 STALE_CLIENT_SHARES): the
|
|
3806
|
+
// request was rejected BEFORE any room/ceremony work, and the onError
|
|
3807
|
+
// wrappers above kept it away from the host — so the host request is
|
|
3808
|
+
// still pending. Heal the local shares from backup and transparently
|
|
3809
|
+
// retry once; the host only ever sees the (successful) outcome.
|
|
3810
|
+
if (!staleShareRetry && isStaleClientSharesError(error)) {
|
|
3811
|
+
// Heal first (re-sync local shares + walletMap from the server), then
|
|
3812
|
+
// retry regardless of whether shares were replaced: even a "fresh"
|
|
3813
|
+
// verdict means the heal's getWallets() refreshed the stale
|
|
3814
|
+
// walletMap.shareSetId the server rejected. The retry is guarded to
|
|
3815
|
+
// run at most once; if it 409s again the error surfaces normally.
|
|
3816
|
+
await this.healStaleShares({
|
|
3817
|
+
accountAddress,
|
|
3818
|
+
chainName,
|
|
3819
|
+
password,
|
|
3820
|
+
signedSessionId,
|
|
3821
|
+
walletOperation: WalletOperation.SIGN_MESSAGE,
|
|
3822
|
+
mfaToken,
|
|
3823
|
+
dynamicRequestId,
|
|
3824
|
+
traceContext,
|
|
3825
|
+
bitcoinConfig,
|
|
3826
|
+
trigger: 'server 409 stale-shares preflight',
|
|
3827
|
+
confirmedMismatch: true
|
|
3828
|
+
});
|
|
3829
|
+
this.logger.info('[StaleShareCheck] retrying sign transparently after 409 stale-shares preflight', _extends({
|
|
3830
|
+
accountAddress,
|
|
3831
|
+
chainName,
|
|
3832
|
+
dynamicRequestId
|
|
3833
|
+
}, this.getTraceContext(traceContext)));
|
|
3834
|
+
return await this.internalSign({
|
|
3835
|
+
accountAddress,
|
|
3836
|
+
message,
|
|
3837
|
+
chainName,
|
|
3838
|
+
password,
|
|
3839
|
+
isFormatted,
|
|
3840
|
+
signedSessionId,
|
|
3841
|
+
mfaToken,
|
|
3842
|
+
elevatedAccessToken,
|
|
3843
|
+
context,
|
|
3844
|
+
onError,
|
|
3845
|
+
traceContext,
|
|
3846
|
+
bitcoinConfig,
|
|
3847
|
+
staleShareRetry: true
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3568
3850
|
logError({
|
|
3569
3851
|
message: 'Error in sign',
|
|
3570
3852
|
error: error,
|
|
@@ -3578,12 +3860,13 @@ class DynamicWalletClient {
|
|
|
3578
3860
|
throw error;
|
|
3579
3861
|
}
|
|
3580
3862
|
}
|
|
3581
|
-
async refreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
|
|
3863
|
+
async refreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, sessionPublicKey, mfaToken, elevatedAccessToken, traceContext }) {
|
|
3582
3864
|
return WalletQueueManager.queueHeavyOperation(accountAddress, WalletOperation.REFRESH, ()=>this.internalRefreshWalletAccountShares({
|
|
3583
3865
|
accountAddress,
|
|
3584
3866
|
chainName,
|
|
3585
3867
|
password,
|
|
3586
3868
|
signedSessionId,
|
|
3869
|
+
sessionPublicKey,
|
|
3587
3870
|
mfaToken,
|
|
3588
3871
|
elevatedAccessToken,
|
|
3589
3872
|
traceContext
|
|
@@ -3650,7 +3933,7 @@ class DynamicWalletClient {
|
|
|
3650
3933
|
addressType: addressType
|
|
3651
3934
|
} : undefined;
|
|
3652
3935
|
}
|
|
3653
|
-
async internalRefreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
|
|
3936
|
+
async internalRefreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, sessionPublicKey, mfaToken, elevatedAccessToken, traceContext, staleShareRetry = false }) {
|
|
3654
3937
|
const dynamicRequestId = v4();
|
|
3655
3938
|
try {
|
|
3656
3939
|
this.assertPasswordRequired(password);
|
|
@@ -3742,16 +4025,19 @@ class DynamicWalletClient {
|
|
|
3742
4025
|
try {
|
|
3743
4026
|
refreshResults = await Promise.all(clientKeyShares.map((clientKeyShare)=>mpcSigner.refresh(roomId, clientKeyShare)));
|
|
3744
4027
|
} catch (refreshError) {
|
|
3745
|
-
//
|
|
3746
|
-
await this.
|
|
3747
|
-
error: refreshError,
|
|
4028
|
+
// Heal stale local shares before rethrowing so a retry succeeds.
|
|
4029
|
+
await this.healStaleShares({
|
|
3748
4030
|
accountAddress,
|
|
4031
|
+
chainName,
|
|
3749
4032
|
password,
|
|
3750
4033
|
signedSessionId,
|
|
3751
4034
|
walletOperation: WalletOperation.REFRESH,
|
|
3752
4035
|
mfaToken,
|
|
4036
|
+
dynamicRequestId,
|
|
3753
4037
|
traceContext,
|
|
3754
|
-
|
|
4038
|
+
bitcoinConfig,
|
|
4039
|
+
trigger: 'refresh failure',
|
|
4040
|
+
confirmedMismatch: isPublicKeyMismatchError(refreshError)
|
|
3755
4041
|
});
|
|
3756
4042
|
throw refreshError;
|
|
3757
4043
|
}
|
|
@@ -3777,7 +4063,8 @@ class DynamicWalletClient {
|
|
|
3777
4063
|
accountAddress,
|
|
3778
4064
|
clientKeyShares: refreshResults,
|
|
3779
4065
|
password: password != null ? password : this.environmentId,
|
|
3780
|
-
signedSessionId
|
|
4066
|
+
signedSessionId,
|
|
4067
|
+
sessionPublicKey
|
|
3781
4068
|
})
|
|
3782
4069
|
});
|
|
3783
4070
|
await this.setClientKeySharesToStorage({
|
|
@@ -3803,6 +4090,42 @@ class DynamicWalletClient {
|
|
|
3803
4090
|
});
|
|
3804
4091
|
throw error;
|
|
3805
4092
|
}
|
|
4093
|
+
// Server-side stale-share preflight (409 STALE_CLIENT_SHARES): rejected
|
|
4094
|
+
// BEFORE any room/ceremony work, so nothing was rotated. Heal the local
|
|
4095
|
+
// shares from backup and transparently retry once — even a "fresh" heal
|
|
4096
|
+
// verdict matters, because its getWallets() refreshed the stale
|
|
4097
|
+
// walletMap.shareSetId the server rejected.
|
|
4098
|
+
if (!staleShareRetry && isStaleClientSharesError(error)) {
|
|
4099
|
+
await this.healStaleShares({
|
|
4100
|
+
accountAddress,
|
|
4101
|
+
chainName,
|
|
4102
|
+
password,
|
|
4103
|
+
signedSessionId,
|
|
4104
|
+
walletOperation: WalletOperation.REFRESH,
|
|
4105
|
+
mfaToken,
|
|
4106
|
+
dynamicRequestId,
|
|
4107
|
+
traceContext,
|
|
4108
|
+
bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress),
|
|
4109
|
+
trigger: 'server 409 stale-shares preflight (refresh)',
|
|
4110
|
+
confirmedMismatch: true
|
|
4111
|
+
});
|
|
4112
|
+
this.logger.info('[StaleShareCheck] retrying refresh transparently after 409 stale-shares preflight', _extends({
|
|
4113
|
+
accountAddress,
|
|
4114
|
+
chainName,
|
|
4115
|
+
dynamicRequestId
|
|
4116
|
+
}, this.getTraceContext(traceContext)));
|
|
4117
|
+
return await this.internalRefreshWalletAccountShares({
|
|
4118
|
+
accountAddress,
|
|
4119
|
+
chainName,
|
|
4120
|
+
password,
|
|
4121
|
+
signedSessionId,
|
|
4122
|
+
sessionPublicKey,
|
|
4123
|
+
mfaToken,
|
|
4124
|
+
elevatedAccessToken,
|
|
4125
|
+
traceContext,
|
|
4126
|
+
staleShareRetry: true
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
3806
4129
|
logError({
|
|
3807
4130
|
message: 'Error in refreshWalletAccountShares',
|
|
3808
4131
|
error: error,
|
|
@@ -3894,7 +4217,7 @@ class DynamicWalletClient {
|
|
|
3894
4217
|
existingClientKeyShares
|
|
3895
4218
|
};
|
|
3896
4219
|
}
|
|
3897
|
-
async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource }) {
|
|
4220
|
+
async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource }) {
|
|
3898
4221
|
return WalletQueueManager.queueHeavyOperation(accountAddress, WalletOperation.RESHARE, ()=>this.internalReshare({
|
|
3899
4222
|
chainName,
|
|
3900
4223
|
accountAddress,
|
|
@@ -3902,6 +4225,7 @@ class DynamicWalletClient {
|
|
|
3902
4225
|
newThresholdSignatureScheme,
|
|
3903
4226
|
password,
|
|
3904
4227
|
signedSessionId,
|
|
4228
|
+
sessionPublicKey,
|
|
3905
4229
|
cloudProviders,
|
|
3906
4230
|
delegateToProjectEnvironment,
|
|
3907
4231
|
mfaToken,
|
|
@@ -4386,7 +4710,7 @@ class DynamicWalletClient {
|
|
|
4386
4710
|
clientKeySharesBackupInfo: updatedBackupInfo
|
|
4387
4711
|
} : {}));
|
|
4388
4712
|
}
|
|
4389
|
-
async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource }) {
|
|
4713
|
+
async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, staleShareRetry = false }) {
|
|
4390
4714
|
const dynamicRequestId = v4();
|
|
4391
4715
|
// Password validation - wrapped in try-catch for consistent error handling
|
|
4392
4716
|
// This path should NOT wipe key shares on failure (shares remain valid)
|
|
@@ -4598,15 +4922,18 @@ class DynamicWalletClient {
|
|
|
4598
4922
|
existingReshareResults = existingSettled.map((r)=>r.value);
|
|
4599
4923
|
newReshareResults = newSettled.map((r)=>r.value);
|
|
4600
4924
|
} catch (reshareError) {
|
|
4601
|
-
//
|
|
4602
|
-
await this.
|
|
4603
|
-
error: reshareError,
|
|
4925
|
+
// Heal stale local shares before rethrowing so a retry succeeds.
|
|
4926
|
+
await this.healStaleShares({
|
|
4604
4927
|
accountAddress,
|
|
4928
|
+
chainName,
|
|
4605
4929
|
password,
|
|
4606
4930
|
signedSessionId,
|
|
4607
4931
|
walletOperation: WalletOperation.RESHARE,
|
|
4608
4932
|
mfaToken,
|
|
4609
|
-
dynamicRequestId
|
|
4933
|
+
dynamicRequestId,
|
|
4934
|
+
bitcoinConfig,
|
|
4935
|
+
trigger: 'reshare failure',
|
|
4936
|
+
confirmedMismatch: isPublicKeyMismatchError(reshareError)
|
|
4610
4937
|
});
|
|
4611
4938
|
throw reshareError;
|
|
4612
4939
|
}
|
|
@@ -4641,6 +4968,7 @@ class DynamicWalletClient {
|
|
|
4641
4968
|
accountAddress,
|
|
4642
4969
|
password,
|
|
4643
4970
|
signedSessionId,
|
|
4971
|
+
sessionPublicKey,
|
|
4644
4972
|
distribution,
|
|
4645
4973
|
googleDriveAccessToken,
|
|
4646
4974
|
googleDriveTokenSource
|
|
@@ -4678,6 +5006,47 @@ class DynamicWalletClient {
|
|
|
4678
5006
|
}
|
|
4679
5007
|
});
|
|
4680
5008
|
} catch (error) {
|
|
5009
|
+
// Server-side stale-share preflight (409 STALE_CLIENT_SHARES): rejected
|
|
5010
|
+
// BEFORE any room/ceremony work, so local shares are intact (nothing to
|
|
5011
|
+
// wipe). Heal from backup and transparently retry once — even a "fresh"
|
|
5012
|
+
// heal verdict matters, because its getWallets() refreshed the stale
|
|
5013
|
+
// walletMap.shareSetId the server rejected.
|
|
5014
|
+
if (!staleShareRetry && isStaleClientSharesError(error)) {
|
|
5015
|
+
await this.healStaleShares({
|
|
5016
|
+
accountAddress,
|
|
5017
|
+
chainName,
|
|
5018
|
+
password,
|
|
5019
|
+
signedSessionId,
|
|
5020
|
+
walletOperation: WalletOperation.RESHARE,
|
|
5021
|
+
mfaToken,
|
|
5022
|
+
dynamicRequestId,
|
|
5023
|
+
bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress),
|
|
5024
|
+
trigger: 'server 409 stale-shares preflight (reshare)',
|
|
5025
|
+
confirmedMismatch: true
|
|
5026
|
+
});
|
|
5027
|
+
this.logger.info('[StaleShareCheck] retrying reshare transparently after 409 stale-shares preflight', {
|
|
5028
|
+
accountAddress,
|
|
5029
|
+
chainName,
|
|
5030
|
+
dynamicRequestId
|
|
5031
|
+
});
|
|
5032
|
+
return await this.internalReshare({
|
|
5033
|
+
chainName,
|
|
5034
|
+
accountAddress,
|
|
5035
|
+
oldThresholdSignatureScheme,
|
|
5036
|
+
newThresholdSignatureScheme,
|
|
5037
|
+
password,
|
|
5038
|
+
signedSessionId,
|
|
5039
|
+
sessionPublicKey,
|
|
5040
|
+
cloudProviders,
|
|
5041
|
+
delegateToProjectEnvironment,
|
|
5042
|
+
mfaToken,
|
|
5043
|
+
elevatedAccessToken,
|
|
5044
|
+
revokeDelegation,
|
|
5045
|
+
googleDriveAccessToken,
|
|
5046
|
+
googleDriveTokenSource,
|
|
5047
|
+
staleShareRetry: true
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
4681
5050
|
logError({
|
|
4682
5051
|
message: 'Error in reshare, resetting wallet to previous state',
|
|
4683
5052
|
error: error,
|
|
@@ -4692,9 +5061,12 @@ class DynamicWalletClient {
|
|
|
4692
5061
|
dynamicRequestId
|
|
4693
5062
|
}
|
|
4694
5063
|
});
|
|
4695
|
-
// Skip the wipe on mismatch —
|
|
4696
|
-
//
|
|
4697
|
-
|
|
5064
|
+
// Skip the wipe on mismatch — healStaleShares just restored these
|
|
5065
|
+
// shares from backup, undoing that would break the caller's retry.
|
|
5066
|
+
// Same for a 409 stale-shares preflight (only reachable here on the
|
|
5067
|
+
// retried attempt): the request was rejected before any ceremony, so
|
|
5068
|
+
// local shares are consistent — wiping would strand the user.
|
|
5069
|
+
if (!isPublicKeyMismatchError(error) && !isStaleClientSharesError(error)) {
|
|
4698
5070
|
this.updateWalletMap(accountAddress, {
|
|
4699
5071
|
thresholdSignatureScheme: oldThresholdSignatureScheme
|
|
4700
5072
|
});
|
|
@@ -4706,7 +5078,7 @@ class DynamicWalletClient {
|
|
|
4706
5078
|
throw error;
|
|
4707
5079
|
}
|
|
4708
5080
|
}
|
|
4709
|
-
async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
|
|
5081
|
+
async performDelegationOperation({ accountAddress, password, signedSessionId, sessionPublicKey, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
|
|
4710
5082
|
try {
|
|
4711
5083
|
const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
|
|
4712
5084
|
if (!delegateToProjectEnvironment) {
|
|
@@ -4735,6 +5107,7 @@ class DynamicWalletClient {
|
|
|
4735
5107
|
newThresholdSignatureScheme,
|
|
4736
5108
|
password,
|
|
4737
5109
|
signedSessionId,
|
|
5110
|
+
sessionPublicKey,
|
|
4738
5111
|
cloudProviders: activeProviders,
|
|
4739
5112
|
// Server soft-deletes the wallet API key on revoke; a delegated
|
|
4740
5113
|
// distribution would 400 on the follow-up delivery POST.
|
|
@@ -4753,7 +5126,7 @@ class DynamicWalletClient {
|
|
|
4753
5126
|
throw error;
|
|
4754
5127
|
}
|
|
4755
5128
|
}
|
|
4756
|
-
async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
|
|
5129
|
+
async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken }) {
|
|
4757
5130
|
var _this_featureFlags;
|
|
4758
5131
|
const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
|
|
4759
5132
|
if (useShareSetReshare) {
|
|
@@ -4778,6 +5151,7 @@ class DynamicWalletClient {
|
|
|
4778
5151
|
accountAddress,
|
|
4779
5152
|
password,
|
|
4780
5153
|
signedSessionId,
|
|
5154
|
+
sessionPublicKey,
|
|
4781
5155
|
mfaToken,
|
|
4782
5156
|
// FF on: reshare to 2/2 — the new delegated share set is always 2/2
|
|
4783
5157
|
// (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
|
|
@@ -4792,7 +5166,7 @@ class DynamicWalletClient {
|
|
|
4792
5166
|
const delegatedKeyShares = backupInfo.backups[BackupLocation.DELEGATED] || [];
|
|
4793
5167
|
return delegatedKeyShares;
|
|
4794
5168
|
}
|
|
4795
|
-
async revokeDelegation({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
|
|
5169
|
+
async revokeDelegation({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken }) {
|
|
4796
5170
|
var _this_featureFlags;
|
|
4797
5171
|
const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
|
|
4798
5172
|
if (useShareSetReshare) {
|
|
@@ -4838,6 +5212,7 @@ class DynamicWalletClient {
|
|
|
4838
5212
|
accountAddress,
|
|
4839
5213
|
password,
|
|
4840
5214
|
signedSessionId,
|
|
5215
|
+
sessionPublicKey,
|
|
4841
5216
|
mfaToken,
|
|
4842
5217
|
newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
|
|
4843
5218
|
revokeDelegation: true,
|
|
@@ -4854,6 +5229,22 @@ class DynamicWalletClient {
|
|
|
4854
5229
|
return new BIP340KeygenResult(extractedPubkey, secretShare);
|
|
4855
5230
|
}
|
|
4856
5231
|
async exportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
|
|
5232
|
+
// Public entry point delegates to the private implementation. Keeping the
|
|
5233
|
+
// `staleShareRetry` flag off the public signature mirrors sign/refresh/
|
|
5234
|
+
// reshare and ensures the transparent 409 retry can't be suppressed by
|
|
5235
|
+
// external callers or intercepted by subclass overrides.
|
|
5236
|
+
return this.internalExportKey({
|
|
5237
|
+
accountAddress,
|
|
5238
|
+
chainName,
|
|
5239
|
+
bitcoinConfig,
|
|
5240
|
+
password,
|
|
5241
|
+
signedSessionId,
|
|
5242
|
+
mfaToken,
|
|
5243
|
+
elevatedAccessToken,
|
|
5244
|
+
traceContext
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
async internalExportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext, staleShareRetry = false }) {
|
|
4857
5248
|
const dynamicRequestId = v4();
|
|
4858
5249
|
try {
|
|
4859
5250
|
const wallet = await this.getWallet({
|
|
@@ -4898,10 +5289,51 @@ class DynamicWalletClient {
|
|
|
4898
5289
|
roomId: data.roomId
|
|
4899
5290
|
}, this.getTraceContext(traceContext)));
|
|
4900
5291
|
const derivedPrivateKey = await this.derivePrivateKeyFromExport(mpcSigner, keyExportRaw, wallet);
|
|
5292
|
+
// derivePrivateKeyFromExport returns undefined for unsupported signer
|
|
5293
|
+
// types; fail loudly rather than returning undefined as a string.
|
|
5294
|
+
if (derivedPrivateKey === undefined) {
|
|
5295
|
+
throw new Error(`Private key export is not supported for chain ${chainName}`);
|
|
5296
|
+
}
|
|
4901
5297
|
return {
|
|
4902
5298
|
derivedPrivateKey
|
|
4903
5299
|
};
|
|
4904
5300
|
} catch (error) {
|
|
5301
|
+
// Server-side stale-share preflight (409 STALE_CLIENT_SHARES): rejected
|
|
5302
|
+
// BEFORE any room/ceremony work. Heal the local shares from backup and
|
|
5303
|
+
// transparently retry once — even a "fresh" heal verdict matters,
|
|
5304
|
+
// because its getWallets() refreshed the stale walletMap.shareSetId the
|
|
5305
|
+
// server rejected.
|
|
5306
|
+
if (!staleShareRetry && isStaleClientSharesError(error)) {
|
|
5307
|
+
await this.healStaleShares({
|
|
5308
|
+
accountAddress,
|
|
5309
|
+
chainName,
|
|
5310
|
+
password,
|
|
5311
|
+
signedSessionId,
|
|
5312
|
+
walletOperation: WalletOperation.EXPORT_PRIVATE_KEY,
|
|
5313
|
+
mfaToken,
|
|
5314
|
+
dynamicRequestId,
|
|
5315
|
+
traceContext,
|
|
5316
|
+
bitcoinConfig,
|
|
5317
|
+
trigger: 'server 409 stale-shares preflight (export)',
|
|
5318
|
+
confirmedMismatch: true
|
|
5319
|
+
});
|
|
5320
|
+
this.logger.info('[StaleShareCheck] retrying export transparently after 409 stale-shares preflight', _extends({
|
|
5321
|
+
accountAddress,
|
|
5322
|
+
chainName,
|
|
5323
|
+
dynamicRequestId
|
|
5324
|
+
}, this.getTraceContext(traceContext)));
|
|
5325
|
+
return await this.internalExportKey({
|
|
5326
|
+
accountAddress,
|
|
5327
|
+
chainName,
|
|
5328
|
+
bitcoinConfig,
|
|
5329
|
+
password,
|
|
5330
|
+
signedSessionId,
|
|
5331
|
+
mfaToken,
|
|
5332
|
+
elevatedAccessToken,
|
|
5333
|
+
traceContext,
|
|
5334
|
+
staleShareRetry: true
|
|
5335
|
+
});
|
|
5336
|
+
}
|
|
4905
5337
|
logError({
|
|
4906
5338
|
message: 'Error in exportKey',
|
|
4907
5339
|
error: error,
|
|
@@ -4911,6 +5343,22 @@ class DynamicWalletClient {
|
|
|
4911
5343
|
dynamicRequestId
|
|
4912
5344
|
}, this.getTraceContext(traceContext))
|
|
4913
5345
|
});
|
|
5346
|
+
// Heal stale local shares before rethrowing so a retry succeeds —
|
|
5347
|
+
// export reconstructs from the local share, so a generation rotated on
|
|
5348
|
+
// another device fails here too.
|
|
5349
|
+
await this.healStaleShares({
|
|
5350
|
+
accountAddress,
|
|
5351
|
+
chainName,
|
|
5352
|
+
password,
|
|
5353
|
+
signedSessionId,
|
|
5354
|
+
walletOperation: WalletOperation.EXPORT_PRIVATE_KEY,
|
|
5355
|
+
mfaToken,
|
|
5356
|
+
dynamicRequestId,
|
|
5357
|
+
traceContext,
|
|
5358
|
+
bitcoinConfig,
|
|
5359
|
+
trigger: 'export failure',
|
|
5360
|
+
confirmedMismatch: isPublicKeyMismatchError(error)
|
|
5361
|
+
});
|
|
4914
5362
|
throw error;
|
|
4915
5363
|
}
|
|
4916
5364
|
}
|
|
@@ -5280,6 +5728,14 @@ class DynamicWalletClient {
|
|
|
5280
5728
|
* Auto-recovery logic has been removed in favor of the queue pattern.
|
|
5281
5729
|
* Callers should handle recovery explicitly if needed.
|
|
5282
5730
|
*/ async ensureClientShare(accountAddress) {
|
|
5731
|
+
var _WalletQueueManager_getPendingRecoveryPromise;
|
|
5732
|
+
// A recovery may be in flight (e.g. a stale-share heal scheduled when a
|
|
5733
|
+
// previous sign hit a public-key mismatch). Wait for it so this operation
|
|
5734
|
+
// reads the recovered shares instead of the superseded ones. Recovery
|
|
5735
|
+
// failures are logged where they happen; this operation should proceed
|
|
5736
|
+
// with whatever shares exist rather than fail on the recovery.
|
|
5737
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
5738
|
+
await ((_WalletQueueManager_getPendingRecoveryPromise = WalletQueueManager.getPendingRecoveryPromise(accountAddress)) == null ? void 0 : _WalletQueueManager_getPendingRecoveryPromise.catch(()=>{}));
|
|
5283
5739
|
const shares = await this.getClientKeySharesFromStorage({
|
|
5284
5740
|
accountAddress
|
|
5285
5741
|
});
|
|
@@ -5288,13 +5744,14 @@ class DynamicWalletClient {
|
|
|
5288
5744
|
}
|
|
5289
5745
|
return shares;
|
|
5290
5746
|
}
|
|
5291
|
-
async backupToDynamicBackend({ walletId, clientShares, password, signedSessionId, dynamicRequestId, chainName, bitcoinConfig, isPasswordEncrypted, accountAddress, preEncryptedShares, passwordUpdateBatchId, encryptionVersion = DEFAULT_ENCRYPTION_VERSION }) {
|
|
5747
|
+
async backupToDynamicBackend({ walletId, clientShares, password, signedSessionId, sessionPublicKey, dynamicRequestId, chainName, bitcoinConfig, isPasswordEncrypted, accountAddress, preEncryptedShares, passwordUpdateBatchId, encryptionVersion = DEFAULT_ENCRYPTION_VERSION }) {
|
|
5292
5748
|
var _this_getWalletFromMap;
|
|
5293
5749
|
const encryptedDynamicShares = preEncryptedShares != null ? preEncryptedShares : await Promise.all(clientShares.map((keyShare)=>this.encryptKeyShare({
|
|
5294
5750
|
keyShare,
|
|
5295
5751
|
password,
|
|
5296
5752
|
version: encryptionVersion
|
|
5297
5753
|
})));
|
|
5754
|
+
const resolvedSessionPublicKey = sessionPublicKey != null ? sessionPublicKey : await (this.getSessionPublicKeyCallback == null ? void 0 : this.getSessionPublicKeyCallback.call(this));
|
|
5298
5755
|
const data = await this.apiClient.storeEncryptedBackupByWallet({
|
|
5299
5756
|
walletId,
|
|
5300
5757
|
shareSetId: (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.shareSetId,
|
|
@@ -5302,6 +5759,7 @@ class DynamicWalletClient {
|
|
|
5302
5759
|
passwordEncrypted: isPasswordEncrypted,
|
|
5303
5760
|
encryptionVersion,
|
|
5304
5761
|
signedSessionId,
|
|
5762
|
+
sessionPublicKey: resolvedSessionPublicKey,
|
|
5305
5763
|
requiresSignedSessionId: this.requiresSignedSessionId(),
|
|
5306
5764
|
dynamicRequestId
|
|
5307
5765
|
});
|
|
@@ -5450,7 +5908,7 @@ class DynamicWalletClient {
|
|
|
5450
5908
|
}
|
|
5451
5909
|
return location;
|
|
5452
5910
|
}
|
|
5453
|
-
async backupSharesWithDistribution({ accountAddress, password, signedSessionId, distribution, preserveDelegatedLocation = false, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
|
|
5911
|
+
async backupSharesWithDistribution({ accountAddress, password, signedSessionId, sessionPublicKey, distribution, preserveDelegatedLocation = false, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
|
|
5454
5912
|
const dynamicRequestId = v4();
|
|
5455
5913
|
// Get wallet data early for logging context (also used in catch block)
|
|
5456
5914
|
const walletData = this.getWalletFromMap(accountAddress);
|
|
@@ -5529,6 +5987,7 @@ class DynamicWalletClient {
|
|
|
5529
5987
|
clientShares: distribution.clientShares,
|
|
5530
5988
|
password,
|
|
5531
5989
|
signedSessionId: resolvedSignedSessionId,
|
|
5990
|
+
sessionPublicKey,
|
|
5532
5991
|
dynamicRequestId,
|
|
5533
5992
|
chainName: walletData.chainName,
|
|
5534
5993
|
bitcoinConfig,
|
|
@@ -5725,29 +6184,23 @@ class DynamicWalletClient {
|
|
|
5725
6184
|
// re-enter the outer operation with correct context. Skipped on internal post-rotation
|
|
5726
6185
|
// flows that pass clientKeyShares explicitly (the refresh path).
|
|
5727
6186
|
async ensureLocalSharesAreFresh({ accountAddress, walletData, localShares, password, signedSessionId }) {
|
|
5728
|
-
var _freshBackupInfo_backups;
|
|
5729
6187
|
// Always round-trip to the server — walletMap is populated at wallet-load and is
|
|
5730
6188
|
// not invalidated when another tab rotates shares, so trusting it would
|
|
5731
6189
|
// false-positive on cross-tab refresh.
|
|
5732
6190
|
const { chainName } = walletData;
|
|
5733
|
-
const
|
|
5734
|
-
|
|
5735
|
-
baseRelayUrl: this.baseMPCRelayApiUrl,
|
|
5736
|
-
bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress)
|
|
6191
|
+
const freshBackupInfo = await this.fetchFreshWalletBackupInfo({
|
|
6192
|
+
accountAddress
|
|
5737
6193
|
});
|
|
5738
|
-
const
|
|
5739
|
-
this.fetchFreshWalletBackupInfo({
|
|
5740
|
-
accountAddress
|
|
5741
|
-
}),
|
|
5742
|
-
Promise.all(localShares.map((share)=>this.getKeygenIdForShare(mpcSigner, share)))
|
|
5743
|
-
]);
|
|
5744
|
-
var _freshBackupInfo_backups_BackupLocation_DYNAMIC;
|
|
5745
|
-
const recordedKeygenIds = ((_freshBackupInfo_backups_BackupLocation_DYNAMIC = freshBackupInfo == null ? void 0 : (_freshBackupInfo_backups = freshBackupInfo.backups) == null ? void 0 : _freshBackupInfo_backups[BackupLocation.DYNAMIC]) != null ? _freshBackupInfo_backups_BackupLocation_DYNAMIC : []).map((entry)=>entry.keygenId).filter((id)=>typeof id === 'string' && id.length > 0);
|
|
6194
|
+
const recordedKeygenIds = recordedDynamicKeygenIds(freshBackupInfo);
|
|
5746
6195
|
// First-time backup or legacy data (server didn't populate keygenId yet) — nothing to compare.
|
|
5747
6196
|
if (recordedKeygenIds.length === 0) return;
|
|
5748
|
-
const
|
|
5749
|
-
|
|
5750
|
-
|
|
6197
|
+
const { fresh, localKeygenIds } = await this.compareShareGenerations({
|
|
6198
|
+
accountAddress,
|
|
6199
|
+
chainName,
|
|
6200
|
+
localShares,
|
|
6201
|
+
recordedKeygenIds
|
|
6202
|
+
});
|
|
6203
|
+
if (fresh) return;
|
|
5751
6204
|
this.logger.warn('[storeEncryptedBackupByWallet] Stale local shares detected; refreshing local storage', {
|
|
5752
6205
|
accountAddress,
|
|
5753
6206
|
walletId: walletData.walletId,
|
|
@@ -5774,7 +6227,7 @@ class DynamicWalletClient {
|
|
|
5774
6227
|
const recoveryPassword = freshBackupInfo.passwordEncrypted ? password : undefined;
|
|
5775
6228
|
try {
|
|
5776
6229
|
// walletMap was already refreshed by fetchFreshWalletBackupInfo above, so we skip the
|
|
5777
|
-
// getWallets() refresh that
|
|
6230
|
+
// getWallets() refresh that verifyAndRecoverStaleShare performs for its own callers.
|
|
5778
6231
|
await this.internalRecoverEncryptedBackupByWallet({
|
|
5779
6232
|
accountAddress,
|
|
5780
6233
|
password: recoveryPassword,
|
|
@@ -5832,7 +6285,7 @@ class DynamicWalletClient {
|
|
|
5832
6285
|
* @param params.signedSessionId - Optional signed session ID for authentication
|
|
5833
6286
|
* @param params.backupToGoogleDrive - Whether to backup to Google Drive (defaults to false)
|
|
5834
6287
|
* @returns Promise with backup metadata including share locations and IDs
|
|
5835
|
-
*/ async storeEncryptedBackupByWallet({ accountAddress, clientKeyShares = undefined, password = undefined, signedSessionId, cloudProviders = [], delegatedKeyshare = undefined, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
|
|
6288
|
+
*/ async storeEncryptedBackupByWallet({ accountAddress, clientKeyShares = undefined, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegatedKeyshare = undefined, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
|
|
5836
6289
|
var _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
5837
6290
|
const hasProvidedShares = clientKeyShares !== undefined;
|
|
5838
6291
|
const keySharesToBackup = clientKeyShares != null ? clientKeyShares : await this.getClientKeySharesFromStorage({
|
|
@@ -5902,6 +6355,7 @@ class DynamicWalletClient {
|
|
|
5902
6355
|
accountAddress,
|
|
5903
6356
|
password,
|
|
5904
6357
|
signedSessionId,
|
|
6358
|
+
sessionPublicKey,
|
|
5905
6359
|
distribution,
|
|
5906
6360
|
preserveDelegatedLocation,
|
|
5907
6361
|
passwordUpdateBatchId,
|
|
@@ -5974,7 +6428,7 @@ class DynamicWalletClient {
|
|
|
5974
6428
|
errorContext
|
|
5975
6429
|
} : {}));
|
|
5976
6430
|
}
|
|
5977
|
-
async updatePassword({ accountAddress, existingPassword, newPassword, signedSessionId, passwordUpdateBatchId }) {
|
|
6431
|
+
async updatePassword({ accountAddress, existingPassword, newPassword, signedSessionId, sessionPublicKey, passwordUpdateBatchId }) {
|
|
5978
6432
|
const dynamicRequestId = v4();
|
|
5979
6433
|
try {
|
|
5980
6434
|
const wallet = this.getWalletFromMap(accountAddress);
|
|
@@ -5994,6 +6448,7 @@ class DynamicWalletClient {
|
|
|
5994
6448
|
accountAddress,
|
|
5995
6449
|
password: newPassword,
|
|
5996
6450
|
signedSessionId,
|
|
6451
|
+
sessionPublicKey,
|
|
5997
6452
|
passwordUpdateBatchId
|
|
5998
6453
|
});
|
|
5999
6454
|
this.logPasswordOperationSuccess('updatePassword', {
|
|
@@ -6023,7 +6478,7 @@ class DynamicWalletClient {
|
|
|
6023
6478
|
throw error;
|
|
6024
6479
|
}
|
|
6025
6480
|
}
|
|
6026
|
-
async setPassword({ accountAddress, newPassword, signedSessionId, passwordUpdateBatchId }) {
|
|
6481
|
+
async setPassword({ accountAddress, newPassword, signedSessionId, sessionPublicKey, passwordUpdateBatchId }) {
|
|
6027
6482
|
const dynamicRequestId = v4();
|
|
6028
6483
|
try {
|
|
6029
6484
|
const wallet = this.getWalletFromMap(accountAddress);
|
|
@@ -6034,6 +6489,7 @@ class DynamicWalletClient {
|
|
|
6034
6489
|
accountAddress,
|
|
6035
6490
|
password: newPassword,
|
|
6036
6491
|
signedSessionId,
|
|
6492
|
+
sessionPublicKey,
|
|
6037
6493
|
passwordUpdateBatchId
|
|
6038
6494
|
});
|
|
6039
6495
|
this.logPasswordOperationSuccess('setPassword', {
|
|
@@ -6454,7 +6910,7 @@ class DynamicWalletClient {
|
|
|
6454
6910
|
* Internal helper method that handles the complete flow for ensuring wallet key shares are backed up to a cloud provider.
|
|
6455
6911
|
* - For 2-of-2 wallets: Automatically reshares to 2-of-3 threshold, then distributes shares (1 to backend, 1 to cloud)
|
|
6456
6912
|
* - For 2-of-3 wallets: Call storeEncryptedBackupByWallet to backup for backend and cloud
|
|
6457
|
-
*/ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, backupLocation, googleDriveAccessToken, googleDriveTokenSource }) {
|
|
6913
|
+
*/ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, sessionPublicKey, backupLocation, googleDriveAccessToken, googleDriveTokenSource }) {
|
|
6458
6914
|
try {
|
|
6459
6915
|
await this.getWallet({
|
|
6460
6916
|
accountAddress,
|
|
@@ -6473,6 +6929,7 @@ class DynamicWalletClient {
|
|
|
6473
6929
|
newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_THREE,
|
|
6474
6930
|
password,
|
|
6475
6931
|
signedSessionId,
|
|
6932
|
+
sessionPublicKey,
|
|
6476
6933
|
cloudProviders: [
|
|
6477
6934
|
backupLocation
|
|
6478
6935
|
],
|
|
@@ -6484,6 +6941,7 @@ class DynamicWalletClient {
|
|
|
6484
6941
|
accountAddress,
|
|
6485
6942
|
password,
|
|
6486
6943
|
signedSessionId,
|
|
6944
|
+
sessionPublicKey,
|
|
6487
6945
|
cloudProviders: [
|
|
6488
6946
|
backupLocation
|
|
6489
6947
|
],
|
|
@@ -6514,7 +6972,7 @@ class DynamicWalletClient {
|
|
|
6514
6972
|
* @param params.signedSessionId - Optional signed session ID for authentication
|
|
6515
6973
|
* @param params.googleDriveAccessToken - Optional Google OAuth access token. When provided, the SDK uses it directly
|
|
6516
6974
|
* instead of looking up the user's linked Google account and exchanging it for a token via the Dynamic backend.
|
|
6517
|
-
*/ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, googleDriveAccessToken }) {
|
|
6975
|
+
*/ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, sessionPublicKey, googleDriveAccessToken }) {
|
|
6518
6976
|
// Track whether the Drive token was supplied by the caller (external) or
|
|
6519
6977
|
// will be resolved from the user's stored OAuth account (stored_oauth), so
|
|
6520
6978
|
// backups using a token we don't control are auditable. Enum only — the
|
|
@@ -6534,6 +6992,7 @@ class DynamicWalletClient {
|
|
|
6534
6992
|
accountAddress,
|
|
6535
6993
|
password,
|
|
6536
6994
|
signedSessionId,
|
|
6995
|
+
sessionPublicKey,
|
|
6537
6996
|
backupLocation: BackupLocation.GOOGLE_DRIVE,
|
|
6538
6997
|
// Forward the resolved token so the downstream upload path doesn't
|
|
6539
6998
|
// re-fetch it via fetchGoogleDriveAccessToken.
|
|
@@ -6605,11 +7064,12 @@ class DynamicWalletClient {
|
|
|
6605
7064
|
* @param params.accountAddress - The wallet account address to backup
|
|
6606
7065
|
* @param params.password - Optional password for encryption (uses environment ID if not provided)
|
|
6607
7066
|
* @param params.signedSessionId - Optional signed session ID for authentication
|
|
6608
|
-
*/ async backupKeySharesToICloud({ accountAddress, password, signedSessionId }) {
|
|
7067
|
+
*/ async backupKeySharesToICloud({ accountAddress, password, signedSessionId, sessionPublicKey }) {
|
|
6609
7068
|
return this.backupKeySharesToCloudProvider({
|
|
6610
7069
|
accountAddress,
|
|
6611
7070
|
password,
|
|
6612
7071
|
signedSessionId,
|
|
7072
|
+
sessionPublicKey,
|
|
6613
7073
|
backupLocation: BackupLocation.ICLOUD
|
|
6614
7074
|
});
|
|
6615
7075
|
}
|
|
@@ -7764,13 +8224,16 @@ class DynamicWalletClient {
|
|
|
7764
8224
|
webLocksAvailable
|
|
7765
8225
|
});
|
|
7766
8226
|
// If less than or only 1 room left after removing one, trigger async creation for more rooms.
|
|
8227
|
+
// The catch prevents an unhandled promise rejection when the API
|
|
8228
|
+
// rate-limits this best-effort background call — createRooms already
|
|
8229
|
+
// logs the error at the appropriate level (warn for 429).
|
|
7767
8230
|
const remainingRooms = DynamicWalletClient.rooms[numberOfParties] || [];
|
|
7768
8231
|
if (remainingRooms.length <= 1) {
|
|
7769
8232
|
this.createRooms({
|
|
7770
8233
|
roomType,
|
|
7771
8234
|
thresholdSignatureScheme,
|
|
7772
8235
|
roomCount: ROOM_CACHE_COUNT
|
|
7773
|
-
});
|
|
8236
|
+
}).catch(()=>{});
|
|
7774
8237
|
}
|
|
7775
8238
|
return room;
|
|
7776
8239
|
}
|
|
@@ -7819,6 +8282,10 @@ class DynamicWalletClient {
|
|
|
7819
8282
|
if (internalOptions == null ? void 0 : internalOptions.getSignedSessionId) {
|
|
7820
8283
|
this.getSignedSessionIdCallback = internalOptions.getSignedSessionId;
|
|
7821
8284
|
}
|
|
8285
|
+
// Set session public key callback if provided (internal use only)
|
|
8286
|
+
if (internalOptions == null ? void 0 : internalOptions.getSessionPublicKey) {
|
|
8287
|
+
this.getSessionPublicKeyCallback = internalOptions.getSessionPublicKey;
|
|
8288
|
+
}
|
|
7822
8289
|
// Read the callback lazily so it tracks `getSignedSessionIdCallback` even
|
|
7823
8290
|
// when tests reassign it after construction.
|
|
7824
8291
|
this.signedSession = new SignedSessionManager({
|
|
@@ -7892,4 +8359,4 @@ DynamicWalletClient.roomsPersistChain = Promise.resolve();
|
|
|
7892
8359
|
// rooms back into the new user's storage (TOCTOU on the create→merge gap).
|
|
7893
8360
|
DynamicWalletClient.roomsGeneration = 0;
|
|
7894
8361
|
|
|
7895
|
-
export { BACKUP_FILENAME, CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX, DynamicWalletClient, ENVIRONMENT_SETTINGS_STORAGE_KEY, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_CREATE_WALLET_ACCOUNT, ERROR_EXPORT_PRIVATE_KEY, ERROR_IMPORT_PRIVATE_KEY, ERROR_INCORRECT_PASSWORD, ERROR_KEYGEN_FAILED, ERROR_MULTIPLE_WALLETS_PER_CHAIN, ERROR_NO_KEY_SHARES_BACKED_UP, ERROR_PASSWORD_MISMATCH, ERROR_PASSWORD_REQUIRED, ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET, ERROR_PUBLIC_KEY_MISMATCH, ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE, ERROR_SIGN_MESSAGE, ERROR_SIGN_TYPED_DATA, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, HEAVY_QUEUE_OPERATIONS, OperationSource, RECOVER_QUEUE_OPERATIONS, ROOM_CACHE_COUNT, ROOM_EXPIRATION_TIME, SIGNED_SESSION_ID_MIN_VERSION, SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE, SIGN_QUEUE_OPERATIONS, STORAGE_KEY, StaleLocalSharesError, WalletBusyError, WalletNotReadyError, cancelICloudAuth, createAddCloudProviderToExistingDelegationDistribution, createBackupData, createCloudProviderDistribution, createDelegationOnlyDistribution, createDelegationWithCloudProviderDistribution, createDynamicOnlyDistribution, deleteICloudBackup, downloadStringAsFile, extractPubkey, formatEvmMessage, formatMessage, formatTronMessage, getActiveCloudProviders, getBitcoinAddressTypeFromDerivationPath, getClientKeyShareBackupInfo, getClientKeyShareExportFileName, getDelegatedShareSet, getGoogleOAuthAccountId, getHttpStatus, getICloudBackup, getMPCSignatureScheme, getMPCSigner, hasCloudProviderBackup, hasDelegatedBackup, hasEncryptedSharesCached, initializeCloudKit, isBrowser, isHeavyQueueOperation, isHexString, isICloudAuthenticated, isNonRetryableCeremonyError, isPublicKeyMismatchError, isRecoverQueueOperation, isSignQueueOperation, listICloudBackups, markCeremonyErrorNonRetryable, readEnvironmentSettings, retryPromise, shouldReshareToSameBackups, timeoutPromise };
|
|
8362
|
+
export { BACKUP_FILENAME, CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX, DynamicWalletClient, ENVIRONMENT_SETTINGS_STORAGE_KEY, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_CREATE_WALLET_ACCOUNT, ERROR_EXPORT_PRIVATE_KEY, ERROR_IMPORT_PRIVATE_KEY, ERROR_INCORRECT_PASSWORD, ERROR_KEYGEN_FAILED, ERROR_MULTIPLE_WALLETS_PER_CHAIN, ERROR_NO_KEY_SHARES_BACKED_UP, ERROR_PASSWORD_MISMATCH, ERROR_PASSWORD_REQUIRED, ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET, ERROR_PUBLIC_KEY_MISMATCH, ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE, ERROR_SIGN_MESSAGE, ERROR_SIGN_TYPED_DATA, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, HEAVY_QUEUE_OPERATIONS, OperationSource, RECOVER_QUEUE_OPERATIONS, ROOM_CACHE_COUNT, ROOM_EXPIRATION_TIME, SIGNED_SESSION_ID_MIN_VERSION, SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE, SIGN_QUEUE_OPERATIONS, STORAGE_KEY, StaleLocalSharesError, WalletBusyError, WalletNotReadyError, cancelICloudAuth, createAddCloudProviderToExistingDelegationDistribution, createBackupData, createCloudProviderDistribution, createDelegationOnlyDistribution, createDelegationWithCloudProviderDistribution, createDynamicOnlyDistribution, deleteICloudBackup, downloadStringAsFile, extractPubkey, formatEvmMessage, formatMessage, formatTronMessage, getActiveCloudProviders, getBitcoinAddressTypeFromDerivationPath, getClientKeyShareBackupInfo, getClientKeyShareExportFileName, getDelegatedShareSet, getGoogleOAuthAccountId, getHttpStatus, getICloudBackup, getMPCSignatureScheme, getMPCSigner, hasCloudProviderBackup, hasDelegatedBackup, hasEncryptedSharesCached, initializeCloudKit, isBrowser, isHeavyQueueOperation, isHexString, isICloudAuthenticated, isNonRetryableCeremonyError, isPublicKeyMismatchError, isRecoverQueueOperation, isSignQueueOperation, isStaleClientSharesError, listICloudBackups, markCeremonyErrorNonRetryable, readEnvironmentSettings, retryPromise, shouldReshareToSameBackups, timeoutPromise };
|