@valve-tech/wallet-adapter 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,193 @@
1
+ # AGENTS.md
2
+
3
+ Terse reference for AI agents (Claude Code, Cursor, Aider, etc.) integrating
4
+ `@valve-tech/wallet-adapter`. The full README is for humans; this file is for
5
+ agents that need to ground their work in the package's actual surface
6
+ quickly.
7
+
8
+ ## What this package does
9
+
10
+ Framework-agnostic **vocabulary** for EVM dapp wallet integration —
11
+ not a wallet implementation. Pure types + a few `as const` lifecycle
12
+ constants + two thin helpers that do the wallet-side and chain-side
13
+ phase wiring around `wallet.sendTransaction` and
14
+ `waitForTransactionReceipt`.
15
+
16
+ The point: every SDK / dapp / UI in your stack speaks the same wallet
17
+ shape, the same lifecycle phases, the same in-flight tx vocabulary.
18
+ No bespoke "awaiting signature" state machine per integration; no
19
+ `hash → request` side-channel maps in your callbacks.
20
+
21
+ `@valve-tech/viem-errors` is the only runtime dep (used for the
22
+ wallet-rejection discriminator). `viem ^2.0.0` is the peer.
23
+
24
+ ## Public API
25
+
26
+ All exports live under `src/index.ts`. Single subpath; no sub-exports.
27
+
28
+ ```ts
29
+ import {
30
+ // helpers
31
+ sendTransactionWithHooks, // wallet-side
32
+ awaitReceiptWithHooks, // chain-side; fetches containing block by default
33
+ // typed errors (use instanceof to discriminate)
34
+ WalletRejectedError,
35
+ ContractRevertedError,
36
+ // lifecycle constants
37
+ TX_STATUS, // 'preparing' | 'awaiting-signature' | 'pending' | 'confirmed' | 'failed' | 'replaced' | 'dropped'
38
+ TX_FLOW, // intentionally empty — protocols extend
39
+ STALE_TX_AGE_MS,
40
+ CONFIRMED_DISPLAY_MS,
41
+ FAILED_DISPLAY_MS,
42
+ // types
43
+ type WalletAdapter,
44
+ type WalletSendTransactionRequest,
45
+ type WalletReadContractRequest,
46
+ type WriteHookParams,
47
+ type WritePhase,
48
+ type WritePhaseSteps,
49
+ type WritePhaseEvent,
50
+ type TxContext,
51
+ type TrackedTx,
52
+ type TrackedTxGas,
53
+ type TrackedTxStatus,
54
+ type TxFlow,
55
+ type TxConfirmedCallback,
56
+ type SendTransactionWithHooksOptions,
57
+ type AwaitReceiptWithHooksOptions,
58
+ type ReceiptAwaiter,
59
+ } from '@valve-tech/wallet-adapter'
60
+ ```
61
+
62
+ ## Six types you must know
63
+
64
+ | Type | What it is |
65
+ |---|---|
66
+ | `WalletAdapter` | The contract an SDK accepts in lieu of coupling to wagmi / ethers / viem direct / a smart account. `{ address?, sendTransaction(req), readContract?(req) }`. |
67
+ | `WriteHookParams` | Per-phase callback bag. Six named hooks (`onAwaitingSignature`, `onTransactionHash`, `onConfirmed`, `onFailed`, `onDropped`, `onReplaced`) + complementary `onPhase(event)` discriminated-union shape. Both shapes fire for every transition — exactly once each. |
68
+ | `TxContext<Extra>` | The always-present info bag carried on every event: `{ chainId, request } & Extra`. Consumers never have to side-channel chain ID or the original send request. |
69
+ | `WritePhaseSteps` | Per-phase data delta map (`pending: { hash }`, `confirmed: { hash, receipt, block? }`, `failed: { error, hash?, receipt?, block? }`, etc.). Open to declaration merging — extend it from your code if you have additional phases. |
70
+ | `WalletRejectedError` | Thrown by `sendTransactionWithHooks` on user rejection. `Error` subclass with `cause: Error`. Discriminate via `instanceof`. |
71
+ | `ContractRevertedError` | Thrown by `awaitReceiptWithHooks` on `status: reverted`. Carries `hash` + the full `receipt` for log inspection. |
72
+
73
+ ## The two helpers — what they fire and when
74
+
75
+ ### `sendTransactionWithHooks({ wallet, request, hooks?, onTransactionHash? })`
76
+
77
+ Wallet-side. Returns `Promise<Hex>` resolving to the tx hash.
78
+
79
+ | Phase | Fires |
80
+ |---|---|
81
+ | Pre-wallet (always once) | `onAwaitingSignature(ctx)` + `onPhase('awaiting-signature', ctx)` |
82
+ | Hash returned (always once on success) | `onTransactionHash(ctx + { hash })` (per-call) AND the global `onTransactionHash` if passed AND `onPhase('pending', ctx + { hash })` |
83
+ | Wallet rejection | `onFailed(ctx + { error: WalletRejectedError })` + `onPhase('failed', ...)`, then **throws `WalletRejectedError`** |
84
+ | Other thrown error | `onFailed(ctx + { error: <thrown> })` + `onPhase('failed', ...)`, then **re-throws unchanged** |
85
+
86
+ `onTransactionHash` accepts both a per-call hook (in `hooks.onTransactionHash`) AND a top-level argument for analytics/global-channel observers. Both fire exactly once.
87
+
88
+ ### `awaitReceiptWithHooks({ publicClient, hash, request, includeBlock?, hooks? })`
89
+
90
+ Chain-side. Returns `Promise<TransactionReceipt>` on success.
91
+
92
+ | Outcome | Fires |
93
+ |---|---|
94
+ | `receipt.status === 'success'` | Fetches containing block (unless `includeBlock: false`), then `onConfirmed(ctx + { hash, receipt, block? })` + `onPhase('confirmed', ...)` |
95
+ | `receipt.status === 'reverted'` | Fetches containing block, then `onFailed(ctx + { hash, receipt, block?, error: ContractRevertedError })` + `onPhase('failed', ...)`, then **throws `ContractRevertedError`** |
96
+ | Network/RPC/abort during await | `onFailed(ctx + { error: <thrown> })` (no `hash`/`receipt`/`block`) + `onPhase('failed', ...)`, then **re-throws unchanged** |
97
+
98
+ `request` is **load-bearing** — populates `TxContext` on every emitted event. Don't omit it.
99
+
100
+ `includeBlock: true` (the default) fetches the containing block once after a successful receipt-await so downstream consumers (notably `@valve-tech/tx-tracker`) skip the round trip for `timestamp` / `baseFeePerGas`. Pass `false` if you don't need block-level data.
101
+
102
+ ## What the helpers DON'T fire
103
+
104
+ `onDropped` and `onReplaced` are part of the `WriteHookParams` contract but **not** fired by this package's helpers. Honestly distinguishing "still propagating" from "permanently dropped" requires multi-block observation; replacement detection requires nonce-watching across the same nonce — that's `@valve-tech/tx-tracker`'s job.
105
+
106
+ The hooks live here so consumers wire **one** set of callbacks; the tracker fires them when it ships. Wiring them against `awaitReceiptWithHooks` alone is harmless but they will never fire from this package.
107
+
108
+ ## Lifecycle vocabulary (`TX_STATUS` / `TrackedTx`)
109
+
110
+ For "in-flight transaction" UIs (toast strips, inline indicators, history panes):
111
+
112
+ ```ts
113
+ type TrackedTxStatus =
114
+ | 'preparing' // pre-wallet (no hash)
115
+ | 'awaiting-signature' // wallet popup open (no hash)
116
+ | 'pending' // hash returned, waiting for inclusion
117
+ | 'confirmed' // receipt arrived, status: success
118
+ | 'failed' // wallet reject, on-chain revert, or timeout
119
+ | 'dropped' // never observed in mempool/block (tx-tracker territory)
120
+ | 'replaced' // different tx mined for same nonce (tx-tracker territory)
121
+ ```
122
+
123
+ `TX_FLOW = {} as const` is intentionally empty. Every protocol's flow names (`fulfillIntent`, `addFunds`, `mintNFT`) are its own concern — extend `TxFlow` from your code:
124
+
125
+ ```ts
126
+ const MY_FLOW = { addFunds: 'add-funds', mintNFT: 'mint-nft' } as const
127
+ type MyFlow = typeof MY_FLOW[keyof typeof MY_FLOW]
128
+ // MyFlow extends TxFlow (which is `string`) automatically.
129
+ ```
130
+
131
+ Two pre-hash states exist (`preparing`, `awaitingSignature`) so the UI has something to show during gas-estimation + wallet-sign — without them the strip stays blank until after the wallet returns. They carry no `hash` and cannot be receipt-polled.
132
+
133
+ ## Pitfalls (read these)
134
+
135
+ 1. **Forgetting `request` in `awaitReceiptWithHooks`.** It's a required option, not optional, and is what populates `TxContext` on every emitted event. The TS error will catch it but it's an easy field to skip when copy-pasting.
136
+
137
+ 2. **Catching errors without `instanceof` discrimination.** `WalletRejectedError`, `ContractRevertedError`, and unspecified network errors all flow through `onFailed` AND through the helpers' throw paths. Use `instanceof` to map to your SDK's typed errors:
138
+ ```ts
139
+ try { await awaitReceiptWithHooks({ ... }) }
140
+ catch (err) {
141
+ if (err instanceof WalletRejectedError) { /* user rejected */ }
142
+ if (err instanceof ContractRevertedError) { /* on-chain revert; err.receipt for logs */ }
143
+ throw err
144
+ }
145
+ ```
146
+
147
+ 3. **Re-implementing wallet-rejection detection.** The three-signal check (EIP-1193 `code === 4001`, viem class name, message regex, walking the cause chain) lives in `@valve-tech/viem-errors`. `sendTransactionWithHooks` already throws `WalletRejectedError` correctly — don't duplicate the matcher.
148
+
149
+ 4. **Side-channel `hash → request` maps.** Old code that pre-dates rich `TxContext` payloads typically maintained a map from hash to the originating request. With `TxContext`, every event carries `{ chainId, request }` — drop the side channel.
150
+
151
+ 5. **Reading `client.chain?.id` from inside callbacks** when `info.chainId` is already in scope. The whole point of `TxContext` is that the chain ID is part of the event payload; no need to thread the client into the callback.
152
+
153
+ 6. **Wiring `onDropped` / `onReplaced` against this package alone.** They will never fire from `sendTransactionWithHooks` / `awaitReceiptWithHooks`. To get them, attach `@valve-tech/tx-tracker` and let it dispatch into the same `WriteHookParams` shape.
154
+
155
+ 7. **Skipping `includeBlock: false` opt-out** when the consumer doesn't need block data. The default fetches the block once (saving downstream callers an RPC), but if NO downstream cares, that's a wasted round-trip. Only the default if `block?` will be consumed.
156
+
157
+ 8. **Treating `onPhase` and the named hooks as alternatives.** They fire BOTH for every transition — exactly once each. Wiring named hooks doesn't preclude `onPhase` and vice versa. Pick whichever shape fits the consumer (state-machine code likes `onPhase`; React component callbacks like the named hooks).
158
+
159
+ ## Composition with sibling packages
160
+
161
+ ```ts
162
+ import { sendTransactionWithHooks, awaitReceiptWithHooks } from '@valve-tech/wallet-adapter'
163
+ import { createTxTracker } from '@valve-tech/tx-tracker'
164
+
165
+ const tracker = createTxTracker({ source, chainId: 1 })
166
+
167
+ const hash = await sendTransactionWithHooks({
168
+ wallet, request, hooks: { onTransactionHash: ({ hash }) => tracker.track(hash) },
169
+ })
170
+ const receipt = await awaitReceiptWithHooks({
171
+ publicClient, hash, request, hooks: { /* onConfirmed / onFailed → your UI */ },
172
+ })
173
+ ```
174
+
175
+ `@valve-tech/tx-flight-react` wraps both helpers for React consumers — if the user wants an in-flight tx strip in a React app, redirect to that package's skill rather than wiring hooks by hand.
176
+
177
+ ## Skills (for AI agents)
178
+
179
+ `skills/` ships in the npm tarball. If you're an AI agent working in a
180
+ project that has installed this package, look in
181
+ `node_modules/@valve-tech/wallet-adapter/skills/wallet-adapter-integration/SKILL.md`
182
+ for trigger conditions, anti-pattern flags, and recipes for the
183
+ helpers' phase wiring.
184
+
185
+ ## Verifying provenance
186
+
187
+ ```bash
188
+ npm view @valve-tech/wallet-adapter@latest --json | jq .dist.attestations
189
+ npm audit signatures
190
+ ```
191
+
192
+ The attestation links the published tarball to the GitHub Actions
193
+ workflow run that built it.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/).
8
8
 
9
+ ## [0.11.0] — 2026-05-11
10
+
11
+ ### Added
12
+
13
+ - Five worked `examples/` covering the common wallet-plumbing classes
14
+ — each is runnable end-to-end via a no-network fake transport, and
15
+ documents the real `npm install` consumers would add to wire it up
16
+ for production. `typecheck:examples` is wired into the package and
17
+ the root workspace so all five are gated by CI.
18
+
19
+ | Example | Covers | Helper |
20
+ |---|---|---|
21
+ | `01-reown-adapter.ts` | Reown / WalletConnect, MetaMask SDK, RainbowKit, raw `window.ethereum`, hardware wallets in-browser (Ledger Live / Trezor Suite). Universal EIP-1193 path. | `walletAdapterFromEip1193(...)` |
22
+ | `02-wagmi-adapter.ts` | wagmi React stack — wraps `useWalletClient()`'s viem `WalletClient` directly. | `walletAdapterFromWalletClient(...)` |
23
+ | `03-server-relayer.ts` | Backend signing via private key (env / KMS); hard-fail on cross-chain. | `walletAdapterFromRelayer(...)` |
24
+ | `04-erc4337-smart-account.ts` | ERC-4337 account abstraction via permissionless.js or similar; `adapter.address` is the smart-account address. | `walletAdapterFromSmartAccount(...)` |
25
+ | `05-hardware-wallet-direct.ts` | Direct USB/HID-attached Ledger via `@ledgerhq/hw-app-eth` (Trezor via `@trezor/connect` is the same shape). For backend / kiosk / dev tooling. | `walletAdapterFromLedger(...)` |
26
+
27
+ Closes the long-standing "how do I make this work with Reown / wagmi
28
+ / a smart account / a backend signer / a Ledger?" docs gap.
29
+
30
+ ### Changed
31
+
32
+ - README "Quick start" gains a "Bridging a real wallet to
33
+ `WalletAdapter`" subsection pointing at the five examples with a
34
+ one-line "what each covers" table.
35
+
36
+ ## [0.10.1] — 2026-05-08
37
+
38
+ Synchronized release — no changes to this package. Republished at
39
+ 0.10.1 alongside the rest of the toolkit; v0.10.0 only got
40
+ trueblocks-sdk publishing wrong (missing `repository` field tripped
41
+ provenance validation), so the rest of the line had to bump to
42
+ re-sync.
43
+
9
44
  ## [0.10.0] — 2026-05-08
10
45
 
11
46
  Synchronized release — no changes to this package. Republished at
package/README.md CHANGED
@@ -195,6 +195,34 @@ function subtitle(tx: TrackedTx): string {
195
195
  }
196
196
  ```
197
197
 
198
+ ### Bridging a real wallet to `WalletAdapter`
199
+
200
+ The package is vocabulary, not a connection layer — you bring the
201
+ wallet plumbing. The most universal bridge is **EIP-1193 provider →
202
+ viem `WalletClient` → `WalletAdapter`**, which works for Reown
203
+ (WalletConnect, 200+ wallets), MetaMask SDK, RainbowKit, raw
204
+ `window.ethereum`, hardware wallets in browser context (Ledger
205
+ Live, MetaMask + Ledger, Trezor Suite — they all surface as standard
206
+ EIP-1193 providers), and anything else that surfaces an EIP-1193
207
+ provider.
208
+
209
+ The `examples/` directory has runnable bridges for the five common
210
+ classes of wallet plumbing — read the comments at the top of each
211
+ to see what it covers and which `npm install` you'd add for the real
212
+ thing:
213
+
214
+ | Example | Covers | Bridge helper |
215
+ |---|---|---|
216
+ | [`01-reown-adapter.ts`](./examples/01-reown-adapter.ts) | Reown / WalletConnect / MetaMask SDK / RainbowKit / raw `window.ethereum` / hardware wallets in-browser. Universal EIP-1193 path. | `walletAdapterFromEip1193(...)` |
217
+ | [`02-wagmi-adapter.ts`](./examples/02-wagmi-adapter.ts) | wagmi React stack — wraps the `useWalletClient()` viem `WalletClient` directly, skipping the round-trip through EIP-1193. | `walletAdapterFromWalletClient(...)` |
218
+ | [`03-server-relayer.ts`](./examples/03-server-relayer.ts) | Backend code: relayer signing from a private key (env var / KMS). No provider, no chain-switching, hard-fail on cross-chain. Right for sponsored-tx services, indexer write paths, integration tests. | `walletAdapterFromRelayer(...)` |
219
+ | [`04-erc4337-smart-account.ts`](./examples/04-erc4337-smart-account.ts) | ERC-4337 smart accounts via permissionless.js or similar. `adapter.address` is the smart-account address (not the EOA signer). | `walletAdapterFromSmartAccount(...)` |
220
+ | [`05-hardware-wallet-direct.ts`](./examples/05-hardware-wallet-direct.ts) | Hardware wallets attached **directly** via USB/HID (no wallet app in between) — `@ledgerhq/hw-app-eth` for Ledger; the same shape works for Trezor via `@trezor/connect`. For backend code, kiosk apps, dev tooling. | `walletAdapterFromLedger(...)` |
221
+
222
+ Each example includes a no-network sanity check at the bottom so you
223
+ can run it (`yarn tsx examples/0X-...ts`) without installing any of
224
+ the wallet libraries.
225
+
198
226
  ## Exports
199
227
 
200
228
  | Export | Kind | Shape |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valve-tech/wallet-adapter",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Framework-agnostic vocabulary + runtime helpers for EVM dapp wallet integration. WalletAdapter interface (sign + send), WriteHookParams full lifecycle with rich TxContext payloads (chainId + original request) on every event so consumers don't side-channel; six named hooks (onAwaitingSignature, onTransactionHash, onConfirmed, onFailed, onDropped, onReplaced) plus complementary onPhase(event) discriminated-union shape derived from the WritePhaseSteps phase-map; sendTransactionWithHooks + awaitReceiptWithHooks helpers that fire the hooks at real boundaries (with awaitReceiptWithHooks fetching the containing block once on behalf of all downstream consumers); typed WalletRejectedError + ContractRevertedError for instanceof-discriminated catch; plus TX_STATUS / TX_FLOW / TrackedTx for tx-state UI. Lets SDKs and dapps share one contract instead of each redefining it. Part of the valve-tech/evm-toolkit synchronized release line.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/valve-tech/evm-toolkit/tree/main/packages/wallet-adapter#readme",
@@ -33,20 +33,23 @@
33
33
  },
34
34
  "files": [
35
35
  "dist",
36
+ "skills",
36
37
  "README.md",
38
+ "AGENTS.md",
37
39
  "CHANGELOG.md",
38
40
  "LICENSE"
39
41
  ],
40
42
  "scripts": {
41
43
  "build": "tsc -p .",
42
44
  "typecheck": "tsc -p . --noEmit",
45
+ "typecheck:examples": "tsc -p examples",
43
46
  "lint": "eslint src",
44
47
  "test": "vitest run",
45
48
  "test:coverage": "vitest run --coverage",
46
49
  "prepare": "yarn build"
47
50
  },
48
51
  "dependencies": {
49
- "@valve-tech/viem-errors": "^0.10.0"
52
+ "@valve-tech/viem-errors": "^0.11.0"
50
53
  },
51
54
  "peerDependencies": {
52
55
  "viem": "^2.0.0"
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: wallet-adapter-integration
3
+ description: Integrate `@valve-tech/wallet-adapter` — framework-agnostic vocabulary for EVM wallet integration — into an SDK, dapp, or in-flight tx UI. Use when the user is wiring up `sendTransactionWithHooks` or `awaitReceiptWithHooks`, defining a `WalletAdapter` to decouple their SDK from wagmi/ethers/viem-direct/smart-account, building a transaction-status UI on top of `TX_STATUS` / `TrackedTx`, or asks "how do I detect wallet rejection vs on-chain revert" (`WalletRejectedError` vs `ContractRevertedError` instanceof discrimination). Also fires on imports of `@valve-tech/wallet-adapter` and questions about the `WriteHookParams` lifecycle (`onAwaitingSignature`, `onTransactionHash`, `onConfirmed`, `onFailed`, `onDropped`, `onReplaced`), the `onPhase` discriminated-union shape, the `TxContext` info bag (`{ chainId, request }` carried on every event), `WritePhaseSteps` declaration merging, the `includeBlock` block-fetch toggle, or composing the helpers with `@valve-tech/tx-tracker` (which fires the `onDropped`/`onReplaced` hooks the helpers themselves don't fire). Skip when the user wants per-tx state-machine work without the wallet helpers (delegate to tx-tracker-integration), wants a ready-made React UI for in-flight txs (delegate to tx-flight-react-integration — that package wraps these helpers), or only wants to detect viem error shapes without the helpers (delegate to viem-errors-integration).
4
+ ---
5
+
6
+ # Integrating `@valve-tech/wallet-adapter`
7
+
8
+ Framework-agnostic vocabulary for EVM dapp wallet integration: pure
9
+ types + a few `as const` lifecycle constants + two thin helpers
10
+ (`sendTransactionWithHooks` for the wallet side,
11
+ `awaitReceiptWithHooks` for the chain side). The whole point: every
12
+ SDK / dapp / UI in your stack speaks the same wallet shape, the same
13
+ phases, the same in-flight tx vocabulary — no bespoke "awaiting
14
+ signature" state machine per integration.
15
+
16
+ ## Decision tree: which surface to use
17
+
18
+ ```
19
+ Is the user building an SDK that needs to accept ANY wallet
20
+ (wagmi, ethers, viem-direct, smart account, custom)?
21
+ ├── Yes — accept `WalletAdapter` as a constructor arg. Define an adapter
22
+ │ once per consumer (one for wagmi, one for ethers, etc.) and
23
+ │ your SDK stays decoupled from the wallet library.
24
+ └── No — does the user have one specific wallet library and just wants
25
+ the lifecycle hooks?
26
+ ├── Yes — call `sendTransactionWithHooks` + `awaitReceiptWithHooks`
27
+ │ directly. Pass your wallet-library's send/wait
28
+ │ function via `wallet` / `publicClient`. The helpers
29
+ │ do the phase wiring; you handle protocol-specific work
30
+ │ in between.
31
+ └── No — does the user want an in-flight tx UI strip (React)?
32
+ └── Yes — redirect to `@valve-tech/tx-flight-react`
33
+ (it wraps these helpers + adds React state).
34
+ ```
35
+
36
+ ## How to recognize this package in the user's code
37
+
38
+ ```ts
39
+ import {
40
+ sendTransactionWithHooks,
41
+ awaitReceiptWithHooks,
42
+ WalletRejectedError,
43
+ ContractRevertedError,
44
+ TX_STATUS,
45
+ type WalletAdapter,
46
+ type WriteHookParams,
47
+ type TxContext,
48
+ } from '@valve-tech/wallet-adapter'
49
+ ```
50
+
51
+ `package.json` will show `"@valve-tech/wallet-adapter": "^0.10.x"`.
52
+
53
+ ## The two-helper pattern (canonical SDK shape)
54
+
55
+ ```ts
56
+ import {
57
+ sendTransactionWithHooks,
58
+ awaitReceiptWithHooks,
59
+ WalletRejectedError,
60
+ ContractRevertedError,
61
+ type WalletAdapter,
62
+ type WriteHookParams,
63
+ } from '@valve-tech/wallet-adapter'
64
+
65
+ export class MyClient {
66
+ constructor(
67
+ private wallet: WalletAdapter,
68
+ private publicClient: PublicClient,
69
+ private chainId: number,
70
+ ) {}
71
+
72
+ async deposit(params: DepositParams & WriteHookParams) {
73
+ const request = {
74
+ to: this.escrow,
75
+ data: this.encodeDeposit(params),
76
+ chainId: this.chainId,
77
+ }
78
+ try {
79
+ const hash = await sendTransactionWithHooks({ wallet: this.wallet, request, hooks: params })
80
+ const receipt = await awaitReceiptWithHooks({ publicClient: this.publicClient, hash, request, hooks: params })
81
+ // protocol-specific work here (decode logs, etc.) — onConfirmed already fired
82
+ return { hash, receipt }
83
+ } catch (err) {
84
+ if (err instanceof WalletRejectedError) throw new MySdkError('WALLET_REJECTED', err.message, err.cause)
85
+ if (err instanceof ContractRevertedError) throw new MySdkError('TX_REVERTED', err.message, err)
86
+ throw err
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ Two splits, on purpose: the wallet side and the chain side. Protocol-specific work (gating-service signatures, log decoding, indexer sync) goes between the two helpers. Each helper fires its own subset of `WriteHookParams` callbacks.
93
+
94
+ ## What each helper fires
95
+
96
+ `sendTransactionWithHooks` fires:
97
+ - **once** before wallet popup: `onAwaitingSignature` + `onPhase('awaiting-signature')`
98
+ - **once** after hash returned: `onTransactionHash` (per-call AND any global one passed) + `onPhase('pending', { hash })`
99
+ - **once** on rejection: `onFailed({ error: WalletRejectedError })` + `onPhase('failed', ...)`, then **throws** `WalletRejectedError`
100
+ - **once** on any other thrown error: `onFailed({ error: <thrown> })` + `onPhase('failed', ...)`, then re-throws unchanged
101
+
102
+ `awaitReceiptWithHooks` fires (must pass `request` — it populates `TxContext`):
103
+ - on `status: success`: fetches containing block (unless `includeBlock: false`), then `onConfirmed({ hash, receipt, block? })` + `onPhase('confirmed', ...)`
104
+ - on `status: reverted`: fetches block, then `onFailed({ hash, receipt, block?, error: ContractRevertedError })` + `onPhase('failed', ...)`, **throws** `ContractRevertedError`
105
+ - on network/RPC/abort: `onFailed({ error: <thrown> })` (no hash/receipt/block) + `onPhase('failed', ...)`, re-throws unchanged
106
+
107
+ **Neither helper fires `onDropped` or `onReplaced`.** Those are part of the `WriteHookParams` contract but require multi-block observation + nonce-watching — that's `@valve-tech/tx-tracker`'s job. The hooks live here so consumers wire **one** set of callbacks.
108
+
109
+ ## Anti-patterns to flag
110
+
111
+ When reviewing user code, watch for these and suggest fixes:
112
+
113
+ 1. **Catching errors without `instanceof` discrimination.** All three error classes (`WalletRejectedError`, `ContractRevertedError`, generic `Error`) flow through `onFailed` AND through the throw paths. Without `instanceof`, the SDK can't map them to its own typed errors:
114
+ ```ts
115
+ // ❌ loses the discrimination
116
+ try { ... } catch (err) { throw new MyError(err.message) }
117
+
118
+ // ✅ preserves it
119
+ try { ... } catch (err) {
120
+ if (err instanceof WalletRejectedError) throw new MyError('REJECTED', err.cause)
121
+ if (err instanceof ContractRevertedError) throw new MyError('REVERTED', err.receipt)
122
+ throw err
123
+ }
124
+ ```
125
+
126
+ 2. **Re-implementing wallet-rejection detection.** The three-signal check (EIP-1193 `code === 4001`, viem class name, message regex, walking the cause chain) lives in `@valve-tech/viem-errors`. `sendTransactionWithHooks` already throws `WalletRejectedError` correctly — don't duplicate the matcher.
127
+
128
+ 3. **Side-channel `hash → request` maps in callbacks.** Old code that pre-dates `TxContext` typically maintains a map from hash to the originating request. Every event now carries `{ chainId, request }` in its info bag — drop the side channel:
129
+ ```ts
130
+ // ❌ legacy
131
+ const requestByHash = new Map<Hex, Request>()
132
+ onTransactionHash: (hash) => requestByHash.set(hash, request)
133
+ onConfirmed: (receipt) => doThing(requestByHash.get(receipt.transactionHash), receipt)
134
+
135
+ // ✅ current
136
+ onConfirmed: (info) => doThing(info.request, info.receipt)
137
+ ```
138
+
139
+ 4. **Reading `client.chain?.id` from inside callbacks** when `info.chainId` is in scope. Drop the client capture — `TxContext` already has it.
140
+
141
+ 5. **Forgetting `request` in `awaitReceiptWithHooks`.** Required option (TS will catch it), but easy to skip when copy-pasting from older examples that didn't have `TxContext`.
142
+
143
+ 6. **Wiring `onDropped` / `onReplaced` and expecting them to fire from these helpers.** They won't — they're part of the contract but live behind tx-tracker. Either accept they're silent (stub them out) or attach tx-tracker to dispatch them.
144
+
145
+ 7. **`includeBlock: true` (the default) when no downstream cares about the block.** The default fetches the containing block to amortize the round-trip across consumers; if no consumer reads `block`, that's wasted RPC. Pass `includeBlock: false` explicitly.
146
+
147
+ 8. **Wrapping `sendTransactionWithHooks` in another `try/catch` that swallows the throw.** The helpers fire `onFailed` AND throw — the throw is the SDK's signal to halt the rest of the pipeline. Swallowing it means `awaitReceiptWithHooks` runs with an undefined hash.
148
+
149
+ 9. **Treating `onPhase` and the named hooks as alternatives.** They fire BOTH for every transition — exactly once each. Wiring named hooks doesn't preclude `onPhase`. Use whichever fits the consumer (state-machine code prefers `onPhase`; React component callbacks like the named hooks).
150
+
151
+ 10. **Hardcoding `TX_STATUS` strings instead of using the const.** `tx.status === 'mined'` won't typecheck — the constant is `'confirmed'`. Use `tx.status === TX_STATUS.confirmed` so renames propagate.
152
+
153
+ ## Defining a `WalletAdapter`
154
+
155
+ When the user is writing an adapter for a specific wallet library, the
156
+ shape is:
157
+
158
+ ```ts
159
+ import type { WalletAdapter } from '@valve-tech/wallet-adapter'
160
+
161
+ const wagmiAdapter = (config: WagmiConfig): WalletAdapter => ({
162
+ get address() { return getAccount(config).address },
163
+ sendTransaction: async (req) => {
164
+ return sendTransaction(config, {
165
+ to: req.to, data: req.data, value: req.value, chainId: req.chainId,
166
+ maxFeePerGas: req.maxFeePerGas, maxPriorityFeePerGas: req.maxPriorityFeePerGas,
167
+ })
168
+ },
169
+ // readContract is optional — only implement if your SDK uses it
170
+ })
171
+ ```
172
+
173
+ The `WalletAdapter` interface is intentionally minimal — `sendTransaction` is the only required method. `readContract` is optional for SDKs that need wallet-side reads (account-bound contracts, signature checks).
174
+
175
+ ## Composing with tx-tracker (gets you `onDropped` / `onReplaced`)
176
+
177
+ ```ts
178
+ import { createTxTracker } from '@valve-tech/tx-tracker'
179
+ import { sendTransactionWithHooks, awaitReceiptWithHooks } from '@valve-tech/wallet-adapter'
180
+
181
+ const tracker = createTxTracker({ source, chainId: 1 })
182
+
183
+ const hash = await sendTransactionWithHooks({
184
+ wallet, request,
185
+ hooks: {
186
+ onTransactionHash: ({ hash }) => tracker.track(hash, {
187
+ onDropped: () => userHooks.onDropped?.({ chainId: request.chainId, request, hash }),
188
+ onReplaced: ({ replacement }) => userHooks.onReplaced?.({
189
+ chainId: request.chainId, request, original: hash, replacement,
190
+ }),
191
+ }),
192
+ },
193
+ })
194
+ ```
195
+
196
+ For per-tx state-machine work (subscribe by hash, watch for replacement, detect drops, reorg-safety), redirect to the tx-tracker integration skill at `node_modules/@valve-tech/tx-tracker/skills/tx-tracker-integration/SKILL.md`.
197
+
198
+ For React in-flight tx UIs, redirect to `@valve-tech/tx-flight-react` — it wraps these helpers + `tx-tracker` into a Provider + headless components.
199
+
200
+ ## In-flight UI on `TX_STATUS`
201
+
202
+ ```ts
203
+ import { TX_STATUS, type TrackedTx } from '@valve-tech/wallet-adapter'
204
+
205
+ function subtitle(tx: TrackedTx): string {
206
+ switch (tx.status) {
207
+ case TX_STATUS.preparing: return 'preparing transaction'
208
+ case TX_STATUS.awaitingSignature: return 'awaiting wallet signature'
209
+ case TX_STATUS.pending: return 'waiting for inclusion'
210
+ case TX_STATUS.confirmed: return 'confirmed on-chain'
211
+ case TX_STATUS.failed: return tx.notes ?? 'transaction failed'
212
+ case TX_STATUS.dropped: return 'dropped from mempool'
213
+ case TX_STATUS.replaced: return 'replaced by speed-up'
214
+ }
215
+ }
216
+ ```
217
+
218
+ Pre-hash states (`preparing`, `awaitingSignature`) carry no `hash` — they exist so the strip has something to show during gas-estimation + wallet-sign.
219
+
220
+ ## Where to find more
221
+
222
+ - Full API + types: `node_modules/@valve-tech/wallet-adapter/AGENTS.md`
223
+ - Human-facing docs: `node_modules/@valve-tech/wallet-adapter/README.md`
224
+ - Compiled output (when types alone aren't enough): `node_modules/@valve-tech/wallet-adapter/dist/`
225
+ - Sibling skills:
226
+ - tx-tracker for the `onDropped`/`onReplaced` firing path
227
+ - tx-flight-react for React in-flight UI
228
+ - viem-errors for the wallet-rejection discriminator internals