create-miden-app 1.0.4 → 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 (56) hide show
  1. package/cli.js +6 -6
  2. package/package.json +1 -1
  3. package/template/.claude/commands/review-security.md +67 -0
  4. package/template/.claude/hooks/check-artifacts.sh +45 -0
  5. package/template/.claude/hooks/run-affected-tests.sh +31 -0
  6. package/template/.claude/hooks/typecheck.sh +27 -0
  7. package/template/.claude/settings.json +29 -0
  8. package/template/.claude/settings.local.json +24 -0
  9. package/template/.claude/skills/frontend-pitfalls/SKILL.md +186 -0
  10. package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
  11. package/template/.claude/skills/miden-concepts/SKILL.md +110 -0
  12. package/template/.claude/skills/react-sdk-patterns/SKILL.md +562 -0
  13. package/template/.claude/skills/signer-integration/SKILL.md +177 -0
  14. package/template/.claude/skills/testing-patterns/SKILL.md +338 -0
  15. package/template/.claude/skills/vite-wasm-setup/SKILL.md +134 -0
  16. package/template/.claude/skills/web-client-usage/SKILL.md +454 -0
  17. package/template/.env.example +18 -0
  18. package/template/.mcp.json +9 -0
  19. package/template/CLAUDE.md +243 -0
  20. package/template/README.md +119 -14
  21. package/template/index.html +1 -1
  22. package/template/package.json +18 -8
  23. package/template/public/packages/counter_account.masp +0 -0
  24. package/template/public/packages/increment_note.masp +0 -0
  25. package/template/src/App.tsx +6 -59
  26. package/template/src/__tests__/fixtures/accounts.ts +68 -0
  27. package/template/src/__tests__/fixtures/index.ts +22 -0
  28. package/template/src/__tests__/fixtures/notes.ts +33 -0
  29. package/template/src/__tests__/mocks/miden-sdk-react.ts +261 -0
  30. package/template/src/__tests__/patterns/README.md +44 -0
  31. package/template/src/__tests__/patterns/mutation-hook.test.tsx +146 -0
  32. package/template/src/__tests__/patterns/provider-setup.test.tsx +77 -0
  33. package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
  34. package/template/src/{App.css → components/AppContent.css} +9 -9
  35. package/template/src/components/AppContent.tsx +80 -0
  36. package/template/src/components/ConfiguredCounter.tsx +48 -0
  37. package/template/src/components/Counter.css +27 -0
  38. package/template/src/components/Counter.tsx +16 -0
  39. package/template/src/components/__tests__/AppContent.test.tsx +274 -0
  40. package/template/src/components/__tests__/ConfiguredCounter.test.tsx +116 -0
  41. package/template/src/components/__tests__/Counter.test.tsx +44 -0
  42. package/template/src/config.ts +41 -0
  43. package/template/src/hooks/__tests__/useIncrementCounter.test.tsx +257 -0
  44. package/template/src/hooks/useIncrementCounter.ts +195 -0
  45. package/template/src/index.css +7 -0
  46. package/template/src/lib/miden.ts +9 -0
  47. package/template/src/main.tsx +6 -6
  48. package/template/src/providers.tsx +27 -0
  49. package/template/src/vite-env.d.ts +1 -0
  50. package/template/tsconfig.app.json +8 -4
  51. package/template/tsconfig.node.json +1 -3
  52. package/template/vite.config.ts +5 -17
  53. package/template/vitest.config.ts +25 -0
  54. package/template/vitest.setup.ts +1 -0
  55. package/template/yarn.lock +1687 -815
  56. package/template/src/miden/lib/demo.ts +0 -106
@@ -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
+ });
@@ -0,0 +1,195 @@
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";
10
+ import {
11
+ TransactionRequestBuilder,
12
+ Package,
13
+ NoteScript,
14
+ Note,
15
+ NoteAssets,
16
+ NoteMetadata,
17
+ NoteRecipient,
18
+ NoteStorage,
19
+ NoteTag,
20
+ NoteType,
21
+ NoteAttachment,
22
+ NoteExecutionHint,
23
+ NoteArray,
24
+ AccountId,
25
+ Felt,
26
+ FeltArray,
27
+ Word,
28
+ } from "@miden-sdk/miden-sdk";
29
+ import { randomWord } from "@/lib/miden";
30
+ import {
31
+ COUNTER_SLOT_NAME,
32
+ EXPLORER_BASE_URL,
33
+ NETWORK_POLL_INTERVAL_MS,
34
+ NETWORK_POLL_TIMEOUT_MS,
35
+ } from "@/config";
36
+
37
+ export function useIncrementCounter(counterAddress: string) {
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [isSubmitting, setIsSubmitting] = useState(false);
40
+ const [isWaiting, setIsWaiting] = useState(false);
41
+ const [count, setCount] = useState<number | null>(null);
42
+
43
+ const { runExclusive, isReady } = useMiden();
44
+ const {
45
+ address: walletAddress,
46
+ connected: walletConnected,
47
+ requestTransaction,
48
+ } = useMidenFiWallet();
49
+ const client = useMidenClient();
50
+
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]);
90
+
91
+ useEffect(() => {
92
+ loadCount().catch((err) => {
93
+ setError(err instanceof Error ? err.message : String(err));
94
+ });
95
+ }, [loadCount]);
96
+
97
+ const increment = useCallback(async () => {
98
+ if (!walletAddress) {
99
+ setError(
100
+ "No wallet account available. Connect MidenFi to a testnet account before incrementing.",
101
+ );
102
+ return;
103
+ }
104
+ setError(null);
105
+ setIsSubmitting(true);
106
+ try {
107
+ const buf = await fetch("/packages/increment_note.masp").then((r) =>
108
+ r.arrayBuffer(),
109
+ );
110
+ const pkg = Package.deserialize(new Uint8Array(buf));
111
+ const noteScript = NoteScript.fromPackage(pkg);
112
+
113
+ const counterAccountId = AccountId.fromBech32(counterAddress);
114
+ const walletAccountId = AccountId.fromBech32(walletAddress);
115
+
116
+ const serialNum = randomWord();
117
+ const storage = new NoteStorage(new FeltArray());
118
+ const recipient = new NoteRecipient(serialNum, noteScript, storage);
119
+
120
+ const tag = NoteTag.withAccountTarget(counterAccountId);
121
+ const attachment = NoteAttachment.newNetworkAccountTarget(
122
+ counterAccountId,
123
+ NoteExecutionHint.always(),
124
+ );
125
+ const metadata = new NoteMetadata(
126
+ walletAccountId,
127
+ NoteType.Public,
128
+ tag,
129
+ ).withAttachment(attachment);
130
+
131
+ const note = new Note(new NoteAssets(), metadata, recipient);
132
+ const txRequest = new TransactionRequestBuilder()
133
+ .withOwnOutputNotes(new NoteArray([note]))
134
+ .build();
135
+
136
+ if (!requestTransaction) {
137
+ throw new Error("Wallet does not support requestTransaction");
138
+ }
139
+ const tx = Transaction.createCustomTransaction(
140
+ walletAddress,
141
+ counterAddress,
142
+ txRequest,
143
+ );
144
+ await requestTransaction(tx);
145
+ setIsSubmitting(false);
146
+
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
+
153
+ setIsWaiting(true);
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
+ }
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
+ }
179
+ } catch (err) {
180
+ setIsSubmitting(false);
181
+ setIsWaiting(false);
182
+ setError(err instanceof Error ? err.message : String(err));
183
+ }
184
+ }, [walletAddress, requestTransaction, counterAddress, loadCount, count]);
185
+
186
+ return {
187
+ increment,
188
+ count,
189
+ isSubmitting,
190
+ isWaiting,
191
+ error,
192
+ walletConnected,
193
+ explorerUrl: `${EXPLORER_BASE_URL}/account/${counterAddress}`,
194
+ };
195
+ }
@@ -54,6 +54,13 @@ button:focus-visible {
54
54
  outline: 4px auto -webkit-focus-ring-color;
55
55
  }
56
56
 
57
+ #root {
58
+ max-width: 1280px;
59
+ margin: 0 auto;
60
+ padding: 2rem;
61
+ text-align: center;
62
+ }
63
+
57
64
  @media (prefers-color-scheme: light) {
58
65
  :root {
59
66
  color: #213547;
@@ -0,0 +1,9 @@
1
+ import { Felt, Word } from "@miden-sdk/miden-sdk";
2
+
3
+ /** Generate a random 4-felt Word (used as note serial number). */
4
+ export function randomWord(): Word {
5
+ const felts = Array.from({ length: 4 }, () =>
6
+ new Felt(BigInt(Math.floor(Math.random() * 2 ** 32))),
7
+ );
8
+ return Word.newFromFelts(felts);
9
+ }
@@ -1,10 +1,10 @@
1
- import { StrictMode } from 'react'
2
- import { createRoot } from 'react-dom/client'
3
- import './index.css'
4
- import App from './App.tsx'
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App.tsx";
5
5
 
6
- createRoot(document.getElementById('root')!).render(
6
+ createRoot(document.getElementById("root")!).render(
7
7
  <StrictMode>
8
8
  <App />
9
9
  </StrictMode>,
10
- )
10
+ );
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ import { MidenProvider } from "@miden-sdk/react";
3
+ import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react";
4
+ import { WalletAdapterNetwork } from "@miden-sdk/miden-wallet-adapter-base";
5
+ import { APP_NAME, MIDEN_RPC_URL, MIDEN_PROVER } from "@/config";
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.
10
+ export function AppProviders({ children }: { children: ReactNode }) {
11
+ return (
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>
26
+ );
27
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -5,7 +5,7 @@
5
5
  "useDefineForClassFields": true,
6
6
  "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
7
  "module": "ESNext",
8
- "types": ["vite/client"],
8
+ "types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"],
9
9
  "skipLibCheck": true,
10
10
 
11
11
  /* Bundler mode */
@@ -16,13 +16,17 @@
16
16
  "noEmit": true,
17
17
  "jsx": "react-jsx",
18
18
 
19
+ /* Path aliases */
20
+ "baseUrl": ".",
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ },
24
+
19
25
  /* Linting */
20
26
  "strict": true,
21
27
  "noUnusedLocals": true,
22
28
  "noUnusedParameters": true,
23
- "erasableSyntaxOnly": true,
24
- "noFallthroughCasesInSwitch": true,
25
- "noUncheckedSideEffectImports": true
29
+ "noFallthroughCasesInSwitch": true
26
30
  },
27
31
  "include": ["src"]
28
32
  }
@@ -18,9 +18,7 @@
18
18
  "strict": true,
19
19
  "noUnusedLocals": true,
20
20
  "noUnusedParameters": true,
21
- "erasableSyntaxOnly": true,
22
- "noFallthroughCasesInSwitch": true,
23
- "noUncheckedSideEffectImports": true
21
+ "noFallthroughCasesInSwitch": true
24
22
  },
25
23
  "include": ["vite.config.ts"]
26
24
  }
@@ -1,26 +1,14 @@
1
+ import path from "node:path";
1
2
  import { defineConfig } from "vite";
2
3
  import react from "@vitejs/plugin-react";
3
- import wasm from "vite-plugin-wasm";
4
- import topLevelAwait from "vite-plugin-top-level-await";
5
- import path from "path";
4
+ import { midenVitePlugin } from "@miden-sdk/vite-plugin";
6
5
 
7
- // https://vite.dev/config/
8
6
  export default defineConfig({
9
- plugins: [react(), wasm(), topLevelAwait()],
7
+ plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
10
8
  resolve: {
9
+ dedupe: ["react", "react-dom", "react/jsx-runtime"],
11
10
  alias: {
12
- // Help resolve dexie when imported from linked packages
13
- dexie: path.resolve(__dirname, "node_modules/dexie"),
14
- },
15
- },
16
- optimizeDeps: {
17
- exclude: ["@miden-sdk/miden-sdk"],
18
- include: ["dexie"],
19
- },
20
- server: {
21
- headers: {
22
- "Cross-Origin-Opener-Policy": "same-origin",
23
- "Cross-Origin-Embedder-Policy": "require-corp",
11
+ "@": path.resolve(__dirname, "./src"),
24
12
  },
25
13
  },
26
14
  });
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ import { defineConfig } from "vitest/config";
3
+ import react from "@vitejs/plugin-react";
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ resolve: {
8
+ alias: {
9
+ "@": path.resolve(__dirname, "./src"),
10
+ },
11
+ },
12
+ test: {
13
+ environment: "jsdom",
14
+ globals: true,
15
+ setupFiles: ["./vitest.setup.ts"],
16
+ include: ["src/**/*.{test,spec}.{ts,tsx}"],
17
+ passWithNoTests: true,
18
+ server: {
19
+ deps: {
20
+ // Tests mock the wallet adapter at the module level, so externalizing is safe.
21
+ external: [/@miden-sdk\/miden-wallet-adapter-react/],
22
+ },
23
+ },
24
+ },
25
+ });
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";