diff --git a/packages/web-wallet/index.html b/packages/web-wallet/index.html
index 39d7b50..7201ce3 100644
--- a/packages/web-wallet/index.html
+++ b/packages/web-wallet/index.html
@@ -3,6 +3,7 @@
+
WebZjs Web Wallet
diff --git a/packages/web-wallet/src/App.tsx b/packages/web-wallet/src/App.tsx
index 9e48408..69cd730 100644
--- a/packages/web-wallet/src/App.tsx
+++ b/packages/web-wallet/src/App.tsx
@@ -11,7 +11,7 @@ function App() {
const { installedSnap } = useMetaMaskContext();
const { state } = useWebZjsContext();
- const interval = installedSnap && !state.syncInProgress ? RESCAN_INTERVAL : null;
+ const interval = installedSnap && state.initialized && !state.syncInProgress ? RESCAN_INTERVAL : null;
useInterval(() => {
triggerRescan();
diff --git a/packages/web-wallet/src/context/WebzjsContext.tsx b/packages/web-wallet/src/context/WebzjsContext.tsx
index a2c31bd..1c11092 100644
--- a/packages/web-wallet/src/context/WebzjsContext.tsx
+++ b/packages/web-wallet/src/context/WebzjsContext.tsx
@@ -4,6 +4,7 @@ import React, {
useReducer,
useEffect,
useCallback,
+ useRef,
} from 'react';
import { get } from 'idb-keyval';
@@ -26,6 +27,7 @@ export interface WebZjsState {
activeAccount?: number | null;
syncInProgress: boolean;
loading: boolean;
+ initialized: boolean;
}
type Action =
@@ -35,7 +37,8 @@ type Action =
| { type: 'set-chain-height'; payload: bigint }
| { type: 'set-active-account'; payload: number }
| { type: 'set-sync-in-progress'; payload: boolean }
- | { type: 'set-loading'; payload: boolean };
+ | { type: 'set-loading'; payload: boolean }
+ | { type: 'set-initialized'; payload: boolean };
const initialState: WebZjsState = {
webWallet: null,
@@ -45,7 +48,8 @@ const initialState: WebZjsState = {
chainHeight: undefined,
activeAccount: null,
syncInProgress: false,
- loading: true,
+ loading: false,
+ initialized: false,
};
function reducer(state: WebZjsState, action: Action): WebZjsState {
@@ -64,6 +68,8 @@ function reducer(state: WebZjsState, action: Action): WebZjsState {
return { ...state, syncInProgress: action.payload };
case 'set-loading':
return { ...state, loading: action.payload };
+ case 'set-initialized':
+ return { ...state, initialized: action.payload };
default:
return state;
}
@@ -72,11 +78,13 @@ function reducer(state: WebZjsState, action: Action): WebZjsState {
interface WebZjsContextType {
state: WebZjsState;
dispatch: React.Dispatch;
+ initWallet: () => Promise;
}
const WebZjsContext = createContext({
state: initialState,
dispatch: () => {},
+ initWallet: async () => {},
});
export function useWebZjsContext(): WebZjsContextType {
@@ -85,6 +93,7 @@ export function useWebZjsContext(): WebZjsContextType {
export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
+ const initializingRef = useRef(false);
const initAll = useCallback(async () => {
try {
@@ -147,6 +156,7 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => {
}
dispatch({ type: 'set-loading', payload: false });
+ dispatch({ type: 'set-initialized', payload: true });
} catch (err) {
console.error('Initialization error:', err);
dispatch({ type: 'set-error', payload: Error(String(err)) });
@@ -154,9 +164,19 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => {
}
}, []);
- useEffect(() => {
- initAll().catch(console.error);
- }, [initAll]);
+ // Lazy-load WASM: call this when user wants to use wallet features
+ const initWallet = useCallback(async () => {
+ if (state.initialized || initializingRef.current) {
+ return; // Already initialized or in progress
+ }
+ initializingRef.current = true;
+ dispatch({ type: 'set-loading', payload: true });
+ try {
+ await initAll();
+ } finally {
+ initializingRef.current = false;
+ }
+ }, [state.initialized, initAll]);
useEffect(() => {
if (state.error) {
@@ -166,7 +186,7 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => {
return (
-
+
{children}
diff --git a/packages/web-wallet/src/pages/Home.tsx b/packages/web-wallet/src/pages/Home.tsx
index 0507460..a6b9357 100644
--- a/packages/web-wallet/src/pages/Home.tsx
+++ b/packages/web-wallet/src/pages/Home.tsx
@@ -8,7 +8,7 @@ import Loader from '../components/Loader/Loader';
const Home: React.FC = () => {
const navigate = useNavigate();
- const { state, dispatch } = useWebZjsContext();
+ const { state, dispatch, initWallet } = useWebZjsContext();
const { getAccountData, connectWebZjsSnap, recoverWallet } = useWebZjsActions();
const { installedSnap } = useMetaMask();
const { isPendingRequest } = useMetaMaskContext();
@@ -23,8 +23,19 @@ const Home: React.FC = () => {
e.preventDefault();
connectingRef.current = true;
try {
+ // Lazy-load WASM on first user interaction
+ await initWallet();
await connectWebZjsSnap();
navigate('/dashboard/account-summary');
+ } catch (err: any) {
+ // Handle user rejection gracefully (code 4001)
+ if (err?.code === 4001) {
+ console.log('User rejected MetaMask connection');
+ return;
+ }
+ // Other errors should be shown to user
+ console.error('Connection failed:', err);
+ dispatch({ type: 'set-error', payload: err instanceof Error ? err : new Error(String(err)) });
} finally {
connectingRef.current = false;
}
@@ -52,9 +63,24 @@ const Home: React.FC = () => {
return;
}
- // Case 2: No account but snap is installed - auto-recover (once only)
- // Skip if connect is in progress to avoid duplicate viewingKey prompts
- if (!recoveryAttemptedRef.current && !connectingRef.current) {
+ // Case 2: Wallet not initialized yet - initialize it first
+ if (!state.initialized && !connectingRef.current) {
+ try {
+ setRecovering(true);
+ await initWallet();
+ // After init completes, effect will re-run with updated state
+ } catch (err) {
+ console.error('Wallet initialization failed:', err);
+ dispatch({ type: 'set-error', payload: err instanceof Error ? err : new Error(String(err)) });
+ setShowResetInstructions(true);
+ } finally {
+ setRecovering(false);
+ }
+ return;
+ }
+
+ // Case 3: Wallet initialized but no account - attempt recovery (once only)
+ if (state.initialized && !recoveryAttemptedRef.current && !connectingRef.current) {
recoveryAttemptedRef.current = true;
try {
setRecovering(true);
@@ -71,7 +97,7 @@ const Home: React.FC = () => {
};
homeReload();
- }, [state.loading, state.activeAccount, installedSnap, navigate, dispatch, getAccountData, recoverWallet]);
+ }, [state.loading, state.activeAccount, state.initialized, installedSnap, navigate, dispatch, getAccountData, recoverWallet, initWallet]);
return (