@dynamic-labs-wallet/browser 0.0.338 → 0.0.340
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 +250 -157
- package/index.esm.js +249 -157
- package/package.json +3 -3
- package/src/backup/providers/googleDrive.d.ts +17 -0
- package/src/backup/providers/googleDrive.d.ts.map +1 -1
- package/src/backup/utils.d.ts.map +1 -1
- package/src/client.d.ts +21 -10
- package/src/client.d.ts.map +1 -1
- package/src/utils.d.ts +13 -8
- package/src/utils.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -355,6 +355,21 @@ class InvalidPasswordError extends Error {
|
|
|
355
355
|
};
|
|
356
356
|
|
|
357
357
|
const GOOGLE_DRIVE_UPLOAD_API = 'https://www.googleapis.com';
|
|
358
|
+
const USER_ACTIONABLE_REASONS = new Set([
|
|
359
|
+
'auth_denied',
|
|
360
|
+
'storage_full',
|
|
361
|
+
// Google Drive credentials are provided per-customer (each project's own
|
|
362
|
+
// Google Cloud OAuth app), so 429s hit the customer's project quota, not
|
|
363
|
+
// Dynamic's. From an oncall perspective there is nothing to act on -- the
|
|
364
|
+
// end user just needs to wait and retry.
|
|
365
|
+
'rate_limited'
|
|
366
|
+
]);
|
|
367
|
+
/**
|
|
368
|
+
* Returns true when the failure reason represents a problem the end user
|
|
369
|
+
* has to wait out or resolve themselves (e.g. grant Drive permission, free
|
|
370
|
+
* up storage, wait for a per-user/per-project rate limit to clear) and not
|
|
371
|
+
* a system-side issue oncall should investigate.
|
|
372
|
+
*/ const isUserActionableGoogleDriveBackupErrorReason = (reason)=>USER_ACTIONABLE_REASONS.has(reason);
|
|
358
373
|
const NON_RETRYABLE_AUTH_REASONS = new Set([
|
|
359
374
|
'insufficientPermissions',
|
|
360
375
|
'unauthorized',
|
|
@@ -372,7 +387,8 @@ const RATE_LIMIT_REASONS = new Set([
|
|
|
372
387
|
]);
|
|
373
388
|
const NETWORK_ERROR = {
|
|
374
389
|
message: 'Could not reach Google Drive. Please check your internet connection and try again.',
|
|
375
|
-
isRetryable: true
|
|
390
|
+
isRetryable: true,
|
|
391
|
+
errorReason: 'network'
|
|
376
392
|
};
|
|
377
393
|
/**
|
|
378
394
|
* Maps a Google Drive API error to an actionable user-facing message and a retry hint.
|
|
@@ -385,31 +401,36 @@ const NETWORK_ERROR = {
|
|
|
385
401
|
if (httpStatus === 401 || reason && NON_RETRYABLE_AUTH_REASONS.has(reason) || detailReason && NON_RETRYABLE_AUTH_DETAIL_REASONS.has(detailReason)) {
|
|
386
402
|
return {
|
|
387
403
|
message: 'Google Drive access denied: missing or insufficient permissions. Make sure the app is requesting Drive permission and that you allowed it during sign-in, then try again.',
|
|
388
|
-
isRetryable: false
|
|
404
|
+
isRetryable: false,
|
|
405
|
+
errorReason: 'auth_denied'
|
|
389
406
|
};
|
|
390
407
|
}
|
|
391
408
|
if (reason === 'storageQuotaExceeded') {
|
|
392
409
|
return {
|
|
393
410
|
message: 'Google Drive storage is full. Free up space in your Google Drive and try again.',
|
|
394
|
-
isRetryable: false
|
|
411
|
+
isRetryable: false,
|
|
412
|
+
errorReason: 'storage_full'
|
|
395
413
|
};
|
|
396
414
|
}
|
|
397
415
|
if (httpStatus === 429 || reason && RATE_LIMIT_REASONS.has(reason)) {
|
|
398
416
|
return {
|
|
399
417
|
message: 'Google Drive is temporarily rate-limited. Please try again in a few moments.',
|
|
400
|
-
isRetryable: true
|
|
418
|
+
isRetryable: true,
|
|
419
|
+
errorReason: 'rate_limited'
|
|
401
420
|
};
|
|
402
421
|
}
|
|
403
422
|
if (httpStatus >= 500 && httpStatus < 600) {
|
|
404
423
|
return {
|
|
405
424
|
message: 'Google Drive is temporarily unavailable. Please try again shortly.',
|
|
406
|
-
isRetryable: true
|
|
425
|
+
isRetryable: true,
|
|
426
|
+
errorReason: 'service_unavailable'
|
|
407
427
|
};
|
|
408
428
|
}
|
|
409
429
|
const detail = reason != null ? reason : error == null ? void 0 : error.status;
|
|
410
430
|
return {
|
|
411
431
|
message: detail ? `Google Drive upload failed (HTTP ${httpStatus}: ${detail}).` : `Google Drive upload failed (HTTP ${httpStatus}).`,
|
|
412
|
-
isRetryable: false
|
|
432
|
+
isRetryable: false,
|
|
433
|
+
errorReason: 'unknown'
|
|
413
434
|
};
|
|
414
435
|
};
|
|
415
436
|
const parseGoogleDriveErrorBody = async (response)=>{
|
|
@@ -422,6 +443,7 @@ const parseGoogleDriveErrorBody = async (response)=>{
|
|
|
422
443
|
const createGoogleDriveError = (mapped)=>{
|
|
423
444
|
const error = new Error(mapped.message);
|
|
424
445
|
error.isRetryable = mapped.isRetryable;
|
|
446
|
+
error.errorReason = mapped.errorReason;
|
|
425
447
|
return error;
|
|
426
448
|
};
|
|
427
449
|
const uploadFileToGoogleDriveAppStorage = async ({ accessToken, fileName, jsonData })=>{
|
|
@@ -586,6 +608,62 @@ const ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE = '[DynamicWaasWalletClient]:
|
|
|
586
608
|
* resolve to the same wallet entry.
|
|
587
609
|
*/ const normalizeAddress = (address)=>address.toLowerCase();
|
|
588
610
|
|
|
611
|
+
const ERROR_PASSWORD_MISMATCH = '[DynamicWaasWalletClient]: Password does not match the password used for existing wallets. All wallets must use the same password for encrypted backups.';
|
|
612
|
+
const ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET = '[DynamicWaasWalletClient]: Password is required for refresh/reshare of a password-encrypted wallet.';
|
|
613
|
+
const ERROR_EXISTING_PASSWORD_REQUIRED = '[DynamicWaasWalletClient]: Existing password is required to update the password for encrypted wallets.';
|
|
614
|
+
const ERROR_WALLETS_ALREADY_ENCRYPTED = '[DynamicWaasWalletClient]: Cannot set password: wallets are already password-encrypted. Use updatePassword instead.';
|
|
615
|
+
/**
|
|
616
|
+
* Checks if password consistency validation is needed.
|
|
617
|
+
* Returns false if no password provided or password is the environmentId default.
|
|
618
|
+
*/ function shouldValidatePassword(password, environmentId) {
|
|
619
|
+
if (!password || password === environmentId) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Finds the first password-encrypted wallet in the walletMap.
|
|
626
|
+
* Returns [normalizedAddress, walletProperties] or undefined.
|
|
627
|
+
*/ function findPasswordEncryptedWallet(walletMap) {
|
|
628
|
+
for (const [address, wallet] of Object.entries(walletMap)){
|
|
629
|
+
var _wallet_clientKeySharesBackupInfo;
|
|
630
|
+
if ((_wallet_clientKeySharesBackupInfo = wallet.clientKeySharesBackupInfo) == null ? void 0 : _wallet_clientKeySharesBackupInfo.passwordEncrypted) {
|
|
631
|
+
return [
|
|
632
|
+
address,
|
|
633
|
+
wallet
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Extracts the first encrypted key share credential from cached encrypted data.
|
|
641
|
+
* Returns the base64-encoded encrypted credential string, or undefined if none found.
|
|
642
|
+
*/ function extractFirstEncryptedCredential(encryptedData) {
|
|
643
|
+
try {
|
|
644
|
+
const parsed = JSON.parse(encryptedData);
|
|
645
|
+
const keyShares = parsed == null ? void 0 : parsed.keyShares;
|
|
646
|
+
if (!Array.isArray(keyShares) || keyShares.length === 0) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
return keyShares[0].encryptedAccountCredential;
|
|
650
|
+
} catch (e) {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Checks if a wallet uses password-based encryption for its key share backups.
|
|
656
|
+
*/ function isWalletPasswordEncrypted(wallet) {
|
|
657
|
+
var _wallet_clientKeySharesBackupInfo;
|
|
658
|
+
var _wallet_clientKeySharesBackupInfo_passwordEncrypted;
|
|
659
|
+
return (_wallet_clientKeySharesBackupInfo_passwordEncrypted = wallet == null ? void 0 : (_wallet_clientKeySharesBackupInfo = wallet.clientKeySharesBackupInfo) == null ? void 0 : _wallet_clientKeySharesBackupInfo.passwordEncrypted) != null ? _wallet_clientKeySharesBackupInfo_passwordEncrypted : false;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Checks if an error indicates a password mismatch (wrong password during decryption).
|
|
663
|
+
*/ function isPasswordMismatchError(error) {
|
|
664
|
+
return error instanceof InvalidPasswordError;
|
|
665
|
+
}
|
|
666
|
+
|
|
589
667
|
const isBrowser = ()=>typeof window !== 'undefined';
|
|
590
668
|
/**
|
|
591
669
|
* Helper function to extract pubkey from potentially nested structure
|
|
@@ -643,36 +721,6 @@ const getClientKeyShareBackupInfo = (params)=>{
|
|
|
643
721
|
passwordEncrypted
|
|
644
722
|
};
|
|
645
723
|
};
|
|
646
|
-
/**
|
|
647
|
-
* Helper function to merge keyshares and remove duplicates based on pubkey and secretShare
|
|
648
|
-
* @param existingKeyShares - Array of existing keyshares
|
|
649
|
-
* @param newKeyShares - Array of new keyshares to merge
|
|
650
|
-
* @returns Array of merged unique keyshares
|
|
651
|
-
*/ const mergeUniqueKeyShares = (existingKeyShares, newKeyShares)=>{
|
|
652
|
-
const hasDegeneratePubkeyToString = [
|
|
653
|
-
...existingKeyShares,
|
|
654
|
-
...newKeyShares
|
|
655
|
-
].some((share)=>{
|
|
656
|
-
var _share_pubkey;
|
|
657
|
-
return 'pubkey' in share && ((_share_pubkey = share.pubkey) == null ? void 0 : _share_pubkey.toString()) === '[object Object]' // NOSONAR
|
|
658
|
-
;
|
|
659
|
-
});
|
|
660
|
-
if (hasDegeneratePubkeyToString) {
|
|
661
|
-
core.Logger.warn('[mergeUniqueKeyShares] pubkey.toString() returned "[object Object]" — dedup falls back to secretShare-only comparison', {
|
|
662
|
-
existingCount: existingKeyShares.length,
|
|
663
|
-
newCount: newKeyShares.length
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
const uniqueKeyShares = newKeyShares.filter((newShare)=>!existingKeyShares.some((existingShare)=>{
|
|
667
|
-
if (!('pubkey' in newShare) || !('pubkey' in existingShare)) return false;
|
|
668
|
-
if (!newShare.pubkey || !existingShare.pubkey) return false;
|
|
669
|
-
return newShare.pubkey.toString() === existingShare.pubkey.toString() && newShare.secretShare === existingShare.secretShare;
|
|
670
|
-
}));
|
|
671
|
-
return [
|
|
672
|
-
...existingKeyShares,
|
|
673
|
-
...uniqueKeyShares
|
|
674
|
-
];
|
|
675
|
-
};
|
|
676
724
|
const timeoutPromise = ({ timeInMs, activity = 'Ceremony' })=>{
|
|
677
725
|
return new Promise((_, reject)=>setTimeout(()=>reject(new Error(`${activity} did not complete in ${timeInMs}ms`)), timeInMs));
|
|
678
726
|
};
|
|
@@ -742,6 +790,31 @@ const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)
|
|
|
742
790
|
// TypeScript needs this even though it's unreachable
|
|
743
791
|
throw new Error('Unreachable code');
|
|
744
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Classifies whether a wallet-creation / wallet-import ceremony error is
|
|
795
|
+
* worth retrying. Returns true for terminal errors that won't get better on
|
|
796
|
+
* retry (wrong password, missing passcode, public-key mismatch, etc.) so the
|
|
797
|
+
* caller can mark them with `isRetryable: false` and short-circuit
|
|
798
|
+
* `retryPromise`.
|
|
799
|
+
*/ const isNonRetryableCeremonyError = (error)=>{
|
|
800
|
+
if (!error) return false;
|
|
801
|
+
if ((error == null ? void 0 : error.isRetryable) === false) return true;
|
|
802
|
+
if (isPasswordMismatchError(error)) return true;
|
|
803
|
+
if (error instanceof InvalidPasswordError) return true;
|
|
804
|
+
const message = error instanceof Error ? error.message : '';
|
|
805
|
+
if (!message) return false;
|
|
806
|
+
if (message.includes(ERROR_PASSCODE_REQUIRED)) return true;
|
|
807
|
+
if (message.includes(ERROR_PUBLIC_KEY_MISMATCH)) return true;
|
|
808
|
+
return false;
|
|
809
|
+
};
|
|
810
|
+
/**
|
|
811
|
+
* Marks an error as non-retryable in place so `retryPromise` will skip
|
|
812
|
+
* remaining attempts. Used at ceremony retry boundaries.
|
|
813
|
+
*/ const markCeremonyErrorNonRetryable = (error)=>{
|
|
814
|
+
if (error && typeof error === 'object' && isNonRetryableCeremonyError(error)) {
|
|
815
|
+
error.isRetryable = false;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
745
818
|
const formatEvmMessage = (message)=>{
|
|
746
819
|
if (typeof message === 'string' && message.startsWith('0x')) {
|
|
747
820
|
const serializedTxBytes = Uint8Array.from(Buffer.from(message.slice(2), 'hex'));
|
|
@@ -1250,21 +1323,25 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
|
|
|
1250
1323
|
* Processes a single upload result and logs appropriately
|
|
1251
1324
|
* @returns Failure details if rejected, undefined if successful
|
|
1252
1325
|
*/ const processUploadResult = (result, locationName, logContext, logger)=>{
|
|
1253
|
-
var _result_reason;
|
|
1254
1326
|
if (result.status === 'fulfilled') {
|
|
1255
1327
|
logger.info(`[DynamicWaasWalletClient] Successfully uploaded keyshares to ${locationName}`, logContext);
|
|
1256
1328
|
return undefined;
|
|
1257
1329
|
}
|
|
1258
1330
|
const { message, stack } = getErrorDetails(result.reason);
|
|
1259
|
-
const
|
|
1331
|
+
const reasonProps = result.reason;
|
|
1332
|
+
const isRetryable = (reasonProps == null ? void 0 : reasonProps.isRetryable) !== false;
|
|
1333
|
+
var _reasonProps_errorReason;
|
|
1334
|
+
const errorReason = (_reasonProps_errorReason = reasonProps == null ? void 0 : reasonProps.errorReason) != null ? _reasonProps_errorReason : 'unknown';
|
|
1260
1335
|
logger.error(`[DynamicWaasWalletClient] Failed to upload keyshares to ${locationName}`, _extends({}, logContext, {
|
|
1261
1336
|
error: message,
|
|
1262
|
-
errorStack: stack
|
|
1337
|
+
errorStack: stack,
|
|
1338
|
+
errorReason
|
|
1263
1339
|
}));
|
|
1264
1340
|
return {
|
|
1265
1341
|
locationName,
|
|
1266
1342
|
message,
|
|
1267
|
-
isRetryable
|
|
1343
|
+
isRetryable,
|
|
1344
|
+
errorReason
|
|
1268
1345
|
};
|
|
1269
1346
|
};
|
|
1270
1347
|
/**
|
|
@@ -1310,8 +1387,24 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
|
|
|
1310
1387
|
processUploadResult(results[1], 'Google Drive Personal', logContext, logger)
|
|
1311
1388
|
].filter((failure)=>failure !== undefined);
|
|
1312
1389
|
if (failures.length > 0) {
|
|
1313
|
-
|
|
1390
|
+
const uniqueReasons = [
|
|
1391
|
+
...new Set(failures.map((f)=>f.errorReason))
|
|
1392
|
+
];
|
|
1393
|
+
const isUserActionable = failures.every((f)=>isUserActionableGoogleDriveBackupErrorReason(f.errorReason));
|
|
1394
|
+
// OAuth scope and storage-quota failures are end-user issues (grant
|
|
1395
|
+
// Drive permission, free up storage) — not anything oncall can fix.
|
|
1396
|
+
// Downgrade them to warn so monitors can filter on status:error and
|
|
1397
|
+
// still catch real system-side failures (5xx/network/unknown) without
|
|
1398
|
+
// the user-side noise drowning out the signal.
|
|
1399
|
+
const logFn = isUserActionable ? logger.warn : logger.error;
|
|
1400
|
+
// When every location failed for the same reason (the dominant case)
|
|
1401
|
+
// surface a single string; otherwise emit the array so consumers can
|
|
1402
|
+
// see the mix.
|
|
1403
|
+
const logErrorReason = uniqueReasons.length === 1 ? uniqueReasons[0] : uniqueReasons;
|
|
1404
|
+
logFn.call(logger, '[DynamicWaasWalletClient] Google Drive backup failed', _extends({}, logContext, {
|
|
1314
1405
|
errorCount: failures.length,
|
|
1406
|
+
errorReason: logErrorReason,
|
|
1407
|
+
isUserActionable,
|
|
1315
1408
|
errors: failures.map((f)=>`Failed to backup keyshares to ${f.locationName}: ${f.message}`)
|
|
1316
1409
|
}));
|
|
1317
1410
|
// Both upload destinations (appDataFolder + personal) commonly fail with
|
|
@@ -1325,67 +1418,12 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
|
|
|
1325
1418
|
const finalMessage = uniqueMessages.length === 1 ? uniqueMessages[0] : failures.map((f)=>`${f.locationName}: ${f.message}`).join(' | ');
|
|
1326
1419
|
const aggregatedError = new Error(finalMessage);
|
|
1327
1420
|
aggregatedError.isRetryable = failures.every((f)=>f.isRetryable);
|
|
1421
|
+
aggregatedError.errorReason = uniqueReasons.length === 1 ? uniqueReasons[0] : 'unknown';
|
|
1328
1422
|
throw aggregatedError;
|
|
1329
1423
|
}
|
|
1330
1424
|
logger.info('[DynamicWaasWalletClient] Google Drive backup completed successfully', logContext);
|
|
1331
1425
|
};
|
|
1332
1426
|
|
|
1333
|
-
const ERROR_PASSWORD_MISMATCH = '[DynamicWaasWalletClient]: Password does not match the password used for existing wallets. All wallets must use the same password for encrypted backups.';
|
|
1334
|
-
const ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET = '[DynamicWaasWalletClient]: Password is required for refresh/reshare of a password-encrypted wallet.';
|
|
1335
|
-
const ERROR_EXISTING_PASSWORD_REQUIRED = '[DynamicWaasWalletClient]: Existing password is required to update the password for encrypted wallets.';
|
|
1336
|
-
const ERROR_WALLETS_ALREADY_ENCRYPTED = '[DynamicWaasWalletClient]: Cannot set password: wallets are already password-encrypted. Use updatePassword instead.';
|
|
1337
|
-
/**
|
|
1338
|
-
* Checks if password consistency validation is needed.
|
|
1339
|
-
* Returns false if no password provided or password is the environmentId default.
|
|
1340
|
-
*/ function shouldValidatePassword(password, environmentId) {
|
|
1341
|
-
if (!password || password === environmentId) {
|
|
1342
|
-
return false;
|
|
1343
|
-
}
|
|
1344
|
-
return true;
|
|
1345
|
-
}
|
|
1346
|
-
/**
|
|
1347
|
-
* Finds the first password-encrypted wallet in the walletMap.
|
|
1348
|
-
* Returns [normalizedAddress, walletProperties] or undefined.
|
|
1349
|
-
*/ function findPasswordEncryptedWallet(walletMap) {
|
|
1350
|
-
for (const [address, wallet] of Object.entries(walletMap)){
|
|
1351
|
-
var _wallet_clientKeySharesBackupInfo;
|
|
1352
|
-
if ((_wallet_clientKeySharesBackupInfo = wallet.clientKeySharesBackupInfo) == null ? void 0 : _wallet_clientKeySharesBackupInfo.passwordEncrypted) {
|
|
1353
|
-
return [
|
|
1354
|
-
address,
|
|
1355
|
-
wallet
|
|
1356
|
-
];
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
return undefined;
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* Extracts the first encrypted key share credential from cached encrypted data.
|
|
1363
|
-
* Returns the base64-encoded encrypted credential string, or undefined if none found.
|
|
1364
|
-
*/ function extractFirstEncryptedCredential(encryptedData) {
|
|
1365
|
-
try {
|
|
1366
|
-
const parsed = JSON.parse(encryptedData);
|
|
1367
|
-
const keyShares = parsed == null ? void 0 : parsed.keyShares;
|
|
1368
|
-
if (!Array.isArray(keyShares) || keyShares.length === 0) {
|
|
1369
|
-
return undefined;
|
|
1370
|
-
}
|
|
1371
|
-
return keyShares[0].encryptedAccountCredential;
|
|
1372
|
-
} catch (e) {
|
|
1373
|
-
return undefined;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Checks if a wallet uses password-based encryption for its key share backups.
|
|
1378
|
-
*/ function isWalletPasswordEncrypted(wallet) {
|
|
1379
|
-
var _wallet_clientKeySharesBackupInfo;
|
|
1380
|
-
var _wallet_clientKeySharesBackupInfo_passwordEncrypted;
|
|
1381
|
-
return (_wallet_clientKeySharesBackupInfo_passwordEncrypted = wallet == null ? void 0 : (_wallet_clientKeySharesBackupInfo = wallet.clientKeySharesBackupInfo) == null ? void 0 : _wallet_clientKeySharesBackupInfo.passwordEncrypted) != null ? _wallet_clientKeySharesBackupInfo_passwordEncrypted : false;
|
|
1382
|
-
}
|
|
1383
|
-
/**
|
|
1384
|
-
* Checks if an error indicates a password mismatch (wrong password during decryption).
|
|
1385
|
-
*/ function isPasswordMismatchError(error) {
|
|
1386
|
-
return error instanceof InvalidPasswordError;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
1427
|
/**
|
|
1390
1428
|
* Returns true for errors that represent expected user-facing conditions
|
|
1391
1429
|
* (e.g. wrong password). These are logged at `info` instead of `error`
|
|
@@ -2313,7 +2351,18 @@ class DynamicWalletClient {
|
|
|
2313
2351
|
clientKeygenResults
|
|
2314
2352
|
};
|
|
2315
2353
|
}
|
|
2316
|
-
async keyGen(
|
|
2354
|
+
async keyGen(args) {
|
|
2355
|
+
return retryPromise(()=>this.runKeyGenAttempt(args), {
|
|
2356
|
+
maxAttempts: 5,
|
|
2357
|
+
retryInterval: 500,
|
|
2358
|
+
operationName: 'keyGen',
|
|
2359
|
+
logContext: _extends({
|
|
2360
|
+
chainName: args.chainName,
|
|
2361
|
+
thresholdSignatureScheme: args.thresholdSignatureScheme
|
|
2362
|
+
}, this.getTraceContext(args.traceContext))
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
async runKeyGenAttempt({ chainName, thresholdSignatureScheme, bitcoinConfig, onCeremonyComplete, traceContext, password, signedSessionId }) {
|
|
2317
2366
|
const dynamicRequestId = uuid.v4();
|
|
2318
2367
|
try {
|
|
2319
2368
|
this.assertPasswordRequired(password);
|
|
@@ -2375,6 +2424,7 @@ class DynamicWalletClient {
|
|
|
2375
2424
|
clientKeyShares
|
|
2376
2425
|
};
|
|
2377
2426
|
} catch (error) {
|
|
2427
|
+
markCeremonyErrorNonRetryable(error);
|
|
2378
2428
|
logError({
|
|
2379
2429
|
message: 'Error in keyGen',
|
|
2380
2430
|
error: error,
|
|
@@ -2387,7 +2437,18 @@ class DynamicWalletClient {
|
|
|
2387
2437
|
throw error;
|
|
2388
2438
|
}
|
|
2389
2439
|
}
|
|
2390
|
-
async importRawPrivateKey(
|
|
2440
|
+
async importRawPrivateKey(args) {
|
|
2441
|
+
return retryPromise(()=>this.runImportRawPrivateKeyAttempt(args), {
|
|
2442
|
+
maxAttempts: 5,
|
|
2443
|
+
retryInterval: 500,
|
|
2444
|
+
operationName: 'importRawPrivateKey',
|
|
2445
|
+
logContext: _extends({
|
|
2446
|
+
chainName: args.chainName,
|
|
2447
|
+
thresholdSignatureScheme: args.thresholdSignatureScheme
|
|
2448
|
+
}, this.getTraceContext(args.traceContext))
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
async runImportRawPrivateKeyAttempt({ chainName, privateKey, thresholdSignatureScheme, bitcoinConfig, onError, onCeremonyComplete, traceContext, legacyWalletId, password, signedSessionId }) {
|
|
2391
2452
|
const dynamicRequestId = uuid.v4();
|
|
2392
2453
|
try {
|
|
2393
2454
|
this.assertPasswordRequired(password);
|
|
@@ -2473,6 +2534,7 @@ class DynamicWalletClient {
|
|
|
2473
2534
|
clientKeyShares: clientKeygenResults
|
|
2474
2535
|
};
|
|
2475
2536
|
} catch (error) {
|
|
2537
|
+
markCeremonyErrorNonRetryable(error);
|
|
2476
2538
|
logError({
|
|
2477
2539
|
message: 'Error in importRawPrivateKey',
|
|
2478
2540
|
error: error,
|
|
@@ -2673,11 +2735,9 @@ class DynamicWalletClient {
|
|
|
2673
2735
|
mfaToken,
|
|
2674
2736
|
storeRecoveredShares: false
|
|
2675
2737
|
});
|
|
2676
|
-
// Overwrite local key shares with recovered ones (not merge)
|
|
2677
2738
|
await this.setClientKeySharesToStorage({
|
|
2678
2739
|
accountAddress,
|
|
2679
|
-
clientKeyShares: recoveredKeyShares
|
|
2680
|
-
overwriteOrMerge: 'overwrite'
|
|
2740
|
+
clientKeyShares: recoveredKeyShares
|
|
2681
2741
|
});
|
|
2682
2742
|
this.logger.info('[DynamicWaasWalletClient] Key shares recovered, retrying operation', _extends({
|
|
2683
2743
|
accountAddress,
|
|
@@ -3011,8 +3071,7 @@ class DynamicWalletClient {
|
|
|
3011
3071
|
});
|
|
3012
3072
|
await this.setClientKeySharesToStorage({
|
|
3013
3073
|
accountAddress,
|
|
3014
|
-
clientKeyShares: refreshResults
|
|
3015
|
-
overwriteOrMerge: 'overwrite'
|
|
3074
|
+
clientKeyShares: refreshResults
|
|
3016
3075
|
});
|
|
3017
3076
|
} catch (error) {
|
|
3018
3077
|
// Expected condition — not a server error, no need to page on this.
|
|
@@ -3367,8 +3426,7 @@ class DynamicWalletClient {
|
|
|
3367
3426
|
// only after backup succeeds.
|
|
3368
3427
|
await this.setClientKeySharesToStorage({
|
|
3369
3428
|
accountAddress,
|
|
3370
|
-
clientKeyShares: distribution.clientShares
|
|
3371
|
-
overwriteOrMerge: 'overwrite'
|
|
3429
|
+
clientKeyShares: distribution.clientShares
|
|
3372
3430
|
});
|
|
3373
3431
|
} catch (error) {
|
|
3374
3432
|
logError({
|
|
@@ -3391,8 +3449,7 @@ class DynamicWalletClient {
|
|
|
3391
3449
|
});
|
|
3392
3450
|
await this.setClientKeySharesToStorage({
|
|
3393
3451
|
accountAddress,
|
|
3394
|
-
clientKeyShares: []
|
|
3395
|
-
overwriteOrMerge: 'overwrite'
|
|
3452
|
+
clientKeyShares: []
|
|
3396
3453
|
});
|
|
3397
3454
|
throw error;
|
|
3398
3455
|
}
|
|
@@ -3738,63 +3795,44 @@ class DynamicWalletClient {
|
|
|
3738
3795
|
* Helper function to store client key shares in storage.
|
|
3739
3796
|
* Uses secureStorage when available (mobile), otherwise falls back to localStorage (browser).
|
|
3740
3797
|
*/ /**
|
|
3741
|
-
*
|
|
3742
|
-
|
|
3743
|
-
* persistence-info log so we can audit storage operations from traces.
|
|
3744
|
-
*/ async resolveSharesToPersist({ accountAddress, clientKeyShares, overwriteOrMerge, source, readExisting }) {
|
|
3745
|
-
let sharesToStore;
|
|
3746
|
-
let existingCount = 0;
|
|
3747
|
-
if (overwriteOrMerge === 'overwrite') {
|
|
3748
|
-
sharesToStore = clientKeyShares;
|
|
3749
|
-
} else {
|
|
3750
|
-
const existing = await readExisting();
|
|
3751
|
-
existingCount = existing.length;
|
|
3752
|
-
sharesToStore = mergeUniqueKeyShares(existing, clientKeyShares);
|
|
3753
|
-
}
|
|
3798
|
+
* Emits a persistence-info log so storage writes are auditable from traces.
|
|
3799
|
+
*/ logSharePersistence({ accountAddress, clientKeyShares, source }) {
|
|
3754
3800
|
this.logger.info('[DynamicWaasWalletClient] Persisting client key shares', {
|
|
3755
3801
|
accountAddress,
|
|
3756
3802
|
source,
|
|
3757
|
-
|
|
3758
|
-
inputCount: clientKeyShares.length,
|
|
3759
|
-
existingCount,
|
|
3760
|
-
finalCount: sharesToStore.length
|
|
3803
|
+
inputCount: clientKeyShares.length
|
|
3761
3804
|
});
|
|
3762
|
-
return sharesToStore;
|
|
3763
3805
|
}
|
|
3764
|
-
async setClientKeySharesToLocalStorage({ accountAddress, clientKeyShares
|
|
3806
|
+
async setClientKeySharesToLocalStorage({ accountAddress, clientKeyShares }) {
|
|
3765
3807
|
accountAddress = normalizeAddress(accountAddress);
|
|
3766
|
-
|
|
3808
|
+
this.logSharePersistence({
|
|
3767
3809
|
accountAddress,
|
|
3768
3810
|
clientKeyShares,
|
|
3769
|
-
|
|
3770
|
-
source: 'localStorage',
|
|
3771
|
-
readExisting: ()=>this.getClientKeySharesFromLocalStorage({
|
|
3772
|
-
accountAddress
|
|
3773
|
-
})
|
|
3811
|
+
source: 'localStorage'
|
|
3774
3812
|
});
|
|
3775
3813
|
const stringifiedClientKeyShares = JSON.stringify({
|
|
3776
|
-
clientKeyShares
|
|
3814
|
+
clientKeyShares
|
|
3777
3815
|
});
|
|
3778
3816
|
await this.storage.setItem(accountAddress, stringifiedClientKeyShares);
|
|
3779
3817
|
}
|
|
3780
3818
|
/**
|
|
3781
|
-
*
|
|
3819
|
+
* Replaces the client key shares stored for `accountAddress` with `clientKeyShares`.
|
|
3782
3820
|
* Uses secureStorage when available (mobile), otherwise falls back to localStorage (browser).
|
|
3783
|
-
|
|
3821
|
+
*
|
|
3822
|
+
* A WaaS wallet holds exactly one client share per device (see share-distribution
|
|
3823
|
+
* factories in types.ts), so writes are always a full replacement — there is no
|
|
3824
|
+
* merge mode. Callers that need to clear local shares pass `clientKeyShares: []`.
|
|
3825
|
+
*/ async setClientKeySharesToStorage({ accountAddress, clientKeyShares }) {
|
|
3784
3826
|
accountAddress = normalizeAddress(accountAddress);
|
|
3785
3827
|
// Use secure storage if available (mobile)
|
|
3786
3828
|
if (this.secureStorage) {
|
|
3787
3829
|
try {
|
|
3788
|
-
|
|
3830
|
+
this.logSharePersistence({
|
|
3789
3831
|
accountAddress,
|
|
3790
3832
|
clientKeyShares,
|
|
3791
|
-
|
|
3792
|
-
source: 'secureStorage',
|
|
3793
|
-
readExisting: ()=>this.getClientKeySharesFromStorage({
|
|
3794
|
-
accountAddress
|
|
3795
|
-
})
|
|
3833
|
+
source: 'secureStorage'
|
|
3796
3834
|
});
|
|
3797
|
-
await this.secureStorage.setClientKeyShare(accountAddress,
|
|
3835
|
+
await this.secureStorage.setClientKeyShare(accountAddress, clientKeyShares);
|
|
3798
3836
|
return;
|
|
3799
3837
|
} catch (error) {
|
|
3800
3838
|
logError({
|
|
@@ -3810,8 +3848,7 @@ class DynamicWalletClient {
|
|
|
3810
3848
|
// Fallback to localStorage (browser)
|
|
3811
3849
|
await this.setClientKeySharesToLocalStorage({
|
|
3812
3850
|
accountAddress,
|
|
3813
|
-
clientKeyShares
|
|
3814
|
-
overwriteOrMerge
|
|
3851
|
+
clientKeyShares
|
|
3815
3852
|
});
|
|
3816
3853
|
}
|
|
3817
3854
|
/**
|
|
@@ -4603,8 +4640,7 @@ class DynamicWalletClient {
|
|
|
4603
4640
|
if (storeRecoveredShares) {
|
|
4604
4641
|
await this.setClientKeySharesToStorage({
|
|
4605
4642
|
accountAddress,
|
|
4606
|
-
clientKeyShares: decryptedKeyShares
|
|
4607
|
-
overwriteOrMerge: 'merge'
|
|
4643
|
+
clientKeyShares: decryptedKeyShares
|
|
4608
4644
|
});
|
|
4609
4645
|
await this.storage.setItem(this.storageKey, JSON.stringify(this.walletMap));
|
|
4610
4646
|
}
|
|
@@ -5240,13 +5276,8 @@ class DynamicWalletClient {
|
|
|
5240
5276
|
signedSessionId,
|
|
5241
5277
|
shareCount
|
|
5242
5278
|
});
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
});
|
|
5246
|
-
await this.setClientKeySharesToStorage({
|
|
5247
|
-
accountAddress,
|
|
5248
|
-
clientKeyShares: mergeUniqueKeyShares(existingKeyShares, decryptedKeyShares)
|
|
5249
|
-
});
|
|
5279
|
+
// recoverEncryptedBackupByWallet (storeRecoveredShares: true by default) has
|
|
5280
|
+
// already written decryptedKeyShares to storage. No further write needed.
|
|
5250
5281
|
this.logger.debug('[DynamicWaasWalletClient] Recovered backup', {
|
|
5251
5282
|
decryptedKeyShares
|
|
5252
5283
|
});
|
|
@@ -5331,6 +5362,45 @@ class DynamicWalletClient {
|
|
|
5331
5362
|
});
|
|
5332
5363
|
}
|
|
5333
5364
|
/**
|
|
5365
|
+
* Fetches the encrypted-shares blob from the server and caches it in this.storage.
|
|
5366
|
+
* Returns the serialized blob, or null if the wallet is unknown or the API returns
|
|
5367
|
+
* no data. Does not decrypt — callers (e.g. unlockWallet, getWallet eager-load)
|
|
5368
|
+
* own the decrypt step.
|
|
5369
|
+
*/ async fetchAndCacheEncryptedShares({ accountAddress, signedSessionId, mfaToken }) {
|
|
5370
|
+
const walletData = this.getWalletFromMap(accountAddress);
|
|
5371
|
+
if (!walletData) {
|
|
5372
|
+
return null;
|
|
5373
|
+
}
|
|
5374
|
+
const { shares } = this.recoverStrategy({
|
|
5375
|
+
clientKeyShareBackupInfo: walletData.clientKeySharesBackupInfo,
|
|
5376
|
+
thresholdSignatureScheme: walletData.thresholdSignatureScheme,
|
|
5377
|
+
walletOperation: core.WalletOperation.RECOVER
|
|
5378
|
+
});
|
|
5379
|
+
const externalKeyShareIds = shares[core.BackupLocation.DYNAMIC] || [];
|
|
5380
|
+
this.logger.info('[unlockWallet] cache miss after getWallet, fetching encrypted shares from server', {
|
|
5381
|
+
context: {
|
|
5382
|
+
accountAddress,
|
|
5383
|
+
walletId: walletData.walletId,
|
|
5384
|
+
externalKeyShareIds
|
|
5385
|
+
}
|
|
5386
|
+
});
|
|
5387
|
+
const data = await this.apiClient.recoverEncryptedBackupByWallet({
|
|
5388
|
+
walletId: walletData.walletId,
|
|
5389
|
+
externalKeyShareIds,
|
|
5390
|
+
signedSessionId,
|
|
5391
|
+
mfaToken,
|
|
5392
|
+
requiresSignedSessionId: this.requiresSignedSessionId(),
|
|
5393
|
+
userId: this.userId
|
|
5394
|
+
});
|
|
5395
|
+
if (!data) {
|
|
5396
|
+
return null;
|
|
5397
|
+
}
|
|
5398
|
+
const serialized = JSON.stringify(data);
|
|
5399
|
+
const encryptedStorageKey = `${normalizeAddress(accountAddress)}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
|
|
5400
|
+
await this.storage.setItem(encryptedStorageKey, serialized);
|
|
5401
|
+
return serialized;
|
|
5402
|
+
}
|
|
5403
|
+
/**
|
|
5334
5404
|
* Unlocks a password-encrypted wallet by decrypting cached encrypted shares.
|
|
5335
5405
|
* This method should be called after getWalletRecoveryState returns LOCKED state.
|
|
5336
5406
|
*
|
|
@@ -5338,7 +5408,7 @@ class DynamicWalletClient {
|
|
|
5338
5408
|
* @param password - The password to decrypt the shares
|
|
5339
5409
|
* @param signedSessionId - The signed session ID for authentication
|
|
5340
5410
|
* @returns The unlocked wallet properties
|
|
5341
|
-
*/ async unlockWallet({ accountAddress, password, signedSessionId }) {
|
|
5411
|
+
*/ async unlockWallet({ accountAddress, password, signedSessionId, mfaToken }) {
|
|
5342
5412
|
const dynamicRequestId = uuid.v4();
|
|
5343
5413
|
try {
|
|
5344
5414
|
await this.requireWalletFromMap(accountAddress, signedSessionId);
|
|
@@ -5358,6 +5428,18 @@ class DynamicWalletClient {
|
|
|
5358
5428
|
});
|
|
5359
5429
|
encryptedData = await this.storage.getItem(encryptedStorageKey);
|
|
5360
5430
|
}
|
|
5431
|
+
// getWallet short-circuits via checkWalletFields when client key shares are
|
|
5432
|
+
// durable but the encrypted-shares cache in this.storage was evicted (e.g. RN
|
|
5433
|
+
// secureStorage survives a WebKit content-process recycle while localStorage
|
|
5434
|
+
// does not). Populate the cache directly so the single decrypt path below
|
|
5435
|
+
// can proceed.
|
|
5436
|
+
if (!encryptedData) {
|
|
5437
|
+
encryptedData = await this.fetchAndCacheEncryptedShares({
|
|
5438
|
+
accountAddress,
|
|
5439
|
+
signedSessionId,
|
|
5440
|
+
mfaToken
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5361
5443
|
if (!encryptedData) {
|
|
5362
5444
|
throw new Error('No encrypted shares found for wallet');
|
|
5363
5445
|
}
|
|
@@ -5371,7 +5453,17 @@ class DynamicWalletClient {
|
|
|
5371
5453
|
const otherEncryptedWallets = Object.entries(this.walletMap).filter(([addr, w])=>addr !== normalizedAccountAddress && w.walletReadyState !== core.WalletReadyState.READY && isWalletPasswordEncrypted(w));
|
|
5372
5454
|
const results = await Promise.allSettled(otherEncryptedWallets.map(async ([otherAddr])=>{
|
|
5373
5455
|
const otherEncryptedStorageKey = `${otherAddr}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
|
|
5374
|
-
|
|
5456
|
+
let otherEncryptedData = await this.storage.getItem(otherEncryptedStorageKey);
|
|
5457
|
+
// Same cache-eviction scenario as the main wallet above: on RN, the
|
|
5458
|
+
// encrypted-shares blob can be gone while client shares survive in
|
|
5459
|
+
// secureStorage. Re-fetch from the server before giving up.
|
|
5460
|
+
if (!otherEncryptedData) {
|
|
5461
|
+
otherEncryptedData = await this.fetchAndCacheEncryptedShares({
|
|
5462
|
+
accountAddress: otherAddr,
|
|
5463
|
+
signedSessionId,
|
|
5464
|
+
mfaToken
|
|
5465
|
+
});
|
|
5466
|
+
}
|
|
5375
5467
|
if (!otherEncryptedData) {
|
|
5376
5468
|
return;
|
|
5377
5469
|
}
|
|
@@ -5876,11 +5968,12 @@ exports.isBrowser = isBrowser;
|
|
|
5876
5968
|
exports.isHeavyQueueOperation = isHeavyQueueOperation;
|
|
5877
5969
|
exports.isHexString = isHexString;
|
|
5878
5970
|
exports.isICloudAuthenticated = isICloudAuthenticated;
|
|
5971
|
+
exports.isNonRetryableCeremonyError = isNonRetryableCeremonyError;
|
|
5879
5972
|
exports.isPublicKeyMismatchError = isPublicKeyMismatchError;
|
|
5880
5973
|
exports.isRecoverQueueOperation = isRecoverQueueOperation;
|
|
5881
5974
|
exports.isSignQueueOperation = isSignQueueOperation;
|
|
5882
5975
|
exports.listICloudBackups = listICloudBackups;
|
|
5883
|
-
exports.
|
|
5976
|
+
exports.markCeremonyErrorNonRetryable = markCeremonyErrorNonRetryable;
|
|
5884
5977
|
exports.readEnvironmentSettings = readEnvironmentSettings;
|
|
5885
5978
|
exports.retryPromise = retryPromise;
|
|
5886
5979
|
exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
|