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,80 @@
1
+ import { useMiden, useSyncState } from "@miden-sdk/react";
2
+ import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
3
+ import { WalletReadyState } from "@miden-sdk/miden-wallet-adapter-base";
4
+ import reactLogo from "@/assets/react.svg";
5
+ import midenLogo from "@/assets/miden.svg";
6
+ import viteLogo from "/vite.svg";
7
+ import { Counter } from "@/components/Counter";
8
+ import "./AppContent.css";
9
+
10
+ function WalletButton() {
11
+ // Use the MidenFi-specific hook (not the generic `useSigner()`) so we can
12
+ // gate on `wallet.readyState`. `useSigner().connect()` calls through to the
13
+ // same provider, but at the moment the user clicks the button the adapter
14
+ // may not yet have detected `window.midenWallet` — detection is polled, see
15
+ // `scopePollingDetectionStrategy` in @miden-sdk/miden-wallet-adapter-base.
16
+ // When readyState is NotDetected, MidenFiSignerProvider falls back to
17
+ // `window.open(adapter.url, "_blank")` — the Chrome Web Store URL — which
18
+ // on some platforms redirects to the Play Store. Disabling the button
19
+ // until the extension is detected prevents the fallback from firing.
20
+ const { wallet, connected, connecting, connect, disconnect } =
21
+ useMidenFiWallet();
22
+ const readyState = wallet?.readyState;
23
+ const walletReady =
24
+ readyState === WalletReadyState.Installed ||
25
+ readyState === WalletReadyState.Loadable;
26
+
27
+ if (!walletReady) {
28
+ return <button disabled>Install MidenFi Wallet</button>;
29
+ }
30
+ if (connected) {
31
+ return <button onClick={disconnect}>Disconnect Wallet</button>;
32
+ }
33
+ if (connecting) {
34
+ return <button disabled>Connecting...</button>;
35
+ }
36
+ return <button onClick={connect}>Connect Wallet</button>;
37
+ }
38
+
39
+ export function AppContent() {
40
+ const { isReady, isInitializing, error } = useMiden();
41
+ const { syncHeight } = useSyncState();
42
+
43
+ if (error) {
44
+ return (
45
+ <div className="loading">
46
+ <p>Failed to initialize Miden client</p>
47
+ <p className="error">{error.message}</p>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ if (isInitializing || !isReady) {
53
+ return <div className="loading">Initializing Miden client...</div>;
54
+ }
55
+
56
+ return (
57
+ <>
58
+ <div>
59
+ <a href="https://vite.dev" target="_blank" rel="noreferrer">
60
+ <img src={viteLogo} className="logo" alt="Vite logo" />
61
+ </a>
62
+ <a href="https://react.dev" target="_blank" rel="noreferrer">
63
+ <img src={reactLogo} className="logo react" alt="React logo" />
64
+ </a>
65
+ <a href="https://docs.miden.xyz" target="_blank" rel="noreferrer">
66
+ <img src={midenLogo} className="logo miden" alt="Miden logo" />
67
+ </a>
68
+ </div>
69
+ <h1>Vite + React + Miden</h1>
70
+ <div className="wallet-section">
71
+ <WalletButton />
72
+ </div>
73
+ <Counter />
74
+ <p className="read-the-docs">
75
+ Testnet block: {syncHeight ?? "syncing..."} | Click on the Vite, React,
76
+ and Miden logos to learn more
77
+ </p>
78
+ </>
79
+ );
80
+ }
@@ -0,0 +1,48 @@
1
+ import { useIncrementCounter } from "@/hooks/useIncrementCounter";
2
+ import "./Counter.css";
3
+
4
+ export function ConfiguredCounter({
5
+ counterAddress,
6
+ }: {
7
+ counterAddress: string;
8
+ }) {
9
+ const {
10
+ increment,
11
+ count,
12
+ isSubmitting,
13
+ isWaiting,
14
+ error,
15
+ walletConnected,
16
+ explorerUrl,
17
+ } = useIncrementCounter(counterAddress);
18
+
19
+ const busy = isSubmitting || isWaiting;
20
+ const buttonLabel = isSubmitting
21
+ ? "Submitting..."
22
+ : isWaiting
23
+ ? "Waiting for network..."
24
+ : `count is ${count ?? "..."}`;
25
+
26
+ return (
27
+ <div className="card">
28
+ <button
29
+ className="counter-button"
30
+ onClick={increment}
31
+ disabled={busy || count === null || !walletConnected}
32
+ >
33
+ {buttonLabel}
34
+ </button>
35
+ <p>
36
+ <a
37
+ href={explorerUrl}
38
+ target="_blank"
39
+ rel="noreferrer"
40
+ className="account-id"
41
+ >
42
+ Counter: {counterAddress}
43
+ </a>
44
+ </p>
45
+ {error && <p className="error">{error}</p>}
46
+ </div>
47
+ );
48
+ }
@@ -0,0 +1,27 @@
1
+ .card {
2
+ padding: 2em;
3
+ }
4
+
5
+ .counter-button {
6
+ font-size: 1.2em;
7
+ padding: 0.8em 1.6em;
8
+ }
9
+
10
+ .account-id {
11
+ font-size: 0.8em;
12
+ color: #888;
13
+ font-family: monospace;
14
+ margin-top: 0.5rem;
15
+ text-decoration: none;
16
+ }
17
+
18
+ .account-id:hover {
19
+ text-decoration: underline;
20
+ }
21
+
22
+ .error {
23
+ color: #ff4444;
24
+ font-size: 0.9em;
25
+ margin-top: 0.5rem;
26
+ word-break: break-word;
27
+ }
@@ -0,0 +1,16 @@
1
+ import { COUNTER_ADDRESS } from "@/config";
2
+ import { ConfiguredCounter } from "./ConfiguredCounter";
3
+
4
+ export function Counter() {
5
+ if (!COUNTER_ADDRESS) {
6
+ return (
7
+ <div className="card">
8
+ <p>
9
+ Counter address not configured — see README for deployment
10
+ instructions.
11
+ </p>
12
+ </div>
13
+ );
14
+ }
15
+ return <ConfiguredCounter counterAddress={COUNTER_ADDRESS} />;
16
+ }
@@ -0,0 +1,274 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { vi, describe, it, expect, beforeEach } from "vitest";
3
+
4
+ vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
5
+ vi.mock("@miden-sdk/miden-wallet-adapter-react", () => ({
6
+ useMidenFiWallet: vi.fn(() => ({
7
+ autoConnect: false,
8
+ wallets: [],
9
+ wallet: null,
10
+ address: null,
11
+ publicKey: null,
12
+ connected: false,
13
+ connecting: false,
14
+ disconnecting: false,
15
+ select: vi.fn(),
16
+ connect: vi.fn(async () => undefined),
17
+ disconnect: vi.fn(async () => undefined),
18
+ requestTransaction: vi.fn(async () => "0xtx"),
19
+ requestAssets: undefined,
20
+ requestPrivateNotes: undefined,
21
+ signBytes: undefined,
22
+ importPrivateNote: undefined,
23
+ requestConsumableNotes: undefined,
24
+ waitForTransaction: undefined,
25
+ requestSend: undefined,
26
+ requestConsume: undefined,
27
+ createAccount: undefined,
28
+ })),
29
+ }));
30
+ vi.mock("@miden-sdk/miden-wallet-adapter-base", () => ({
31
+ WalletReadyState: {
32
+ Installed: "Installed",
33
+ NotDetected: "NotDetected",
34
+ Loadable: "Loadable",
35
+ Unsupported: "Unsupported",
36
+ },
37
+ }));
38
+ vi.mock("@/components/Counter", () => ({
39
+ Counter: () => <div data-testid="counter">Counter Mock</div>,
40
+ }));
41
+
42
+ import { useMiden, useSyncState } from "@miden-sdk/react";
43
+ import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
44
+ import userEvent from "@testing-library/user-event";
45
+ import { AppContent } from "../AppContent";
46
+
47
+ type WalletState = ReturnType<typeof useMidenFiWallet>;
48
+ type WalletInner = NonNullable<WalletState["wallet"]>;
49
+
50
+ function walletState(
51
+ overrides: Partial<{
52
+ readyState: "Installed" | "NotDetected" | "Loadable" | "Unsupported";
53
+ connected: boolean;
54
+ connecting: boolean;
55
+ address: string | null;
56
+ connect: () => Promise<void>;
57
+ disconnect: () => Promise<void>;
58
+ requestTransaction: WalletState["requestTransaction"];
59
+ }> = {},
60
+ ): WalletState {
61
+ const {
62
+ readyState = "Installed",
63
+ connected = false,
64
+ connecting = false,
65
+ address = connected ? "mtst1arwk88k8smzcq5p30upr6eerw5npmnyz" : null,
66
+ connect = vi.fn(async () => undefined),
67
+ disconnect = vi.fn(async () => undefined),
68
+ requestTransaction = vi.fn(async () => "0xtx"),
69
+ } = overrides;
70
+ // Build a shape that satisfies WalletContextState; the inner `Wallet`
71
+ // (`{ adapter, readyState }`) shape requires an Adapter, which we stub
72
+ // with a structural cast since the component only reads `readyState`.
73
+ const innerWallet = {
74
+ adapter: {} as WalletInner["adapter"],
75
+ readyState,
76
+ } as WalletInner;
77
+ return {
78
+ autoConnect: false,
79
+ wallets: [innerWallet],
80
+ wallet: innerWallet,
81
+ address,
82
+ publicKey: null,
83
+ connected,
84
+ connecting,
85
+ disconnecting: false,
86
+ select: vi.fn(),
87
+ connect,
88
+ disconnect,
89
+ requestTransaction,
90
+ requestAssets: undefined,
91
+ requestPrivateNotes: undefined,
92
+ signBytes: undefined,
93
+ importPrivateNote: undefined,
94
+ requestConsumableNotes: undefined,
95
+ waitForTransaction: undefined,
96
+ requestSend: undefined,
97
+ requestConsume: undefined,
98
+ createAccount: undefined,
99
+ };
100
+ }
101
+
102
+ const midenReady = {
103
+ client: null,
104
+ isReady: true,
105
+ isInitializing: false,
106
+ error: null,
107
+ sync: vi.fn(),
108
+ runExclusive: vi.fn(),
109
+ prover: null,
110
+ signerAccountId: null,
111
+ signerConnected: null,
112
+ };
113
+
114
+ describe("AppContent", () => {
115
+ beforeEach(() => {
116
+ vi.resetAllMocks();
117
+ // Restore default ready state after any test that overrides useMiden
118
+ vi.mocked(useMiden).mockReturnValue(midenReady);
119
+ vi.mocked(useSyncState).mockReturnValue({
120
+ syncHeight: 12345,
121
+ isSyncing: false,
122
+ lastSyncTime: Date.now(),
123
+ error: null,
124
+ sync: vi.fn(),
125
+ });
126
+ vi.mocked(useMidenFiWallet).mockReturnValue(
127
+ walletState({ readyState: "NotDetected" }),
128
+ );
129
+ });
130
+
131
+ it("renders main content when Miden is ready", () => {
132
+ render(<AppContent />);
133
+
134
+ expect(screen.getByText("Vite + React + Miden")).toBeInTheDocument();
135
+ expect(screen.getByAltText("Vite logo")).toBeInTheDocument();
136
+ expect(screen.getByAltText("React logo")).toBeInTheDocument();
137
+ expect(screen.getByAltText("Miden logo")).toBeInTheDocument();
138
+ expect(screen.getByTestId("counter")).toBeInTheDocument();
139
+ });
140
+
141
+ it("shows sync height from testnet", () => {
142
+ render(<AppContent />);
143
+ expect(screen.getByText(/Testnet block: 12345/)).toBeInTheDocument();
144
+ });
145
+
146
+ it("shows syncing indicator when syncHeight is null", () => {
147
+ vi.mocked(useSyncState).mockReturnValue({
148
+ syncHeight: null as unknown as number,
149
+ isSyncing: true,
150
+ lastSyncTime: null,
151
+ error: null,
152
+ sync: vi.fn(),
153
+ });
154
+
155
+ render(<AppContent />);
156
+ expect(screen.getByText(/syncing\.\.\./)).toBeInTheDocument();
157
+ });
158
+
159
+ it("shows loading message during initialization", () => {
160
+ vi.mocked(useMiden).mockReturnValue({
161
+ client: null,
162
+ isReady: false,
163
+ isInitializing: true,
164
+ error: null,
165
+ sync: vi.fn(),
166
+ runExclusive: vi.fn(),
167
+ prover: null,
168
+ signerAccountId: null,
169
+ signerConnected: null,
170
+ });
171
+
172
+ render(<AppContent />);
173
+ expect(
174
+ screen.getByText("Initializing Miden client..."),
175
+ ).toBeInTheDocument();
176
+ expect(screen.queryByText("Vite + React + Miden")).not.toBeInTheDocument();
177
+ });
178
+
179
+ it("shows disabled install-wallet button when extension is not detected", () => {
180
+ render(<AppContent />);
181
+ const button = screen.getByRole("button", { name: "Install MidenFi Wallet" });
182
+ expect(button).toBeDisabled();
183
+ });
184
+
185
+ it("shows connect button when wallet is installed and disconnected", () => {
186
+ vi.mocked(useMidenFiWallet).mockReturnValue(
187
+ walletState({ readyState: "Installed", connected: false }),
188
+ );
189
+
190
+ render(<AppContent />);
191
+ expect(
192
+ screen.getByRole("button", { name: "Connect Wallet" }),
193
+ ).toBeEnabled();
194
+ });
195
+
196
+ it("shows disconnect button when wallet is connected", () => {
197
+ vi.mocked(useMidenFiWallet).mockReturnValue(
198
+ walletState({ readyState: "Installed", connected: true }),
199
+ );
200
+
201
+ render(<AppContent />);
202
+ expect(
203
+ screen.getByRole("button", { name: "Disconnect Wallet" }),
204
+ ).toBeInTheDocument();
205
+ });
206
+
207
+ it("shows connecting state while the wallet request is in flight", () => {
208
+ vi.mocked(useMidenFiWallet).mockReturnValue(
209
+ walletState({
210
+ readyState: "Installed",
211
+ connected: false,
212
+ connecting: true,
213
+ }),
214
+ );
215
+
216
+ render(<AppContent />);
217
+ const button = screen.getByRole("button", { name: /Connecting/ });
218
+ expect(button).toBeDisabled();
219
+ });
220
+
221
+ it("calls connect on wallet button click", async () => {
222
+ const mockConnect = vi.fn(async () => undefined);
223
+ vi.mocked(useMidenFiWallet).mockReturnValue(
224
+ walletState({
225
+ readyState: "Installed",
226
+ connected: false,
227
+ connect: mockConnect,
228
+ }),
229
+ );
230
+
231
+ render(<AppContent />);
232
+ const user = userEvent.setup();
233
+ await user.click(screen.getByRole("button", { name: "Connect Wallet" }));
234
+ expect(mockConnect).toHaveBeenCalledOnce();
235
+ });
236
+
237
+ it("calls disconnect on wallet button click", async () => {
238
+ const mockDisconnect = vi.fn(async () => undefined);
239
+ vi.mocked(useMidenFiWallet).mockReturnValue(
240
+ walletState({
241
+ readyState: "Installed",
242
+ connected: true,
243
+ disconnect: mockDisconnect,
244
+ }),
245
+ );
246
+
247
+ render(<AppContent />);
248
+ const user = userEvent.setup();
249
+ await user.click(
250
+ screen.getByRole("button", { name: "Disconnect Wallet" }),
251
+ );
252
+ expect(mockDisconnect).toHaveBeenCalledOnce();
253
+ });
254
+
255
+ it("shows error message on initialization failure", () => {
256
+ vi.mocked(useMiden).mockReturnValue({
257
+ client: null,
258
+ isReady: false,
259
+ isInitializing: false,
260
+ error: new Error("WASM failed to load"),
261
+ sync: vi.fn(),
262
+ runExclusive: vi.fn(),
263
+ prover: null,
264
+ signerAccountId: null,
265
+ signerConnected: null,
266
+ });
267
+
268
+ render(<AppContent />);
269
+ expect(
270
+ screen.getByText("Failed to initialize Miden client"),
271
+ ).toBeInTheDocument();
272
+ expect(screen.getByText("WASM failed to load")).toBeInTheDocument();
273
+ });
274
+ });
@@ -0,0 +1,116 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { vi, describe, it, expect, beforeEach } from "vitest";
4
+
5
+ vi.mock("@/hooks/useIncrementCounter", () => ({
6
+ useIncrementCounter: vi.fn(),
7
+ }));
8
+
9
+ import { useIncrementCounter } from "@/hooks/useIncrementCounter";
10
+ import { ConfiguredCounter } from "../ConfiguredCounter";
11
+
12
+ const FIXTURE_ADDRESS = "0xdeadbeef00000001";
13
+
14
+ const defaultHookReturn = {
15
+ increment: vi.fn(),
16
+ count: 42,
17
+ isSubmitting: false,
18
+ isWaiting: false,
19
+ error: null,
20
+ walletConnected: true,
21
+ explorerUrl: `https://testnet.midenscan.com/account/${FIXTURE_ADDRESS}`,
22
+ };
23
+
24
+ describe("ConfiguredCounter", () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ vi.mocked(useIncrementCounter).mockReturnValue(defaultHookReturn);
28
+ });
29
+
30
+ it("displays the current count on the button", () => {
31
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
32
+ expect(
33
+ screen.getByRole("button", { name: "count is 42" }),
34
+ ).toBeInTheDocument();
35
+ });
36
+
37
+ it("calls increment on button click", async () => {
38
+ const mockIncrement = vi.fn();
39
+ vi.mocked(useIncrementCounter).mockReturnValue({
40
+ ...defaultHookReturn,
41
+ increment: mockIncrement,
42
+ });
43
+
44
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
45
+ const user = userEvent.setup();
46
+ await user.click(screen.getByRole("button", { name: "count is 42" }));
47
+ expect(mockIncrement).toHaveBeenCalledOnce();
48
+ });
49
+
50
+ it("shows submitting state", () => {
51
+ vi.mocked(useIncrementCounter).mockReturnValue({
52
+ ...defaultHookReturn,
53
+ isSubmitting: true,
54
+ });
55
+
56
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
57
+ const button = screen.getByRole("button", { name: "Submitting..." });
58
+ expect(button).toBeDisabled();
59
+ });
60
+
61
+ it("shows waiting for network state", () => {
62
+ vi.mocked(useIncrementCounter).mockReturnValue({
63
+ ...defaultHookReturn,
64
+ isWaiting: true,
65
+ });
66
+
67
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
68
+ const button = screen.getByRole("button", {
69
+ name: "Waiting for network...",
70
+ });
71
+ expect(button).toBeDisabled();
72
+ });
73
+
74
+ it("disables button when wallet not connected", () => {
75
+ vi.mocked(useIncrementCounter).mockReturnValue({
76
+ ...defaultHookReturn,
77
+ walletConnected: false,
78
+ });
79
+
80
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
81
+ expect(screen.getByRole("button")).toBeDisabled();
82
+ });
83
+
84
+ it("disables button when count is loading (null)", () => {
85
+ vi.mocked(useIncrementCounter).mockReturnValue({
86
+ ...defaultHookReturn,
87
+ count: null,
88
+ });
89
+
90
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
91
+ const button = screen.getByRole("button", { name: "count is ..." });
92
+ expect(button).toBeDisabled();
93
+ });
94
+
95
+ it("displays error message", () => {
96
+ vi.mocked(useIncrementCounter).mockReturnValue({
97
+ ...defaultHookReturn,
98
+ error: "Transaction failed: insufficient funds",
99
+ });
100
+
101
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
102
+ expect(
103
+ screen.getByText("Transaction failed: insufficient funds"),
104
+ ).toBeInTheDocument();
105
+ });
106
+
107
+ it("links to explorer with counter address", () => {
108
+ render(<ConfiguredCounter counterAddress={FIXTURE_ADDRESS} />);
109
+ const link = screen.getByRole("link");
110
+ expect(link).toHaveAttribute(
111
+ "href",
112
+ `https://testnet.midenscan.com/account/${FIXTURE_ADDRESS}`,
113
+ );
114
+ expect(link).toHaveAttribute("target", "_blank");
115
+ });
116
+ });
@@ -0,0 +1,44 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { vi, describe, it, expect, beforeEach } from "vitest";
3
+
4
+ vi.mock("@/components/ConfiguredCounter", () => ({
5
+ ConfiguredCounter: ({ counterAddress }: { counterAddress: string }) => (
6
+ <div data-testid="configured-counter">{counterAddress}</div>
7
+ ),
8
+ }));
9
+
10
+ vi.mock("@/config", async () => {
11
+ const actual = await vi.importActual<typeof import("@/config")>("@/config");
12
+ return { ...actual };
13
+ });
14
+
15
+ import { Counter } from "../Counter";
16
+ import * as config from "@/config";
17
+
18
+ describe("Counter gate", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ it("renders ConfiguredCounter when COUNTER_ADDRESS is set", () => {
24
+ render(<Counter />);
25
+ const configured = screen.getByTestId("configured-counter");
26
+ expect(configured).toBeInTheDocument();
27
+ expect(configured).toHaveTextContent(config.COUNTER_ADDRESS!);
28
+ });
29
+
30
+ it("shows not-configured message when COUNTER_ADDRESS is null", () => {
31
+ // The env-wired resolver in `config.ts` returns `null` when
32
+ // `VITE_MIDEN_COUNTER_ADDRESS=""` (explicit empty string). Simulate that
33
+ // by overriding the module's exported value directly.
34
+ vi.spyOn(config, "COUNTER_ADDRESS", "get").mockReturnValue(null);
35
+
36
+ render(<Counter />);
37
+ expect(
38
+ screen.getByText(/counter address not configured/i),
39
+ ).toBeInTheDocument();
40
+ expect(
41
+ screen.queryByTestId("configured-counter"),
42
+ ).not.toBeInTheDocument();
43
+ });
44
+ });
@@ -0,0 +1,41 @@
1
+ // Network counter account deployed on Miden testnet.
2
+ //
3
+ // Resolution rules for `COUNTER_ADDRESS`:
4
+ // - `VITE_MIDEN_COUNTER_ADDRESS` unset (or omitted) → use the live default
5
+ // deployment (the testnet counter the template ships with).
6
+ // - `VITE_MIDEN_COUNTER_ADDRESS=""` (explicit empty string) → unconfigured,
7
+ // `<Counter>` renders the "address not configured" card.
8
+ // - Any other string → that string is used verbatim (e.g. your own deploy).
9
+ const DEFAULT_COUNTER_ADDRESS = "mtst1aqmx7qv6h3y92sqsmunh8uht4ujmfy4j";
10
+ const configuredCounterAddress: string | undefined =
11
+ import.meta.env.VITE_MIDEN_COUNTER_ADDRESS;
12
+
13
+ export const COUNTER_ADDRESS: string | null =
14
+ configuredCounterAddress === ""
15
+ ? null
16
+ : (configuredCounterAddress ?? DEFAULT_COUNTER_ADDRESS);
17
+
18
+ // StorageMap slot name for the counter
19
+ export const COUNTER_SLOT_NAME =
20
+ "miden_counter_account::counter_contract::count_map";
21
+
22
+ // Block explorer base URL
23
+ export const EXPLORER_BASE_URL = "https://testnet.midenscan.com";
24
+
25
+ // Poll interval (ms) while waiting for the network operator to consume an
26
+ // increment note and update the counter's on-chain state.
27
+ export const NETWORK_POLL_INTERVAL_MS = 2_500;
28
+
29
+ // Hard cap (ms) on how long to poll for the post-increment state change before
30
+ // giving up and showing whatever value the counter currently has. Covers
31
+ // ~3 block cycles at testnet's ~3s block time with margin.
32
+ export const NETWORK_POLL_TIMEOUT_MS = 30_000;
33
+
34
+ // Application display name (used by wallet adapter)
35
+ export const APP_NAME = "Miden Template";
36
+
37
+ // Miden SDK configuration — override via environment variables
38
+ export const MIDEN_RPC_URL =
39
+ import.meta.env.VITE_MIDEN_RPC_URL ?? "testnet";
40
+ export const MIDEN_PROVER =
41
+ (import.meta.env.VITE_MIDEN_PROVER as "devnet" | "testnet" | "local") ?? "testnet";