Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
531b9cb
refactor: extract shared wallet lifecycle utils to internal module
fernandomg Apr 7, 2026
45ef50b
feat: add address param and explorerAddressUrl to useReadOnly
fernandomg Apr 7, 2026
c7a1a93
feat: add getWallet, getWalletByChainId, connectedAddresses to useMul…
fernandomg Apr 7, 2026
0d365be
feat: add manual pre-step control to useTransaction (prepare, execute…
fernandomg Apr 7, 2026
22857be
feat: add transforming hooks (beforeCall, afterCall) to wrapAdapter
fernandomg Apr 7, 2026
4122b4d
feat: add multi-chain WalletGuard with require prop for bridge-style …
fernandomg Apr 7, 2026
b69dc6c
docs: add consumer error handling guide to adapter architecture spec
fernandomg Apr 7, 2026
27d1307
refactor: migrate token infrastructure from useWeb3Status to useWallet
fernandomg Apr 7, 2026
bc37713
refactor: migrate remaining useWeb3Status consumers to SDK hooks
fernandomg Apr 7, 2026
abbc7be
refactor: replace TransactionNotificationProvider with lifecycle hooks
fernandomg Apr 7, 2026
3c4b15d
refactor: migrate demo pages from LegacyTransactionButton to adapter-…
fernandomg Apr 7, 2026
3225739
chore: delete legacy hooks, providers, and duplicate files
fernandomg Apr 7, 2026
21ef305
refactor: replace WalletStatusVerifier with WalletGuard in SignMessag…
fernandomg Apr 7, 2026
af90917
refactor: migrate Optimism demo to adapter TransactionButton, delete …
fernandomg Apr 7, 2026
f744d92
fix: prevent undefined toast id from crashing zag-js toaster
fernandomg Apr 8, 2026
cfc9ee6
fix: show shortMessage instead of full viem error dump in OP demo
fernandomg Apr 8, 2026
83ad760
refactor: split createEvmWalletAdapter — remove React from core, crea…
fernandomg Apr 8, 2026
280fd9e
refactor: make WalletGuard and ConnectWalletButton headless with rend…
fernandomg Apr 8, 2026
18b9c1f
fix: remove cross-domain Avatar import from RainbowKit connector
fernandomg Apr 8, 2026
ca78f6a
refactor: document React type-only import in core adapters provider
fernandomg Apr 8, 2026
9e9fca1
chore: delete remaining deprecated hooks and dead wallet types
fernandomg Apr 8, 2026
a6ff037
fix: return null instead of throwing on invalid hash in getExplorerLink
fernandomg Apr 8, 2026
d4fa503
fix: extract tx hash from transaction object in hash handling demo
fernandomg Apr 8, 2026
7555281
fix: show friendly message instead of raw viem error in ENS demo
fernandomg Apr 8, 2026
a49a4e1
fix: add BigInt.toJSON polyfill to prevent React 19 dev mode serializ…
fernandomg Apr 8, 2026
183fc7f
fix: use viem shortMessage in prepare error reason for user-friendly …
fernandomg Apr 8, 2026
6514228
feat: add formatErrorMessage utility for user-friendly blockchain err…
fernandomg Apr 8, 2026
51dbac1
fix: switch ERC20 and native token demos from Sepolia to Base Sepolia…
fernandomg Apr 8, 2026
032e40b
fix: pass signer account to gas estimation in prepare() to prevent ze…
fernandomg Apr 8, 2026
b74d51d
feat: expose resolveAdapters and accept explicit adapters on useTrans…
fernandomg Apr 9, 2026
9e57a51
fix: complete DbC annotations and dedup prepare() adapter lookup in u…
fernandomg Apr 9, 2026
9b54fd0
fix: export ResolvedAdapters type from hooks barrel
fernandomg Apr 9, 2026
3fa6a7d
feat: accept explicit adapter prop on WalletGuard
fernandomg Apr 9, 2026
269aad6
feat: export useProviderContext from public API
fernandomg Apr 9, 2026
a72c0db
fix: enforce chains.length >= 1 precondition on createEvmWalletAdapter
fernandomg Apr 9, 2026
5be2482
fix: enforce chains.length >= 1 precondition on createEvmTransactionA…
fernandomg Apr 9, 2026
c95286b
fix: add resolveAdapters to UseTransactionReturn mocks in Transaction…
fernandomg Apr 9, 2026
032ec05
refactor: make ReadClientFactory generic and add readClientFactory to…
fernandomg Apr 9, 2026
fe9a96f
feat: add evmReadClientFactory for typed EVM read-only clients
fernandomg Apr 9, 2026
45bc394
feat: add createReadClient and resolveReadClient core utilities
fernandomg Apr 9, 2026
d130939
feat: make useReadOnly generic with factory bypass option
fernandomg Apr 9, 2026
0825c12
feat: add useEvmReadOnly typed hook for EVM read-only clients
fernandomg Apr 9, 2026
05b8c89
feat: auto-contribute read client factories from wallet bundles
fernandomg Apr 9, 2026
0af3215
fix: add type assertions to generic ReadClientFactory mock factories …
fernandomg Apr 9, 2026
c6d74c4
fix: enforce transport-per-chain precondition on createEvmTransaction…
fernandomg Apr 9, 2026
d0de245
fix: enforce privateKey format precondition on createEvmServerWallet
fernandomg Apr 9, 2026
c541d49
fix: enforce coreConnector.createConfig precondition on createEvmWall…
fernandomg Apr 9, 2026
f56c340
refactor: introduce @expects tag and relabel unenforced @precondition…
fernandomg Apr 9, 2026
681b0f1
refactor: decouple EVM connectors from app env — accept metadata via …
fernandomg Apr 9, 2026
8a03d9f
test: add integration tests for createEvmWalletBundle with real wagmi
fernandomg Apr 9, 2026
146b27c
fix: auto-reset execution state on useTransaction.execute() for multi…
fernandomg Apr 10, 2026
8de6187
fix: consolidate shared Wrapper component and remove duplicate deboun…
fernandomg Apr 10, 2026
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ The `/issue` skill applies these labels automatically when creating issues via C
- Path aliases: `@/src/*` and `@packageJSON`
- All env vars prefixed with `PUBLIC_` and validated in `src/env.ts`
- JSDoc comments on exported functions/components (follow existing patterns)
- Use `@precondition` in JSDoc ONLY for conditions enforced at runtime with a throw/guard. Use `@expects` for documented assumptions the caller is responsible for (TypeScript-enforced, gracefully handled, or downstream-validated). If you write `@precondition`, the function MUST validate and throw on violation.

## Styling

Expand Down
109 changes: 109 additions & 0 deletions docs/architecture/adapter-architecture-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,115 @@ const bridge = useTransactionFlow([

---

## Consumer Error Handling Guide

This section maps every user-facing action to the exact SDK errors it can produce, documents whether the hook handles those errors internally or propagates them, and clarifies the two distinct error handling patterns consumers encounter.

### Per-action error table

Each row lists a user action, the SDK error classes that can be thrown, and how the hook layer handles them. "Propagates" means the consumer must `try/catch`. "Catches + re-throws" means the hook sets its `error` state AND re-throws — the consumer can read `error` reactively or `try/catch` the async call.

| User Action | Possible Errors | Hook Behavior |
|---|---|---|
| Connect wallet | `WalletNotInstalledError`, `WalletConnectionRejectedError`, `ChainNotSupportedError` | `useWallet().connect()` propagates — consumer must try/catch |
| Disconnect wallet | _(none — no-op if already disconnected)_ | `useWallet().disconnect()` propagates (but does not throw in practice) |
| Sign message | `WalletNotConnectedError`, `SigningRejectedError` | `useWallet().signMessage()` propagates — consumer must try/catch |
| Sign typed data | `WalletNotConnectedError`, `SigningRejectedError`, `CapabilityNotSupportedError` | `useWallet().signTypedData()` propagates — consumer must try/catch. `CapabilityNotSupportedError` is thrown synchronously if the adapter does not support the capability. |
| Switch chain | `WalletNotConnectedError`, `ChainNotSupportedError` | `useWallet().switchChain()` propagates — consumer must try/catch |
| Execute transaction | `AdapterNotFoundError`, `WalletNotConnectedError`, `TransactionNotReadyError`, `PreStepsNotExecutedError`, `InsufficientFundsError`, `InvalidSignerError`, `ChainNotSupportedError` | `useTransaction().execute()` catches all, sets `error` state, fires `lifecycle.onError`, AND re-throws |
| Prepare transaction | `AdapterNotFoundError`, `TransactionNotReadyError`, `InsufficientFundsError` | `useTransaction().prepare()` catches all, sets `error` state AND re-throws |
| Execute single pre-step | `Error` (prepare not called), `RangeError` (index out of bounds), `AdapterNotFoundError`, `WalletNotConnectedError` | `useTransaction().executePreStep()` — precondition errors (`Error`, `RangeError`, `AdapterNotFoundError`, `WalletNotConnectedError`) propagate directly without setting `error` state. Execution errors (from adapter `execute`/`confirm`) are caught, set `error` state, and re-thrown. |
| Execute all pre-steps | Same as single pre-step (delegates to `executePreStep`) | `useTransaction().executeAllPreSteps()` — errors propagate from `executePreStep` |
| Resolve wallet adapter | `AdapterNotFoundError`, `AmbiguousAdapterError` | `useWallet()` throws synchronously during render — React error boundary catches |
| Provider initialization | `ChainRegistryConflictError`, `Error` (chainType mismatch) | `DAppBoosterProvider` throws synchronously during render — React error boundary catches |

### Error semantics: `ChainNotSupportedError` vs `AdapterNotFoundError`

Both errors mean "this chain doesn't work" to a consumer, but they occur at different levels:

- **`AdapterNotFoundError`** — thrown by the hook resolution layer. No registered adapter's `supportedChains` includes the requested `chainId`. This means the SDK has no adapter at all for this chain.
- **Throw sites:** `useWallet()` (via `resolveAdapter()`), `useTransaction().execute()`, `useTransaction().prepare()`, `useTransaction().executePreStep()`

- **`ChainNotSupportedError`** — thrown by the adapter itself. An adapter was found but its own validation rejects the `chainId`. This occurs inside adapter methods like `connect()`, `switchChain()`, and `execute()`.
- **Throw sites:** `WalletAdapter.connect()` (when `options.chainId` is not in `supportedChains`), `WalletAdapter.switchChain()`, `TransactionAdapter.execute()`

In practice, `AdapterNotFoundError` fires first (during resolution) if no adapter matches. `ChainNotSupportedError` fires later (during execution) if the adapter was resolved via a different chain but the target chain is not supported.

### Error semantics: `InvalidSignerError`

`InvalidSignerError` is an internal safety check at the adapter boundary. The `TransactionAdapter.execute()` method validates that the signer it receives is the correct type (e.g., a viem `WalletClient` for EVM). This guards against passing an SVM signer to an EVM adapter. Consumers should not normally encounter this error — it indicates a misconfigured adapter stack, not a user action failure.

### Hook error handling patterns

The SDK uses two distinct patterns for error handling at the hook layer:

#### Pattern 1: `useWallet` methods — raw passthrough

All `useWallet()` methods (`connect`, `disconnect`, `signMessage`, `signTypedData`, `switchChain`, `getSigner`) delegate directly to the adapter. Errors propagate unmodified to the consumer. There is no `error` state on the return object.

```ts
const { connect, signMessage } = useWallet({ chainType: 'evm' })

try {
await connect()
} catch (err) {
if (err instanceof WalletConnectionRejectedError) {
// User cancelled — show a dismissable message
}
if (err instanceof WalletNotInstalledError) {
// Wallet not found — show install instructions
}
}

try {
const result = await signMessage({ message: 'Hello' })
} catch (err) {
if (err instanceof SigningRejectedError) {
// User cancelled signing
}
}
```

The `signMessage` and `signTypedData` wrappers fire `walletLifecycle.onSignError` before re-throwing, but the error still propagates to the consumer.

#### Pattern 2: `useTransaction` methods — catch, set state, re-throw

All `useTransaction()` async methods (`execute`, `prepare`, `executePreStep`, `executeAllPreSteps`) catch errors, set the `error` property on the hook return, and re-throw. Consumers can handle errors in two ways:

**Reactive (read `error` state):**

```ts
const { execute, error, phase } = useTransaction()

// In a click handler:
execute(params).catch(() => {}) // swallow — read error reactively

// In JSX:
{error && <ErrorBanner message={error.message} />}
```

**Imperative (try/catch):**

```ts
const { execute } = useTransaction()

try {
const result = await execute(params)
// success
} catch (err) {
if (err instanceof TransactionNotReadyError) {
// preparation failed
}
if (err instanceof PreStepsNotExecutedError) {
// manual pre-steps not completed
}
}
```

**Important nuance for `executePreStep`:** Precondition checks (prepare not called, index out of bounds, adapter not found, wallet not connected) throw directly without setting `error` state. Only errors during the actual on-chain execution (adapter `execute`/`confirm` calls) go through the catch-set-rethrow pattern. This means the reactive `error` state only captures execution failures, not programmer errors.

---

## 12. Migration Path

### From current codebase to adapter architecture
Expand Down
29 changes: 29 additions & 0 deletions src/chakra/ConnectWalletButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { FC } from 'react'
import { ConnectWalletButton as HeadlessConnectWalletButton } from '@/src/sdk/react/components/ConnectWalletButton'
import type { UseWalletOptions } from '@/src/sdk/react/hooks/useWallet'
import ConnectButton from '@/src/wallet/components/ConnectButton'

/**
* Chakra-styled connect/account button.
*
* Composes the headless ConnectWalletButton with the Chakra ConnectButton
* for styled rendering.
*/
export const ConnectWalletButton: FC<UseWalletOptions & { label?: string }> = ({
label = 'Connect',
...walletOptions
}) => {
return (
<HeadlessConnectWalletButton
{...walletOptions}
render={({ status, truncatedAddress, onConnect, onManageAccount }) => (
<ConnectButton
isConnected={status.connected}
onClick={status.connected ? onManageAccount : onConnect}
>
{status.connected && truncatedAddress ? truncatedAddress : label}
</ConnectButton>
)}
/>
)
}
50 changes: 50 additions & 0 deletions src/chakra/WalletGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { FC, ReactNode } from 'react'
import {
WalletGuard as HeadlessWalletGuard,
type WalletRequirement,
} from '@/src/sdk/react/components/WalletGuard'
import SwitchChainButton from '@/src/wallet/components/SwitchChainButton'
import { ConnectWalletButton } from './ConnectWalletButton'

interface ChakraWalletGuardProps {
chainId?: string | number
chainType?: string
children?: ReactNode
require?: WalletRequirement[]
switchChainLabel?: string
}

/**
* Chakra-styled WalletGuard.
*
* Composes the headless WalletGuard with Chakra-styled ConnectWalletButton
* and SwitchChainButton as default render props.
*/
export const WalletGuard: FC<ChakraWalletGuardProps> = ({
chainId,
chainType,
children,
require: requirements,
switchChainLabel = 'Switch to',
}) => {
return (
<HeadlessWalletGuard
chainId={chainId}
chainType={chainType}
require={requirements}
renderConnect={() => (
<ConnectWalletButton
chainId={chainId}
chainType={chainType}
/>
)}
renderSwitchChain={({ chainName, onSwitch }) => (
<SwitchChainButton onClick={onSwitch}>
{switchChainLabel} {chainName}
</SwitchChainButton>
)}
>
{children}
</HeadlessWalletGuard>
)
}
2 changes: 2 additions & 0 deletions src/chakra/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ConnectWalletButton } from './ConnectWalletButton'
export { WalletGuard } from './WalletGuard'
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { OptionsDropdown } from '@/src/components/pageComponents/home/Examples/d
import { Spinner } from '@/src/core/components'

const EnsNameSearch = ({ address }: { address?: Address }) => {
const { data, error, status } = useEnsName({
const { data, status } = useEnsName({
address: address,
chainId: mainnet.id,
})
Expand All @@ -19,7 +19,7 @@ const EnsNameSearch = ({ address }: { address?: Address }) => {
{status === 'pending' ? (
<Spinner size="md" />
) : status === 'error' ? (
`Error fetching ENS name (${error.message})`
'ENS resolution unavailable'
) : data === undefined || data === null ? (
'Not available'
) : (
Expand All @@ -39,10 +39,7 @@ const EnsName = () => {
}, debounceTime)

const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value as Address

setValue(value)
debouncedSearch(value)
setValue(e.target.value as Address)
}

const addresses = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const Hash: FC<Props> = ({ chain, hash, truncatedHashLength }) => {
borderRadius="8px"
color="var(--theme-hash-color)"
cursor="default"
explorerURL={getExplorerLink({ chain, hashOrAddress: hash })}
explorerURL={getExplorerLink({ chain, hashOrAddress: hash }) ?? ''}
fontSize="14px"
hash={hash}
minHeight="64px"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import hashHandling from './index'

const system = createSystem(defaultConfig)

vi.mock('@/src/wallet/hooks/useWeb3Status', () => ({
useWeb3Status: vi.fn(() => ({
isWalletConnected: false,
walletChainId: undefined,
vi.mock('@/src/sdk/react/hooks', () => ({
useWallet: vi.fn(() => ({
status: { connected: false, activeAccount: null, connectedChainIds: [], connecting: false },
})),
}))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Icon from '@/src/components/pageComponents/home/Examples/demos/HashHandli
import Wrapper from '@/src/components/pageComponents/home/Examples/wrapper'
import { HashInput, Spinner } from '@/src/core/components'
import type { DetectedHash } from '@/src/core/utils'
import { useWeb3Status } from '@/src/wallet/hooks'
import { useWallet } from '@/src/sdk/react/hooks'

const AlertIcon = () => (
<chakra.svg
Expand Down Expand Up @@ -61,7 +61,9 @@ const HashHandling = ({ ...restProps }) => {
const [loading, setLoading] = useState<boolean | undefined>()
const notFound = searchResult && searchResult.type === null
const found = searchResult && searchResult.type !== null
const { isWalletConnected, walletChainId } = useWeb3Status()
const { status } = useWallet()
const isWalletConnected = status.connected
const walletChainId = status.connectedChainIds[0] as number | undefined

const onLoading = (isLoading: boolean) => {
setLoading(isLoading)
Expand Down Expand Up @@ -185,7 +187,14 @@ const HashHandling = ({ ...restProps }) => {
)}
<Hash
chain={currentChain}
hash={searchResult?.data as Address}
hash={
searchResult?.type === 'transaction' &&
searchResult.data &&
typeof searchResult.data === 'object' &&
'hash' in searchResult.data
? (searchResult.data.hash as Address)
: (searchResult?.data as Address)
}
truncatedHashLength="disabled"
/>
</Box>
Expand Down
Loading
Loading