Skip to content

Commit f730b82

Browse files
authored
Merge pull request #82 from ProvableHQ/feat/dynamic-dispatch-example
Dynamic dispatch example + testing-wallet-adapter-changes skill
2 parents eeaf06d + cdebb84 commit f730b82

9 files changed

Lines changed: 837 additions & 48 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
name: testing-wallet-adapter-changes
3+
description: Use when the user lands a change to any wallet adapter package in this monorepo (core, react, react-ui, or any `wallets/*` adapter) or to `packages/aleo-types` that adds or modifies a field consumed by dapps. Walks Claude through writing a **self-contained example component in `examples/react-app`** that exercises the change, and testing it end-to-end against a real wallet extension installed in the browser.
4+
---
5+
6+
# Testing a wallet-adapter change end-to-end
7+
8+
## When this applies
9+
10+
Invoke this skill whenever a change lands that alters the dapp-facing surface of the wallet adapter. Concretely:
11+
12+
- A new field on `TransactionOptions` / `AleoDeployment` / any adapter method argument (defined in `packages/aleo-types/src/`).
13+
- A new method on `BaseAleoWalletAdapter` or any `wallets/<name>/src/<Name>WalletAdapter.ts`.
14+
- A change to the `useWallet()` context shape in `packages/aleo-wallet-adaptor/react/src/context.ts`.
15+
- Any wire-protocol change the adapter forwards to the extension (even if the adapter code itself just spreads options — the demo is still needed to prove the value flows).
16+
17+
If the change is purely internal to an adapter (refactor, logging, no visible surface change), this skill does not apply — write a unit test in that package instead.
18+
19+
## The plan in one line
20+
21+
Add a **new sibling component** under `examples/react-app/src/components/functions/` that exercises the new surface, then build and run against a real wallet extension and a testnet program chosen to exercise the specific semantics.
22+
23+
## Guardrails (do NOT violate)
24+
25+
- **Never modify existing demo components** (`ExecuteTransaction.tsx`, `DeployProgram.tsx`, etc.) to add coverage for a new feature. Add a sibling. Existing components are reference material for the docs site.
26+
- **Never touch wallet-adapter packages or `aleo-types`** from the example to work around missing plumbing. If the adapter doesn't pass your field through, fix the adapter (and write a PR for it), don't hack around it in the demo.
27+
- **Never hardcode private keys, mnemonics, or funded addresses** in committed code. The demo should default to `useWallet().address` for any "from" field.
28+
- **Never add a new runtime dependency** to `examples/react-app` solely for this test. If you need a helper (e.g. program-ID → field), ship a tiny lookup or compute it via primitives already in the workspace.
29+
30+
## Step-by-step procedure
31+
32+
### 1. Understand the change
33+
34+
Before writing a line of dapp code, answer:
35+
36+
- **What is the new surface?** Which type gained a field, or which method gained an argument? Locate the type definition in `packages/aleo-types/` or the method in `packages/aleo-wallet-adaptor/wallets/<name>/src/`.
37+
- **Does the adapter forward the new surface?** Grep the target adapter for the new field/argument. If it's passed through via object spread (`{ ...options }`) or explicit forwarding, you're good. If the adapter drops it, the change is incomplete — file that first.
38+
- **Does the extension handle the new surface?** For Shield, that's `~/dev/shield-extension`. Look for the matching end-to-end path (typically: messaging validation → service → worker). If the extension doesn't handle it yet, the demo will succeed at the adapter boundary but you won't see the feature land on-chain.
39+
- **What success looks like** for this specific feature — return value from the extension? A specific shape on the testnet explorer? Different popup UI? Write it down before coding.
40+
41+
### 2. Pick a testnet target that exercises the change
42+
43+
Use an already-deployed program on testnet so nothing needs deploying. Canonical sources of truth:
44+
45+
- `https://api.provable.com/v2/testnet/programs/<name>.aleo` — raw Aleo-instructions source (JSON with a `"program"` field).
46+
- `https://testnet.explorer.provable.com/program/<name>.aleo` — human-readable browser view.
47+
48+
Prefer a program whose transition:
49+
- Has only **public** inputs (avoids record-state bootstrapping).
50+
- Has **low finalize-state dependency** (either no finalize, or finalize state that can be seeded from the dapp in one extra button click).
51+
- Exercises the specific semantics of the change — e.g. `call.dynamic` programs for `imports`, deployment flow for a deployment-option change, a record-returning transition for a decryption-flow change.
52+
53+
If you can't find one, note it explicitly and degrade to a *plumbing-only* test: prove the wallet accepts the new option and returns a `transactionId`, regardless of finalize.
54+
55+
### 3. Scaffold the new component
56+
57+
Each of these five edit points mirrors how every existing demo in this example is registered. Follow the pattern exactly — do not improvise.
58+
59+
1. `examples/react-app/src/components/functions/<Feature>.tsx` — the component. Use `useWallet()` for `connected`, `address`, `executeTransaction` (or whichever method is under test), `transactionStatus`, and `network`. Use `useWalletModal()` from `@provablehq/aleo-wallet-adaptor-react-ui` for the `openWalletModal` affordance.
60+
2. `examples/react-app/src/pages/<Feature>Page.tsx` — trivial wrapper: `<div className="max-w-4xl mx-auto"><Feature /></div>`.
61+
3. `examples/react-app/src/pages/index.ts` — add the export.
62+
4. `examples/react-app/src/routes.tsx` — add `{ path: '<slug>', element: <FeaturePage /> }`.
63+
5. `examples/react-app/src/components/layout/Sidebar.tsx` — add entry to the appropriate `navigationGroups` group (Transactions / Signatures / Data / etc.) with a `lucide-react` icon.
64+
6. `examples/react-app/src/lib/codeExamples.ts` — add a `<feature>` entry and any new `PLACEHOLDERS` keys. Render via `<CodePanel code={codeExamples.<feature>} language="tsx" highlightValues={{ [PLACEHOLDERS.X]: state.x, … }} />`.
65+
66+
UI primitives are in `examples/react-app/src/components/ui/`: `button`, `input`, `label`, `alert`, `separator`, `tabs`, `select`, `checkbox`, `card`, `textarea`, `tooltip`, `badge`.
67+
68+
### 4. Copy the status-polling block verbatim from `ExecuteTransaction.tsx`
69+
70+
Don't abstract. The `pollTransactionStatus` + status-polling `<Alert>` block is duplicated across several demos on purpose (it keeps each demo readable in isolation). Copy it, adapt state names if needed, keep the `TransactionStatus.ACCEPTED / REJECTED / FAILED` branching.
71+
72+
### 5. Make the demo self-sufficient where feasible
73+
74+
If the feature-under-test needs on-chain preconditions (a record, a balance, an approval) that can be satisfied from the dapp, add small "prep" buttons *inside the same component* — do not send users to a different page. Example: a feature that moves tokens through a router should include a "Mint yourself tokens" button so the finalize step can land, not just a note telling the reader to go mint elsewhere.
75+
76+
Rules of thumb for prep buttons:
77+
- One labeled `<Input>` for the per-action amount (shared across same-kind buttons).
78+
- A `pendingAction` enum (`'prep-a' | 'prep-b' | 'main' | null`) so only one button runs at a time and the others disable.
79+
- Clear labels on spinners (`"Minting toka…"`, `"Approving router…"`) — not a generic "Loading".
80+
- A shared status Alert for the most recent call (no need for per-action status panes).
81+
82+
If a precondition *cannot* be satisfied from the dapp (e.g. it needs a deployer key), say so in the component's explanatory `<Alert>` — don't pretend it can.
83+
84+
### 6. Types must compile without `as any`
85+
86+
The whole point of landing the new surface in `aleo-types` is that `useWallet().executeTransaction(...)` accepts the new field by type. If you find yourself casting `as any` or `@ts-ignore`, either:
87+
- The `aleo-types` change didn't ship — fix the upstream.
88+
- Your local `node_modules` is stale — `pnpm install` at the monorepo root.
89+
90+
### 7. Verify the baseline compiles
91+
92+
```bash
93+
cd <monorepo-root>
94+
pnpm install
95+
pnpm -w build
96+
pnpm --filter react-app-example dev
97+
```
98+
99+
`pnpm -w build` does **not** build the example (it's excluded via turbo filter in the root `package.json`). To typecheck the example itself:
100+
101+
```bash
102+
pnpm --filter react-app-example build
103+
# or just start the dev server — vite typechecks the module graph on demand
104+
```
105+
106+
### 8. Test against a real wallet extension
107+
108+
Preconditions — ask before prescribing:
109+
110+
- **Q1 — Is the matching extension build loaded in Chrome?** For Shield that's `~/dev/shield-extension` on a branch whose commits implement the feature end-to-end. If yes, skip to Q2. If unsure, rebuild: `cd ~/dev/shield-extension && yarn install && yarn build`, then click **↻ Reload** on the Shield card in `chrome://extensions` (or **Load unpacked** on `dist/chrome-mv3` if it wasn't loaded at all).
111+
- **Q2 — Is there a Testnet account in the wallet?** If yes, skip. If no, create/import from the extension popup, select **Testnet** in the network picker, copy the address.
112+
- *(Very optional — skip unless the account is genuinely empty)* fund the address from a faucet.
113+
114+
### Authorize every program the demo calls — before connecting
115+
116+
Shield (and similar permissioned wallets) binds an allowed-programs list to the connection at connect time via the adapter's `connect(network, decryptPermission, programs)` argument. Any `executeTransaction` for a program not in that list is rejected at the extension boundary with an error like:
117+
118+
> `<program>.aleo is not in the allowed programs, request it when connect`
119+
120+
The example app manages this list through the **Programs** button in the header (top-right, with a count badge). It's persisted in the `programsAtom` jotai atom (`examples/react-app/src/lib/store/global.ts`) and read at the `AleoWalletProvider` level in `src/App.tsx`.
121+
122+
Before connecting the wallet on the dev server:
123+
124+
1. Click the **Programs** button in the header.
125+
2. Add every program your new demo component calls. For the dynamic-dispatch example that's `token_router.aleo`, `toka_token.aleo`, and `tokb_token.aleo` — include both prep-button targets (mint) and the main-action targets (dispatch). Anything you omit will blow up at call time with the error above.
126+
3. If you're already connected when you add a program, you must **disconnect and reconnect** — the allowed list is negotiated only at connect time, not per call.
127+
128+
As a rule of thumb: if your new component exercises a program outside `credits.aleo` / `hello_world.aleo` (the defaults), you must update this list before a manual test run. Bake the list into your worked-example docs.
129+
130+
Then:
131+
132+
1. Run the dev server.
133+
2. **Connect Wallet** → pick the wallet under test → approve in the extension popup (the popup will ask you to authorize each program in the list).
134+
3. Click the prep buttons (if any) and watch the status. These test the *base* adapter path without the new feature — confirming you haven't broken the default.
135+
4. Click the main action that exercises the new surface.
136+
5. Read the response in the status Alert. Cross-reference with the extension's popup (did it show the new feature? did the preview match?) and with the testnet explorer page for the returned `transactionId`.
137+
138+
### 9. Success criteria
139+
140+
Plumbing success is the first and sometimes only signal: the wallet returns a `transactionId`. That alone proves the new surface made it across the dapp → adapter → extension → SDK boundary.
141+
142+
Feature-specific success will usually show up in one of:
143+
- **The extension popup's preview pane** — e.g. a new list of resolved imports, a new permission prompt, a new metadata field.
144+
- **The explorer transaction page** — e.g. a nested transition from a dynamically-dispatched program, a new transition type, updated mapping values.
145+
- **The wallet's local UI** — e.g. a balance change, a record appearing in the user's list.
146+
147+
Write down which of these is the real signal for the change you're testing and look at it explicitly; "the tx went through" is not enough if the feature is supposed to change *how* it went through.
148+
149+
### 10. Interpret partial success honestly
150+
151+
A returned `transactionId` with a `Rejected` finalize isn't necessarily a feature bug — it may be the program's finalize asserting something about testnet state. Before filing a bug:
152+
153+
- Reproduce with a *base-case* `executeTransaction` (without the new field) against the same function. Does it fail in the same way? If yes, the issue is state, not the feature.
154+
- Check the explorer's "finalize error" string. `balance insufficient`, `allowance not found`, `record spent` all point at state.
155+
- Only file a bug when the new surface is the difference between success and failure.
156+
157+
## Worked example reference
158+
159+
The first feature tested with this skill was the `imports?: string[]` field added to `TransactionOptions` to support `call.dynamic` dapps. The resulting artifacts are:
160+
161+
- `examples/react-app/src/components/functions/DynamicDispatch.tsx`
162+
- `examples/react-app/src/pages/DynamicDispatchPage.tsx`
163+
- `examples/react-app/src/lib/programIdField.ts`
164+
- route `/dynamic-dispatch`, sidebar entry in the Transactions group
165+
- `codeExamples.dynamicDispatch` snippet
166+
167+
Read those files for a concrete shape: two prep buttons (mint on either of two token programs), a tab selector for the target program, the main dispatch action, a shared status Alert, and a live `CodePanel` mirroring form state. That's the template you're aiming for.
168+
169+
## Common failure modes (look here first)
170+
171+
- **`<program>.aleo is not in the allowed programs, request it when connect`** → the program isn't in the dapp's authorized list. Open the **Programs** dropdown in the header, add it, then disconnect + reconnect (the allowed list is bound at connect time, not per call).
172+
- **Types reject the new field** → upstream `aleo-types` change isn't on the current branch or isn't rebuilt. `pnpm install` at monorepo root.
173+
- **Extension popup rejects at messaging-validation** → extension build predates the plumbing commit. `yarn build` and reload.
174+
- **Extension shows the call but no `transactionId` returns** → offscreen worker error. Extension DevTools → `offscreen.html` console.
175+
- **`transactionId` returns but explorer shows no trace of the new feature** → wallet-adapter or extension didn't actually forward the field. Grep the adapter for the field name; grep the extension's messaging validator and service layer for the same.
176+
- **`programIdToField` (or equivalent helper) throws on a placeholder** → someone shipped a literal-lookup helper with a TODO. Compute the value via snarkvm / `snarkos developer` / the extension's offscreen-worker SDK console, paste it in.
177+
178+
## Stopping rule
179+
180+
You're done when:
181+
182+
1. The new component compiles without `as any`.
183+
2. Clicking the main action against the real extension returns a `transactionId`.
184+
3. The explorer (or whatever the designated success signal is) shows the feature-specific evidence.
185+
4. You can describe in one sentence, without waffling, what would be the symptom if the feature regressed.
186+
187+
If you can't do (4), the component doesn't yet exercise the feature tightly enough — go back to step 1.

examples/react-app/src/components/ProgramAutocomplete.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface ProgramAutocompleteProps {
1212
onAdd: (programId?: string) => void;
1313
disabled?: boolean;
1414
selectedPrograms?: string[];
15+
programIdAllowlist?: string[];
1516
}
1617

1718
export const ProgramAutocomplete = ({
@@ -20,6 +21,7 @@ export const ProgramAutocomplete = ({
2021
onAdd,
2122
disabled,
2223
selectedPrograms = [],
24+
programIdAllowlist,
2325
}: ProgramAutocompleteProps) => {
2426
const [network] = useAtom(networkAtom);
2527
const [isOpen, setIsOpen] = useState(false);
@@ -28,9 +30,15 @@ export const ProgramAutocomplete = ({
2830
const containerRef = useRef<HTMLDivElement>(null);
2931
const inputRef = useRef<HTMLInputElement>(null);
3032

31-
const { data: searchResults, isLoading } = useProgramsSearch(network, searchTerm);
33+
const { data: searchResults, isLoading: networkLoading } = useProgramsSearch(network, searchTerm);
3234

33-
const programs = searchResults?.programs || [];
35+
const programs = programIdAllowlist
36+
? programIdAllowlist
37+
.filter(id => (!searchTerm ? true : id.toLowerCase().includes(searchTerm.toLowerCase())))
38+
.map(id => ({ id, name: id }))
39+
: searchResults?.programs || [];
40+
41+
const isLoading = programIdAllowlist ? false : networkLoading;
3442

3543
useEffect(() => {
3644
const handleClickOutside = (event: MouseEvent) => {

0 commit comments

Comments
 (0)