+
Cloud Backup
+
+ {/* Status */}
+ {isEnabled && autoBackupStatus && (
+
+
Auto-backup enabled ({autoBackupStatus.method})
+ {autoBackupStatus.lastBackupAt && (
+
Last backup: {new Date(autoBackupStatus.lastBackupAt).toLocaleString()}
+ )}
+ {autoBackupStatus.lastError &&
Error: {autoBackupStatus.lastError}
}
+ {autoBackupStatus.needsGoogleReauth && (
+
+ )}
+
+ )}
+
+ {/* Auth */}
+ {!isEnabled && (
+ <>
+
+
+ {/* Encryption method selector */}
+
+
+
+
+
+ {/* Password input (only for password method) */}
+ {encryptionMethod === 'password' && (
+
setBackupPassword(e.target.value)}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
+ />
+ )}
+
+ {/* Enable */}
+
+ >
+ )}
+
+ {/* Restore + Disable */}
+ {isEnabled && (
+ <>
+
+
+ >
+ )}
+
+ {/* Status message */}
+ {status &&
{status}
}
+
+ );
+};
+
+export default CloudBackupSettings;
diff --git a/src/lib/intercom/desktop-adapter.ts b/src/lib/intercom/desktop-adapter.ts
index 43ed6b336..4c5fd1d25 100644
--- a/src/lib/intercom/desktop-adapter.ts
+++ b/src/lib/intercom/desktop-adapter.ts
@@ -7,6 +7,7 @@
*/
import * as Actions from 'lib/miden/back/actions';
+import { registerAutoBackupHooks } from 'lib/miden/back/auto-backup-manager';
import { store, toFront } from 'lib/miden/back/store';
import { MidenMessageType } from 'lib/miden/types';
import { WalletMessageType, WalletRequest, WalletResponse } from 'lib/shared/types';
@@ -36,6 +37,8 @@ export class DesktopIntercomAdapter {
this.notifySubscribers({ type: WalletMessageType.StateUpdated });
});
+ registerAutoBackupHooks();
+
this.initialized = true;
console.log('DesktopIntercomAdapter: Backend initialized');
}
diff --git a/src/lib/intercom/mobile-adapter.ts b/src/lib/intercom/mobile-adapter.ts
index 24d85e984..065055982 100644
--- a/src/lib/intercom/mobile-adapter.ts
+++ b/src/lib/intercom/mobile-adapter.ts
@@ -1,9 +1,20 @@
import * as Actions from 'lib/miden/back/actions';
+import {
+ disableAutoBackup,
+ enableAutoBackup,
+ getStatus as getAutoBackupStatus,
+ registerAutoBackupHooks,
+ restoreFromBackupNow
+} from 'lib/miden/back/auto-backup-manager';
import { store, toFront } from 'lib/miden/back/store';
+import { doCoreSyncState } from 'lib/miden/back/sync-manager';
+import { GoogleDriveProvider } from 'lib/miden/backup/google-drive-provider';
+import { probeCloudBackup, restoreCloudBackup, RestoreEncryptionArgs } from 'lib/miden/backup/restore-service';
import { MidenMessageType } from 'lib/miden/types';
-import { WalletMessageType, WalletRequest, WalletResponse } from 'lib/shared/types';
+import { b64ToU8 } from 'lib/shared/helpers';
+import { WalletMessageType, WalletNotification, WalletRequest, WalletResponse } from 'lib/shared/types';
-type SubscriptionCallback = (data: any) => void;
+type SubscriptionCallback = (data: WalletNotification) => void;
/**
* Mobile adapter for intercom that directly calls backend handlers
@@ -28,6 +39,8 @@ export class MobileIntercomAdapter {
this.notifySubscribers({ type: WalletMessageType.StateUpdated });
});
+ registerAutoBackupHooks();
+
this.initialized = true;
console.log('MobileIntercomAdapter: Backend initialized');
}
@@ -67,7 +80,7 @@ export class MobileIntercomAdapter {
};
case WalletMessageType.NewWalletRequest:
- await Actions.registerNewWallet((req as any).password, (req as any).mnemonic, (req as any).ownMnemonic);
+ await Actions.registerNewWallet(req.password, req.mnemonic, req.ownMnemonic);
return { type: WalletMessageType.NewWalletResponse };
case WalletMessageType.ImportFromClientRequest:
@@ -75,7 +88,7 @@ export class MobileIntercomAdapter {
return { type: WalletMessageType.ImportFromClientResponse };
case WalletMessageType.UnlockRequest:
- await Actions.unlock((req as any).password);
+ await Actions.unlock(req.password);
return { type: WalletMessageType.UnlockResponse };
case WalletMessageType.LockRequest:
@@ -83,76 +96,73 @@ export class MobileIntercomAdapter {
return { type: WalletMessageType.LockResponse };
case WalletMessageType.CreateAccountRequest:
- await Actions.createHDAccount((req as any).walletType, (req as any).name);
+ await Actions.createHDAccount(req.walletType, req.name);
return { type: WalletMessageType.CreateAccountResponse };
case WalletMessageType.UpdateCurrentAccountRequest:
- await Actions.updateCurrentAccount((req as any).accountPublicKey);
+ await Actions.updateCurrentAccount(req.accountPublicKey);
return { type: WalletMessageType.UpdateCurrentAccountResponse };
- case WalletMessageType.RevealMnemonicRequest:
- const mnemonic = await Actions.revealMnemonic((req as any).password);
+ case WalletMessageType.RevealMnemonicRequest: {
+ const mnemonic = await Actions.revealMnemonic(req.password);
return {
type: WalletMessageType.RevealMnemonicResponse,
mnemonic
};
+ }
case WalletMessageType.RemoveAccountRequest:
- await Actions.removeAccount((req as any).accountPublicKey, (req as any).password);
- return {
- type: WalletMessageType.RemoveAccountResponse
- };
+ Actions.removeAccount(req.accountPublicKey, req.password);
+ return { type: WalletMessageType.RemoveAccountResponse };
case WalletMessageType.EditAccountRequest:
- await Actions.editAccount((req as any).accountPublicKey, (req as any).name);
- return {
- type: WalletMessageType.EditAccountResponse
- };
+ Actions.editAccount(req.accountPublicKey, req.name);
+ return { type: WalletMessageType.EditAccountResponse };
case WalletMessageType.ImportAccountRequest:
- await Actions.importAccount((req as any).privateKey, (req as any).encPassword);
- return {
- type: WalletMessageType.ImportAccountResponse
- };
+ Actions.importAccount(req.privateKey, req.encPassword);
+ return { type: WalletMessageType.ImportAccountResponse };
case WalletMessageType.UpdateSettingsRequest:
- await Actions.updateSettings((req as any).settings);
- return {
- type: WalletMessageType.UpdateSettingsResponse
- };
+ await Actions.updateSettings(req.settings);
+ return { type: WalletMessageType.UpdateSettingsResponse };
- case WalletMessageType.SignTransactionRequest:
- const signature = await Actions.signTransaction((req as any).publicKey, (req as any).signingInputs);
+ case WalletMessageType.SignTransactionRequest: {
+ const signature = await Actions.signTransaction(req.publicKey, req.signingInputs);
return {
type: WalletMessageType.SignTransactionResponse,
signature
};
+ }
- case WalletMessageType.GetAuthSecretKeyRequest:
- const key = await Actions.getAuthSecretKey((req as any).key);
+ case WalletMessageType.GetAuthSecretKeyRequest: {
+ const key = await Actions.getAuthSecretKey(req.key);
return {
type: WalletMessageType.GetAuthSecretKeyResponse,
key
};
+ }
- case MidenMessageType.DAppGetAllSessionsRequest:
+ case MidenMessageType.DAppGetAllSessionsRequest: {
const allSessions = await Actions.getAllDAppSessions();
return {
type: MidenMessageType.DAppGetAllSessionsResponse,
sessions: allSessions
};
+ }
- case MidenMessageType.DAppRemoveSessionRequest:
- const sessions = await Actions.removeDAppSession((req as any).origin);
+ case MidenMessageType.DAppRemoveSessionRequest: {
+ const sessions = await Actions.removeDAppSession(req.origin);
return {
type: MidenMessageType.DAppRemoveSessionResponse,
sessions
};
+ }
- case MidenMessageType.PageRequest:
+ case MidenMessageType.PageRequest: {
const dAppEnabled = await Actions.isDAppEnabled();
if (dAppEnabled) {
- if ((req as any).payload === 'PING') {
+ if (req.payload === 'PING') {
return {
type: MidenMessageType.PageResponse,
payload: 'PONG'
@@ -160,11 +170,8 @@ export class MobileIntercomAdapter {
}
// PR-4 chunk 8: thread the multi-instance session id through if
// present so confirmation prompts route to the right session.
- const resPayload = await Actions.processDApp(
- (req as any).origin,
- (req as any).payload,
- (req as any).sessionId
- );
+ const pageReq = req as typeof req & { sessionId?: string };
+ const resPayload = await Actions.processDApp(req.origin, req.payload, pageReq.sessionId);
return {
type: MidenMessageType.PageResponse,
/* c8 ignore next -- dApp response nullish fallback, mobile-only */
@@ -172,6 +179,56 @@ export class MobileIntercomAdapter {
};
}
break;
+ }
+
+ case WalletMessageType.CloudBackupRestoreRequest: {
+ const restoreProvider = new GoogleDriveProvider(req.accessToken);
+ const restoreArgs: RestoreEncryptionArgs =
+ req.encryption.method === 'password'
+ ? { type: 'password', backupPassword: req.encryption.backupPassword }
+ : { type: 'passkey', keyMaterial: b64ToU8(req.encryption.keyMaterial) };
+ const content = await restoreCloudBackup(restoreArgs, restoreProvider);
+ return {
+ type: WalletMessageType.CloudBackupRestoreResponse,
+ walletAccounts: content.walletAccounts,
+ walletSettings: content.walletSettings
+ };
+ }
+
+ case WalletMessageType.CloudBackupProbeRequest: {
+ const probeProvider = new GoogleDriveProvider(req.accessToken);
+ const probe = await probeCloudBackup(probeProvider);
+ return { type: WalletMessageType.CloudBackupProbeResponse, ...probe };
+ }
+
+ case WalletMessageType.CloudBackupRegisterRequest: {
+ await Actions.registerFromCloudBackup(req.password ?? '', req.mnemonic, req.walletAccounts, req.walletSettings);
+ return { type: WalletMessageType.CloudBackupRegisterResponse };
+ }
+
+ case WalletMessageType.AutoBackupSetEnabledRequest: {
+ if (req.enabled && req.encryption && req.accessToken && req.expiresAt) {
+ await enableAutoBackup(req.encryption, req.accessToken, req.expiresAt, req.skipInitialBackup);
+ } else {
+ await disableAutoBackup();
+ }
+ return { type: WalletMessageType.AutoBackupSetEnabledResponse };
+ }
+
+ case WalletMessageType.AutoBackupStatusRequest: {
+ return { type: WalletMessageType.AutoBackupStatusResponse, ...getAutoBackupStatus() };
+ }
+
+ case WalletMessageType.AutoBackupRestoreNowRequest: {
+ await restoreFromBackupNow();
+ return { type: WalletMessageType.AutoBackupRestoreNowResponse };
+ }
+
+ case WalletMessageType.SyncRequest: {
+ await doCoreSyncState();
+ this.notifySubscribers({ type: WalletMessageType.SyncCompleted });
+ return { type: WalletMessageType.SyncResponse };
+ }
default:
console.warn('MobileIntercomAdapter: Unknown request type', req?.type);
@@ -181,7 +238,7 @@ export class MobileIntercomAdapter {
/**
* Notify all subscribers of a state change
*/
- private notifySubscribers(data: any): void {
+ private notifySubscribers(data: WalletNotification): void {
this.subscribers.forEach(callback => {
try {
callback(data);
diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts
index 0eb52a402..33afd786b 100644
--- a/src/lib/miden-chain/constants.ts
+++ b/src/lib/miden-chain/constants.ts
@@ -17,7 +17,7 @@ export enum MIDEN_NETWORK_NAME {
* Use `yarn build:devnet` to build for devnet.
*/
export const DEFAULT_NETWORK = (process.env.MIDEN_NETWORK as MIDEN_NETWORK_NAME) || MIDEN_NETWORK_NAME.TESTNET;
-
+console.log(`Using default network: ${DEFAULT_NETWORK}`);
export enum MIDEN_TRANSPORT_LAYER_NAME {
TESTNET = 'testnet',
LOCALNET = 'localnet'
diff --git a/src/lib/miden/activity/index.ts b/src/lib/miden/activity/index.ts
index 1c1392fa0..2c8844089 100644
--- a/src/lib/miden/activity/index.ts
+++ b/src/lib/miden/activity/index.ts
@@ -1,4 +1,3 @@
-import { isExtension } from 'lib/platform';
import { WalletMessageType } from 'lib/shared/types';
import { getIntercom } from 'lib/store';
@@ -8,10 +7,12 @@ export * from './notes';
/**
* Tell the service worker to start processing queued transactions.
- * No-op if not running as an extension. Fire-and-forget.
+ * On extension, this triggers the SW transaction processor.
+ * On mobile, transactions are processed in the frontend — this just
+ * notifies the backend so auto-backup can trigger.
+ * Fire-and-forget.
*/
export function requestSWTransactionProcessing(): void {
- if (!isExtension()) return;
getIntercom()
.request({ type: WalletMessageType.ProcessTransactionsRequest })
.catch(() => {});
diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts
index 033a627fa..c08f579c7 100644
--- a/src/lib/miden/activity/transactions.ts
+++ b/src/lib/miden/activity/transactions.ts
@@ -1,6 +1,7 @@
import { InputNoteState, Note, TransactionResult } from '@miden-sdk/miden-sdk/lazy';
import { liveQuery } from 'dexie';
+import { triggerBackup } from 'lib/miden/back/auto-backup-manager';
import * as Repo from 'lib/miden/repo';
import { isExtension, isMobile } from 'lib/platform';
import { u8ToB64 } from 'lib/shared/helpers';
@@ -719,6 +720,7 @@ export const generateTransactionsLoop = async (
// Call safely to cancel transaction and unlock records if something goes wrong
try {
await generateTransaction(nextTransaction, signCallback);
+ triggerBackup();
return true;
} catch (e) {
logger.warning('Failed to generate transaction', e);
diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts
index a35d66523..986efd765 100644
--- a/src/lib/miden/back/actions.ts
+++ b/src/lib/miden/back/actions.ts
@@ -129,6 +129,22 @@ export function registerImportedWallet(password?: string, mnemonic?: string, wal
});
}
+export function registerFromCloudBackup(
+ password: string,
+ mnemonic: string,
+ accounts: WalletAccount[],
+ settings: WalletSettings
+) {
+ return withInited(async () => {
+ const vault = await Vault.spawnFromCloudBackup(password, mnemonic, accounts, settings);
+ const vaultAccounts = await vault.fetchAccounts();
+ const vaultSettings = await vault.fetchSettings();
+ const currentAccount = await vault.getCurrentAccount();
+ const ownMnemonicFlag = await vault.isOwnMnemonic();
+ unlocked({ vault, accounts: vaultAccounts, settings: vaultSettings, currentAccount, ownMnemonic: ownMnemonicFlag });
+ });
+}
+
export function lock() {
return withInited(async () => {
// Wait for any in-flight WASM operation (e.g. TransactionProcessor's
diff --git a/src/lib/miden/back/auto-backup-manager.ts b/src/lib/miden/back/auto-backup-manager.ts
new file mode 100644
index 000000000..ac3c74085
--- /dev/null
+++ b/src/lib/miden/back/auto-backup-manager.ts
@@ -0,0 +1,299 @@
+/**
+ * Auto-backup manager — triggers cloud backup after explicit events
+ * (transaction completion, account creation, settings change).
+ *
+ * Encryption key material is persisted in vault storage (encrypted with the
+ * vault key). On each backup the key is read from the vault, used, and
+ * discarded. No secret key material is held in module-level variables.
+ *
+ * Supports both password (PBKDF2) and passkey (WebAuthn PRF) encryption.
+ */
+
+import { createCloudBackupWithKey } from 'lib/miden/backup/backup-service';
+import { refreshExtensionAccessToken } from 'lib/miden/backup/google-drive-auth';
+import { GoogleDriveProvider } from 'lib/miden/backup/google-drive-provider';
+import { restoreCloudBackupWithKey } from 'lib/miden/backup/restore-service';
+import { deriveKeyBytes, generateSalt, importVaultKey } from 'lib/miden/passworder';
+import { runWhenClientIdle } from 'lib/miden/sdk/miden-client';
+import { BackupEncryptionMethod } from 'lib/passkey/types';
+import { isExtension } from 'lib/platform';
+import { b64ToU8, u8ToB64 } from 'lib/shared/helpers';
+import { AutoBackupEncryption, AutoBackupSettings, AutoBackupStatus } from 'lib/shared/types';
+
+import * as Actions from './actions';
+import { accountsUpdated, locked, settingsUpdated, store, unlocked, withUnlocked } from './store';
+
+// ---- Module-level transient state (no secrets) ----
+
+let cachedAccessToken: string | null = null;
+let cachedTokenExpiresAt = 0;
+let needsGoogleReauth = false;
+let isBackingUp = false;
+let isPaused = false;
+let lastError: string | null = null;
+let settingsUpdateInProgress = false;
+let restoreInProgress = false;
+let hooksRegistered = false;
+
+// ---- Public API ----
+
+/**
+ * Wire auto-backup side effects to store events. Must be called once per
+ * backend entry point (extension service worker, mobile adapter, desktop
+ * adapter). Without this, account/settings changes won't trigger uploads.
+ * Idempotent: safe to call multiple times (subsequent calls are no-ops).
+ */
+export function registerAutoBackupHooks(): void {
+ if (hooksRegistered) return;
+ hooksRegistered = true;
+
+ accountsUpdated.watch(() => {
+ if (!restoreInProgress) triggerBackup();
+ });
+ settingsUpdated.watch(() => {
+ if (!settingsUpdateInProgress) triggerBackup();
+ });
+ unlocked.watch(() => onWalletUnlocked());
+ locked.watch(() => onWalletLocked());
+}
+
+export async function enableAutoBackup(
+ encryption: AutoBackupEncryption,
+ accessToken: string,
+ expiresAt: number,
+ skipInitialBackup: boolean = false
+): Promise