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.
- package/cli.js +6 -6
- package/package.json +1 -6
- 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 +35 -0
- package/template/.claude/skills/frontend-pitfalls/SKILL.md +189 -0
- package/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
- package/template/.claude/skills/miden-concepts/SKILL.md +108 -0
- package/template/.claude/skills/react-sdk-patterns/SKILL.md +296 -0
- package/template/.claude/skills/signer-integration/SKILL.md +158 -0
- package/template/.claude/skills/testing-patterns/SKILL.md +177 -0
- package/template/.claude/skills/vite-wasm-setup/SKILL.md +128 -0
- package/template/.env.example +5 -0
- package/template/CLAUDE.md +210 -0
- package/template/README.md +53 -14
- package/template/create-miden-app/template/.claude/hooks/typecheck.sh +27 -0
- package/template/create-miden-app/template/.claude/settings.json +17 -0
- package/template/create-miden-app/template/.claude/skills/frontend-pitfalls/SKILL.md +189 -0
- package/template/create-miden-app/template/.claude/skills/frontend-source-guide/SKILL.md +163 -0
- package/template/create-miden-app/template/.claude/skills/miden-concepts/SKILL.md +108 -0
- package/template/create-miden-app/template/.claude/skills/react-sdk-patterns/SKILL.md +294 -0
- package/template/create-miden-app/template/.claude/skills/signer-integration/SKILL.md +158 -0
- package/template/create-miden-app/template/.claude/skills/vite-wasm-setup/SKILL.md +128 -0
- package/template/create-miden-app/template/.env.example +5 -0
- package/template/create-miden-app/template/CLAUDE.md +116 -0
- package/template/create-miden-app/template/README.md +61 -0
- package/template/create-miden-app/template/eslint.config.js +23 -0
- package/template/create-miden-app/template/index.html +13 -0
- package/template/create-miden-app/template/package.json +34 -0
- package/template/create-miden-app/template/public/vite.svg +1 -0
- package/template/create-miden-app/template/src/App.tsx +10 -0
- package/template/create-miden-app/template/src/assets/miden.svg +3 -0
- package/template/create-miden-app/template/src/assets/react.svg +1 -0
- package/template/{src/App.css → create-miden-app/template/src/components/AppContent.css} +9 -9
- package/template/create-miden-app/template/src/components/AppContent.tsx +50 -0
- package/template/create-miden-app/template/src/components/Counter.css +27 -0
- package/template/create-miden-app/template/src/components/Counter.tsx +45 -0
- package/template/create-miden-app/template/src/config.ts +21 -0
- package/template/create-miden-app/template/src/hooks/useIncrementCounter.ts +136 -0
- package/template/create-miden-app/template/src/index.css +75 -0
- package/template/create-miden-app/template/src/lib/miden.ts +9 -0
- package/template/create-miden-app/template/src/main.tsx +10 -0
- package/template/create-miden-app/template/src/providers.tsx +31 -0
- package/template/create-miden-app/template/src/vite-env.d.ts +1 -0
- package/template/create-miden-app/template/tsconfig.app.json +32 -0
- package/template/create-miden-app/template/tsconfig.json +7 -0
- package/template/create-miden-app/template/tsconfig.node.json +24 -0
- package/template/create-miden-app/template/vite.config.ts +17 -0
- package/template/create-miden-app/template/yarn.lock +1697 -0
- package/template/index.html +1 -1
- package/template/package.json +17 -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 +57 -0
- package/template/src/__tests__/fixtures/index.ts +21 -0
- package/template/src/__tests__/fixtures/notes.ts +33 -0
- package/template/src/__tests__/mocks/miden-sdk-react.ts +244 -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 +75 -0
- package/template/src/__tests__/patterns/query-hook.test.tsx +143 -0
- package/template/src/components/AppContent.css +45 -0
- package/template/src/components/AppContent.tsx +50 -0
- package/template/src/components/Counter.css +27 -0
- package/template/src/components/Counter.tsx +45 -0
- package/template/src/components/__tests__/AppContent.test.tsx +86 -0
- package/template/src/components/__tests__/Counter.test.tsx +114 -0
- package/template/src/config.ts +21 -0
- package/template/src/hooks/useIncrementCounter.ts +136 -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 +31 -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 +26 -0
- package/template/vitest.setup.ts +1 -0
- package/template/yarn.lock +1318 -799
- package/template/src/miden/lib/demo.ts +0 -105
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-sdk-patterns
|
|
3
|
+
description: Complete guide to building Miden frontends with @miden-sdk/react hooks. Covers MidenProvider setup, all query hooks (useAccounts, useAccount, useNotes, useSyncState, useAssetMetadata), all mutation hooks (useCreateWallet, useSend, useMultiSend, useMint, useConsume, useSwap, useTransaction, useCreateFaucet), transaction stages, signer integration, and utility functions. Use when writing, editing, or reviewing Miden React frontend code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Miden React SDK Patterns
|
|
7
|
+
|
|
8
|
+
## SDK Choice
|
|
9
|
+
|
|
10
|
+
ALWAYS use `@miden-sdk/react` hooks. Only fall back to raw `@miden-sdk/miden-sdk` WebClient via `useMidenClient()` for operations not covered by hooks. The React SDK handles WASM safety (runExclusive), state management (Zustand), auto-sync, and transaction stage tracking automatically.
|
|
11
|
+
|
|
12
|
+
## MidenProvider Configuration
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { MidenProvider } from "@miden-sdk/react";
|
|
16
|
+
|
|
17
|
+
<MidenProvider
|
|
18
|
+
config={{
|
|
19
|
+
rpcUrl: "devnet", // "devnet" | "testnet" | "localhost" | custom URL
|
|
20
|
+
prover: "devnet", // "local" | "devnet" | "testnet" | custom URL
|
|
21
|
+
autoSyncInterval: 15000, // ms, set to 0 to disable. Default: 15000
|
|
22
|
+
noteTransportUrl: "...", // optional: for private note delivery
|
|
23
|
+
}}
|
|
24
|
+
loadingComponent={<Loading />} // shown during WASM init
|
|
25
|
+
errorComponent={<Error />} // shown on init failure (receives Error prop)
|
|
26
|
+
>
|
|
27
|
+
<App />
|
|
28
|
+
</MidenProvider>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Network | rpcUrl | Use When |
|
|
32
|
+
|---------|--------|----------|
|
|
33
|
+
| Devnet | `"devnet"` | Development, testing with fake tokens |
|
|
34
|
+
| Testnet | `"testnet"` | Pre-production testing |
|
|
35
|
+
| Localhost | `"localhost"` | Local node at `http://localhost:57291` |
|
|
36
|
+
|
|
37
|
+
## Query Hooks
|
|
38
|
+
|
|
39
|
+
Each returns its own result shape plus `isLoading`, `error`, `refetch`.
|
|
40
|
+
|
|
41
|
+
### useAccounts()
|
|
42
|
+
```tsx
|
|
43
|
+
const { accounts, wallets, faucets, isLoading, error, refetch } = useAccounts();
|
|
44
|
+
// wallets — AccountHeader[] (regular wallet accounts)
|
|
45
|
+
// faucets — AccountHeader[] (token faucet accounts)
|
|
46
|
+
// accounts — AccountHeader[] (everything)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### useAccount(accountId: string)
|
|
50
|
+
```tsx
|
|
51
|
+
const { account, assets, getBalance, isLoading, error, refetch } = useAccount(accountId);
|
|
52
|
+
// account — Account object (.id, .nonce, .bech32id())
|
|
53
|
+
// assets — AssetBalance[] (assetId, amount, symbol?, decimals?)
|
|
54
|
+
// getBalance(faucetId) — bigint balance for specific token
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### useNotes(filter?)
|
|
58
|
+
```tsx
|
|
59
|
+
const { notes, consumableNotes, noteSummaries, consumableNoteSummaries, isLoading, error, refetch } = useNotes();
|
|
60
|
+
// notes — InputNoteRecord[]
|
|
61
|
+
// consumableNotes — ConsumableNoteRecord[]
|
|
62
|
+
// noteSummaries — NoteSummary[] (id, assets, sender)
|
|
63
|
+
// consumableNoteSummaries — NoteSummary[]
|
|
64
|
+
|
|
65
|
+
// Filter by account:
|
|
66
|
+
const { notes } = useNotes({ accountId: "0x..." });
|
|
67
|
+
// Filter by status:
|
|
68
|
+
const { notes } = useNotes({ status: "committed" });
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### useSyncState()
|
|
72
|
+
```tsx
|
|
73
|
+
const { syncHeight, isSyncing, lastSyncTime, sync, error } = useSyncState();
|
|
74
|
+
await sync(); // Manual sync
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### useAssetMetadata(faucetId: string | string[])
|
|
78
|
+
```tsx
|
|
79
|
+
const { assetMetadata } = useAssetMetadata(faucetId);
|
|
80
|
+
// assetMetadata — Map<string, AssetMetadata>
|
|
81
|
+
// Each entry: { assetId, symbol?, decimals? }
|
|
82
|
+
const meta = assetMetadata.get(faucetId);
|
|
83
|
+
// meta.symbol — "TEST"
|
|
84
|
+
// meta.decimals — 8
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### useTransactionHistory(options?)
|
|
88
|
+
```tsx
|
|
89
|
+
const { records, record, status, isLoading, error, refetch } = useTransactionHistory({ id: txId });
|
|
90
|
+
// status: "pending" | "committed" | "discarded" | null
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Mutation Hooks
|
|
94
|
+
|
|
95
|
+
Each returns its own action function plus `isLoading`, `stage`, `error`, `reset`.
|
|
96
|
+
|
|
97
|
+
**Transaction stages**: `"idle"` → `"executing"` → `"proving"` → `"submitting"` → `"complete"`
|
|
98
|
+
|
|
99
|
+
### useCreateWallet()
|
|
100
|
+
```tsx
|
|
101
|
+
const { createWallet, wallet, isCreating, error, reset } = useCreateWallet();
|
|
102
|
+
const account = await createWallet({
|
|
103
|
+
storageMode: "private", // "private" | "public" | "network". Default: "private"
|
|
104
|
+
mutable: true, // Default: true
|
|
105
|
+
authScheme: 0, // 0 = RpoFalcon512, 1 = EcdsaK256Keccak. Default: 0
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### useCreateFaucet()
|
|
110
|
+
```tsx
|
|
111
|
+
const { createFaucet, faucet, isCreating, error, reset } = useCreateFaucet();
|
|
112
|
+
const account = await createFaucet({
|
|
113
|
+
tokenSymbol: "TEST",
|
|
114
|
+
decimals: 8, // Default: 8
|
|
115
|
+
maxSupply: 1000000n, // bigint!
|
|
116
|
+
storageMode: "public", // Default: "private"
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### useSend()
|
|
121
|
+
```tsx
|
|
122
|
+
const { send, result, isLoading, stage, error, reset } = useSend();
|
|
123
|
+
await send({
|
|
124
|
+
from: senderAccountId,
|
|
125
|
+
to: recipientAccountId,
|
|
126
|
+
assetId: faucetId, // token faucet ID
|
|
127
|
+
amount: 1000n, // bigint!
|
|
128
|
+
noteType: "private", // "private" | "public". Default: "private"
|
|
129
|
+
recallHeight: 100, // optional: sender can reclaim after this block
|
|
130
|
+
timelockHeight: 50, // optional: recipient can consume after this block
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### useMultiSend()
|
|
135
|
+
```tsx
|
|
136
|
+
const { sendMany, result, isLoading, stage, error, reset } = useMultiSend();
|
|
137
|
+
await sendMany({
|
|
138
|
+
from: senderAccountId,
|
|
139
|
+
assetId: faucetId,
|
|
140
|
+
recipients: [
|
|
141
|
+
{ to: recipient1, amount: 500n },
|
|
142
|
+
{ to: recipient2, amount: 300n },
|
|
143
|
+
],
|
|
144
|
+
noteType: "private",
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### useMint()
|
|
149
|
+
```tsx
|
|
150
|
+
const { mint, result, isLoading, stage, error, reset } = useMint();
|
|
151
|
+
await mint({
|
|
152
|
+
targetAccountId: recipientId,
|
|
153
|
+
faucetId: myFaucetId,
|
|
154
|
+
amount: 10000n, // bigint!
|
|
155
|
+
noteType: "public",
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### useConsume()
|
|
160
|
+
```tsx
|
|
161
|
+
const { consume, result, isLoading, stage, error, reset } = useConsume();
|
|
162
|
+
await consume({
|
|
163
|
+
accountId: myAccountId,
|
|
164
|
+
noteIds: [noteId1, noteId2],
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### useSwap()
|
|
169
|
+
```tsx
|
|
170
|
+
const { swap, result, isLoading, stage, error, reset } = useSwap();
|
|
171
|
+
await swap({
|
|
172
|
+
accountId: myAccountId,
|
|
173
|
+
offeredFaucetId: tokenA,
|
|
174
|
+
offeredAmount: 100n,
|
|
175
|
+
requestedFaucetId: tokenB,
|
|
176
|
+
requestedAmount: 50n,
|
|
177
|
+
noteType: "private",
|
|
178
|
+
paybackNoteType: "private",
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### useTransaction() — Escape Hatch
|
|
183
|
+
```tsx
|
|
184
|
+
const { execute, result, isLoading, stage, error, reset } = useTransaction();
|
|
185
|
+
|
|
186
|
+
// With pre-built TransactionRequest:
|
|
187
|
+
await execute({ accountId, request: txRequest });
|
|
188
|
+
|
|
189
|
+
// With factory function (gets access to client):
|
|
190
|
+
await execute({
|
|
191
|
+
accountId,
|
|
192
|
+
request: (client) => client.newSwapTransactionRequest(/* ... */),
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### useWaitForCommit()
|
|
197
|
+
```tsx
|
|
198
|
+
const { waitForCommit } = useWaitForCommit();
|
|
199
|
+
await waitForCommit(result.transactionId, {
|
|
200
|
+
timeoutMs: 10000, // Default: 10000
|
|
201
|
+
intervalMs: 1000, // Default: 1000
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### useWaitForNotes()
|
|
206
|
+
```tsx
|
|
207
|
+
const { waitForConsumableNotes } = useWaitForNotes();
|
|
208
|
+
await waitForConsumableNotes({
|
|
209
|
+
accountId: myAccountId,
|
|
210
|
+
minCount: 1, // Default: 1
|
|
211
|
+
timeoutMs: 10000,
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Transaction Progress UI
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
function SendButton({ from, to, assetId, amount }) {
|
|
219
|
+
const { send, stage, isLoading, error } = useSend();
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div>
|
|
223
|
+
<button onClick={() => send({ from, to, assetId, amount })} disabled={isLoading}>
|
|
224
|
+
{isLoading ? `${stage}...` : "Send"}
|
|
225
|
+
</button>
|
|
226
|
+
{error && <p>Error: {error.message}</p>}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Signer Integration
|
|
233
|
+
|
|
234
|
+
### Local Keystore (Default)
|
|
235
|
+
No signer provider needed. Keys are managed in the browser via IndexedDB.
|
|
236
|
+
|
|
237
|
+
### External Signers
|
|
238
|
+
Wrap MidenProvider with a signer provider. Three pre-built options:
|
|
239
|
+
- `ParaSignerProvider` from `@miden-sdk/para` — EVM wallets
|
|
240
|
+
- `TurnkeySignerProvider` from `@miden-sdk/miden-turnkey-react` — passkey auth
|
|
241
|
+
- `MidenFiSignerProvider` from `@miden-sdk/wallet-adapter-react` — MidenFi wallet
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
// Example: Para signer wrapping MidenProvider
|
|
245
|
+
import { ParaSignerProvider } from "@miden-sdk/para";
|
|
246
|
+
<ParaSignerProvider apiKey="..." environment="PRODUCTION">
|
|
247
|
+
<MidenProvider config={...}><App /></MidenProvider>
|
|
248
|
+
</ParaSignerProvider>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### useSigner() — Unified Interface
|
|
252
|
+
```tsx
|
|
253
|
+
const { isConnected, connect, disconnect, name } = useSigner();
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Custom Signer
|
|
257
|
+
Implement `SignerContextValue` interface via `SignerContext.Provider`. Requires: `name`, `storeName` (unique per user for DB isolation), `accountConfig`, `signCb`, `connect`, `disconnect`. See `frontend-source-guide` skill for source references.
|
|
258
|
+
|
|
259
|
+
## Utility Functions
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import { formatAssetAmount, parseAssetAmount, getNoteSummary, formatNoteSummary, toBech32AccountId } from "@miden-sdk/react";
|
|
263
|
+
|
|
264
|
+
formatAssetAmount(1000000n, 8) // "0.01"
|
|
265
|
+
parseAssetAmount("0.01", 8) // 1000000n
|
|
266
|
+
const summary = getNoteSummary(note); // { id, assets, sender }
|
|
267
|
+
formatNoteSummary(summary); // "1.5 TEST"
|
|
268
|
+
toBech32AccountId("0x1234..."); // "miden1qy35..."
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Direct Client Access
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
const client = useMidenClient(); // throws if not ready
|
|
275
|
+
const { runExclusive } = useMiden();
|
|
276
|
+
|
|
277
|
+
// For operations not covered by hooks:
|
|
278
|
+
await runExclusive(async (client) => {
|
|
279
|
+
const header = await client.getBlockHeaderByNumber(100);
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Type Imports
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import type {
|
|
287
|
+
MidenConfig, QueryResult, MutationResult, TransactionStage,
|
|
288
|
+
AccountsResult, AccountResult, AssetBalance, NotesResult, NoteSummary,
|
|
289
|
+
SendOptions, MultiSendOptions, MintOptions, ConsumeOptions, SwapOptions,
|
|
290
|
+
CreateWalletOptions, CreateFaucetOptions, ExecuteTransactionOptions,
|
|
291
|
+
TransactionResult, SyncState, WaitForCommitOptions, WaitForNotesOptions,
|
|
292
|
+
Account, AccountId, InputNoteRecord, ConsumableNoteRecord,
|
|
293
|
+
TransactionRecord, TransactionRequest, NoteType, AccountStorageMode,
|
|
294
|
+
SignerContextValue, SignCallback, SignerAccountConfig,
|
|
295
|
+
} from "@miden-sdk/react";
|
|
296
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: signer-integration
|
|
3
|
+
description: Guide to integrating external signers (Para, Turnkey, MidenFi wallet adapter) and building custom signers for Miden React frontends. Covers provider setup, passkey authentication, unified signer interface, custom SignerContext implementation, and custom account components. Use when adding wallet connection, authentication, or external key management to a Miden frontend.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Miden Signer Integration
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
By default, MidenProvider uses a **local keystore** (keys in IndexedDB, no wallet connection needed). For production apps, wrap MidenProvider with a signer provider to use external key management.
|
|
11
|
+
|
|
12
|
+
Signer providers must wrap MidenProvider (outer → inner):
|
|
13
|
+
```
|
|
14
|
+
<SignerProvider> ← manages keys + auth
|
|
15
|
+
<MidenProvider> ← manages Miden client
|
|
16
|
+
<App />
|
|
17
|
+
</MidenProvider>
|
|
18
|
+
</SignerProvider>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Pre-Built Signer Providers
|
|
22
|
+
|
|
23
|
+
### Para (EVM Wallets)
|
|
24
|
+
```tsx
|
|
25
|
+
import { ParaSignerProvider } from "@miden-sdk/para";
|
|
26
|
+
|
|
27
|
+
<ParaSignerProvider apiKey="your-api-key" environment="PRODUCTION">
|
|
28
|
+
<MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
29
|
+
<App />
|
|
30
|
+
</MidenProvider>
|
|
31
|
+
</ParaSignerProvider>
|
|
32
|
+
|
|
33
|
+
const { para, wallet, isConnected } = useParaSigner();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Turnkey (Passkey Authentication)
|
|
37
|
+
```tsx
|
|
38
|
+
import { TurnkeySignerProvider } from "@miden-sdk/miden-turnkey-react";
|
|
39
|
+
|
|
40
|
+
// Config is optional — defaults to https://api.turnkey.com
|
|
41
|
+
// and reads VITE_TURNKEY_ORG_ID from environment
|
|
42
|
+
<TurnkeySignerProvider>
|
|
43
|
+
<MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
44
|
+
<App />
|
|
45
|
+
</MidenProvider>
|
|
46
|
+
</TurnkeySignerProvider>
|
|
47
|
+
|
|
48
|
+
// Or with explicit config:
|
|
49
|
+
<TurnkeySignerProvider config={{
|
|
50
|
+
apiBaseUrl: "https://api.turnkey.com",
|
|
51
|
+
defaultOrganizationId: "your-org-id",
|
|
52
|
+
}}>
|
|
53
|
+
...
|
|
54
|
+
</TurnkeySignerProvider>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Connect via passkey:
|
|
58
|
+
```tsx
|
|
59
|
+
import { useSigner } from "@miden-sdk/react";
|
|
60
|
+
import { useTurnkeySigner } from "@miden-sdk/miden-turnkey-react";
|
|
61
|
+
|
|
62
|
+
const { isConnected, connect, disconnect } = useSigner();
|
|
63
|
+
await connect(); // triggers passkey flow, auto-selects account
|
|
64
|
+
|
|
65
|
+
// Turnkey-specific extras
|
|
66
|
+
const { client, account, setAccount } = useTurnkeySigner();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### MidenFi Wallet Adapter (Browser Extension)
|
|
70
|
+
```tsx
|
|
71
|
+
import { MidenFiSignerProvider } from "@miden-sdk/wallet-adapter-react";
|
|
72
|
+
|
|
73
|
+
<MidenFiSignerProvider network="Testnet">
|
|
74
|
+
<MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
75
|
+
<App />
|
|
76
|
+
</MidenProvider>
|
|
77
|
+
</MidenFiSignerProvider>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Unified Signer Interface
|
|
81
|
+
|
|
82
|
+
Works with any signer provider above:
|
|
83
|
+
```tsx
|
|
84
|
+
import { useSigner } from "@miden-sdk/react";
|
|
85
|
+
|
|
86
|
+
const { isConnected, connect, disconnect, name } = useSigner();
|
|
87
|
+
|
|
88
|
+
if (!isConnected) {
|
|
89
|
+
return <button onClick={connect}>Connect {name}</button>;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Building a Custom Signer
|
|
94
|
+
|
|
95
|
+
Implement `SignerContextValue` via `SignerContext.Provider`:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { SignerContext } from "@miden-sdk/react";
|
|
99
|
+
|
|
100
|
+
<SignerContext.Provider value={{
|
|
101
|
+
name: "MyWallet",
|
|
102
|
+
storeName: `mywallet_${userAddress}`, // unique per user for DB isolation
|
|
103
|
+
isConnected: true,
|
|
104
|
+
accountConfig: {
|
|
105
|
+
publicKey: userPublicKeyCommitment, // Uint8Array
|
|
106
|
+
storageMode: "private",
|
|
107
|
+
},
|
|
108
|
+
signCb: async (pubKey, signingInputs) => {
|
|
109
|
+
// Route to your signing service
|
|
110
|
+
return signature; // Uint8Array
|
|
111
|
+
},
|
|
112
|
+
connect: async () => { /* trigger wallet connection */ },
|
|
113
|
+
disconnect: async () => { /* clear session */ },
|
|
114
|
+
}}>
|
|
115
|
+
<MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
116
|
+
<App />
|
|
117
|
+
</MidenProvider>
|
|
118
|
+
</SignerContext.Provider>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Required fields:**
|
|
122
|
+
- `name` — Display name for the signer
|
|
123
|
+
- `storeName` — Unique string per user (isolates IndexedDB data between users)
|
|
124
|
+
- `accountConfig` — Public key commitment + storage mode
|
|
125
|
+
- `signCb` — Callback that signs transaction data with your key management service
|
|
126
|
+
- `connect` / `disconnect` — Session lifecycle handlers
|
|
127
|
+
|
|
128
|
+
## Custom Account Components
|
|
129
|
+
|
|
130
|
+
Attach application-specific `AccountComponent` instances (e.g., DEX logic from `.masp` packages) to accounts created by the signer:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { type SignerAccountConfig } from "@miden-sdk/react";
|
|
134
|
+
import { AccountComponent } from "@miden-sdk/miden-sdk";
|
|
135
|
+
|
|
136
|
+
const myDexComponent: AccountComponent = await loadCompiledComponent();
|
|
137
|
+
|
|
138
|
+
const accountConfig: SignerAccountConfig = {
|
|
139
|
+
publicKeyCommitment: userPublicKeyCommitment,
|
|
140
|
+
accountType: "RegularAccountUpdatableCode",
|
|
141
|
+
storageMode: myStorageMode,
|
|
142
|
+
customComponents: [myDexComponent],
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Components are appended to the `AccountBuilder` after the default basic wallet component. The field is optional — omitting it preserves default behavior.
|
|
147
|
+
|
|
148
|
+
## Which Signer to Choose
|
|
149
|
+
|
|
150
|
+
| Signer | Auth Method | Keys Stored | Best For |
|
|
151
|
+
|--------|-------------|-------------|----------|
|
|
152
|
+
| Local keystore (default) | None | Browser IndexedDB | Development, demos |
|
|
153
|
+
| Para | EVM wallet | Para servers | Apps with existing EVM users |
|
|
154
|
+
| Turnkey | Passkey (biometric) | Turnkey servers | Consumer apps, no seed phrases |
|
|
155
|
+
| MidenFi Wallet | Browser extension | Extension | Power users with MidenFi wallet |
|
|
156
|
+
| Custom | Your choice | Your infrastructure | Enterprise, custom auth flows |
|
|
157
|
+
|
|
158
|
+
**Key trade-off**: Local keystore requires no setup but keys are lost if the user clears browser data. External signers persist keys server-side but add a dependency.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-patterns
|
|
3
|
+
description: Testing conventions, mock factory, fixtures, and TDD workflow for Miden frontend development. Covers Vitest + testing-library setup, @miden-sdk/react module mocking, realistic fixture data, test patterns for query and mutation hooks, and the automated verification pipeline. Use when writing, running, or debugging tests for Miden React components.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Miden Frontend Testing Patterns
|
|
7
|
+
|
|
8
|
+
## Test Stack
|
|
9
|
+
|
|
10
|
+
- **Vitest** — Test runner (extends Vite config for consistent behavior)
|
|
11
|
+
- **@testing-library/react** — Component rendering and queries
|
|
12
|
+
- **@testing-library/user-event** — User interaction simulation
|
|
13
|
+
- **@testing-library/jest-dom** — DOM assertion matchers (toBeInTheDocument, toBeDisabled, etc.)
|
|
14
|
+
- **jsdom** — Browser environment for tests
|
|
15
|
+
|
|
16
|
+
## Mock Factory: `@miden-sdk/react`
|
|
17
|
+
|
|
18
|
+
All Miden SDK hooks are mocked via `src/__tests__/mocks/miden-sdk-react.ts`. This module exports mock implementations of every hook with realistic default return values.
|
|
19
|
+
|
|
20
|
+
### Usage in test files
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// 1. Mock the entire module (hoisted to top by vitest)
|
|
24
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
25
|
+
|
|
26
|
+
// 2. Import hooks you want to override
|
|
27
|
+
import { useAccounts, useSend } from "@miden-sdk/react";
|
|
28
|
+
|
|
29
|
+
// 3. Override per-test
|
|
30
|
+
it("shows empty state", () => {
|
|
31
|
+
vi.mocked(useAccounts).mockReturnValue({
|
|
32
|
+
accounts: [],
|
|
33
|
+
wallets: [],
|
|
34
|
+
faucets: [],
|
|
35
|
+
isLoading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
refetch: vi.fn(),
|
|
38
|
+
});
|
|
39
|
+
render(<MyComponent />);
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Default mock return values
|
|
44
|
+
|
|
45
|
+
**Query hooks** return populated data by default:
|
|
46
|
+
- `useAccounts()` — 2 wallets, 1 faucet
|
|
47
|
+
- `useAccount()` — account with 10.0 TEST token balance
|
|
48
|
+
- `useNotes()` — 1 input note, 1 consumable note
|
|
49
|
+
- `useSyncState()` — syncHeight: 12345, not syncing
|
|
50
|
+
- `useAssetMetadata()` — TEST token metadata (symbol, decimals: 8)
|
|
51
|
+
- `useMiden()` — isReady: true
|
|
52
|
+
|
|
53
|
+
**Mutation hooks** return idle state by default:
|
|
54
|
+
- `useSend()` — `{ send: vi.fn(), stage: "idle", isLoading: false }`
|
|
55
|
+
- `useMint()`, `useConsume()`, `useSwap()`, `useTransaction()` — similar pattern
|
|
56
|
+
- `useCreateWallet()` — `{ createWallet: vi.fn(), isCreating: false }`
|
|
57
|
+
|
|
58
|
+
### Simulating transaction stages
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// Show "proving" stage
|
|
62
|
+
vi.mocked(useSend).mockReturnValue({
|
|
63
|
+
send: vi.fn(),
|
|
64
|
+
result: null,
|
|
65
|
+
isLoading: true,
|
|
66
|
+
stage: "proving",
|
|
67
|
+
error: null,
|
|
68
|
+
reset: vi.fn(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Show completed transaction
|
|
72
|
+
vi.mocked(useSend).mockReturnValue({
|
|
73
|
+
send: vi.fn(),
|
|
74
|
+
result: { transactionId: "0xabc123" },
|
|
75
|
+
isLoading: false,
|
|
76
|
+
stage: "complete",
|
|
77
|
+
error: null,
|
|
78
|
+
reset: vi.fn(),
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Fixtures
|
|
83
|
+
|
|
84
|
+
Realistic test data in `src/__tests__/fixtures/`:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import {
|
|
88
|
+
WALLET_ID_1, // "mtst1qy35qfqdvpjx2e5zf9hkp4vr"
|
|
89
|
+
WALLET_ID_2, // "mtst1qa7k9qjf8dp4x2e5zf9hkp5vr"
|
|
90
|
+
FAUCET_ID, // "mtst1qx9y8zjf2dp4x2e5zf9hkp3vr"
|
|
91
|
+
COUNTER_ID, // "mtst1aru8adnrqspgcsr3drk2n990lyc070ll"
|
|
92
|
+
MOCK_WALLET_HEADER, // { id, nonce, storageCommitment }
|
|
93
|
+
MOCK_FAUCET_HEADER, // { id, nonce, storageCommitment }
|
|
94
|
+
MOCK_ASSET_BALANCE, // { assetId, amount: 1000000000n, symbol: "TEST", decimals: 8 }
|
|
95
|
+
MOCK_ACCOUNT, // { id, nonce, bech32id() }
|
|
96
|
+
MOCK_TRANSACTION_RESULT, // { transactionId: "0x..." }
|
|
97
|
+
MOCK_NOTE_SUMMARY, // { id, assets, sender }
|
|
98
|
+
} from "@/__tests__/fixtures";
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Key characteristics:
|
|
102
|
+
- Account IDs use bech32 format (`mtst1...`)
|
|
103
|
+
- Amounts are `bigint` (e.g., `1000000000n` = 10.0 with 8 decimals)
|
|
104
|
+
- Asset metadata uses TEST token with 8 decimals
|
|
105
|
+
|
|
106
|
+
## Test Patterns (copy-adaptable)
|
|
107
|
+
|
|
108
|
+
Reference tests in `src/__tests__/patterns/`:
|
|
109
|
+
|
|
110
|
+
| Pattern | File | Tests |
|
|
111
|
+
|---------|------|-------|
|
|
112
|
+
| Provider/context setup | `provider-setup.test.tsx` | ready, loading, error states |
|
|
113
|
+
| Query hook component | `query-hook.test.tsx` | data, loading, error, empty states |
|
|
114
|
+
| Mutation hook component | `mutation-hook.test.tsx` | idle, stages, success, error, argument verification |
|
|
115
|
+
|
|
116
|
+
### Minimum test coverage per component
|
|
117
|
+
|
|
118
|
+
Every component test should cover:
|
|
119
|
+
1. **Success state** — renders correctly with data
|
|
120
|
+
2. **Loading state** — shows loading indicator
|
|
121
|
+
3. **Error state** — shows error message, recovery action
|
|
122
|
+
4. **User interactions** — buttons, forms trigger correct handler calls
|
|
123
|
+
|
|
124
|
+
## Mocking the wallet adapter
|
|
125
|
+
|
|
126
|
+
The app uses `@miden-sdk/miden-wallet-adapter`. Mock it at the module level:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
vi.mock("@miden-sdk/miden-wallet-adapter", () => ({
|
|
130
|
+
WalletMultiButton: () => <button>Connect Wallet</button>,
|
|
131
|
+
useWallet: vi.fn(() => ({
|
|
132
|
+
address: "mtst1...",
|
|
133
|
+
connected: true,
|
|
134
|
+
requestTransaction: vi.fn(),
|
|
135
|
+
})),
|
|
136
|
+
}));
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Vitest config externalizes `@miden-sdk/miden-wallet-adapter*` sub-packages to prevent broken transitive resolution from the reactui sub-package.
|
|
140
|
+
|
|
141
|
+
## Automated Verification Pipeline
|
|
142
|
+
|
|
143
|
+
Hooks in `.claude/settings.json` enforce quality automatically:
|
|
144
|
+
|
|
145
|
+
1. **PostToolUse: typecheck** — `npx tsc -b --noEmit` on every `.ts`/`.tsx` edit in `src/`
|
|
146
|
+
2. **PostToolUse: affected tests** — `npx vitest --changed --run` on every `.ts`/`.tsx` edit in `src/`
|
|
147
|
+
3. **Stop hook** — Full `vitest --run && tsc -b --noEmit && vite build` before task completion
|
|
148
|
+
|
|
149
|
+
If any hook fails (exit code 2), the agent is blocked from proceeding until the issue is fixed.
|
|
150
|
+
|
|
151
|
+
## TDD Flow
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
1. Write test (describe expected behavior)
|
|
155
|
+
↓
|
|
156
|
+
2. yarn test → RED (test fails)
|
|
157
|
+
↓
|
|
158
|
+
3. Implement code
|
|
159
|
+
↓
|
|
160
|
+
4. Auto hooks fire → typecheck + affected tests
|
|
161
|
+
↓
|
|
162
|
+
5. yarn test → GREEN (all pass)
|
|
163
|
+
↓
|
|
164
|
+
6. Refactor if needed
|
|
165
|
+
↓
|
|
166
|
+
7. Task complete → Stop hook: full suite + build
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Common Mistakes
|
|
170
|
+
|
|
171
|
+
**Forgetting vi.clearAllMocks()**: Always call in `beforeEach` to prevent mock state leaking between tests.
|
|
172
|
+
|
|
173
|
+
**Not mocking the SDK**: Components importing from `@miden-sdk/react` will fail without `vi.mock()` because the real SDK requires WASM initialization.
|
|
174
|
+
|
|
175
|
+
**Using number instead of bigint**: Mock amounts must use `bigint` (`1000n`, not `1000`). The SDK enforces this at the type level.
|
|
176
|
+
|
|
177
|
+
**Testing implementation details**: Test what the user sees (text, buttons, states), not internal hook calls. Use `screen.getByRole`, `screen.getByText`, not internal component state.
|