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,177 @@
|
|
|
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/miden-wallet-adapter-react";
|
|
72
|
+
import { WalletAdapterNetwork } from "@miden-sdk/miden-wallet-adapter-base";
|
|
73
|
+
|
|
74
|
+
<MidenFiSignerProvider
|
|
75
|
+
appName="My App" // optional: passed to MidenWalletAdapter
|
|
76
|
+
network={WalletAdapterNetwork.Testnet} // WalletAdapterNetwork enum: Devnet | Testnet | Localnet
|
|
77
|
+
autoConnect // reconnect on mount. Default: false
|
|
78
|
+
accountType="RegularAccountImmutableCode" // Default: "RegularAccountImmutableCode"
|
|
79
|
+
storageMode="public" // "private" | "public" | "network". Default: "public"
|
|
80
|
+
customComponents={[myComponent]} // optional: custom AccountComponents
|
|
81
|
+
privateDataPermission={permission} // optional: private data access level
|
|
82
|
+
allowedPrivateData={allowedData} // optional: allowed private data types
|
|
83
|
+
>
|
|
84
|
+
<MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
85
|
+
<App />
|
|
86
|
+
</MidenProvider>
|
|
87
|
+
</MidenFiSignerProvider>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
With `MidenFiSignerProvider` in place, use `useSigner()` from the React SDK to manage connection state. The regular React SDK hooks (`useSend`, `useConsume`, etc.) automatically sign via the connected wallet — no additional wiring needed.
|
|
91
|
+
|
|
92
|
+
### This template's MidenFi-specific pattern
|
|
93
|
+
|
|
94
|
+
This template deviates from the generic `useSigner()` approach in two places — worth knowing because it's a pattern you'll likely want when the wallet extension is the primary signer:
|
|
95
|
+
|
|
96
|
+
- **Wallet button uses `useMidenFiWallet()` + `WalletReadyState`** (`src/components/AppContent.tsx`). The button gates on `wallet.readyState` so it can render a disabled "Install MidenFi Wallet" state before the extension is detected. `useSigner().connect()` would silently fall through to the adapter's `window.open(adapter.url, ...)` install fallback (Chrome Web Store → Play Store redirect on some platforms); gating on `readyState` avoids that path entirely.
|
|
97
|
+
- **Custom transaction flow calls `wallet.requestTransaction(...)` directly** (`src/hooks/useIncrementCounter.ts`). The counter increment builds a bespoke `TransactionRequest` (via `TransactionRequestBuilder`, a custom `Note` with `NoteAttachment.newNetworkAccountTarget`, etc.) and hands it to the wallet for signing + submission. The React SDK mutation hooks (`useSend`, `useConsume`, ...) don't cover this kind of custom note construction, and the tx is submitted by the wallet rather than the local client — so `useWaitForCommit` doesn't apply either.
|
|
98
|
+
|
|
99
|
+
## Unified Signer Interface
|
|
100
|
+
|
|
101
|
+
Works with any signer provider above:
|
|
102
|
+
```tsx
|
|
103
|
+
import { useSigner } from "@miden-sdk/react";
|
|
104
|
+
|
|
105
|
+
const { isConnected, connect, disconnect, name } = useSigner();
|
|
106
|
+
|
|
107
|
+
if (!isConnected) {
|
|
108
|
+
return <button onClick={connect}>Connect {name}</button>;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Building a Custom Signer
|
|
113
|
+
|
|
114
|
+
Implement `SignerContextValue` via `SignerContext.Provider`:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { SignerContext } from "@miden-sdk/react";
|
|
118
|
+
|
|
119
|
+
<SignerContext.Provider value={{
|
|
120
|
+
name: "MyWallet",
|
|
121
|
+
storeName: `mywallet_${userAddress}`, // unique per user for DB isolation
|
|
122
|
+
isConnected: true,
|
|
123
|
+
accountConfig: {
|
|
124
|
+
publicKey: userPublicKeyCommitment, // Uint8Array
|
|
125
|
+
storageMode: "private",
|
|
126
|
+
},
|
|
127
|
+
signCb: async (pubKey, signingInputs) => {
|
|
128
|
+
// Route to your signing service
|
|
129
|
+
return signature; // Uint8Array
|
|
130
|
+
},
|
|
131
|
+
connect: async () => { /* trigger wallet connection */ },
|
|
132
|
+
disconnect: async () => { /* clear session */ },
|
|
133
|
+
}}>
|
|
134
|
+
<MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
135
|
+
<App />
|
|
136
|
+
</MidenProvider>
|
|
137
|
+
</SignerContext.Provider>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Required fields:**
|
|
141
|
+
- `name` — Display name for the signer
|
|
142
|
+
- `storeName` — Unique string per user (isolates IndexedDB data between users)
|
|
143
|
+
- `accountConfig` — Public key commitment + storage mode
|
|
144
|
+
- `signCb` — Callback that signs transaction data with your key management service
|
|
145
|
+
- `connect` / `disconnect` — Session lifecycle handlers
|
|
146
|
+
|
|
147
|
+
## Custom Account Components
|
|
148
|
+
|
|
149
|
+
Attach application-specific `AccountComponent` instances (e.g., DEX logic from `.masp` packages) to accounts created by the signer:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { type SignerAccountConfig } from "@miden-sdk/react";
|
|
153
|
+
import { AccountComponent } from "@miden-sdk/miden-sdk";
|
|
154
|
+
|
|
155
|
+
const myDexComponent: AccountComponent = await loadCompiledComponent();
|
|
156
|
+
|
|
157
|
+
const accountConfig: SignerAccountConfig = {
|
|
158
|
+
publicKeyCommitment: userPublicKeyCommitment,
|
|
159
|
+
accountType: "RegularAccountUpdatableCode",
|
|
160
|
+
storageMode: myStorageMode,
|
|
161
|
+
customComponents: [myDexComponent],
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Components are appended to the `AccountBuilder` after the default basic wallet component. The field is optional — omitting it preserves default behavior.
|
|
166
|
+
|
|
167
|
+
## Which Signer to Choose
|
|
168
|
+
|
|
169
|
+
| Signer | Auth Method | Keys Stored | Best For |
|
|
170
|
+
|--------|-------------|-------------|----------|
|
|
171
|
+
| Local keystore (default) | None | Browser IndexedDB | Development, demos |
|
|
172
|
+
| Para | EVM wallet | Para servers | Apps with existing EVM users |
|
|
173
|
+
| Turnkey | Passkey (biometric) | Turnkey servers | Consumer apps, no seed phrases |
|
|
174
|
+
| MidenFi Wallet | Browser extension | Extension | Power users with MidenFi wallet |
|
|
175
|
+
| Custom | Your choice | Your infrastructure | Enterprise, custom auth flows |
|
|
176
|
+
|
|
177
|
+
**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,338 @@
|
|
|
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 }`. Its `result` type is `SendResult { txId, note }` - distinct from `TransactionResult { transactionId }` used by `useMint`/`useConsume`/`useSwap`/`useMultiSend`/`useTransaction`.
|
|
55
|
+
- `useMint()`, `useConsume()`, `useSwap()`, `useTransaction()`, `useMultiSend()` - idle shape with `result: TransactionResult | null`.
|
|
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 - useSend returns SendResult { txId, note }
|
|
72
|
+
vi.mocked(useSend).mockReturnValue({
|
|
73
|
+
send: vi.fn(),
|
|
74
|
+
result: { txId: "0xabc123", note: null },
|
|
75
|
+
isLoading: false,
|
|
76
|
+
stage: "complete",
|
|
77
|
+
error: null,
|
|
78
|
+
reset: vi.fn(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Other mutation hooks return TransactionResult { transactionId }
|
|
82
|
+
vi.mocked(useMint).mockReturnValue({
|
|
83
|
+
mint: vi.fn(),
|
|
84
|
+
result: { transactionId: "0xdef456" },
|
|
85
|
+
isLoading: false,
|
|
86
|
+
stage: "complete",
|
|
87
|
+
error: null,
|
|
88
|
+
reset: vi.fn(),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Fixtures
|
|
93
|
+
|
|
94
|
+
Realistic test data in `src/__tests__/fixtures/`:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import {
|
|
98
|
+
WALLET_ID_1, // "0x0a00000000000001"
|
|
99
|
+
WALLET_ID_2, // "0x0a00000000000002"
|
|
100
|
+
FAUCET_ID, // "0x0a00000000000003"
|
|
101
|
+
COUNTER_ID, // "0x0a00000000000004"
|
|
102
|
+
MOCK_WALLET_HEADER, // { id, nonce, storageCommitment }
|
|
103
|
+
MOCK_FAUCET_HEADER, // { id, nonce, storageCommitment }
|
|
104
|
+
MOCK_ASSET_BALANCE, // { assetId, amount: 1000000000n, symbol: "TEST", decimals: 8 }
|
|
105
|
+
MOCK_ACCOUNT, // { id, nonce, bech32id() }
|
|
106
|
+
MOCK_TRANSACTION_RESULT, // { transactionId: "0x..." } - useMint / useConsume / useSwap / useMultiSend / useTransaction
|
|
107
|
+
MOCK_SEND_RESULT, // { txId: "0x...", note: null } - useSend
|
|
108
|
+
MOCK_NOTE_SUMMARY, // { id, assets, sender }
|
|
109
|
+
} from "@/__tests__/fixtures";
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Key characteristics:
|
|
113
|
+
- Account IDs use hex format (`0x...`) - network-agnostic test fixtures
|
|
114
|
+
- Amounts are `bigint` (e.g., `1000000000n` = 10.0 with 8 decimals)
|
|
115
|
+
- Asset metadata uses TEST token with 8 decimals
|
|
116
|
+
|
|
117
|
+
## Test Patterns (copy-adaptable)
|
|
118
|
+
|
|
119
|
+
Reference tests in `src/__tests__/patterns/`:
|
|
120
|
+
|
|
121
|
+
| Pattern | File | Tests |
|
|
122
|
+
|---------|------|-------|
|
|
123
|
+
| Provider/context setup | `provider-setup.test.tsx` | ready, loading, error states |
|
|
124
|
+
| Query hook component | `query-hook.test.tsx` | data, loading, error, empty states |
|
|
125
|
+
| Mutation hook component | `mutation-hook.test.tsx` | idle, stages, success, error, argument verification |
|
|
126
|
+
|
|
127
|
+
### Minimum test coverage per component
|
|
128
|
+
|
|
129
|
+
Every component test should cover:
|
|
130
|
+
1. **Success state** - renders correctly with data
|
|
131
|
+
2. **Loading state** - shows loading indicator
|
|
132
|
+
3. **Error state** - shows error message, recovery action
|
|
133
|
+
4. **User interactions** - buttons, forms trigger correct handler calls
|
|
134
|
+
|
|
135
|
+
## Wallet connection state in tests
|
|
136
|
+
|
|
137
|
+
This template's wallet button (`src/components/AppContent.tsx`) drives off **`useMidenFiWallet()`** from `@miden-sdk/miden-wallet-adapter-react`, not the generic `useSigner()`. The button gates on `wallet.readyState` (from `@miden-sdk/miden-wallet-adapter-base`) so the UI can render an "Install MidenFi Wallet" state before the extension is detected, rather than falling through to the adapter's Chrome-Web-Store fallback. When testing wallet-connect UI, mock both modules and override per test.
|
|
138
|
+
|
|
139
|
+
The mock factory must return the **full `WalletContextState`** shape - `useIncrementCounter` reads `address` and `requestTransaction` directly off the hook return, and the wallet button reads `wallet.readyState`. A partial mock will compile (with broad casts) and silently miss contract drift. Setup:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
143
|
+
vi.mock("@miden-sdk/miden-wallet-adapter-react", () => ({
|
|
144
|
+
useMidenFiWallet: vi.fn(() => ({
|
|
145
|
+
autoConnect: false,
|
|
146
|
+
wallets: [],
|
|
147
|
+
wallet: null,
|
|
148
|
+
address: null,
|
|
149
|
+
publicKey: null,
|
|
150
|
+
connected: false,
|
|
151
|
+
connecting: false,
|
|
152
|
+
disconnecting: false,
|
|
153
|
+
select: vi.fn(),
|
|
154
|
+
connect: vi.fn(async () => undefined),
|
|
155
|
+
disconnect: vi.fn(async () => undefined),
|
|
156
|
+
requestTransaction: vi.fn(async () => "0xtx"),
|
|
157
|
+
requestAssets: undefined,
|
|
158
|
+
requestPrivateNotes: undefined,
|
|
159
|
+
signBytes: undefined,
|
|
160
|
+
importPrivateNote: undefined,
|
|
161
|
+
requestConsumableNotes: undefined,
|
|
162
|
+
waitForTransaction: undefined,
|
|
163
|
+
requestSend: undefined,
|
|
164
|
+
requestConsume: undefined,
|
|
165
|
+
createAccount: undefined,
|
|
166
|
+
})),
|
|
167
|
+
}));
|
|
168
|
+
vi.mock("@miden-sdk/miden-wallet-adapter-base", () => ({
|
|
169
|
+
WalletReadyState: {
|
|
170
|
+
Installed: "Installed",
|
|
171
|
+
NotDetected: "NotDetected",
|
|
172
|
+
Loadable: "Loadable",
|
|
173
|
+
Unsupported: "Unsupported",
|
|
174
|
+
},
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Use a typed factory for per-test overrides - `WalletContextState` is the `useMidenFiWallet()` return type:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
type WalletState = ReturnType<typeof useMidenFiWallet>;
|
|
184
|
+
type WalletInner = NonNullable<WalletState["wallet"]>;
|
|
185
|
+
|
|
186
|
+
function walletState(
|
|
187
|
+
overrides: Partial<{
|
|
188
|
+
readyState: "Installed" | "NotDetected" | "Loadable" | "Unsupported";
|
|
189
|
+
connected: boolean;
|
|
190
|
+
address: string | null;
|
|
191
|
+
requestTransaction: WalletState["requestTransaction"];
|
|
192
|
+
}> = {},
|
|
193
|
+
): WalletState {
|
|
194
|
+
const {
|
|
195
|
+
readyState = "Installed",
|
|
196
|
+
connected = false,
|
|
197
|
+
address = connected ? "mtst1arwk88k8smzcq5p30upr6eerw5npmnyz" : null,
|
|
198
|
+
requestTransaction = vi.fn(async () => "0xtx"),
|
|
199
|
+
} = overrides;
|
|
200
|
+
// The inner Wallet's `adapter` is an `Adapter` (eventemitter + polling
|
|
201
|
+
// strategy) - we stub it structurally because the components under test
|
|
202
|
+
// only read `readyState` off the inner wallet object.
|
|
203
|
+
const innerWallet = {
|
|
204
|
+
adapter: {} as WalletInner["adapter"],
|
|
205
|
+
readyState,
|
|
206
|
+
} as WalletInner;
|
|
207
|
+
return {
|
|
208
|
+
autoConnect: false,
|
|
209
|
+
wallets: [innerWallet],
|
|
210
|
+
wallet: innerWallet,
|
|
211
|
+
address,
|
|
212
|
+
publicKey: null,
|
|
213
|
+
connected,
|
|
214
|
+
connecting: false,
|
|
215
|
+
disconnecting: false,
|
|
216
|
+
select: vi.fn(),
|
|
217
|
+
connect: vi.fn(async () => undefined),
|
|
218
|
+
disconnect: vi.fn(async () => undefined),
|
|
219
|
+
requestTransaction,
|
|
220
|
+
requestAssets: undefined,
|
|
221
|
+
requestPrivateNotes: undefined,
|
|
222
|
+
signBytes: undefined,
|
|
223
|
+
importPrivateNote: undefined,
|
|
224
|
+
requestConsumableNotes: undefined,
|
|
225
|
+
waitForTransaction: undefined,
|
|
226
|
+
requestSend: undefined,
|
|
227
|
+
requestConsume: undefined,
|
|
228
|
+
createAccount: undefined,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// extension not detected - shows disabled "Install MidenFi Wallet"
|
|
233
|
+
vi.mocked(useMidenFiWallet).mockReturnValue(
|
|
234
|
+
walletState({ readyState: "NotDetected" }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// installed + connected with an account - shows "Disconnect Wallet"
|
|
238
|
+
vi.mocked(useMidenFiWallet).mockReturnValue(
|
|
239
|
+
walletState({ readyState: "Installed", connected: true }),
|
|
240
|
+
);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The factory satisfies `WalletContextState` without `as unknown as` over the whole object - the only narrow `as` is the inner adapter stub, which is unavoidable until we want to construct a real `Adapter` in tests. See `src/components/__tests__/AppContent.test.tsx` for the canonical version.
|
|
244
|
+
|
|
245
|
+
For app code that needs the selected signer account for client-side flows (transaction-building hooks, etc.), `useMiden()` exposes `signerAccountId` / `signerConnected` as lower-level provider state - mock those via the `@miden-sdk/react` mock factory.
|
|
246
|
+
|
|
247
|
+
Vitest config externalizes `@miden-sdk/miden-wallet-adapter-react` to prevent broken transitive resolution.
|
|
248
|
+
|
|
249
|
+
## Mocking Classes Called with `new` (Vitest v4)
|
|
250
|
+
|
|
251
|
+
Vitest v4 enforces that mock implementations passed to `vi.fn()` must be `function` declarations (not arrow functions) when the mocked function is invoked with `new`. Arrow functions cannot be called as constructors and will throw `TypeError: ... is not a constructor`.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
// WRONG: arrow function - throws when production code does `new MidenClient(...)`
|
|
255
|
+
vi.mock("@miden-sdk/miden-sdk", () => ({
|
|
256
|
+
MidenClient: vi.fn(() => ({ /* ... */ })),
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
// RIGHT: function expression - usable with `new`
|
|
260
|
+
vi.mock("@miden-sdk/miden-sdk", () => ({
|
|
261
|
+
MidenClient: vi.fn(function () {
|
|
262
|
+
return { /* ... */ };
|
|
263
|
+
}),
|
|
264
|
+
}));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
This applies to any class mocked at module level that production code instantiates with `new` (`new MidenClient(...)`, `new WasmWebClient(...)`, etc.). When tests fail with `TypeError: ... is not a constructor` after a Vitest v4 upgrade, swap the arrow-function bodies for `vi.fn(function () { ... })`.
|
|
268
|
+
|
|
269
|
+
For component-level wallet adapters and hooks that are function references rather than classes (the existing `vi.mock("@miden-sdk/miden-wallet-adapter-react", ...)` example above), arrow-function mocks remain fine.
|
|
270
|
+
|
|
271
|
+
## Testing Time-Dependent Code (Network Sync Delay)
|
|
272
|
+
|
|
273
|
+
Production code that polls or waits on chain state should accept the delay interval as an injectable parameter rather than hardcoding it. This lets tests replace the production default (e.g. `5000` ms) with `0` so the loop drains synchronously without `vi.useFakeTimers()` plumbing.
|
|
274
|
+
|
|
275
|
+
Pattern:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// Production: optional delay parameter with a sensible default
|
|
279
|
+
export function pollUntilCommit(
|
|
280
|
+
txId: string,
|
|
281
|
+
intervalMs = 5000, // production default
|
|
282
|
+
) {
|
|
283
|
+
// ... uses setTimeout(..., intervalMs) or `await sleep(intervalMs)`
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Tests: pass 0 to skip waits
|
|
287
|
+
const result = await pollUntilCommit(txId, 0);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
When the value comes from `src/config.ts` (e.g. `NETWORK_SYNC_DELAY_MS`), expose the same override there so tests can stub it via `vi.mock("@/config", ...)` without touching app code:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// src/config.ts
|
|
294
|
+
export const NETWORK_SYNC_DELAY_MS = Number(import.meta.env.VITE_NETWORK_SYNC_DELAY_MS ?? 5000);
|
|
295
|
+
|
|
296
|
+
// test
|
|
297
|
+
vi.mock("@/config", () => ({ NETWORK_SYNC_DELAY_MS: 0 }));
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Document the production default and the test override at the call site so the contract between app code and tests is obvious.
|
|
301
|
+
|
|
302
|
+
## Automated Verification Pipeline
|
|
303
|
+
|
|
304
|
+
Hooks in `.claude/settings.json` enforce quality automatically:
|
|
305
|
+
|
|
306
|
+
1. **PostToolUse: typecheck** - `npx tsc -b --noEmit` on every `.ts`/`.tsx` edit in `src/`
|
|
307
|
+
2. **PostToolUse: affected tests** - `npx vitest --changed --run` on every `.ts`/`.tsx` edit in `src/`
|
|
308
|
+
3. **PostToolUse: full suite** - Full `vitest --run && tsc -b --noEmit && vite build` after each edit
|
|
309
|
+
|
|
310
|
+
If any hook fails (exit code 2), the agent is blocked from proceeding until the issue is fixed.
|
|
311
|
+
|
|
312
|
+
## TDD Flow
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
1. Write test (describe expected behavior)
|
|
316
|
+
↓
|
|
317
|
+
2. yarn test → RED (test fails)
|
|
318
|
+
↓
|
|
319
|
+
3. Implement code
|
|
320
|
+
↓
|
|
321
|
+
4. Auto hooks fire → typecheck + affected tests
|
|
322
|
+
↓
|
|
323
|
+
5. yarn test → GREEN (all pass)
|
|
324
|
+
↓
|
|
325
|
+
6. Refactor if needed
|
|
326
|
+
↓
|
|
327
|
+
7. Task complete → full suite already ran after last edit
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Common Mistakes
|
|
331
|
+
|
|
332
|
+
**Forgetting vi.clearAllMocks()**: Always call in `beforeEach` to prevent mock state leaking between tests.
|
|
333
|
+
|
|
334
|
+
**Not mocking the SDK**: Components importing from `@miden-sdk/react` will fail without `vi.mock()` because the real SDK requires WASM initialization.
|
|
335
|
+
|
|
336
|
+
**Using number instead of bigint**: Mock amounts must use `bigint` (`1000n`, not `1000`). The SDK enforces this at the type level.
|
|
337
|
+
|
|
338
|
+
**Testing implementation details**: Test what the user sees (text, buttons, states), not internal hook calls. Use `screen.getByRole`, `screen.getByText`, not internal component state.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vite-wasm-setup
|
|
3
|
+
description: Guide to configuring Vite for Miden WASM applications. Covers the midenVitePlugin() setup, COOP/COEP headers, production deployment headers, TypeScript compatibility, and troubleshooting common Vite + WASM issues. Use when setting up a new Miden frontend, debugging build or runtime errors related to WASM or Vite configuration, or deploying to production.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vite + WASM Configuration for Miden
|
|
7
|
+
|
|
8
|
+
## Required vite.config.ts
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { defineConfig } from "vite";
|
|
12
|
+
import react from "@vitejs/plugin-react";
|
|
13
|
+
import { midenVitePlugin } from "@miden-sdk/vite-plugin";
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pass `crossOriginIsolation: true` explicitly. The Miden WASM client uses `SharedArrayBuffer` via Rust atomics, which is only available when the page is cross-origin-isolated (COOP `same-origin` + COEP `require-corp`). Don't rely on the plugin's own default — it has shifted across releases, so the template's config is source-of-truth.
|
|
21
|
+
|
|
22
|
+
If your app must host third-party iframes, OAuth popups, or other cross-origin resources that don't emit `require-corp`, either (a) embed them via `credentialless` COEP as a workaround (see the Gotchas section below), or (b) set `crossOriginIsolation: false` and accept that Miden client operations won't work on that route.
|
|
23
|
+
|
|
24
|
+
## What midenVitePlugin() Handles
|
|
25
|
+
|
|
26
|
+
`@miden-sdk/vite-plugin` abstracts Miden-specific Vite configuration:
|
|
27
|
+
|
|
28
|
+
- **WASM loading** — Configures Vite to correctly import `.wasm` modules
|
|
29
|
+
- **Top-level await** — Enables top-level `await` required by the WASM SDK initialization
|
|
30
|
+
- **optimizeDeps** — Excludes `@miden-sdk/miden-sdk` from pre-bundling (pre-bundling corrupts the WASM binary)
|
|
31
|
+
- **COOP/COEP headers** — Emits `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: require-corp` on the dev server when `crossOriginIsolation: true`
|
|
32
|
+
|
|
33
|
+
You don't need to install or configure `vite-plugin-wasm`, `vite-plugin-top-level-await`, or dexie aliases manually.
|
|
34
|
+
|
|
35
|
+
## Required Dependencies
|
|
36
|
+
|
|
37
|
+
Keep all `@miden-sdk/*` runtime packages aligned. The template's `package.json` pins them as an exact-version set; upgrade all four together and re-run the full verification suite (including the wallet-confirmed increment E2E) whenever you bump.
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@miden-sdk/react": "<matches miden-sdk>",
|
|
43
|
+
"@miden-sdk/miden-sdk": "<authoritative version>",
|
|
44
|
+
"@miden-sdk/miden-wallet-adapter-base": "<may lag by a patch>",
|
|
45
|
+
"@miden-sdk/miden-wallet-adapter-react": "<may lag by a patch>"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@miden-sdk/vite-plugin": "<matches miden-sdk>"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- **Always check `package.json` for the authoritative versions** — this skill intentionally doesn't inline them because they shift across SDK releases.
|
|
55
|
+
- The wallet adapter packages are versioned separately from the core SDK. Their `peerDependencies` typically allow `^<major>.<minor>.x`, so a patch-level gap between the adapter and the core SDK is expected and fine.
|
|
56
|
+
- When you bump, clean-install: `rm -rf node_modules yarn.lock && yarn install`. Vite's dep optimizer caches resolved SDK paths, and stale caches can surface as `ERR_BLOCKED_BY_RESPONSE` or spurious `Failed to fetch` errors on module workers.
|
|
57
|
+
|
|
58
|
+
## Production Deployment Headers
|
|
59
|
+
|
|
60
|
+
COOP/COEP headers must be set on the production server. `midenVitePlugin({ crossOriginIsolation: true })` only affects the Vite dev server.
|
|
61
|
+
|
|
62
|
+
### Nginx
|
|
63
|
+
```nginx
|
|
64
|
+
add_header Cross-Origin-Opener-Policy same-origin;
|
|
65
|
+
add_header Cross-Origin-Embedder-Policy require-corp;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Vercel (vercel.json)
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"headers": [
|
|
72
|
+
{
|
|
73
|
+
"source": "/(.*)",
|
|
74
|
+
"headers": [
|
|
75
|
+
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
76
|
+
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Cloudflare Pages (_headers)
|
|
84
|
+
```
|
|
85
|
+
/*
|
|
86
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
87
|
+
Cross-Origin-Embedder-Policy: require-corp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### WASM MIME Type
|
|
91
|
+
Ensure your server serves `.wasm` files with `application/wasm` MIME type.
|
|
92
|
+
|
|
93
|
+
## COOP/COEP Gotchas
|
|
94
|
+
|
|
95
|
+
These headers break:
|
|
96
|
+
- **Third-party iframes** (YouTube embeds, Twitter embeds, analytics)
|
|
97
|
+
- **External scripts** without CORS headers
|
|
98
|
+
- **OAuth popups** from different origins
|
|
99
|
+
|
|
100
|
+
Workaround: Use `credentialless` for COEP if you need cross-origin resources:
|
|
101
|
+
```
|
|
102
|
+
Cross-Origin-Embedder-Policy: credentialless
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Note: `credentialless` provides weaker isolation but allows most cross-origin resources.
|
|
106
|
+
|
|
107
|
+
## TypeScript Compatibility
|
|
108
|
+
|
|
109
|
+
Standard Vite-compatible tsconfig settings work with Miden. The only actual constraint is ES2020+ for `bigint` support:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"compilerOptions": {
|
|
114
|
+
"target": "ES2022",
|
|
115
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
116
|
+
"module": "ESNext",
|
|
117
|
+
"moduleResolution": "bundler"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`module: "ESNext"` and `moduleResolution: "bundler"` are standard Vite defaults, not Miden-specific requirements. If you're using the Vite-generated tsconfig, no changes are needed beyond ensuring `target` is ES2020+.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
| Issue | Cause | Fix |
|
|
127
|
+
|-------|-------|-----|
|
|
128
|
+
| "SharedArrayBuffer is not defined" | COOP/COEP headers not reaching the browser | Verify `midenVitePlugin({ crossOriginIsolation: true })` is in plugins; check production server headers separately |
|
|
129
|
+
| WASM module not found | SDK not configured correctly | Ensure `midenVitePlugin()` is in plugins array |
|
|
130
|
+
| "Top-level await not supported" | Missing plugin setup | Ensure `midenVitePlugin()` is in plugins array |
|
|
131
|
+
| WASM init hangs | COEP blocking WASM fetch | Check network tab for blocked requests; verify COOP/COEP headers are present |
|
|
132
|
+
| Build succeeds but WASM fails at runtime | Wrong MIME type | Serve .wasm as application/wasm |
|
|
133
|
+
| "recursive use of an object" | Concurrent WASM access | Use runExclusive() from useMiden() |
|
|
134
|
+
| Double initialization in dev | React StrictMode | Use MidenProvider (handles this internally) |
|