Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/ows/registerApprovalSigning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
/** Mark unlocked + refresh addresses after a successful signing ceremony. */
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/ows/registerCredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export type RegisterCredentialsProviderOptions = {
*/
ensureReady?: () => Promise<void>;
/**
* 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<void>;
/** Mark unlocked after a successful PoP / issue ceremony. */
Expand Down
6 changes: 4 additions & 2 deletions src/wallet/registerAddAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -42,6 +43,7 @@ export interface IAddAssetApprovalRequest {
}

export type RegisterAddAssetOptions = {
ensureReady: WalletReadyGate;
knownAssetRepository: IKnownAssetRepository;
trackedAssetRepository: ITrackedAssetRepository;
getOwnerAddress: () => EVMAccountAddressType;
Expand All @@ -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([
Expand Down Expand Up @@ -98,7 +100,7 @@ export function registerAddAssetRpc(
} finally {
await display.hide();
}
},
}),
addAssetParamsSchema,
);
}
6 changes: 4 additions & 2 deletions src/wallet/registerBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -35,6 +36,7 @@ const bridgeParamsSchema = z
export type IBridgeParams = z.infer<typeof bridgeParamsSchema>;

export type RegisterBridgeOptions = {
ensureReady: WalletReadyGate;
getOwnerAddress: () => EVMAccountAddress | null;
getSessionChainId: () => EVMChainIdType;
chainRepository: IChainRepository;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -178,7 +180,7 @@ export function registerBridgeRpc(
} finally {
await display.hide();
}
},
}),
bridgeParamsSchema,
);
}
6 changes: 4 additions & 2 deletions src/wallet/registerGetUpgraded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,6 +24,7 @@ export type IGetUpgradedResult = {
};

export type RegisterGetUpgradedOptions = {
ensureReady: WalletReadyGate;
getOwnerAddress: () => EVMAccountAddress | null;
transactionService: ITransactionService;
};
Expand All @@ -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)) {
Expand Down Expand Up @@ -72,7 +74,7 @@ export function registerGetUpgradedRpc(
: `Failed to check upgrade status for chain ${raw}`,
} satisfies IGetUpgradedResult;
}
},
}),
getUpgradedParamsSchema,
);
}
6 changes: 4 additions & 2 deletions src/wallet/registerOnramp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,6 +20,7 @@ const onrampParamsSchema = z
export type IOnrampParams = z.infer<typeof onrampParamsSchema>;

export type RegisterOnrampOptions = {
ensureReady: WalletReadyGate;
getOwnerAddress: () => EVMAccountAddress | null;
};

Expand All @@ -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) {
Expand Down Expand Up @@ -60,7 +62,7 @@ export function registerOnrampRpc(
} finally {
await display.hide();
}
},
}),
onrampParamsSchema,
);
}
10 changes: 5 additions & 5 deletions src/wallet/registerRequestCancelDelegations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -37,7 +38,8 @@ export type IRequestCancelDelegationsResult = {
export type RegisterRequestCancelDelegationsOptions = {
configProvider: IConfigProvider;
delegationService: IDelegationService;
ensureOnboardedForSigning: () => Promise<void>;
/** Prefer `ensureOnboardedForSigning` — unlock/setup before cancel consent. */
ensureOnboardedForSigning: WalletReadyGate;
resolveChain: (chainId: EVMChainId) => SupportedChain | null;
ask: <T>(
build: (handlers: {
Expand All @@ -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();
Expand Down Expand Up @@ -177,7 +177,7 @@ export function registerRequestCancelDelegationsRpc(
} finally {
await display.hide();
}
},
}),
requestCancelDelegationsParamsSchema,
);
}
12 changes: 11 additions & 1 deletion src/wallet/useWalletAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -589,7 +599,7 @@ export function useWalletAuth({
);
}
await awaitSigner();
if (isWalletCreated()) {
if (useWalletSessionStore.getState().unlocked) {
return;
}
await ensureReadyRef.current();
Expand Down
4 changes: 4 additions & 0 deletions src/wallet/useWalletBoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -778,6 +779,7 @@ export function useWalletBoot({
});

registerGetUpgradedRpc(wallet, {
ensureReady,
getOwnerAddress: () => {
const address = useWalletSessionStore.getState().evmAddress;
if (!address || String(address).toLowerCase() === "0x0") {
Expand All @@ -797,6 +799,7 @@ export function useWalletBoot({
});

registerBridgeRpc(wallet, {
ensureReady,
getOwnerAddress: () => {
const address = useWalletSessionStore.getState().evmAddress;
if (!address || String(address).toLowerCase() === "0x0") {
Expand All @@ -817,6 +820,7 @@ export function useWalletBoot({
});

registerAddAssetRpc(wallet, {
ensureReady,
knownAssetRepository,
trackedAssetRepository,
getOwnerAddress: () => useWalletSessionStore.getState().evmAddress,
Expand Down
20 changes: 12 additions & 8 deletions src/wallet/withWalletReady.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
* 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
* - cached credential id → passkey unlock (+ credential recover when cache empty)
* - 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.
*/
Expand All @@ -27,7 +31,7 @@ export type WalletReadyGate = () => Promise<void>;
/**
* 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<TArgs extends unknown[], TResult>(
Expand Down
Loading