create-miden-app 1.0.3 → 1.0.6

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 (83) hide show
  1. package/cli.js +6 -6
  2. package/package.json +1 -6
  3. package/template/.claude/hooks/check-artifacts.sh +45 -0
  4. package/template/.claude/hooks/run-affected-tests.sh +31 -0
  5. package/template/.claude/hooks/typecheck.sh +27 -0
  6. package/template/.claude/settings.json +35 -0
  7. package/template/.claude/skills/frontend-pitfalls/SKILL.md +189 -0
  8. package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
  9. package/template/.claude/skills/miden-concepts/SKILL.md +108 -0
  10. package/template/.claude/skills/react-sdk-patterns/SKILL.md +296 -0
  11. package/template/.claude/skills/signer-integration/SKILL.md +158 -0
  12. package/template/.claude/skills/testing-patterns/SKILL.md +177 -0
  13. package/template/.claude/skills/vite-wasm-setup/SKILL.md +128 -0
  14. package/template/.env.example +5 -0
  15. package/template/CLAUDE.md +210 -0
  16. package/template/README.md +53 -14
  17. package/template/create-miden-app/template/.claude/hooks/typecheck.sh +27 -0
  18. package/template/create-miden-app/template/.claude/settings.json +17 -0
  19. package/template/create-miden-app/template/.claude/skills/frontend-pitfalls/SKILL.md +189 -0
  20. package/template/create-miden-app/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
  21. package/template/create-miden-app/template/.claude/skills/miden-concepts/SKILL.md +108 -0
  22. package/template/create-miden-app/template/.claude/skills/react-sdk-patterns/SKILL.md +294 -0
  23. package/template/create-miden-app/template/.claude/skills/signer-integration/SKILL.md +158 -0
  24. package/template/create-miden-app/template/.claude/skills/vite-wasm-setup/SKILL.md +128 -0
  25. package/template/create-miden-app/template/.env.example +5 -0
  26. package/template/create-miden-app/template/CLAUDE.md +116 -0
  27. package/template/create-miden-app/template/README.md +61 -0
  28. package/template/create-miden-app/template/eslint.config.js +23 -0
  29. package/template/create-miden-app/template/index.html +13 -0
  30. package/template/create-miden-app/template/package.json +34 -0
  31. package/template/create-miden-app/template/public/vite.svg +1 -0
  32. package/template/create-miden-app/template/src/App.tsx +10 -0
  33. package/template/create-miden-app/template/src/assets/miden.svg +3 -0
  34. package/template/create-miden-app/template/src/assets/react.svg +1 -0
  35. package/template/{src/App.css → create-miden-app/template/src/components/AppContent.css} +9 -9
  36. package/template/create-miden-app/template/src/components/AppContent.tsx +50 -0
  37. package/template/create-miden-app/template/src/components/Counter.css +27 -0
  38. package/template/create-miden-app/template/src/components/Counter.tsx +45 -0
  39. package/template/create-miden-app/template/src/config.ts +21 -0
  40. package/template/create-miden-app/template/src/hooks/useIncrementCounter.ts +136 -0
  41. package/template/create-miden-app/template/src/index.css +75 -0
  42. package/template/create-miden-app/template/src/lib/miden.ts +9 -0
  43. package/template/create-miden-app/template/src/main.tsx +10 -0
  44. package/template/create-miden-app/template/src/providers.tsx +31 -0
  45. package/template/create-miden-app/template/src/vite-env.d.ts +1 -0
  46. package/template/create-miden-app/template/tsconfig.app.json +32 -0
  47. package/template/create-miden-app/template/tsconfig.json +7 -0
  48. package/template/create-miden-app/template/tsconfig.node.json +24 -0
  49. package/template/create-miden-app/template/vite.config.ts +17 -0
  50. package/template/create-miden-app/template/yarn.lock +1697 -0
  51. package/template/index.html +1 -1
  52. package/template/package.json +17 -8
  53. package/template/public/packages/counter_account.masp +0 -0
  54. package/template/public/packages/increment_note.masp +0 -0
  55. package/template/src/App.tsx +6 -59
  56. package/template/src/__tests__/fixtures/accounts.ts +57 -0
  57. package/template/src/__tests__/fixtures/index.ts +21 -0
  58. package/template/src/__tests__/fixtures/notes.ts +33 -0
  59. package/template/src/__tests__/mocks/miden-sdk-react.ts +244 -0
  60. package/template/src/__tests__/patterns/README.md +44 -0
  61. package/template/src/__tests__/patterns/mutation-hook.test.tsx +146 -0
  62. package/template/src/__tests__/patterns/provider-setup.test.tsx +75 -0
  63. package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
  64. package/template/src/components/AppContent.css +45 -0
  65. package/template/src/components/AppContent.tsx +50 -0
  66. package/template/src/components/Counter.css +27 -0
  67. package/template/src/components/Counter.tsx +45 -0
  68. package/template/src/components/__tests__/AppContent.test.tsx +86 -0
  69. package/template/src/components/__tests__/Counter.test.tsx +114 -0
  70. package/template/src/config.ts +21 -0
  71. package/template/src/hooks/useIncrementCounter.ts +136 -0
  72. package/template/src/index.css +7 -0
  73. package/template/src/lib/miden.ts +9 -0
  74. package/template/src/main.tsx +6 -6
  75. package/template/src/providers.tsx +31 -0
  76. package/template/src/vite-env.d.ts +1 -0
  77. package/template/tsconfig.app.json +8 -4
  78. package/template/tsconfig.node.json +1 -3
  79. package/template/vite.config.ts +5 -17
  80. package/template/vitest.config.ts +26 -0
  81. package/template/vitest.setup.ts +1 -0
  82. package/template/yarn.lock +1318 -799
  83. package/template/src/miden/lib/demo.ts +0 -105
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>miden-frontend</title>
7
+ <title>Miden Template</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -7,28 +7,37 @@
7
7
  "dev": "vite",
8
8
  "build": "tsc -b && vite build",
9
9
  "lint": "eslint .",
10
- "preview": "vite preview"
10
+ "preview": "vite preview",
11
+ "test": "vitest --run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest --run --coverage"
11
14
  },
12
15
  "dependencies": {
13
- "@demox-labs/miden-sdk": "^0.12.3",
14
- "dexie": "^4.2.1",
16
+ "@miden-sdk/miden-wallet-adapter": "0.13.5",
17
+ "@miden-sdk/miden-sdk": "0.13.2",
18
+ "@miden-sdk/react": "0.13.3",
15
19
  "react": "^19.1.1",
16
20
  "react-dom": "^19.1.1"
17
21
  },
18
22
  "devDependencies": {
19
23
  "@eslint/js": "^9.36.0",
24
+ "@miden-sdk/vite-plugin": "0.13.4",
25
+ "@testing-library/dom": "^10.4.1",
26
+ "@testing-library/jest-dom": "^6.9.1",
27
+ "@testing-library/react": "^16.3.2",
28
+ "@testing-library/user-event": "^14.6.1",
20
29
  "@types/node": "^24.6.0",
21
30
  "@types/react": "^19.1.16",
22
31
  "@types/react-dom": "^19.1.9",
23
- "@vitejs/plugin-react": "^5.0.4",
32
+ "@vitejs/plugin-react": "^4.7.0",
24
33
  "eslint": "^9.36.0",
25
34
  "eslint-plugin-react-hooks": "^5.2.0",
26
35
  "eslint-plugin-react-refresh": "^0.4.22",
27
36
  "globals": "^16.4.0",
28
- "typescript": "~5.9.3",
37
+ "jsdom": "^28.1.0",
38
+ "typescript": "~5.7.0",
29
39
  "typescript-eslint": "^8.45.0",
30
- "vite": "^7.1.7",
31
- "vite-plugin-top-level-await": "^1.6.0",
32
- "vite-plugin-wasm": "^3.5.0"
40
+ "vite": "^6.0.0",
41
+ "vitest": "^4.0.18"
33
42
  }
34
43
  }
@@ -1,63 +1,10 @@
1
- import { useState } from "react";
2
- import reactLogo from "./assets/react.svg";
3
- import midenLogo from "./assets/miden.svg";
4
- import viteLogo from "/vite.svg";
5
- import "./App.css";
6
- import { demo } from "./miden/lib/demo";
7
-
8
- function App() {
9
- const [count, setCount] = useState(0);
10
- const [isRunning, setIsRunning] = useState(false);
11
- const [result, setResult] = useState<string>("");
12
-
13
- const handleDemoClick = async () => {
14
- setIsRunning(true);
15
- setResult("");
16
- try {
17
- await demo();
18
- setResult("Demo executed successfully! Check console for output.");
19
- } catch (error) {
20
- setResult(
21
- `Error: ${error instanceof Error ? error.message : String(error)}`
22
- );
23
- } finally {
24
- setIsRunning(false);
25
- }
26
- };
1
+ import { AppProviders } from "@/providers";
2
+ import { AppContent } from "@/components/AppContent";
27
3
 
4
+ export default function App() {
28
5
  return (
29
- <>
30
- <div>
31
- <a href="https://vite.dev" target="_blank">
32
- <img src={viteLogo} className="logo" alt="Vite logo" />
33
- </a>
34
- <a href="https://react.dev" target="_blank">
35
- <img src={reactLogo} className="logo react" alt="React logo" />
36
- </a>
37
- <a href="https://docs.miden.xyz" target="_blank">
38
- <img src={midenLogo} className="logo miden" alt="Miden logo" />
39
- </a>
40
- </div>
41
- <h1>Vite + React + Miden</h1>
42
- <div className="card">
43
- <button onClick={() => setCount((count) => count + 1)}>
44
- count is {count}
45
- </button>
46
- <p>
47
- Edit <code>src/App.tsx</code> and save to test HMR
48
- </p>
49
- </div>
50
- <div className="card">
51
- <button onClick={handleDemoClick} disabled={isRunning}>
52
- {isRunning ? "Running Demo..." : "Run Miden Demo"}
53
- </button>
54
- {result && <p style={{ marginTop: "1rem" }}>{result}</p>}
55
- </div>
56
- <p className="read-the-docs">
57
- Click on the Vite, React, and Miden logos to learn more
58
- </p>
59
- </>
6
+ <AppProviders>
7
+ <AppContent />
8
+ </AppProviders>
60
9
  );
61
10
  }
62
-
63
- export default App;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Realistic account fixtures for Miden frontend tests.
3
+ * Uses bech32 IDs and BigInt amounts matching real SDK shapes.
4
+ */
5
+
6
+ export const WALLET_ID_1 = "mtst1qy35qfqdvpjx2e5zf9hkp4vr";
7
+ export const WALLET_ID_2 = "mtst1qa7k9qjf8dp4x2e5zf9hkp5vr";
8
+ export const FAUCET_ID = "mtst1qx9y8zjf2dp4x2e5zf9hkp3vr";
9
+ export const COUNTER_ID = "mtst1aru8adnrqspgcsr3drk2n990lyc070ll";
10
+
11
+ export const MOCK_WALLET_HEADER = {
12
+ id: WALLET_ID_1,
13
+ nonce: 1n,
14
+ storageCommitment: "0x0000000000000000000000000000000000000000000000000000000000000001",
15
+ };
16
+
17
+ export const MOCK_WALLET_HEADER_2 = {
18
+ id: WALLET_ID_2,
19
+ nonce: 0n,
20
+ storageCommitment: "0x0000000000000000000000000000000000000000000000000000000000000000",
21
+ };
22
+
23
+ export const MOCK_FAUCET_HEADER = {
24
+ id: FAUCET_ID,
25
+ nonce: 5n,
26
+ storageCommitment: "0x0000000000000000000000000000000000000000000000000000000000000005",
27
+ };
28
+
29
+ export const MOCK_ASSET_BALANCE = {
30
+ assetId: FAUCET_ID,
31
+ amount: 1000000000n, // 10.0 tokens with 8 decimals
32
+ symbol: "TEST",
33
+ decimals: 8,
34
+ };
35
+
36
+ export const MOCK_ASSET_BALANCE_EMPTY = {
37
+ assetId: FAUCET_ID,
38
+ amount: 0n,
39
+ symbol: "TEST",
40
+ decimals: 8,
41
+ };
42
+
43
+ export const MOCK_ACCOUNT = {
44
+ id: WALLET_ID_1,
45
+ nonce: 1n,
46
+ bech32id: () => WALLET_ID_1,
47
+ };
48
+
49
+ export const MOCK_ASSET_METADATA = {
50
+ assetId: FAUCET_ID,
51
+ symbol: "TEST",
52
+ decimals: 8,
53
+ };
54
+
55
+ export const MOCK_TRANSACTION_RESULT = {
56
+ transactionId: "0xabc123def456789012345678901234567890123456789012345678901234abcd",
57
+ };
@@ -0,0 +1,21 @@
1
+ export {
2
+ WALLET_ID_1,
3
+ WALLET_ID_2,
4
+ FAUCET_ID,
5
+ COUNTER_ID,
6
+ MOCK_WALLET_HEADER,
7
+ MOCK_WALLET_HEADER_2,
8
+ MOCK_FAUCET_HEADER,
9
+ MOCK_ASSET_BALANCE,
10
+ MOCK_ASSET_BALANCE_EMPTY,
11
+ MOCK_ACCOUNT,
12
+ MOCK_ASSET_METADATA,
13
+ MOCK_TRANSACTION_RESULT,
14
+ } from "./accounts";
15
+
16
+ export {
17
+ MOCK_NOTE_ASSET,
18
+ MOCK_NOTE_SUMMARY,
19
+ MOCK_INPUT_NOTE_RECORD,
20
+ MOCK_CONSUMABLE_NOTE_RECORD,
21
+ } from "./notes";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Realistic note fixtures for Miden frontend tests.
3
+ */
4
+
5
+ import { FAUCET_ID, WALLET_ID_1, WALLET_ID_2 } from "./accounts";
6
+
7
+ export const MOCK_NOTE_ASSET = {
8
+ assetId: FAUCET_ID,
9
+ amount: 500000000n, // 5.0 tokens with 8 decimals
10
+ symbol: "TEST",
11
+ decimals: 8,
12
+ };
13
+
14
+ export const MOCK_NOTE_SUMMARY = {
15
+ id: "0xnote1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
16
+ assets: [MOCK_NOTE_ASSET],
17
+ sender: WALLET_ID_2,
18
+ };
19
+
20
+ export const MOCK_INPUT_NOTE_RECORD = {
21
+ id: () => "0xnote1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
22
+ assets: () => [{ faucetId: () => FAUCET_ID, amount: () => 500000000n }],
23
+ metadata: () => ({
24
+ sender: () => ({ toBech32: () => WALLET_ID_2 }),
25
+ noteType: () => 1,
26
+ }),
27
+ status: () => "committed",
28
+ };
29
+
30
+ export const MOCK_CONSUMABLE_NOTE_RECORD = {
31
+ ...MOCK_INPUT_NOTE_RECORD,
32
+ accountId: WALLET_ID_1,
33
+ };
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Mock module for @miden-sdk/react.
3
+ *
4
+ * Usage in test files:
5
+ *
6
+ * vi.mock('@miden-sdk/react', () => import('@/__tests__/mocks/miden-sdk-react'));
7
+ *
8
+ * Override specific hooks in individual tests:
9
+ *
10
+ * import { useAccounts } from '@miden-sdk/react';
11
+ * vi.mocked(useAccounts).mockReturnValue({ wallets: [], ... });
12
+ */
13
+
14
+ import { vi } from "vitest";
15
+ import {
16
+ MOCK_WALLET_HEADER,
17
+ MOCK_WALLET_HEADER_2,
18
+ MOCK_FAUCET_HEADER,
19
+ MOCK_ACCOUNT,
20
+ MOCK_ASSET_BALANCE,
21
+ MOCK_ASSET_METADATA,
22
+ MOCK_INPUT_NOTE_RECORD,
23
+ MOCK_CONSUMABLE_NOTE_RECORD,
24
+ MOCK_NOTE_SUMMARY,
25
+ MOCK_TRANSACTION_RESULT,
26
+ FAUCET_ID,
27
+ } from "../fixtures";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Query hooks
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export const useAccounts = vi.fn(() => ({
34
+ accounts: [MOCK_WALLET_HEADER, MOCK_WALLET_HEADER_2, MOCK_FAUCET_HEADER],
35
+ wallets: [MOCK_WALLET_HEADER, MOCK_WALLET_HEADER_2],
36
+ faucets: [MOCK_FAUCET_HEADER],
37
+ isLoading: false,
38
+ error: null,
39
+ refetch: vi.fn(),
40
+ }));
41
+
42
+ export const useAccount = vi.fn(() => ({
43
+ account: MOCK_ACCOUNT,
44
+ assets: [MOCK_ASSET_BALANCE],
45
+ isLoading: false,
46
+ error: null,
47
+ refetch: vi.fn(),
48
+ getBalance: vi.fn((assetId: string) =>
49
+ assetId === FAUCET_ID ? MOCK_ASSET_BALANCE.amount : 0n,
50
+ ),
51
+ }));
52
+
53
+ export const useNotes = vi.fn(() => ({
54
+ notes: [MOCK_INPUT_NOTE_RECORD],
55
+ consumableNotes: [MOCK_CONSUMABLE_NOTE_RECORD],
56
+ noteSummaries: [MOCK_NOTE_SUMMARY],
57
+ consumableNoteSummaries: [MOCK_NOTE_SUMMARY],
58
+ isLoading: false,
59
+ error: null,
60
+ refetch: vi.fn(),
61
+ }));
62
+
63
+ export const useSyncState = vi.fn(() => ({
64
+ syncHeight: 12345,
65
+ isSyncing: false,
66
+ lastSyncTime: Date.now(),
67
+ error: null,
68
+ sync: vi.fn(),
69
+ }));
70
+
71
+ export const useAssetMetadata = vi.fn(() => ({
72
+ assetMetadata: new Map([[FAUCET_ID, MOCK_ASSET_METADATA]]),
73
+ }));
74
+
75
+ export const useTransactionHistory = vi.fn(() => ({
76
+ records: [],
77
+ record: null,
78
+ status: null,
79
+ isLoading: false,
80
+ error: null,
81
+ refetch: vi.fn(),
82
+ }));
83
+
84
+ export const useNoteStream = vi.fn(() => ({
85
+ notes: [],
86
+ latest: null,
87
+ markHandled: vi.fn(),
88
+ markAllHandled: vi.fn(),
89
+ snapshot: vi.fn(() => ({ ids: new Set<string>(), timestamp: Date.now() })),
90
+ isLoading: false,
91
+ error: null,
92
+ }));
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Mutation hooks
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function createMutationMock(mutateKey: string) {
99
+ return vi.fn(() => ({
100
+ [mutateKey]: vi.fn(async () => MOCK_TRANSACTION_RESULT),
101
+ result: null,
102
+ isLoading: false,
103
+ stage: "idle" as const,
104
+ error: null,
105
+ reset: vi.fn(),
106
+ }));
107
+ }
108
+
109
+ export const useCreateWallet = vi.fn(() => ({
110
+ createWallet: vi.fn(async () => MOCK_ACCOUNT),
111
+ wallet: null,
112
+ isCreating: false,
113
+ error: null,
114
+ reset: vi.fn(),
115
+ }));
116
+
117
+ export const useCreateFaucet = vi.fn(() => ({
118
+ createFaucet: vi.fn(async () => MOCK_ACCOUNT),
119
+ faucet: null,
120
+ isCreating: false,
121
+ error: null,
122
+ reset: vi.fn(),
123
+ }));
124
+
125
+ export const useSend = createMutationMock("send");
126
+ export const useMultiSend = createMutationMock("sendMany");
127
+ export const useMint = createMutationMock("mint");
128
+ export const useConsume = createMutationMock("consume");
129
+ export const useSwap = createMutationMock("swap");
130
+ export const useTransaction = createMutationMock("execute");
131
+
132
+ export const useImportAccount = vi.fn(() => ({
133
+ importAccount: vi.fn(async () => MOCK_ACCOUNT),
134
+ account: null,
135
+ isImporting: false,
136
+ error: null,
137
+ reset: vi.fn(),
138
+ }));
139
+
140
+ export const useInternalTransfer = vi.fn(() => ({
141
+ transfer: vi.fn(async () => ({
142
+ createTransactionId: "0xtx1",
143
+ consumeTransactionId: "0xtx2",
144
+ noteId: "0xnote1",
145
+ })),
146
+ transferChain: vi.fn(async () => []),
147
+ result: null,
148
+ isLoading: false,
149
+ stage: "idle" as const,
150
+ error: null,
151
+ reset: vi.fn(),
152
+ }));
153
+
154
+ export const useWaitForCommit = vi.fn(() => ({
155
+ waitForCommit: vi.fn(async () => undefined),
156
+ }));
157
+
158
+ export const useWaitForNotes = vi.fn(() => ({
159
+ waitForConsumableNotes: vi.fn(async () => []),
160
+ }));
161
+
162
+ export const useSessionAccount = vi.fn(() => ({
163
+ initialize: vi.fn(async () => undefined),
164
+ sessionAccountId: null,
165
+ isReady: false,
166
+ step: "idle" as const,
167
+ error: null,
168
+ reset: vi.fn(),
169
+ }));
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Provider hooks
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export const useMiden = vi.fn(() => ({
176
+ client: null,
177
+ isReady: true,
178
+ isInitializing: false,
179
+ error: null,
180
+ sync: vi.fn(),
181
+ runExclusive: vi.fn(async <T>(fn: () => Promise<T>) => fn()),
182
+ prover: null,
183
+ signerAccountId: null,
184
+ }));
185
+
186
+ export const useMidenClient = vi.fn(() => ({}));
187
+
188
+ export const useSigner = vi.fn(() => null);
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Components
192
+ // ---------------------------------------------------------------------------
193
+
194
+ export function MidenProvider({ children }: { children: React.ReactNode }) {
195
+ return children;
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Utility functions
200
+ // ---------------------------------------------------------------------------
201
+
202
+ export const formatAssetAmount = vi.fn(
203
+ (amount: bigint, decimals = 8) =>
204
+ (Number(amount) / 10 ** decimals).toFixed(decimals > 4 ? 4 : decimals),
205
+ );
206
+
207
+ export const parseAssetAmount = vi.fn(
208
+ (input: string, decimals = 8) => BigInt(Math.round(parseFloat(input) * 10 ** decimals)),
209
+ );
210
+
211
+ export const toBech32AccountId = vi.fn((id: string) => id);
212
+ export const normalizeAccountId = vi.fn((id: string) => id);
213
+ export const accountIdsEqual = vi.fn((a: string, b: string) => a === b);
214
+
215
+ export const getNoteSummary = vi.fn(() => MOCK_NOTE_SUMMARY);
216
+ export const formatNoteSummary = vi.fn(() => "5.0 TEST");
217
+
218
+ export const readNoteAttachment = vi.fn(() => null);
219
+ export const createNoteAttachment = vi.fn(() => ({}));
220
+
221
+ export const clearMidenStorage = vi.fn(async () => undefined);
222
+ export const migrateStorage = vi.fn(async () => true);
223
+ export const createMidenStorage = vi.fn(() => ({
224
+ get: vi.fn(() => null),
225
+ set: vi.fn(),
226
+ remove: vi.fn(),
227
+ clear: vi.fn(),
228
+ }));
229
+
230
+ export const wrapWasmError = vi.fn((e: unknown) =>
231
+ e instanceof Error ? e : new Error(String(e)),
232
+ );
233
+ export const waitForWalletDetection = vi.fn(async () => undefined);
234
+
235
+ export const SignerContext = {
236
+ Provider: vi.fn(({ children }: { children: React.ReactNode }) => children),
237
+ };
238
+
239
+ export const bytesToBigInt = vi.fn(() => 0n);
240
+ export const bigIntToBytes = vi.fn(() => new Uint8Array());
241
+ export const concatBytes = vi.fn((...arrays: Uint8Array[]) => {
242
+ const len = arrays.reduce((acc, a) => acc + a.length, 0);
243
+ return new Uint8Array(len);
244
+ });
@@ -0,0 +1,44 @@
1
+ # Test Patterns
2
+
3
+ Copy-adaptable test patterns for Miden frontend components.
4
+
5
+ ## Available Patterns
6
+
7
+ ### `provider-setup.test.tsx`
8
+ Starting point for any component that uses Miden hooks. Shows mock setup, `vi.mocked()` overrides, and testing ready/loading/error states.
9
+
10
+ ### `query-hook.test.tsx`
11
+ Pattern for components displaying data from query hooks (`useAccounts`, `useNotes`, etc.). Shows loading → data → error → empty state testing.
12
+
13
+ ### `mutation-hook.test.tsx`
14
+ Pattern for components performing transactions (`useSend`, `useMint`, etc.). Shows idle → stage progression → success → error testing, plus argument verification.
15
+
16
+ ## How to Use
17
+
18
+ 1. Copy the closest pattern to your component's `__tests__/` directory
19
+ 2. Rename the test file to match your component
20
+ 3. Replace the example component with your actual component import
21
+ 4. Adapt assertions to your component's specific behavior
22
+ 5. Keep the `vi.mock(...)` and `beforeEach(vi.clearAllMocks)` boilerplate
23
+
24
+ ## Mock Setup
25
+
26
+ All patterns use the shared mock factory:
27
+
28
+ ```tsx
29
+ vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
30
+ ```
31
+
32
+ Override specific hooks per-test:
33
+
34
+ ```tsx
35
+ vi.mocked(useAccounts).mockReturnValue({ wallets: [], ... });
36
+ ```
37
+
38
+ ## Fixtures
39
+
40
+ Realistic test data is in `src/__tests__/fixtures/`. Import what you need:
41
+
42
+ ```tsx
43
+ import { WALLET_ID_1, MOCK_ASSET_BALANCE } from "@/__tests__/fixtures";
44
+ ```
@@ -0,0 +1,146 @@
1
+ /**
2
+ * TEST PATTERN: Mutation Hook Component
3
+ *
4
+ * Shows how to test a component that performs Miden transactions via mutation hooks.
5
+ * Covers: idle state, transaction stages (executing/proving/submitting), success, and error.
6
+ *
7
+ * Key concepts:
8
+ * - Mock mutation functions to resolve or reject
9
+ * - Test transaction stage display during multi-step operations
10
+ * - Test button disable during loading to prevent double-submit
11
+ * - Test error display and reset
12
+ */
13
+
14
+ import { render, screen } from "@testing-library/react";
15
+ import userEvent from "@testing-library/user-event";
16
+ import { vi, describe, it, expect, beforeEach } from "vitest";
17
+
18
+ vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
19
+
20
+ import { useSend } from "@miden-sdk/react";
21
+ import { WALLET_ID_1, WALLET_ID_2, FAUCET_ID } from "@/__tests__/fixtures";
22
+
23
+ // Example component: a send token form — common Miden UI pattern
24
+ function SendTokenForm() {
25
+ const { send, isLoading, stage, error, reset } = useSend();
26
+
27
+ const handleSend = async () => {
28
+ try {
29
+ await send({
30
+ from: WALLET_ID_1,
31
+ to: WALLET_ID_2,
32
+ assetId: FAUCET_ID,
33
+ amount: 100000000n, // 1.0 token
34
+ });
35
+ } catch {
36
+ // Error is captured in the hook's error state
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div>
42
+ <button onClick={handleSend} disabled={isLoading}>
43
+ {isLoading ? `${stage}...` : "Send 1.0 TEST"}
44
+ </button>
45
+ {stage === "complete" && <p>Transaction submitted!</p>}
46
+ {error && (
47
+ <div>
48
+ <p role="alert">Send failed: {error.message}</p>
49
+ <button onClick={reset}>Dismiss</button>
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ describe("Mutation Hook Pattern", () => {
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ });
60
+
61
+ // Default idle state — send button should be enabled
62
+ it("renders idle state with enabled send button", () => {
63
+ render(<SendTokenForm />);
64
+ const button = screen.getByRole("button", { name: "Send 1.0 TEST" });
65
+ expect(button).toBeEnabled();
66
+ });
67
+
68
+ // Simulate transaction in progress — button disabled, stage shown
69
+ it("shows transaction stage and disables button during send", () => {
70
+ vi.mocked(useSend).mockReturnValue({
71
+ send: vi.fn(),
72
+ result: null,
73
+ isLoading: true,
74
+ stage: "proving" as const,
75
+ error: null,
76
+ reset: vi.fn(),
77
+ });
78
+
79
+ render(<SendTokenForm />);
80
+ const button = screen.getByRole("button", { name: "proving..." });
81
+ expect(button).toBeDisabled();
82
+ });
83
+
84
+ // Simulate completed transaction — success message shown
85
+ it("shows success message after transaction completes", () => {
86
+ vi.mocked(useSend).mockReturnValue({
87
+ send: vi.fn(),
88
+ result: { transactionId: "0xabc123" },
89
+ isLoading: false,
90
+ stage: "complete" as const,
91
+ error: null,
92
+ reset: vi.fn(),
93
+ });
94
+
95
+ render(<SendTokenForm />);
96
+ expect(screen.getByText("Transaction submitted!")).toBeInTheDocument();
97
+ });
98
+
99
+ // Simulate error — error message shown with dismiss button
100
+ it("shows error with dismiss button on failure", async () => {
101
+ const mockReset = vi.fn();
102
+ vi.mocked(useSend).mockReturnValue({
103
+ send: vi.fn().mockRejectedValue(new Error("Insufficient balance")),
104
+ result: null,
105
+ isLoading: false,
106
+ stage: "idle" as const,
107
+ error: new Error("Insufficient balance"),
108
+ reset: mockReset,
109
+ });
110
+
111
+ render(<SendTokenForm />);
112
+
113
+ // Error should be visible
114
+ expect(screen.getByRole("alert")).toHaveTextContent("Insufficient balance");
115
+
116
+ // Dismiss should call reset
117
+ const user = userEvent.setup();
118
+ await user.click(screen.getByRole("button", { name: "Dismiss" }));
119
+ expect(mockReset).toHaveBeenCalledOnce();
120
+ });
121
+
122
+ // Test the actual send call — verify correct arguments
123
+ it("calls send with correct arguments on click", async () => {
124
+ const mockSend = vi.fn(async () => ({ transactionId: "0xtx" }));
125
+ vi.mocked(useSend).mockReturnValue({
126
+ send: mockSend,
127
+ result: null,
128
+ isLoading: false,
129
+ stage: "idle" as const,
130
+ error: null,
131
+ reset: vi.fn(),
132
+ });
133
+
134
+ render(<SendTokenForm />);
135
+
136
+ const user = userEvent.setup();
137
+ await user.click(screen.getByRole("button", { name: "Send 1.0 TEST" }));
138
+
139
+ expect(mockSend).toHaveBeenCalledWith({
140
+ from: WALLET_ID_1,
141
+ to: WALLET_ID_2,
142
+ assetId: FAUCET_ID,
143
+ amount: 100000000n,
144
+ });
145
+ });
146
+ });