@dynamic-labs-wallet/browser 0.0.339 → 0.0.341
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 +238 -118
- package/index.esm.js +238 -118
- package/package.json +3 -3
- package/src/backup/errors.d.ts +13 -0
- package/src/backup/errors.d.ts.map +1 -0
- package/src/client.d.ts +21 -8
- package/src/client.d.ts.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.d.ts.map +1 -1
- package/src/utils.d.ts +0 -8
- package/src/utils.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -354,6 +354,17 @@ class InvalidPasswordError extends Error {
|
|
|
354
354
|
return metadata;
|
|
355
355
|
};
|
|
356
356
|
|
|
357
|
+
class StaleLocalSharesError extends Error {
|
|
358
|
+
constructor(args){
|
|
359
|
+
super(args.localSharesRefreshed ? 'Local key shares were stale and have been refreshed from the server. Please retry the operation.' : 'Local key shares are stale and could not be refreshed automatically. Please recover or re-authenticate before re-attempting.');
|
|
360
|
+
this.name = 'StaleLocalSharesError';
|
|
361
|
+
this.accountAddress = args.accountAddress;
|
|
362
|
+
this.localKeygenIds = args.localKeygenIds;
|
|
363
|
+
this.recordedKeygenIds = args.recordedKeygenIds;
|
|
364
|
+
this.localSharesRefreshed = args.localSharesRefreshed;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
357
368
|
const GOOGLE_DRIVE_UPLOAD_API = 'https://www.googleapis.com';
|
|
358
369
|
const USER_ACTIONABLE_REASONS = new Set([
|
|
359
370
|
'auth_denied',
|
|
@@ -721,36 +732,6 @@ const getClientKeyShareBackupInfo = (params)=>{
|
|
|
721
732
|
passwordEncrypted
|
|
722
733
|
};
|
|
723
734
|
};
|
|
724
|
-
/**
|
|
725
|
-
* Helper function to merge keyshares and remove duplicates based on pubkey and secretShare
|
|
726
|
-
* @param existingKeyShares - Array of existing keyshares
|
|
727
|
-
* @param newKeyShares - Array of new keyshares to merge
|
|
728
|
-
* @returns Array of merged unique keyshares
|
|
729
|
-
*/ const mergeUniqueKeyShares = (existingKeyShares, newKeyShares)=>{
|
|
730
|
-
const hasDegeneratePubkeyToString = [
|
|
731
|
-
...existingKeyShares,
|
|
732
|
-
...newKeyShares
|
|
733
|
-
].some((share)=>{
|
|
734
|
-
var _share_pubkey;
|
|
735
|
-
return 'pubkey' in share && ((_share_pubkey = share.pubkey) == null ? void 0 : _share_pubkey.toString()) === '[object Object]' // NOSONAR
|
|
736
|
-
;
|
|
737
|
-
});
|
|
738
|
-
if (hasDegeneratePubkeyToString) {
|
|
739
|
-
core.Logger.warn('[mergeUniqueKeyShares] pubkey.toString() returned "[object Object]" — dedup falls back to secretShare-only comparison', {
|
|
740
|
-
existingCount: existingKeyShares.length,
|
|
741
|
-
newCount: newKeyShares.length
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
const uniqueKeyShares = newKeyShares.filter((newShare)=>!existingKeyShares.some((existingShare)=>{
|
|
745
|
-
if (!('pubkey' in newShare) || !('pubkey' in existingShare)) return false;
|
|
746
|
-
if (!newShare.pubkey || !existingShare.pubkey) return false;
|
|
747
|
-
return newShare.pubkey.toString() === existingShare.pubkey.toString() && newShare.secretShare === existingShare.secretShare;
|
|
748
|
-
}));
|
|
749
|
-
return [
|
|
750
|
-
...existingKeyShares,
|
|
751
|
-
...uniqueKeyShares
|
|
752
|
-
];
|
|
753
|
-
};
|
|
754
735
|
const timeoutPromise = ({ timeInMs, activity = 'Ceremony' })=>{
|
|
755
736
|
return new Promise((_, reject)=>setTimeout(()=>reject(new Error(`${activity} did not complete in ${timeInMs}ms`)), timeInMs));
|
|
756
737
|
};
|
|
@@ -2753,23 +2734,15 @@ class DynamicWalletClient {
|
|
|
2753
2734
|
walletOperation,
|
|
2754
2735
|
dynamicRequestId
|
|
2755
2736
|
}, this.getTraceContext(traceContext)));
|
|
2756
|
-
// Refresh walletMap from API before recovery to ensure backup info is up-to-date
|
|
2737
|
+
// Refresh walletMap from API before recovery to ensure backup info is up-to-date.
|
|
2757
2738
|
await this.getWallets();
|
|
2758
|
-
// Recover key shares from backup (don't auto-store, we'll overwrite below)
|
|
2759
|
-
// Use internal method directly to bypass queueing - we're already inside a queued operation
|
|
2760
2739
|
const recoveredKeyShares = await this.internalRecoverEncryptedBackupByWallet({
|
|
2761
2740
|
accountAddress,
|
|
2762
|
-
password
|
|
2763
|
-
walletOperation,
|
|
2741
|
+
password,
|
|
2764
2742
|
signedSessionId,
|
|
2743
|
+
walletOperation,
|
|
2765
2744
|
mfaToken,
|
|
2766
|
-
storeRecoveredShares:
|
|
2767
|
-
});
|
|
2768
|
-
// Overwrite local key shares with recovered ones (not merge)
|
|
2769
|
-
await this.setClientKeySharesToStorage({
|
|
2770
|
-
accountAddress,
|
|
2771
|
-
clientKeyShares: recoveredKeyShares,
|
|
2772
|
-
overwriteOrMerge: 'overwrite'
|
|
2745
|
+
storeRecoveredShares: true
|
|
2773
2746
|
});
|
|
2774
2747
|
this.logger.info('[DynamicWaasWalletClient] Key shares recovered, retrying operation', _extends({
|
|
2775
2748
|
accountAddress,
|
|
@@ -3103,8 +3076,7 @@ class DynamicWalletClient {
|
|
|
3103
3076
|
});
|
|
3104
3077
|
await this.setClientKeySharesToStorage({
|
|
3105
3078
|
accountAddress,
|
|
3106
|
-
clientKeyShares: refreshResults
|
|
3107
|
-
overwriteOrMerge: 'overwrite'
|
|
3079
|
+
clientKeyShares: refreshResults
|
|
3108
3080
|
});
|
|
3109
3081
|
} catch (error) {
|
|
3110
3082
|
// Expected condition — not a server error, no need to page on this.
|
|
@@ -3126,6 +3098,13 @@ class DynamicWalletClient {
|
|
|
3126
3098
|
throw error;
|
|
3127
3099
|
}
|
|
3128
3100
|
}
|
|
3101
|
+
async getKeygenIdForShare(mpcSigner, share) {
|
|
3102
|
+
// BIP340.exportID accepts the raw secretShare string; other signers accept the whole keygen result.
|
|
3103
|
+
if (mpcSigner instanceof web.BIP340) {
|
|
3104
|
+
return mpcSigner.exportID(share.secretShare);
|
|
3105
|
+
}
|
|
3106
|
+
return mpcSigner.exportID(share);
|
|
3107
|
+
}
|
|
3129
3108
|
async getExportId({ chainName, clientKeyShare, bitcoinConfig }) {
|
|
3130
3109
|
const mpcSigner = getMPCSigner({
|
|
3131
3110
|
chainName,
|
|
@@ -3133,15 +3112,7 @@ class DynamicWalletClient {
|
|
|
3133
3112
|
bitcoinConfig
|
|
3134
3113
|
});
|
|
3135
3114
|
try {
|
|
3136
|
-
|
|
3137
|
-
// The exportID accepts either BIP340KeygenResult or string (secretShare)
|
|
3138
|
-
let exportId;
|
|
3139
|
-
if (mpcSigner instanceof web.BIP340) {
|
|
3140
|
-
const secretShareString = clientKeyShare.secretShare;
|
|
3141
|
-
exportId = await mpcSigner.exportID(secretShareString);
|
|
3142
|
-
} else {
|
|
3143
|
-
exportId = await mpcSigner.exportID(clientKeyShare);
|
|
3144
|
-
}
|
|
3115
|
+
const exportId = await this.getKeygenIdForShare(mpcSigner, clientKeyShare);
|
|
3145
3116
|
this.logger.debug('[DynamicWaasWalletClient] getExportId succeeded', {
|
|
3146
3117
|
chainName,
|
|
3147
3118
|
exportId
|
|
@@ -3459,8 +3430,7 @@ class DynamicWalletClient {
|
|
|
3459
3430
|
// only after backup succeeds.
|
|
3460
3431
|
await this.setClientKeySharesToStorage({
|
|
3461
3432
|
accountAddress,
|
|
3462
|
-
clientKeyShares: distribution.clientShares
|
|
3463
|
-
overwriteOrMerge: 'overwrite'
|
|
3433
|
+
clientKeyShares: distribution.clientShares
|
|
3464
3434
|
});
|
|
3465
3435
|
} catch (error) {
|
|
3466
3436
|
logError({
|
|
@@ -3483,8 +3453,7 @@ class DynamicWalletClient {
|
|
|
3483
3453
|
});
|
|
3484
3454
|
await this.setClientKeySharesToStorage({
|
|
3485
3455
|
accountAddress,
|
|
3486
|
-
clientKeyShares: []
|
|
3487
|
-
overwriteOrMerge: 'overwrite'
|
|
3456
|
+
clientKeyShares: []
|
|
3488
3457
|
});
|
|
3489
3458
|
throw error;
|
|
3490
3459
|
}
|
|
@@ -3830,63 +3799,44 @@ class DynamicWalletClient {
|
|
|
3830
3799
|
* Helper function to store client key shares in storage.
|
|
3831
3800
|
* Uses secureStorage when available (mobile), otherwise falls back to localStorage (browser).
|
|
3832
3801
|
*/ /**
|
|
3833
|
-
*
|
|
3834
|
-
|
|
3835
|
-
* persistence-info log so we can audit storage operations from traces.
|
|
3836
|
-
*/ async resolveSharesToPersist({ accountAddress, clientKeyShares, overwriteOrMerge, source, readExisting }) {
|
|
3837
|
-
let sharesToStore;
|
|
3838
|
-
let existingCount = 0;
|
|
3839
|
-
if (overwriteOrMerge === 'overwrite') {
|
|
3840
|
-
sharesToStore = clientKeyShares;
|
|
3841
|
-
} else {
|
|
3842
|
-
const existing = await readExisting();
|
|
3843
|
-
existingCount = existing.length;
|
|
3844
|
-
sharesToStore = mergeUniqueKeyShares(existing, clientKeyShares);
|
|
3845
|
-
}
|
|
3802
|
+
* Emits a persistence-info log so storage writes are auditable from traces.
|
|
3803
|
+
*/ logSharePersistence({ accountAddress, clientKeyShares, source }) {
|
|
3846
3804
|
this.logger.info('[DynamicWaasWalletClient] Persisting client key shares', {
|
|
3847
3805
|
accountAddress,
|
|
3848
3806
|
source,
|
|
3849
|
-
|
|
3850
|
-
inputCount: clientKeyShares.length,
|
|
3851
|
-
existingCount,
|
|
3852
|
-
finalCount: sharesToStore.length
|
|
3807
|
+
inputCount: clientKeyShares.length
|
|
3853
3808
|
});
|
|
3854
|
-
return sharesToStore;
|
|
3855
3809
|
}
|
|
3856
|
-
async setClientKeySharesToLocalStorage({ accountAddress, clientKeyShares
|
|
3810
|
+
async setClientKeySharesToLocalStorage({ accountAddress, clientKeyShares }) {
|
|
3857
3811
|
accountAddress = normalizeAddress(accountAddress);
|
|
3858
|
-
|
|
3812
|
+
this.logSharePersistence({
|
|
3859
3813
|
accountAddress,
|
|
3860
3814
|
clientKeyShares,
|
|
3861
|
-
|
|
3862
|
-
source: 'localStorage',
|
|
3863
|
-
readExisting: ()=>this.getClientKeySharesFromLocalStorage({
|
|
3864
|
-
accountAddress
|
|
3865
|
-
})
|
|
3815
|
+
source: 'localStorage'
|
|
3866
3816
|
});
|
|
3867
3817
|
const stringifiedClientKeyShares = JSON.stringify({
|
|
3868
|
-
clientKeyShares
|
|
3818
|
+
clientKeyShares
|
|
3869
3819
|
});
|
|
3870
3820
|
await this.storage.setItem(accountAddress, stringifiedClientKeyShares);
|
|
3871
3821
|
}
|
|
3872
3822
|
/**
|
|
3873
|
-
*
|
|
3823
|
+
* Replaces the client key shares stored for `accountAddress` with `clientKeyShares`.
|
|
3874
3824
|
* Uses secureStorage when available (mobile), otherwise falls back to localStorage (browser).
|
|
3875
|
-
|
|
3825
|
+
*
|
|
3826
|
+
* A WaaS wallet holds exactly one client share per device (see share-distribution
|
|
3827
|
+
* factories in types.ts), so writes are always a full replacement — there is no
|
|
3828
|
+
* merge mode. Callers that need to clear local shares pass `clientKeyShares: []`.
|
|
3829
|
+
*/ async setClientKeySharesToStorage({ accountAddress, clientKeyShares }) {
|
|
3876
3830
|
accountAddress = normalizeAddress(accountAddress);
|
|
3877
3831
|
// Use secure storage if available (mobile)
|
|
3878
3832
|
if (this.secureStorage) {
|
|
3879
3833
|
try {
|
|
3880
|
-
|
|
3834
|
+
this.logSharePersistence({
|
|
3881
3835
|
accountAddress,
|
|
3882
3836
|
clientKeyShares,
|
|
3883
|
-
|
|
3884
|
-
source: 'secureStorage',
|
|
3885
|
-
readExisting: ()=>this.getClientKeySharesFromStorage({
|
|
3886
|
-
accountAddress
|
|
3887
|
-
})
|
|
3837
|
+
source: 'secureStorage'
|
|
3888
3838
|
});
|
|
3889
|
-
await this.secureStorage.setClientKeyShare(accountAddress,
|
|
3839
|
+
await this.secureStorage.setClientKeyShare(accountAddress, clientKeyShares);
|
|
3890
3840
|
return;
|
|
3891
3841
|
} catch (error) {
|
|
3892
3842
|
logError({
|
|
@@ -3902,8 +3852,7 @@ class DynamicWalletClient {
|
|
|
3902
3852
|
// Fallback to localStorage (browser)
|
|
3903
3853
|
await this.setClientKeySharesToLocalStorage({
|
|
3904
3854
|
accountAddress,
|
|
3905
|
-
clientKeyShares
|
|
3906
|
-
overwriteOrMerge
|
|
3855
|
+
clientKeyShares
|
|
3907
3856
|
});
|
|
3908
3857
|
}
|
|
3909
3858
|
/**
|
|
@@ -4262,6 +4211,93 @@ class DynamicWalletClient {
|
|
|
4262
4211
|
throw error;
|
|
4263
4212
|
}
|
|
4264
4213
|
}
|
|
4214
|
+
// Defense-in-depth check: before uploading a backup, confirm that local client shares
|
|
4215
|
+
// still match the keygenIds recorded for backups[DYNAMIC] on the server. On mismatch,
|
|
4216
|
+
// refreshes local storage from the server's encrypted backup (so a subsequent retry
|
|
4217
|
+
// sees fresh state) and then throws StaleLocalSharesError. We deliberately don't
|
|
4218
|
+
// continue the upload silently after recovery — callers like updatePassword hold
|
|
4219
|
+
// operation-specific context (existing/new password) that must be re-applied to
|
|
4220
|
+
// freshly-recovered shares. Throwing surfaces the stale-state event so the host can
|
|
4221
|
+
// re-enter the outer operation with correct context. Skipped on internal post-rotation
|
|
4222
|
+
// flows that pass clientKeyShares explicitly (the refresh path).
|
|
4223
|
+
async ensureLocalSharesAreFresh({ accountAddress, walletData, localShares, password, signedSessionId }) {
|
|
4224
|
+
var _freshBackupInfo_backups;
|
|
4225
|
+
// Always round-trip to the server — walletMap is populated at wallet-load and is
|
|
4226
|
+
// not invalidated when another tab rotates shares, so trusting it would
|
|
4227
|
+
// false-positive on cross-tab refresh.
|
|
4228
|
+
const { chainName } = walletData;
|
|
4229
|
+
const mpcSigner = getMPCSigner({
|
|
4230
|
+
chainName,
|
|
4231
|
+
baseRelayUrl: this.baseMPCRelayApiUrl,
|
|
4232
|
+
bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress)
|
|
4233
|
+
});
|
|
4234
|
+
const [freshBackupInfo, localKeygenIds] = await Promise.all([
|
|
4235
|
+
this.fetchFreshWalletBackupInfo({
|
|
4236
|
+
accountAddress
|
|
4237
|
+
}),
|
|
4238
|
+
Promise.all(localShares.map((share)=>this.getKeygenIdForShare(mpcSigner, share)))
|
|
4239
|
+
]);
|
|
4240
|
+
var _freshBackupInfo_backups_BackupLocation_DYNAMIC;
|
|
4241
|
+
const recordedKeygenIds = ((_freshBackupInfo_backups_BackupLocation_DYNAMIC = freshBackupInfo == null ? void 0 : (_freshBackupInfo_backups = freshBackupInfo.backups) == null ? void 0 : _freshBackupInfo_backups[core.BackupLocation.DYNAMIC]) != null ? _freshBackupInfo_backups_BackupLocation_DYNAMIC : []).map((entry)=>entry.keygenId).filter((id)=>typeof id === 'string' && id.length > 0);
|
|
4242
|
+
// First-time backup or legacy data (server didn't populate keygenId yet) — nothing to compare.
|
|
4243
|
+
if (recordedKeygenIds.length === 0) return;
|
|
4244
|
+
const recordedSet = new Set(recordedKeygenIds);
|
|
4245
|
+
const localMatchesRecorded = localKeygenIds.length === recordedSet.size && localKeygenIds.every((id)=>recordedSet.has(id));
|
|
4246
|
+
if (localMatchesRecorded) return;
|
|
4247
|
+
this.logger.warn('[storeEncryptedBackupByWallet] Stale local shares detected; refreshing local storage', {
|
|
4248
|
+
accountAddress,
|
|
4249
|
+
walletId: walletData.walletId,
|
|
4250
|
+
environmentId: this.environmentId,
|
|
4251
|
+
localKeygenIds,
|
|
4252
|
+
recordedKeygenIds
|
|
4253
|
+
});
|
|
4254
|
+
// Resolve a session, falling back to the host's reverse-channel callback when one
|
|
4255
|
+
// wasn't passed in. If neither source produces a session we skip the refresh and
|
|
4256
|
+
// still throw — the host can re-auth from the error.
|
|
4257
|
+
let resolvedSessionId;
|
|
4258
|
+
try {
|
|
4259
|
+
resolvedSessionId = await this.resolveSignedSessionId(signedSessionId);
|
|
4260
|
+
} catch (e) {
|
|
4261
|
+
resolvedSessionId = undefined;
|
|
4262
|
+
}
|
|
4263
|
+
let localSharesRefreshed = false;
|
|
4264
|
+
if (resolvedSessionId) {
|
|
4265
|
+
// The recovery decrypt key is whatever the server's blob was encrypted with — not the
|
|
4266
|
+
// caller's `password` arg, which is the key we'll re-encrypt the *next* upload with.
|
|
4267
|
+
// For setPassword the wallet is currently passwordless (blob encrypted with environmentId)
|
|
4268
|
+
// and the caller passed the new password; passing it through here would
|
|
4269
|
+
// InvalidPasswordError. Fall back to undefined so internalRecover picks environmentId.
|
|
4270
|
+
const recoveryPassword = freshBackupInfo.passwordEncrypted ? password : undefined;
|
|
4271
|
+
try {
|
|
4272
|
+
// walletMap was already refreshed by fetchFreshWalletBackupInfo above, so we skip the
|
|
4273
|
+
// getWallets() refresh that recoverKeySharesOnMismatch performs for its own callers.
|
|
4274
|
+
await this.internalRecoverEncryptedBackupByWallet({
|
|
4275
|
+
accountAddress,
|
|
4276
|
+
password: recoveryPassword,
|
|
4277
|
+
signedSessionId: resolvedSessionId,
|
|
4278
|
+
walletOperation: core.WalletOperation.RECOVER,
|
|
4279
|
+
storeRecoveredShares: true
|
|
4280
|
+
});
|
|
4281
|
+
localSharesRefreshed = true;
|
|
4282
|
+
this.logger.info('[storeEncryptedBackupByWallet] Local shares refreshed from server; caller should retry', {
|
|
4283
|
+
accountAddress,
|
|
4284
|
+
walletId: walletData.walletId
|
|
4285
|
+
});
|
|
4286
|
+
} catch (recoveryError) {
|
|
4287
|
+
this.logger.error('[storeEncryptedBackupByWallet] Failed to refresh local shares from server', {
|
|
4288
|
+
accountAddress,
|
|
4289
|
+
walletId: walletData.walletId,
|
|
4290
|
+
error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError)
|
|
4291
|
+
});
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
throw new StaleLocalSharesError({
|
|
4295
|
+
accountAddress,
|
|
4296
|
+
localKeygenIds,
|
|
4297
|
+
recordedKeygenIds,
|
|
4298
|
+
localSharesRefreshed
|
|
4299
|
+
});
|
|
4300
|
+
}
|
|
4265
4301
|
/**
|
|
4266
4302
|
* Central backup orchestrator that encrypts and stores wallet key shares.
|
|
4267
4303
|
*
|
|
@@ -4294,10 +4330,24 @@ class DynamicWalletClient {
|
|
|
4294
4330
|
* @returns Promise with backup metadata including share locations and IDs
|
|
4295
4331
|
*/ async storeEncryptedBackupByWallet({ accountAddress, clientKeyShares = undefined, password = undefined, signedSessionId, cloudProviders = [], delegatedKeyshare = undefined, passwordUpdateBatchId, googleDriveAccessToken }) {
|
|
4296
4332
|
var _this_getWalletFromMap, _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
4333
|
+
const hasProvidedShares = clientKeyShares !== undefined;
|
|
4297
4334
|
const keySharesToBackup = clientKeyShares != null ? clientKeyShares : await this.getClientKeySharesFromStorage({
|
|
4298
4335
|
accountAddress
|
|
4299
4336
|
});
|
|
4300
|
-
//
|
|
4337
|
+
// Freshness check — only when shares were loaded from local storage. Internal
|
|
4338
|
+
// post-rotation callers (refreshWalletAccountShares) pass clientKeyShares explicitly
|
|
4339
|
+
// and hold shares whose keygenIds are intentionally newer than the recorded backup.
|
|
4340
|
+
const walletDataForFreshness = this.getWalletFromMap(accountAddress);
|
|
4341
|
+
if (!hasProvidedShares && walletDataForFreshness) {
|
|
4342
|
+
await this.ensureLocalSharesAreFresh({
|
|
4343
|
+
accountAddress,
|
|
4344
|
+
walletData: walletDataForFreshness,
|
|
4345
|
+
localShares: keySharesToBackup,
|
|
4346
|
+
password,
|
|
4347
|
+
signedSessionId
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
// Re-read after the freshness check: walletMap may have been updated with fresh server data.
|
|
4301
4351
|
const walletBackupInfo = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.clientKeySharesBackupInfo;
|
|
4302
4352
|
const activeCloudProviders = getActiveCloudProviders(walletBackupInfo);
|
|
4303
4353
|
const shouldBackupToCloudProviders = cloudProviders.length > 0 || activeCloudProviders.length > 0;
|
|
@@ -4695,8 +4745,7 @@ class DynamicWalletClient {
|
|
|
4695
4745
|
if (storeRecoveredShares) {
|
|
4696
4746
|
await this.setClientKeySharesToStorage({
|
|
4697
4747
|
accountAddress,
|
|
4698
|
-
clientKeyShares: decryptedKeyShares
|
|
4699
|
-
overwriteOrMerge: 'merge'
|
|
4748
|
+
clientKeyShares: decryptedKeyShares
|
|
4700
4749
|
});
|
|
4701
4750
|
await this.storage.setItem(this.storageKey, JSON.stringify(this.walletMap));
|
|
4702
4751
|
}
|
|
@@ -5182,24 +5231,27 @@ class DynamicWalletClient {
|
|
|
5182
5231
|
}
|
|
5183
5232
|
return true;
|
|
5184
5233
|
}
|
|
5234
|
+
async fetchWalletBackupInfoFromServer(accountAddress) {
|
|
5235
|
+
var _user_verifiedCredentials;
|
|
5236
|
+
const dynamicRequestId = uuid.v4();
|
|
5237
|
+
const user = await this.apiClient.getUser(dynamicRequestId);
|
|
5238
|
+
const wallet = (_user_verifiedCredentials = user.verifiedCredentials) == null ? void 0 : _user_verifiedCredentials.find((vc)=>{
|
|
5239
|
+
var _vc_address;
|
|
5240
|
+
return ((_vc_address = vc.address) == null ? void 0 : _vc_address.toLowerCase()) === accountAddress.toLowerCase();
|
|
5241
|
+
});
|
|
5242
|
+
return getClientKeyShareBackupInfo({
|
|
5243
|
+
walletProperties: wallet == null ? void 0 : wallet.walletProperties
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5185
5246
|
async getWalletClientKeyShareBackupInfo({ accountAddress }) {
|
|
5186
5247
|
const dynamicRequestId = uuid.v4();
|
|
5187
5248
|
try {
|
|
5188
|
-
var _this_getWalletFromMap, _walletBackupInfo_backups_BackupLocation_DYNAMIC, _walletBackupInfo_backups
|
|
5189
|
-
// Return existing backup info if it exists
|
|
5249
|
+
var _this_getWalletFromMap, _walletBackupInfo_backups_BackupLocation_DYNAMIC, _walletBackupInfo_backups;
|
|
5190
5250
|
const walletBackupInfo = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.clientKeySharesBackupInfo;
|
|
5191
5251
|
if (walletBackupInfo && ((_walletBackupInfo_backups = walletBackupInfo.backups) == null ? void 0 : (_walletBackupInfo_backups_BackupLocation_DYNAMIC = _walletBackupInfo_backups[core.BackupLocation.DYNAMIC]) == null ? void 0 : _walletBackupInfo_backups_BackupLocation_DYNAMIC.length) > 0) {
|
|
5192
5252
|
return walletBackupInfo;
|
|
5193
5253
|
}
|
|
5194
|
-
|
|
5195
|
-
const user = await this.apiClient.getUser(dynamicRequestId);
|
|
5196
|
-
const wallet = (_user_verifiedCredentials = user.verifiedCredentials) == null ? void 0 : _user_verifiedCredentials.find((vc)=>{
|
|
5197
|
-
var _vc_address;
|
|
5198
|
-
return ((_vc_address = vc.address) == null ? void 0 : _vc_address.toLowerCase()) === accountAddress.toLowerCase();
|
|
5199
|
-
});
|
|
5200
|
-
return getClientKeyShareBackupInfo({
|
|
5201
|
-
walletProperties: wallet == null ? void 0 : wallet.walletProperties
|
|
5202
|
-
});
|
|
5254
|
+
return this.fetchWalletBackupInfoFromServer(accountAddress);
|
|
5203
5255
|
} catch (error) {
|
|
5204
5256
|
logError({
|
|
5205
5257
|
message: 'Error in getWalletClientKeyShareBackupInfo',
|
|
@@ -5212,6 +5264,18 @@ class DynamicWalletClient {
|
|
|
5212
5264
|
throw error;
|
|
5213
5265
|
}
|
|
5214
5266
|
}
|
|
5267
|
+
// Bypasses the cache short-circuit in getWalletClientKeyShareBackupInfo. Required when
|
|
5268
|
+
// freshness matters across tabs/contexts — walletMap is populated at wallet-load time and
|
|
5269
|
+
// is not invalidated when another tab rotates shares.
|
|
5270
|
+
async fetchFreshWalletBackupInfo({ accountAddress }) {
|
|
5271
|
+
const freshBackupInfo = await this.fetchWalletBackupInfoFromServer(accountAddress);
|
|
5272
|
+
if (this.getWalletFromMap(accountAddress)) {
|
|
5273
|
+
this.updateWalletMap(accountAddress, {
|
|
5274
|
+
clientKeySharesBackupInfo: freshBackupInfo
|
|
5275
|
+
});
|
|
5276
|
+
}
|
|
5277
|
+
return freshBackupInfo;
|
|
5278
|
+
}
|
|
5215
5279
|
async getWallet({ accountAddress, walletOperation = core.WalletOperation.NO_OPERATION, shareCount = undefined, password = undefined, signedSessionId }) {
|
|
5216
5280
|
const dynamicRequestId = uuid.v4();
|
|
5217
5281
|
try {
|
|
@@ -5332,13 +5396,8 @@ class DynamicWalletClient {
|
|
|
5332
5396
|
signedSessionId,
|
|
5333
5397
|
shareCount
|
|
5334
5398
|
});
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
});
|
|
5338
|
-
await this.setClientKeySharesToStorage({
|
|
5339
|
-
accountAddress,
|
|
5340
|
-
clientKeyShares: mergeUniqueKeyShares(existingKeyShares, decryptedKeyShares)
|
|
5341
|
-
});
|
|
5399
|
+
// recoverEncryptedBackupByWallet (storeRecoveredShares: true by default) has
|
|
5400
|
+
// already written decryptedKeyShares to storage. No further write needed.
|
|
5342
5401
|
this.logger.debug('[DynamicWaasWalletClient] Recovered backup', {
|
|
5343
5402
|
decryptedKeyShares
|
|
5344
5403
|
});
|
|
@@ -5423,6 +5482,45 @@ class DynamicWalletClient {
|
|
|
5423
5482
|
});
|
|
5424
5483
|
}
|
|
5425
5484
|
/**
|
|
5485
|
+
* Fetches the encrypted-shares blob from the server and caches it in this.storage.
|
|
5486
|
+
* Returns the serialized blob, or null if the wallet is unknown or the API returns
|
|
5487
|
+
* no data. Does not decrypt — callers (e.g. unlockWallet, getWallet eager-load)
|
|
5488
|
+
* own the decrypt step.
|
|
5489
|
+
*/ async fetchAndCacheEncryptedShares({ accountAddress, signedSessionId, mfaToken }) {
|
|
5490
|
+
const walletData = this.getWalletFromMap(accountAddress);
|
|
5491
|
+
if (!walletData) {
|
|
5492
|
+
return null;
|
|
5493
|
+
}
|
|
5494
|
+
const { shares } = this.recoverStrategy({
|
|
5495
|
+
clientKeyShareBackupInfo: walletData.clientKeySharesBackupInfo,
|
|
5496
|
+
thresholdSignatureScheme: walletData.thresholdSignatureScheme,
|
|
5497
|
+
walletOperation: core.WalletOperation.RECOVER
|
|
5498
|
+
});
|
|
5499
|
+
const externalKeyShareIds = shares[core.BackupLocation.DYNAMIC] || [];
|
|
5500
|
+
this.logger.info('[unlockWallet] cache miss after getWallet, fetching encrypted shares from server', {
|
|
5501
|
+
context: {
|
|
5502
|
+
accountAddress,
|
|
5503
|
+
walletId: walletData.walletId,
|
|
5504
|
+
externalKeyShareIds
|
|
5505
|
+
}
|
|
5506
|
+
});
|
|
5507
|
+
const data = await this.apiClient.recoverEncryptedBackupByWallet({
|
|
5508
|
+
walletId: walletData.walletId,
|
|
5509
|
+
externalKeyShareIds,
|
|
5510
|
+
signedSessionId,
|
|
5511
|
+
mfaToken,
|
|
5512
|
+
requiresSignedSessionId: this.requiresSignedSessionId(),
|
|
5513
|
+
userId: this.userId
|
|
5514
|
+
});
|
|
5515
|
+
if (!data) {
|
|
5516
|
+
return null;
|
|
5517
|
+
}
|
|
5518
|
+
const serialized = JSON.stringify(data);
|
|
5519
|
+
const encryptedStorageKey = `${normalizeAddress(accountAddress)}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
|
|
5520
|
+
await this.storage.setItem(encryptedStorageKey, serialized);
|
|
5521
|
+
return serialized;
|
|
5522
|
+
}
|
|
5523
|
+
/**
|
|
5426
5524
|
* Unlocks a password-encrypted wallet by decrypting cached encrypted shares.
|
|
5427
5525
|
* This method should be called after getWalletRecoveryState returns LOCKED state.
|
|
5428
5526
|
*
|
|
@@ -5430,7 +5528,7 @@ class DynamicWalletClient {
|
|
|
5430
5528
|
* @param password - The password to decrypt the shares
|
|
5431
5529
|
* @param signedSessionId - The signed session ID for authentication
|
|
5432
5530
|
* @returns The unlocked wallet properties
|
|
5433
|
-
*/ async unlockWallet({ accountAddress, password, signedSessionId }) {
|
|
5531
|
+
*/ async unlockWallet({ accountAddress, password, signedSessionId, mfaToken }) {
|
|
5434
5532
|
const dynamicRequestId = uuid.v4();
|
|
5435
5533
|
try {
|
|
5436
5534
|
await this.requireWalletFromMap(accountAddress, signedSessionId);
|
|
@@ -5450,6 +5548,18 @@ class DynamicWalletClient {
|
|
|
5450
5548
|
});
|
|
5451
5549
|
encryptedData = await this.storage.getItem(encryptedStorageKey);
|
|
5452
5550
|
}
|
|
5551
|
+
// getWallet short-circuits via checkWalletFields when client key shares are
|
|
5552
|
+
// durable but the encrypted-shares cache in this.storage was evicted (e.g. RN
|
|
5553
|
+
// secureStorage survives a WebKit content-process recycle while localStorage
|
|
5554
|
+
// does not). Populate the cache directly so the single decrypt path below
|
|
5555
|
+
// can proceed.
|
|
5556
|
+
if (!encryptedData) {
|
|
5557
|
+
encryptedData = await this.fetchAndCacheEncryptedShares({
|
|
5558
|
+
accountAddress,
|
|
5559
|
+
signedSessionId,
|
|
5560
|
+
mfaToken
|
|
5561
|
+
});
|
|
5562
|
+
}
|
|
5453
5563
|
if (!encryptedData) {
|
|
5454
5564
|
throw new Error('No encrypted shares found for wallet');
|
|
5455
5565
|
}
|
|
@@ -5463,7 +5573,17 @@ class DynamicWalletClient {
|
|
|
5463
5573
|
const otherEncryptedWallets = Object.entries(this.walletMap).filter(([addr, w])=>addr !== normalizedAccountAddress && w.walletReadyState !== core.WalletReadyState.READY && isWalletPasswordEncrypted(w));
|
|
5464
5574
|
const results = await Promise.allSettled(otherEncryptedWallets.map(async ([otherAddr])=>{
|
|
5465
5575
|
const otherEncryptedStorageKey = `${otherAddr}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
|
|
5466
|
-
|
|
5576
|
+
let otherEncryptedData = await this.storage.getItem(otherEncryptedStorageKey);
|
|
5577
|
+
// Same cache-eviction scenario as the main wallet above: on RN, the
|
|
5578
|
+
// encrypted-shares blob can be gone while client shares survive in
|
|
5579
|
+
// secureStorage. Re-fetch from the server before giving up.
|
|
5580
|
+
if (!otherEncryptedData) {
|
|
5581
|
+
otherEncryptedData = await this.fetchAndCacheEncryptedShares({
|
|
5582
|
+
accountAddress: otherAddr,
|
|
5583
|
+
signedSessionId,
|
|
5584
|
+
mfaToken
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5467
5587
|
if (!otherEncryptedData) {
|
|
5468
5588
|
return;
|
|
5469
5589
|
}
|
|
@@ -5938,6 +6058,7 @@ exports.SIGNED_SESSION_ID_MIN_VERSION = SIGNED_SESSION_ID_MIN_VERSION;
|
|
|
5938
6058
|
exports.SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE = SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE;
|
|
5939
6059
|
exports.SIGN_QUEUE_OPERATIONS = SIGN_QUEUE_OPERATIONS;
|
|
5940
6060
|
exports.STORAGE_KEY = STORAGE_KEY;
|
|
6061
|
+
exports.StaleLocalSharesError = StaleLocalSharesError;
|
|
5941
6062
|
exports.WalletBusyError = WalletBusyError;
|
|
5942
6063
|
exports.WalletNotReadyError = WalletNotReadyError;
|
|
5943
6064
|
exports.cancelICloudAuth = cancelICloudAuth;
|
|
@@ -5974,7 +6095,6 @@ exports.isRecoverQueueOperation = isRecoverQueueOperation;
|
|
|
5974
6095
|
exports.isSignQueueOperation = isSignQueueOperation;
|
|
5975
6096
|
exports.listICloudBackups = listICloudBackups;
|
|
5976
6097
|
exports.markCeremonyErrorNonRetryable = markCeremonyErrorNonRetryable;
|
|
5977
|
-
exports.mergeUniqueKeyShares = mergeUniqueKeyShares;
|
|
5978
6098
|
exports.readEnvironmentSettings = readEnvironmentSettings;
|
|
5979
6099
|
exports.retryPromise = retryPromise;
|
|
5980
6100
|
exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
|