diff --git a/crates/sage-api/src/types/error_kind.rs b/crates/sage-api/src/types/error_kind.rs index 623bd4db..99581a66 100644 --- a/crates/sage-api/src/types/error_kind.rs +++ b/crates/sage-api/src/types/error_kind.rs @@ -9,5 +9,6 @@ pub enum ErrorKind { NotFound, Unauthorized, Internal, + DatabaseMigration, Nfc, } diff --git a/crates/sage-cli/src/rpc.rs b/crates/sage-cli/src/rpc.rs index 843553d5..20dfcdca 100644 --- a/crates/sage-cli/src/rpc.rs +++ b/crates/sage-cli/src/rpc.rs @@ -23,6 +23,7 @@ impl_endpoints! { Self::Start => { let mut sage = Sage::new(&path); let mut receiver = sage.initialize().await?; + sage.switch_wallet().await?; tokio::spawn(async move { while let Some(_message) = receiver.recv().await {} }); start_rpc(Arc::new(Mutex::new(sage))).await }, diff --git a/crates/sage-rpc/src/lib.rs b/crates/sage-rpc/src/lib.rs index ba7e8812..afb8aeb5 100644 --- a/crates/sage-rpc/src/lib.rs +++ b/crates/sage-rpc/src/lib.rs @@ -45,9 +45,10 @@ where ErrorKind::Api => StatusCode::BAD_REQUEST, ErrorKind::NotFound => StatusCode::NOT_FOUND, ErrorKind::Unauthorized => StatusCode::UNAUTHORIZED, - ErrorKind::Wallet | ErrorKind::Internal | ErrorKind::Nfc => { - StatusCode::INTERNAL_SERVER_ERROR - } + ErrorKind::DatabaseMigration + | ErrorKind::Wallet + | ErrorKind::Internal + | ErrorKind::Nfc => StatusCode::INTERNAL_SERVER_ERROR, }; (status, error.to_string()).into_response() } diff --git a/crates/sage/src/error.rs b/crates/sage/src/error.rs index 44b3f19a..17744e70 100644 --- a/crates/sage/src/error.rs +++ b/crates/sage/src/error.rs @@ -224,11 +224,11 @@ impl Error { | KeychainError::Bip39(..) | KeychainError::Argon2(..) => ErrorKind::Internal, }, + Self::SqlxMigration(..) => ErrorKind::DatabaseMigration, Self::Send(..) | Self::Io(..) | Self::Client(..) | Self::Sqlx(..) - | Self::SqlxMigration(..) | Self::Bip39(..) | Self::TomlDe(..) | Self::TomlSer(..) diff --git a/crates/sage/src/sage.rs b/crates/sage/src/sage.rs index 53108c12..d8efad5b 100644 --- a/crates/sage/src/sage.rs +++ b/crates/sage/src/sage.rs @@ -69,7 +69,6 @@ impl Sage { self.setup_logging()?; let receiver = self.setup_sync_manager()?; - self.switch_wallet().await?; self.setup_peers().await?; info!("Sage wallet initialized"); diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7b0b2f01..b02548dd 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -146,6 +146,13 @@ pub async fn set_rpc_run_on_startup( Ok(()) } +#[command] +#[specta] +pub async fn switch_wallet(state: State<'_, AppState>) -> Result<()> { + state.lock().await.switch_wallet().await?; + Ok(()) +} + #[command] #[specta] pub async fn move_key(state: State<'_, AppState>, fingerprint: u32, index: u32) -> Result<()> { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 62607eca..6a5dfd4f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -114,6 +114,7 @@ pub fn run() { commands::stop_rpc_server, commands::get_rpc_run_on_startup, commands::set_rpc_run_on_startup, + commands::switch_wallet, commands::move_key, commands::download_cni_offercode, ]) diff --git a/src/bindings.ts b/src/bindings.ts index f987566d..a7443794 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -278,6 +278,9 @@ async getRpcRunOnStartup() : Promise { async setRpcRunOnStartup(runOnStartup: boolean) : Promise { return await TAURI_INVOKE("set_rpc_run_on_startup", { runOnStartup }); }, +async switchWallet() : Promise { + return await TAURI_INVOKE("switch_wallet"); +}, async moveKey(fingerprint: number, index: number) : Promise { return await TAURI_INVOKE("move_key", { fingerprint, index }); }, @@ -363,7 +366,7 @@ export type DerivationMode = { mode: "default" } | export type DerivationRecord = { index: number; public_key: string; address: string } export type DidRecord = { launcher_id: string; name: string | null; visible: boolean; coin_id: string; address: string; amount: Amount; recovery_hash: string | null; created_height: number | null; create_transaction_id: string | null } export type Error = { kind: ErrorKind; reason: string } -export type ErrorKind = "wallet" | "api" | "not_found" | "unauthorized" | "internal" | "nfc" +export type ErrorKind = "wallet" | "api" | "not_found" | "unauthorized" | "internal" | "database_migration" | "nfc" export type FilterUnlockedCoins = { coin_ids: string[] } export type FilterUnlockedCoinsResponse = { coin_ids: string[] } export type GenerateMnemonic = { use_24_words: boolean } @@ -427,7 +430,7 @@ export type GetXchCoinsResponse = { coins: CoinRecord[]; total: number } export type ImportKey = { name: string; key: string; derivation_index?: number; save_secrets?: boolean; login?: boolean } export type ImportKeyResponse = { fingerprint: number } export type ImportOffer = { offer: string } -export type ImportOfferResponse = Record +export type ImportOfferResponse = { offer_id: string } export type IncreaseDerivationIndex = { hardened?: boolean | null; index: number } export type IncreaseDerivationIndexResponse = Record export type InheritedNetwork = "mainnet" | "testnet11" diff --git a/src/contexts/ErrorContext.tsx b/src/contexts/ErrorContext.tsx index 3a75350f..b700b755 100644 --- a/src/contexts/ErrorContext.tsx +++ b/src/contexts/ErrorContext.tsx @@ -110,7 +110,7 @@ export default function ErrorDialog({ error, setError }: ErrorDialogProps) { {kind ? `${kind} ` : ''}Error - + {error?.reason} diff --git a/src/hooks/useInitialization.ts b/src/hooks/useInitialization.ts index f6a68923..8a776218 100644 --- a/src/hooks/useInitialization.ts +++ b/src/hooks/useInitialization.ts @@ -1,24 +1,40 @@ import { commands } from '@/bindings'; import { useCallback, useEffect, useState } from 'react'; import { useErrors } from './useErrors'; +import { logoutAndUpdateState } from '@/state'; export default function useInitialization() { const { addError } = useErrors(); const [initialized, setInitialized] = useState(false); - const initialize = useCallback(async () => { - commands - .initialize() - .then(() => setInitialized(true)) - .catch(addError); + const onInitialize = useCallback(async () => { + try { + await commands.initialize(); + setInitialized(true); + await commands.switchWallet(); + } catch (error: any) { + // Always add the error to be displayed + addError(error); + + // Check if this is a database migration, which is recoverable + if (error.kind === 'database_migration') { + try { + await logoutAndUpdateState(); + } catch (logoutError) { + console.error('Error during logout:', logoutError); + } + } else { + console.error('Unrecoverable initialization error', error); + } + } }, [addError]); useEffect(() => { if (!initialized) { - initialize(); + onInitialize(); } - }, [initialized, initialize]); + }, [initialized, onInitialize]); return initialized; } diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 2a5caf00..a1c341ef 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -91,8 +91,14 @@ export default function Login() { useEffect(() => { commands .getKey({}) - .then((data) => data.key !== null && navigate('/wallet')) - .catch(addError); + .then((data) => { + if (data.key !== null) { + navigate('/wallet'); + } + }) + .catch((error) => { + addError(error); + }); }, [navigate, addError]); const [activeId, setActiveId] = useState(null); @@ -333,13 +339,17 @@ function WalletItem({ draggable, info, keys, setKeys }: WalletItemProps) { const loginSelf = (explicit: boolean) => { if (isMenuOpen && !explicit) return; - loginAndUpdateState(info.fingerprint).then(() => { - commands - .getKey({}) - .then((data) => setWallet(data.key)) - .then(() => navigate('/wallet')) - .catch(addError); - }); + loginAndUpdateState(info.fingerprint, addError) + .then(() => { + commands + .getKey({}) + .then((data) => setWallet(data.key)) + .then(() => navigate('/wallet')) + .catch(addError); + }) + .catch((error) => { + addError(error); + }); }; useEffect(() => { diff --git a/src/state.ts b/src/state.ts index 256ce1d9..a3fc80a1 100644 --- a/src/state.ts +++ b/src/state.ts @@ -6,6 +6,7 @@ import { GetSyncStatusResponse, KeyInfo, } from './bindings'; +import { CustomError } from './contexts/ErrorContext'; export interface WalletState { sync: GetSyncStatusResponse; @@ -85,9 +86,21 @@ events.syncEvent.listen((event) => { } }); -export async function loginAndUpdateState(fingerprint: number): Promise { - await commands.login({ fingerprint }); - await fetchState(); +export async function loginAndUpdateState( + fingerprint: number, + onError?: (error: CustomError) => void, +): Promise { + try { + await commands.login({ fingerprint }); + await fetchState(); + } catch (error) { + if (onError) { + onError(error as CustomError); + } else { + console.error(error); + } + throw error; + } } // Create a separate function to handle wallet state updates