From b8382421cf361ef83561050dd0df8f7287dbc377 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Fri, 25 Sep 2026 09:10:42 -0700 Subject: [PATCH 1/2] Refactor signing readiness gates in wallet authentication - Updated comments and logic for `ensureReady` and `ensureOnboarded` to clarify their roles in the signing process. - Enhanced `ensureOnboardedForSigning` to handle session states more effectively, ensuring proper unlock/setup for signed actions. - Improved documentation across multiple files to reflect changes in the signing and credential handling processes. --- src/ows/registerApprovalSigning.ts | 9 +++++---- src/ows/registerCredentialsProvider.ts | 5 +++-- src/wallet/useWalletAuth.ts | 12 +++++++++++- src/wallet/withWalletReady.ts | 12 +++++++----- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/ows/registerApprovalSigning.ts b/src/ows/registerApprovalSigning.ts index d74a5a0..d21f092 100644 --- a/src/ows/registerApprovalSigning.ts +++ b/src/ows/registerApprovalSigning.ts @@ -11,9 +11,10 @@ import type { EVMSignatureHex, EVMTransactionHash } from "@1shotapi/ows-types"; export type RegisterApprovalSigningOptions = { /** - * Setup-only gate before signed actions: run onboarding when no credential - * exists. With a known credential, skip unlock — the signing ceremony - * authenticates. Pair with {@link onAuthenticated}. + * Readiness gate before signed actions. Prefer `ensureOnboardedForSigning`: + * no-op when the session is already unlocked; otherwise full unlock/setup + * so host-driven SIWE (`personal_sign` / typed data) does not fail while + * locked. Pair with {@link onAuthenticated}. */ ensureReady?: () => Promise; /** Mark unlocked + refresh addresses after a successful signing ceremony. */ @@ -45,7 +46,7 @@ export type RegisterApprovalSigningOptions = { /** * Build SignHelper handlers and register them on the wallet (pre-`start()`). * - * SignHelper adapts EIP-1193 ↔ `approveAndSign*`. Setup (`ensureReady`) runs + * SignHelper adapts EIP-1193 ↔ `approveAndSign*`. Readiness (`ensureReady`) runs * inside approve callbacks (while the display session is held). Unlock * (`onAuthenticated`) is passed through to SignHelper so it runs after * display release — post-sign address refresh must not keep the flyout open. diff --git a/src/ows/registerCredentialsProvider.ts b/src/ows/registerCredentialsProvider.ts index bb1da12..473fdf4 100644 --- a/src/ows/registerCredentialsProvider.ts +++ b/src/ows/registerCredentialsProvider.ts @@ -54,8 +54,9 @@ export type RegisterCredentialsProviderOptions = { */ ensureReady?: () => Promise; /** - * Setup-only when no credential exists. With a known passkey, skip unlock — - * the PoP ceremony authenticates. Pair with {@link onAuthenticated}. + * Signed-action gate (same as `ensureOnboardedForSigning`): no-op when the + * session is unlocked; otherwise full unlock/setup. Pair with + * {@link onAuthenticated} after PoP / issue. */ ensureOnboarded?: () => Promise; /** Mark unlocked after a successful PoP / issue ceremony. */ diff --git a/src/wallet/useWalletAuth.ts b/src/wallet/useWalletAuth.ts index aa25623..e3b05b9 100644 --- a/src/wallet/useWalletAuth.ts +++ b/src/wallet/useWalletAuth.ts @@ -581,6 +581,16 @@ export function useWalletAuth({ await ensureReadyRef.current(); }, [awaitSignerRef]); + /** + * Gate for signed EIP-1193 / in-wallet actions. + * + * When the session is already unlocked (including returning sessions hydrated + * from cache), skip — the signing / PoP ceremony itself authenticates. + * When locked, run full {@link ensureReady} so SIWE `personal_sign` / + * typed-data (and other signed RPCs) prompt unlock or setup instead of + * failing. Do not early-return on {@link isWalletCreated} alone: a stored + * credential with `unlocked === false` still needs unlock. + */ const ensureOnboardedForSigning = useCallback(async () => { const awaitSigner = awaitSignerRef.current; if (!awaitSigner) { @@ -589,7 +599,7 @@ export function useWalletAuth({ ); } await awaitSigner(); - if (isWalletCreated()) { + if (useWalletSessionStore.getState().unlocked) { return; } await ensureReadyRef.current(); diff --git a/src/wallet/withWalletReady.ts b/src/wallet/withWalletReady.ts index 5219ebe..8eff279 100644 --- a/src/wallet/withWalletReady.ts +++ b/src/wallet/withWalletReady.ts @@ -14,10 +14,12 @@ * - otherwise → setup modal (login existing / create new), then recover * * For **signed** EIP-1193 actions (`personal_sign`, typed data, `eth_sendTransaction`), - * in-wallet send, and credential **issue/present PoP**: pass a setup-only gate — - * run setup/login only when no credential id exists. With a known credential, - * skip a separate `getPublicKey` unlock; the signing / PoP ceremony itself - * authenticates. Pair with `onAuthenticated` (inside branding `approveAnd*` / + * in-wallet send, and credential **issue/present PoP**: use + * `ensureOnboardedForSigning` (or equivalent): + * - session already unlocked → no-op; the signing / PoP ceremony authenticates + * - session locked → full `ensureReady` (unlock or setup) so SIWE and other + * host-driven signs never fail with a locked wallet + * Pair with `onAuthenticated` (inside branding `approveAnd*` / * `approveAndAcceptOffer` / `approveAndPresent`) to mark unlocked and refresh * addresses after a successful ceremony. */ @@ -27,7 +29,7 @@ export type WalletReadyGate = () => Promise; /** * Wrap a host RPC / credential handler so it always runs after {@link ensureReady}. * Use for actions that need an unlocked signer before any other work - * (e.g. credential delete). Prefer branding setup-only gates on + * (e.g. credential delete). Prefer `ensureOnboardedForSigning` on * `approveAndSign*` / credential `approveAnd*` for signed actions. */ export function withWalletReady( From 41a91fb45c6708d08a34afaae285786d7e01a5e9 Mon Sep 17 00:00:00 2001 From: Charlie Sibbach Date: Fri, 25 Sep 2026 09:54:52 -0700 Subject: [PATCH 2/2] Implement wallet readiness checks for RPC registrations - Introduced `withWalletReady` to ensure wallet readiness for multiple RPC methods, including `addAsset`, `bridge`, `getUpgraded`, and `onramp`. - Updated `RegisterAddAssetOptions`, `RegisterBridgeOptions`, `RegisterGetUpgradedOptions`, and `RegisterOnrampOptions` to include `ensureReady` parameter. - Refactored RPC registration functions to utilize the new readiness checks, enhancing the reliability of wallet interactions. --- src/wallet/registerAddAsset.ts | 6 ++++-- src/wallet/registerBridge.ts | 6 ++++-- src/wallet/registerGetUpgraded.ts | 6 ++++-- src/wallet/registerOnramp.ts | 6 ++++-- src/wallet/registerRequestCancelDelegations.ts | 10 +++++----- src/wallet/useWalletBoot.ts | 4 ++++ src/wallet/withWalletReady.ts | 8 +++++--- 7 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/wallet/registerAddAsset.ts b/src/wallet/registerAddAsset.ts index 5468cb0..78d6ca8 100644 --- a/src/wallet/registerAddAsset.ts +++ b/src/wallet/registerAddAsset.ts @@ -14,6 +14,7 @@ import type { } from "../lib/interfaces/data"; import { isSafeHttpsIconUrl } from "../lib/utils/tokenIcons"; import { useWalletSessionStore } from "./sessionStore"; +import { withWalletReady, type WalletReadyGate } from "./withWalletReady"; /** Custom RPC — host: `await proxy.rpc("addAsset", { chainId, assetAddress, iconUrl? })`. */ export const ADD_ASSET_RPC_METHOD = "addAsset"; @@ -42,6 +43,7 @@ export interface IAddAssetApprovalRequest { } export type RegisterAddAssetOptions = { + ensureReady: WalletReadyGate; knownAssetRepository: IKnownAssetRepository; trackedAssetRepository: ITrackedAssetRepository; getOwnerAddress: () => EVMAccountAddressType; @@ -60,7 +62,7 @@ export function registerAddAssetRpc( ): void { wallet.registerRpc( ADD_ASSET_RPC_METHOD, - async (params) => { + withWalletReady(options.ensureReady, async (params) => { const { chainId, assetAddress, iconUrl } = params as IAddAssetParams; const owner = options.getOwnerAddress(); const [resolved, display] = await Promise.all([ @@ -98,7 +100,7 @@ export function registerAddAssetRpc( } finally { await display.hide(); } - }, + }), addAssetParamsSchema, ); } diff --git a/src/wallet/registerBridge.ts b/src/wallet/registerBridge.ts index c8cbe96..b942f60 100644 --- a/src/wallet/registerBridge.ts +++ b/src/wallet/registerBridge.ts @@ -15,6 +15,7 @@ import type { IChainRepository } from "../lib/interfaces/data/IChainRepository"; import type { IKnownAssetRepository } from "../lib/interfaces/data/IKnownAssetRepository"; import type { ICCTPUtils } from "../lib/interfaces/business/utils/ICCTPUtils"; import { ECctpTransferSpeed } from "../lib/types/enum/ECctpTransferSpeed"; +import { withWalletReady, type WalletReadyGate } from "./withWalletReady"; /** Custom RPC — host: `await proxy.rpc("bridge", { amount?, sourceChainId?, destinationChainId?, speed?, tokenAddress? })`. */ export const BRIDGE_RPC_METHOD = "bridge"; @@ -35,6 +36,7 @@ const bridgeParamsSchema = z export type IBridgeParams = z.infer; export type RegisterBridgeOptions = { + ensureReady: WalletReadyGate; getOwnerAddress: () => EVMAccountAddress | null; getSessionChainId: () => EVMChainIdType; chainRepository: IChainRepository; @@ -75,7 +77,7 @@ export function registerBridgeRpc( ): void { wallet.registerRpc( BRIDGE_RPC_METHOD, - async (params) => { + withWalletReady(options.ensureReady, async (params) => { const { amount, sourceChainId, destinationChainId, speed, tokenAddress } = params as IBridgeParams; const owner = options.getOwnerAddress(); @@ -178,7 +180,7 @@ export function registerBridgeRpc( } finally { await display.hide(); } - }, + }), bridgeParamsSchema, ); } diff --git a/src/wallet/registerGetUpgraded.ts b/src/wallet/registerGetUpgraded.ts index 366c3fb..171cc47 100644 --- a/src/wallet/registerGetUpgraded.ts +++ b/src/wallet/registerGetUpgraded.ts @@ -6,6 +6,7 @@ import { type EVMContractAddress, } from "@1shotapi/ows-types"; import type { ITransactionService } from "../lib/interfaces/business"; +import { withWalletReady, type WalletReadyGate } from "./withWalletReady"; /** Custom RPC — host: `await proxy.rpc("getUpgraded", { chainId })`. */ export const GET_UPGRADED_RPC_METHOD = "getUpgraded"; @@ -23,6 +24,7 @@ export type IGetUpgradedResult = { }; export type RegisterGetUpgradedOptions = { + ensureReady: WalletReadyGate; getOwnerAddress: () => EVMAccountAddress | null; transactionService: ITransactionService; }; @@ -38,7 +40,7 @@ export function registerGetUpgradedRpc( ): void { wallet.registerRpc( GET_UPGRADED_RPC_METHOD, - async (params) => { + withWalletReady(options.ensureReady, async (params) => { const { chainId: raw } = params as IGetUpgradedParams; if (ChainUtils.isBitcoinChainId(raw)) { @@ -72,7 +74,7 @@ export function registerGetUpgradedRpc( : `Failed to check upgrade status for chain ${raw}`, } satisfies IGetUpgradedResult; } - }, + }), getUpgradedParamsSchema, ); } diff --git a/src/wallet/registerOnramp.ts b/src/wallet/registerOnramp.ts index 2b4fbb9..0bfa16c 100644 --- a/src/wallet/registerOnramp.ts +++ b/src/wallet/registerOnramp.ts @@ -5,6 +5,7 @@ import { type EVMAccountAddress, } from "@1shotapi/ows-types"; import { openOnramp } from "../circle/openOnramp"; +import { withWalletReady, type WalletReadyGate } from "./withWalletReady"; /** Custom RPC — host: `await proxy.rpc("onramp", { chainId?, amount? })`. */ export const ONRAMP_RPC_METHOD = "onramp"; @@ -19,6 +20,7 @@ const onrampParamsSchema = z export type IOnrampParams = z.infer; export type RegisterOnrampOptions = { + ensureReady: WalletReadyGate; getOwnerAddress: () => EVMAccountAddress | null; }; @@ -32,7 +34,7 @@ export function registerOnrampRpc( ): void { wallet.registerRpc( ONRAMP_RPC_METHOD, - async (params) => { + withWalletReady(options.ensureReady, async (params) => { const { chainId, amount } = params as IOnrampParams; const owner = options.getOwnerAddress(); if (!owner) { @@ -60,7 +62,7 @@ export function registerOnrampRpc( } finally { await display.hide(); } - }, + }), onrampParamsSchema, ); } diff --git a/src/wallet/registerRequestCancelDelegations.ts b/src/wallet/registerRequestCancelDelegations.ts index 4ac1d02..d84341c 100644 --- a/src/wallet/registerRequestCancelDelegations.ts +++ b/src/wallet/registerRequestCancelDelegations.ts @@ -16,6 +16,7 @@ import type { IStoredDelegation } from "../lib/types/domain/StoredDelegation"; import type { ActiveModal } from "./modalTypes"; import { loadCachedEvmAddress } from "../storage"; import { useWalletSessionStore } from "./sessionStore"; +import { withWalletReady, type WalletReadyGate } from "./withWalletReady"; /** Custom RPC — host: `await proxy.rpc("requestCancelDelegations", { permissionContexts })`. */ export const REQUEST_CANCEL_DELEGATIONS_RPC_METHOD = @@ -37,7 +38,8 @@ export type IRequestCancelDelegationsResult = { export type RegisterRequestCancelDelegationsOptions = { configProvider: IConfigProvider; delegationService: IDelegationService; - ensureOnboardedForSigning: () => Promise; + /** Prefer `ensureOnboardedForSigning` — unlock/setup before cancel consent. */ + ensureOnboardedForSigning: WalletReadyGate; resolveChain: (chainId: EVMChainId) => SupportedChain | null; ask: ( build: (handlers: { @@ -60,12 +62,10 @@ export function registerRequestCancelDelegationsRpc( ): void { wallet.registerRpc( REQUEST_CANCEL_DELEGATIONS_RPC_METHOD, - async (params) => { + withWalletReady(options.ensureOnboardedForSigning, async (params) => { const { permissionContexts } = params as IRequestCancelDelegationsParams; - await options.ensureOnboardedForSigning(); - const { hostDomain: callerDomain } = await options.configProvider.getConfig(); const callerKey = String(callerDomain).toLowerCase(); @@ -177,7 +177,7 @@ export function registerRequestCancelDelegationsRpc( } finally { await display.hide(); } - }, + }), requestCancelDelegationsParamsSchema, ); } diff --git a/src/wallet/useWalletBoot.ts b/src/wallet/useWalletBoot.ts index 15de8df..bf91495 100644 --- a/src/wallet/useWalletBoot.ts +++ b/src/wallet/useWalletBoot.ts @@ -768,6 +768,7 @@ export function useWalletBoot({ registerFocusModeRpc(wallet, rpcHelper); registerOnrampRpc(wallet, { + ensureReady, getOwnerAddress: () => { const address = useWalletSessionStore.getState().evmAddress; if (!address || String(address).toLowerCase() === "0x0") { @@ -778,6 +779,7 @@ export function useWalletBoot({ }); registerGetUpgradedRpc(wallet, { + ensureReady, getOwnerAddress: () => { const address = useWalletSessionStore.getState().evmAddress; if (!address || String(address).toLowerCase() === "0x0") { @@ -797,6 +799,7 @@ export function useWalletBoot({ }); registerBridgeRpc(wallet, { + ensureReady, getOwnerAddress: () => { const address = useWalletSessionStore.getState().evmAddress; if (!address || String(address).toLowerCase() === "0x0") { @@ -817,6 +820,7 @@ export function useWalletBoot({ }); registerAddAssetRpc(wallet, { + ensureReady, knownAssetRepository, trackedAssetRepository, getOwnerAddress: () => useWalletSessionStore.getState().evmAddress, diff --git a/src/wallet/withWalletReady.ts b/src/wallet/withWalletReady.ts index 8eff279..06c5e7c 100644 --- a/src/wallet/withWalletReady.ts +++ b/src/wallet/withWalletReady.ts @@ -4,9 +4,11 @@ * Branding wrappers (`registerApprovalSigning`, `registerCredentialsProvider`) * take optional setup / unlock callbacks. Some flows (e.g. `present`) also need * credentials in the local cache before they can match / show consent. Custom - * RPCs (`eth_sendTransaction`, future 7710/delegation sends) should use the - * same centralized gate pattern so unlock/setup is never forgotten when new - * methods are added. + * RPCs that need an address or vault (`onramp`, `bridge`, `getUpgraded`, + * `addAsset`, future 7710/delegation sends) should wrap handlers with + * {@link withWalletReady} so unlock/setup is never forgotten when new methods + * are added. Shell-only RPCs (`configure`, `switchChain`, `focusWallet`, + * `createAccount`) do not unlock. * * Full `ensureReady` owns: * - unlocked → no-op