create-miden-app 1.0.6 → 1.0.7

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.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/commands/review-security.md +67 -0
  3. package/template/.claude/settings.json +1 -7
  4. package/template/.claude/settings.local.json +24 -0
  5. package/template/.claude/skills/frontend-pitfalls/SKILL.md +28 -31
  6. package/template/.claude/skills/frontend-source-guide/SKILL.md +14 -14
  7. package/template/.claude/skills/miden-concepts/SKILL.md +4 -2
  8. package/template/.claude/skills/react-sdk-patterns/SKILL.md +294 -28
  9. package/template/.claude/skills/signer-integration/SKILL.md +22 -3
  10. package/template/.claude/skills/testing-patterns/SKILL.md +201 -40
  11. package/template/.claude/skills/vite-wasm-setup/SKILL.md +20 -14
  12. package/template/.claude/skills/web-client-usage/SKILL.md +454 -0
  13. package/template/.env.example +15 -2
  14. package/template/.mcp.json +9 -0
  15. package/template/CLAUDE.md +49 -16
  16. package/template/README.md +85 -19
  17. package/template/package.json +5 -4
  18. package/template/public/packages/counter_account.masp +0 -0
  19. package/template/public/packages/increment_note.masp +0 -0
  20. package/template/src/__tests__/fixtures/accounts.ts +17 -6
  21. package/template/src/__tests__/fixtures/index.ts +1 -0
  22. package/template/src/__tests__/mocks/miden-sdk-react.ts +18 -1
  23. package/template/src/__tests__/patterns/mutation-hook.test.tsx +2 -2
  24. package/template/src/__tests__/patterns/provider-setup.test.tsx +2 -0
  25. package/template/src/components/AppContent.tsx +33 -3
  26. package/template/{create-miden-app/template/src/components/Counter.tsx → src/components/ConfiguredCounter.tsx} +7 -4
  27. package/template/src/components/Counter.tsx +12 -41
  28. package/template/src/components/__tests__/AppContent.test.tsx +192 -4
  29. package/template/src/components/__tests__/ConfiguredCounter.test.tsx +116 -0
  30. package/template/src/components/__tests__/Counter.test.tsx +24 -94
  31. package/template/src/config.ts +26 -6
  32. package/template/src/hooks/__tests__/useIncrementCounter.test.tsx +257 -0
  33. package/template/src/hooks/useIncrementCounter.ts +109 -50
  34. package/template/src/providers.tsx +20 -24
  35. package/template/vite.config.ts +1 -1
  36. package/template/vitest.config.ts +1 -2
  37. package/template/yarn.lock +761 -688
  38. package/template/create-miden-app/template/.claude/hooks/typecheck.sh +0 -27
  39. package/template/create-miden-app/template/.claude/settings.json +0 -17
  40. package/template/create-miden-app/template/.claude/skills/frontend-pitfalls/SKILL.md +0 -189
  41. package/template/create-miden-app/template/.claude/skills/frontend-source-guide/SKILL.md +0 -163
  42. package/template/create-miden-app/template/.claude/skills/miden-concepts/SKILL.md +0 -108
  43. package/template/create-miden-app/template/.claude/skills/react-sdk-patterns/SKILL.md +0 -294
  44. package/template/create-miden-app/template/.claude/skills/signer-integration/SKILL.md +0 -158
  45. package/template/create-miden-app/template/.claude/skills/vite-wasm-setup/SKILL.md +0 -128
  46. package/template/create-miden-app/template/.env.example +0 -5
  47. package/template/create-miden-app/template/CLAUDE.md +0 -116
  48. package/template/create-miden-app/template/README.md +0 -61
  49. package/template/create-miden-app/template/eslint.config.js +0 -23
  50. package/template/create-miden-app/template/index.html +0 -13
  51. package/template/create-miden-app/template/package.json +0 -34
  52. package/template/create-miden-app/template/public/vite.svg +0 -1
  53. package/template/create-miden-app/template/src/App.tsx +0 -10
  54. package/template/create-miden-app/template/src/assets/miden.svg +0 -3
  55. package/template/create-miden-app/template/src/assets/react.svg +0 -1
  56. package/template/create-miden-app/template/src/components/AppContent.css +0 -45
  57. package/template/create-miden-app/template/src/components/AppContent.tsx +0 -50
  58. package/template/create-miden-app/template/src/components/Counter.css +0 -27
  59. package/template/create-miden-app/template/src/config.ts +0 -21
  60. package/template/create-miden-app/template/src/hooks/useIncrementCounter.ts +0 -136
  61. package/template/create-miden-app/template/src/index.css +0 -75
  62. package/template/create-miden-app/template/src/lib/miden.ts +0 -9
  63. package/template/create-miden-app/template/src/main.tsx +0 -10
  64. package/template/create-miden-app/template/src/providers.tsx +0 -31
  65. package/template/create-miden-app/template/src/vite-env.d.ts +0 -1
  66. package/template/create-miden-app/template/tsconfig.app.json +0 -32
  67. package/template/create-miden-app/template/tsconfig.json +0 -7
  68. package/template/create-miden-app/template/tsconfig.node.json +0 -24
  69. package/template/create-miden-app/template/vite.config.ts +0 -17
  70. package/template/create-miden-app/template/yarn.lock +0 -1697
@@ -0,0 +1,257 @@
1
+ import { renderHook, act, waitFor } from "@testing-library/react";
2
+ import { vi, describe, it, expect, beforeEach } from "vitest";
3
+
4
+ // Stub fetch — `increment` fetches `/packages/increment_note.masp`. We never
5
+ // reach Note construction in any test below (the path either short-circuits
6
+ // on a missing wallet address or is short-circuited before `requestTransaction`
7
+ // completes), but the fetch itself runs once and we don't want jsdom to error
8
+ // out on an unhandled network request.
9
+ const mockFetch = vi.fn(async () => ({
10
+ arrayBuffer: async () => new ArrayBuffer(0),
11
+ }));
12
+ vi.stubGlobal("fetch", mockFetch);
13
+
14
+ vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
15
+
16
+ // `useIncrementCounter` reads `requestTransaction` and `address` directly from
17
+ // the hook return per `WalletContextState`. We mock the wallet-adapter-react
18
+ // package and provide a default disconnected stub. Individual tests override
19
+ // via `vi.mocked(useMidenFiWallet).mockReturnValue(...)`.
20
+ const defaultWallet = {
21
+ autoConnect: false,
22
+ wallets: [],
23
+ wallet: null,
24
+ address: null as string | null,
25
+ publicKey: null,
26
+ connected: false,
27
+ connecting: false,
28
+ disconnecting: false,
29
+ select: vi.fn(),
30
+ connect: vi.fn(async () => undefined),
31
+ disconnect: vi.fn(async () => undefined),
32
+ requestTransaction: vi.fn(async () => "0xtx"),
33
+ requestAssets: undefined,
34
+ requestPrivateNotes: undefined,
35
+ signBytes: undefined,
36
+ importPrivateNote: undefined,
37
+ requestConsumableNotes: undefined,
38
+ waitForTransaction: undefined,
39
+ requestSend: undefined,
40
+ requestConsume: undefined,
41
+ createAccount: undefined,
42
+ };
43
+
44
+ const mockGetAccount = vi.fn(async () => null);
45
+ const mockImportAccountById = vi.fn(async () => undefined);
46
+ const mockSyncState = vi.fn(async () => undefined);
47
+
48
+ vi.mock("@miden-sdk/miden-wallet-adapter-react", () => ({
49
+ useMidenFiWallet: vi.fn(() => defaultWallet),
50
+ }));
51
+
52
+ // `useIncrementCounter` calls into many SDK constructors (Word/Felt/AccountId,
53
+ // note builders, etc.) and chained builder methods like
54
+ // `TransactionRequestBuilder().withOwnOutputNotes(...).build()`. We don't need
55
+ // real values from these in unit tests — only that calls don't throw and the
56
+ // hook can reach the poll loop. A Proxy-backed stub satisfies all chained
57
+ // calls (`new X(...)`, `X.staticMethod(...)`, `instance.foo().bar(...)`) by
58
+ // returning another callable+constructable stub for any property access.
59
+ //
60
+ // The one shape we DO need real data from is `Word.toU64s()` (the storage-map
61
+ // value the hook reads to derive the count). The proxy intercepts that
62
+ // specific access and returns a 4-tuple BigUint64Array-like value.
63
+ vi.mock("@miden-sdk/miden-sdk", async () => {
64
+ const stub = (): object =>
65
+ new Proxy(function noop() {}, {
66
+ get: (_t, prop) => {
67
+ if (prop === "toU64s") return () => [0n, 0n, 0n, 0n];
68
+ // Avoid breaking Promise resolution / native Symbol checks
69
+ if (typeof prop === "symbol") return undefined;
70
+ return stub();
71
+ },
72
+ apply: () => stub(),
73
+ construct: () => stub(),
74
+ });
75
+ const exports: Record<string, unknown> = {};
76
+ for (const k of [
77
+ "TransactionRequestBuilder",
78
+ "Package",
79
+ "NoteScript",
80
+ "Note",
81
+ "NoteAssets",
82
+ "NoteMetadata",
83
+ "NoteRecipient",
84
+ "NoteStorage",
85
+ "NoteTag",
86
+ "NoteType",
87
+ "NoteAttachment",
88
+ "NoteExecutionHint",
89
+ "NoteArray",
90
+ "AccountId",
91
+ "Felt",
92
+ "FeltArray",
93
+ "Word",
94
+ ]) {
95
+ exports[k] = stub();
96
+ }
97
+ return exports;
98
+ });
99
+
100
+ vi.mock("@miden-sdk/miden-wallet-adapter-base", () => ({
101
+ Transaction: { createCustomTransaction: vi.fn(() => ({})) },
102
+ }));
103
+
104
+ vi.mock("@/lib/miden", () => ({ randomWord: () => ({}) }));
105
+
106
+ import { useMiden, useMidenClient } from "@miden-sdk/react";
107
+ import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
108
+ import { useIncrementCounter } from "../useIncrementCounter";
109
+ import {
110
+ NETWORK_POLL_INTERVAL_MS,
111
+ NETWORK_POLL_TIMEOUT_MS,
112
+ } from "@/config";
113
+
114
+ const COUNTER_ADDRESS = "mtst1aqmx7qv6h3y92sqsmunh8uht4ujmfy4j";
115
+
116
+ describe("useIncrementCounter", () => {
117
+ beforeEach(() => {
118
+ vi.useRealTimers();
119
+ vi.clearAllMocks();
120
+ mockGetAccount.mockReset();
121
+ mockImportAccountById.mockReset();
122
+ mockSyncState.mockReset();
123
+ mockFetch.mockClear();
124
+
125
+ vi.mocked(useMiden).mockReturnValue({
126
+ client: null,
127
+ isReady: true,
128
+ isInitializing: false,
129
+ error: null,
130
+ sync: vi.fn(),
131
+ runExclusive: <T,>(fn: () => Promise<T>) => fn(),
132
+ prover: null,
133
+ signerAccountId: null,
134
+ signerConnected: null,
135
+ });
136
+ vi.mocked(useMidenClient).mockReturnValue({
137
+ getAccount: mockGetAccount,
138
+ importAccountById: mockImportAccountById,
139
+ syncState: mockSyncState,
140
+ } as unknown as ReturnType<typeof useMidenClient>);
141
+ vi.mocked(useMidenFiWallet).mockReturnValue(defaultWallet);
142
+ });
143
+
144
+ it("surfaces an error when increment is called without a wallet address", async () => {
145
+ // Default stub has `address: null` (wallet not connected to an account).
146
+ // Make the mount-time loadCount succeed so its error path doesn't race
147
+ // with the one we're asserting against.
148
+ const fakeAccount = {
149
+ storage: () => ({ getMapItem: () => null }),
150
+ };
151
+ mockGetAccount.mockResolvedValue(fakeAccount as never);
152
+
153
+ const { result } = renderHook(() => useIncrementCounter(COUNTER_ADDRESS));
154
+ // Wait for mount-effect to finish (count resolves from null → 0).
155
+ await waitFor(() => expect(result.current.count).toBe(0));
156
+
157
+ await act(async () => {
158
+ await result.current.increment();
159
+ });
160
+
161
+ expect(result.current.error).toMatch(/no wallet account available/i);
162
+ expect(result.current.isSubmitting).toBe(false);
163
+ // requestTransaction must NOT have been called — we short-circuit before
164
+ // touching the wallet.
165
+ expect(defaultWallet.requestTransaction).not.toHaveBeenCalled();
166
+ });
167
+
168
+ it("surfaces an error when the counter account is unreachable on-chain", async () => {
169
+ // import + retry both return null → should setError, not stay silent.
170
+ mockGetAccount.mockResolvedValue(null);
171
+
172
+ const { result } = renderHook(() => useIncrementCounter(COUNTER_ADDRESS));
173
+
174
+ await waitFor(() => {
175
+ expect(result.current.error).toMatch(/counter account not found/i);
176
+ });
177
+ expect(result.current.count).toBeNull();
178
+ });
179
+
180
+ it("does not call useMiden().sync() during a poll iteration", async () => {
181
+ // Wallet must be connected so increment doesn't short-circuit before the
182
+ // poll loop. Provide a real address + a working `requestTransaction`.
183
+ const requestTransaction = vi.fn(async () => "0xtx");
184
+ vi.mocked(useMidenFiWallet).mockReturnValue({
185
+ ...defaultWallet,
186
+ address: "mtst1arwk88k8smzcq5p30upr6eerw5npmnyz",
187
+ connected: true,
188
+ requestTransaction,
189
+ });
190
+
191
+ // Spy on the hook-level `sync()`. The fix this test guards against would
192
+ // call this inside the poll loop; production code now does not.
193
+ const sync = vi.fn(async () => undefined);
194
+ vi.mocked(useMiden).mockReturnValue({
195
+ client: null,
196
+ isReady: true,
197
+ isInitializing: false,
198
+ error: null,
199
+ sync,
200
+ runExclusive: <T,>(fn: () => Promise<T>) => fn(),
201
+ prover: null,
202
+ signerAccountId: null,
203
+ signerConnected: null,
204
+ });
205
+
206
+ // Storage-map value drives `count`. `Word.toU64s()` already resolves to
207
+ // [0n, 0n, 0n, 0n] via the SDK proxy stub above, so `loadCount()` always
208
+ // returns 0 → `previousCount === latest`. The loop will keep ticking
209
+ // until the deadline; we only need ONE iteration to elapse, then stop.
210
+ mockGetAccount.mockResolvedValue({
211
+ storage: () => ({ getMapItem: () => ({ toU64s: () => [0n, 0n, 0n, 0n] }) }),
212
+ } as never);
213
+
214
+ vi.useFakeTimers();
215
+ try {
216
+ const { result } = renderHook(() => useIncrementCounter(COUNTER_ADDRESS));
217
+
218
+ // Wait for mount-effect's loadCount to settle.
219
+ await vi.waitFor(() => {
220
+ expect(result.current.count).toBe(0);
221
+ });
222
+
223
+ // Snapshot sync-call count before increment so we measure only what
224
+ // happens during the click + poll iteration.
225
+ expect(sync).not.toHaveBeenCalled();
226
+
227
+ // Trigger increment without awaiting; the call schedules the poll loop.
228
+ let incrementPromise: Promise<void> | undefined;
229
+ await act(async () => {
230
+ incrementPromise = result.current.increment();
231
+ // Let microtasks settle for the requestTransaction await.
232
+ await Promise.resolve();
233
+ });
234
+
235
+ // Advance past one poll interval (2.5s) — exactly one iteration runs.
236
+ await act(async () => {
237
+ await vi.advanceTimersByTimeAsync(NETWORK_POLL_INTERVAL_MS + 50);
238
+ });
239
+
240
+ // The redundant `await sync()` was removed from the poll loop. After
241
+ // one iteration, `useMiden().sync` must not have been called.
242
+ expect(sync).not.toHaveBeenCalled();
243
+
244
+ // requestTransaction was reached — proves the loop actually ran.
245
+ expect(requestTransaction).toHaveBeenCalledTimes(1);
246
+
247
+ // Drain the rest of the deadline so the increment promise resolves
248
+ // cleanly and React unmount can finish.
249
+ await act(async () => {
250
+ await vi.advanceTimersByTimeAsync(NETWORK_POLL_TIMEOUT_MS);
251
+ await incrementPromise;
252
+ });
253
+ } finally {
254
+ vi.useRealTimers();
255
+ }
256
+ });
257
+ });
@@ -1,13 +1,12 @@
1
- import { useMemo, useState, useCallback, useEffect } from "react";
2
- import {
3
- useSyncState,
4
- useAccount,
5
- useImportAccount,
6
- } from "@miden-sdk/react";
7
- import {
8
- useWallet,
9
- Transaction,
10
- } from "@miden-sdk/miden-wallet-adapter";
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import { useMiden, useMidenClient } from "@miden-sdk/react";
3
+ // `useMidenFiWallet()` returns `WalletContextState` (see
4
+ // `@miden-sdk/miden-wallet-adapter-react/dist/MidenFiSignerProvider.d.ts`),
5
+ // which exposes `address`, `connected`, and `requestTransaction` directly
6
+ // at the top level — distinct from the inner `wallet` field on the same
7
+ // return (which is a `Wallet` adapter object, not the address).
8
+ import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
9
+ import { Transaction } from "@miden-sdk/miden-wallet-adapter-base";
11
10
  import {
12
11
  TransactionRequestBuilder,
13
12
  Package,
@@ -16,13 +15,12 @@ import {
16
15
  NoteAssets,
17
16
  NoteMetadata,
18
17
  NoteRecipient,
19
- NoteInputs,
18
+ NoteStorage,
20
19
  NoteTag,
21
20
  NoteType,
22
21
  NoteAttachment,
23
22
  NoteExecutionHint,
24
- OutputNote,
25
- OutputNoteArray,
23
+ NoteArray,
26
24
  AccountId,
27
25
  Felt,
28
26
  FeltArray,
@@ -32,44 +30,80 @@ import { randomWord } from "@/lib/miden";
32
30
  import {
33
31
  COUNTER_SLOT_NAME,
34
32
  EXPLORER_BASE_URL,
35
- NETWORK_SYNC_DELAY_MS,
33
+ NETWORK_POLL_INTERVAL_MS,
34
+ NETWORK_POLL_TIMEOUT_MS,
36
35
  } from "@/config";
37
36
 
38
37
  export function useIncrementCounter(counterAddress: string) {
39
38
  const [error, setError] = useState<string | null>(null);
40
39
  const [isSubmitting, setIsSubmitting] = useState(false);
41
40
  const [isWaiting, setIsWaiting] = useState(false);
41
+ const [count, setCount] = useState<number | null>(null);
42
42
 
43
- const { address: walletAddress, connected, requestTransaction } = useWallet();
44
- const { importAccount } = useImportAccount();
45
- const { account, refetch } = useAccount(counterAddress);
46
- const { sync } = useSyncState();
43
+ const { runExclusive, isReady } = useMiden();
44
+ const {
45
+ address: walletAddress,
46
+ connected: walletConnected,
47
+ requestTransaction,
48
+ } = useMidenFiWallet();
49
+ const client = useMidenClient();
47
50
 
48
- // Import the counter account so the local client tracks it.
49
- // The catch is intentional the account may already be imported.
50
- useEffect(() => {
51
- importAccount({ type: "id", accountId: counterAddress }).catch(() => {});
52
- }, [importAccount, counterAddress]);
51
+ // Fetch the on-chain counter value. Imports the counter account on first
52
+ // call, syncs from the network, and reads the storage map. All WASM calls
53
+ // are serialized via runExclusive to avoid "recursive use of an object"
54
+ // errors. Returns the fetched count (or null if the account is unreachable)
55
+ // and also updates component state.
56
+ const loadCount = useCallback(async (): Promise<number | null> => {
57
+ if (!isReady || !counterAddress) return null;
58
+ return await runExclusive(async () => {
59
+ const counterAccountId = AccountId.fromBech32(counterAddress);
60
+ if (!(await client.getAccount(counterAccountId))) {
61
+ await client.importAccountById(counterAccountId);
62
+ }
63
+ await client.syncState();
64
+
65
+ const account = await client.getAccount(counterAccountId);
66
+ if (!account) {
67
+ setCount(null);
68
+ setError(
69
+ `Counter account not found on-chain (${counterAddress}). Check VITE_MIDEN_COUNTER_ADDRESS / src/config.ts and confirm the counter is deployed on the configured network.`,
70
+ );
71
+ return null;
72
+ }
73
+ const countKey = Word.newFromFelts([
74
+ new Felt(0n),
75
+ new Felt(0n),
76
+ new Felt(0n),
77
+ new Felt(1n),
78
+ ]);
79
+ const value = account
80
+ .storage()
81
+ .getMapItem(COUNTER_SLOT_NAME, countKey);
82
+ // Storage map value is a Word whose first element holds the Felt count.
83
+ const newCount = value ? Number(value.toU64s()[0]) : 0;
84
+ setCount(newCount);
85
+ // Successful fetch — clear any prior unreachable/timeout error.
86
+ setError(null);
87
+ return newCount;
88
+ });
89
+ }, [isReady, client, runExclusive, counterAddress]);
53
90
 
54
- // Read count from StorageMap
55
- const count = useMemo(() => {
56
- if (!account) return null;
57
- const countKey = Word.newFromFelts([
58
- new Felt(0n),
59
- new Felt(0n),
60
- new Felt(0n),
61
- new Felt(1n),
62
- ]);
63
- const value = account.storage().getMapItem(COUNTER_SLOT_NAME, countKey);
64
- return value ? Number(value.toU64s()[3]) : 0;
65
- }, [account]);
91
+ useEffect(() => {
92
+ loadCount().catch((err) => {
93
+ setError(err instanceof Error ? err.message : String(err));
94
+ });
95
+ }, [loadCount]);
66
96
 
67
97
  const increment = useCallback(async () => {
68
- if (!walletAddress || !requestTransaction) return;
98
+ if (!walletAddress) {
99
+ setError(
100
+ "No wallet account available. Connect MidenFi to a testnet account before incrementing.",
101
+ );
102
+ return;
103
+ }
69
104
  setError(null);
70
105
  setIsSubmitting(true);
71
106
  try {
72
- // Load pre-compiled increment-note package
73
107
  const buf = await fetch("/packages/increment_note.masp").then((r) =>
74
108
  r.arrayBuffer(),
75
109
  );
@@ -79,12 +113,10 @@ export function useIncrementCounter(counterAddress: string) {
79
113
  const counterAccountId = AccountId.fromBech32(counterAddress);
80
114
  const walletAccountId = AccountId.fromBech32(walletAddress);
81
115
 
82
- // Build note recipient
83
116
  const serialNum = randomWord();
84
- const inputs = new NoteInputs(new FeltArray());
85
- const recipient = new NoteRecipient(serialNum, noteScript, inputs);
117
+ const storage = new NoteStorage(new FeltArray());
118
+ const recipient = new NoteRecipient(serialNum, noteScript, storage);
86
119
 
87
- // Build note metadata targeting the network counter account
88
120
  const tag = NoteTag.withAccountTarget(counterAccountId);
89
121
  const attachment = NoteAttachment.newNetworkAccountTarget(
90
122
  counterAccountId,
@@ -96,13 +128,14 @@ export function useIncrementCounter(counterAddress: string) {
96
128
  tag,
97
129
  ).withAttachment(attachment);
98
130
 
99
- // Assemble the note and submit via wallet adapter
100
131
  const note = new Note(new NoteAssets(), metadata, recipient);
101
- const outputNote = OutputNote.full(note);
102
132
  const txRequest = new TransactionRequestBuilder()
103
- .withOwnOutputNotes(new OutputNoteArray([outputNote]))
133
+ .withOwnOutputNotes(new NoteArray([note]))
104
134
  .build();
105
135
 
136
+ if (!requestTransaction) {
137
+ throw new Error("Wallet does not support requestTransaction");
138
+ }
106
139
  const tx = Transaction.createCustomTransaction(
107
140
  walletAddress,
108
141
  counterAddress,
@@ -111,18 +144,44 @@ export function useIncrementCounter(counterAddress: string) {
111
144
  await requestTransaction(tx);
112
145
  setIsSubmitting(false);
113
146
 
114
- // Wait for network to process the note, then re-sync
147
+ // Capture the pre-submission count so the poll loop knows what value
148
+ // to wait past. Not stale-safe because React batches state updates and
149
+ // `count` here is the closed-over value from the most recent render —
150
+ // which is exactly what we want: the value the user saw on the button.
151
+ const previousCount = count;
152
+
115
153
  setIsWaiting(true);
116
- await new Promise((r) => setTimeout(r, NETWORK_SYNC_DELAY_MS));
117
- await sync();
118
- await refetch();
154
+ // TODO(0xMiden/miden-client#2111): The React SDK exposes no hook to
155
+ // subscribe to account-state changes driven by the network operator
156
+ // (our counter is Network storage mode). `useWaitForCommit` only
157
+ // watches locally-submitted transactions, and this increment was
158
+ // submitted by the wallet. Until #2111 lands a subscription primitive,
159
+ // we poll the counter's storage map with a bounded timeout.
160
+ const deadline = Date.now() + NETWORK_POLL_TIMEOUT_MS;
161
+ let changed = false;
162
+ while (!changed && Date.now() < deadline) {
163
+ await new Promise((r) => setTimeout(r, NETWORK_POLL_INTERVAL_MS));
164
+ // `loadCount()` calls `client.syncState()` internally inside
165
+ // `runExclusive`; no need for a second `sync()` here.
166
+ const latest = await loadCount();
167
+ changed = latest !== null && latest !== previousCount;
168
+ }
119
169
  setIsWaiting(false);
170
+ if (!changed) {
171
+ // The tx was submitted successfully but the counter hadn't updated
172
+ // by the timeout. This can happen when testnet is slow or the
173
+ // network operator hasn't picked up the note yet. Surface a
174
+ // non-fatal message so the user knows to refresh or retry sync.
175
+ setError(
176
+ `Transaction submitted, but counter update was not observed within ${Math.round(NETWORK_POLL_TIMEOUT_MS / 1000)}s. Refresh or retry sync.`,
177
+ );
178
+ }
120
179
  } catch (err) {
121
180
  setIsSubmitting(false);
122
181
  setIsWaiting(false);
123
182
  setError(err instanceof Error ? err.message : String(err));
124
183
  }
125
- }, [walletAddress, requestTransaction, counterAddress, sync, refetch]);
184
+ }, [walletAddress, requestTransaction, counterAddress, loadCount, count]);
126
185
 
127
186
  return {
128
187
  increment,
@@ -130,7 +189,7 @@ export function useIncrementCounter(counterAddress: string) {
130
189
  isSubmitting,
131
190
  isWaiting,
132
191
  error,
133
- walletConnected: connected,
192
+ walletConnected,
134
193
  explorerUrl: `${EXPLORER_BASE_URL}/account/${counterAddress}`,
135
194
  };
136
195
  }
@@ -1,31 +1,27 @@
1
- import { useMemo, type ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import { MidenProvider } from "@miden-sdk/react";
3
- import {
4
- MidenWalletAdapter,
5
- WalletProvider,
6
- WalletModalProvider,
7
- } from "@miden-sdk/miden-wallet-adapter";
8
- import "@miden-sdk/miden-wallet-adapter/styles.css";
3
+ import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react";
4
+ import { WalletAdapterNetwork } from "@miden-sdk/miden-wallet-adapter-base";
9
5
  import { APP_NAME, MIDEN_RPC_URL, MIDEN_PROVER } from "@/config";
10
6
 
7
+ // MidenFiSignerProvider must wrap MidenProvider — MidenProvider reads
8
+ // SignerContext during initialization (to wire its external-keystore client),
9
+ // so the signer context has to exist before the provider mounts.
11
10
  export function AppProviders({ children }: { children: ReactNode }) {
12
- const wallets = useMemo(
13
- () => [new MidenWalletAdapter({ appName: APP_NAME })],
14
- [],
15
- );
16
-
17
11
  return (
18
- <WalletProvider wallets={wallets} autoConnect>
19
- <WalletModalProvider>
20
- <MidenProvider
21
- config={{ rpcUrl: MIDEN_RPC_URL, prover: MIDEN_PROVER }}
22
- loadingComponent={
23
- <div className="loading">Loading Miden WASM...</div>
24
- }
25
- >
26
- {children}
27
- </MidenProvider>
28
- </WalletModalProvider>
29
- </WalletProvider>
12
+ <MidenFiSignerProvider
13
+ appName={APP_NAME}
14
+ network={WalletAdapterNetwork.Testnet}
15
+ autoConnect
16
+ >
17
+ <MidenProvider
18
+ config={{ rpcUrl: MIDEN_RPC_URL, prover: MIDEN_PROVER }}
19
+ loadingComponent={
20
+ <div className="loading">Loading Miden WASM...</div>
21
+ }
22
+ >
23
+ {children}
24
+ </MidenProvider>
25
+ </MidenFiSignerProvider>
30
26
  );
31
27
  }
@@ -4,7 +4,7 @@ import react from "@vitejs/plugin-react";
4
4
  import { midenVitePlugin } from "@miden-sdk/vite-plugin";
5
5
 
6
6
  export default defineConfig({
7
- plugins: [react(), midenVitePlugin()],
7
+ plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
8
8
  resolve: {
9
9
  dedupe: ["react", "react-dom", "react/jsx-runtime"],
10
10
  alias: {
@@ -17,9 +17,8 @@ export default defineConfig({
17
17
  passWithNoTests: true,
18
18
  server: {
19
19
  deps: {
20
- // The wallet-adapter-reactui sub-package has incorrect exports in package.json.
21
20
  // Tests mock the wallet adapter at the module level, so externalizing is safe.
22
- external: [/@miden-sdk\/miden-wallet-adapter/],
21
+ external: [/@miden-sdk\/miden-wallet-adapter-react/],
23
22
  },
24
23
  },
25
24
  },