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.
- package/cli.js +6 -6
- package/package.json +1 -1
- package/template/.claude/commands/review-security.md +67 -0
- package/template/.claude/hooks/check-artifacts.sh +45 -0
- package/template/.claude/hooks/run-affected-tests.sh +31 -0
- package/template/.claude/hooks/typecheck.sh +27 -0
- package/template/.claude/settings.json +29 -0
- package/template/.claude/settings.local.json +24 -0
- package/template/.claude/skills/frontend-pitfalls/SKILL.md +186 -0
- package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
- package/template/.claude/skills/miden-concepts/SKILL.md +110 -0
- package/template/.claude/skills/react-sdk-patterns/SKILL.md +562 -0
- package/template/.claude/skills/signer-integration/SKILL.md +177 -0
- package/template/.claude/skills/testing-patterns/SKILL.md +338 -0
- package/template/.claude/skills/vite-wasm-setup/SKILL.md +134 -0
- package/template/.claude/skills/web-client-usage/SKILL.md +454 -0
- package/template/.env.example +18 -0
- package/template/.mcp.json +9 -0
- package/template/CLAUDE.md +243 -0
- package/template/README.md +119 -14
- package/template/index.html +1 -1
- package/template/package.json +18 -8
- package/template/public/packages/counter_account.masp +0 -0
- package/template/public/packages/increment_note.masp +0 -0
- package/template/src/App.tsx +6 -59
- package/template/src/__tests__/fixtures/accounts.ts +68 -0
- package/template/src/__tests__/fixtures/index.ts +22 -0
- package/template/src/__tests__/fixtures/notes.ts +33 -0
- package/template/src/__tests__/mocks/miden-sdk-react.ts +261 -0
- package/template/src/__tests__/patterns/README.md +44 -0
- package/template/src/__tests__/patterns/mutation-hook.test.tsx +146 -0
- package/template/src/__tests__/patterns/provider-setup.test.tsx +77 -0
- package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
- package/template/src/{App.css → components/AppContent.css} +9 -9
- package/template/src/components/AppContent.tsx +80 -0
- package/template/src/components/ConfiguredCounter.tsx +48 -0
- package/template/src/components/Counter.css +27 -0
- package/template/src/components/Counter.tsx +16 -0
- package/template/src/components/__tests__/AppContent.test.tsx +274 -0
- package/template/src/components/__tests__/ConfiguredCounter.test.tsx +116 -0
- package/template/src/components/__tests__/Counter.test.tsx +44 -0
- package/template/src/config.ts +41 -0
- package/template/src/hooks/__tests__/useIncrementCounter.test.tsx +257 -0
- package/template/src/hooks/useIncrementCounter.ts +195 -0
- package/template/src/index.css +7 -0
- package/template/src/lib/miden.ts +9 -0
- package/template/src/main.tsx +6 -6
- package/template/src/providers.tsx +27 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.app.json +8 -4
- package/template/tsconfig.node.json +1 -3
- package/template/vite.config.ts +5 -17
- package/template/vitest.config.ts +25 -0
- package/template/vitest.setup.ts +1 -0
- package/template/yarn.lock +1687 -815
- 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";
|