create-miden-app 1.0.4 → 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 -1
- 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 +1580 -781
- package/template/src/miden/lib/demo.ts +0 -106
package/template/index.html
CHANGED
|
@@ -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>
|
|
7
|
+
<title>Miden Template</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
package/template/package.json
CHANGED
|
@@ -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
|
-
"@miden-sdk/miden-
|
|
14
|
-
"
|
|
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": "^
|
|
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
|
-
"
|
|
37
|
+
"jsdom": "^28.1.0",
|
|
38
|
+
"typescript": "~5.7.0",
|
|
29
39
|
"typescript-eslint": "^8.45.0",
|
|
30
|
-
"vite": "^
|
|
31
|
-
"
|
|
32
|
-
"vite-plugin-wasm": "^3.5.0"
|
|
40
|
+
"vite": "^6.0.0",
|
|
41
|
+
"vitest": "^4.0.18"
|
|
33
42
|
}
|
|
34
43
|
}
|
|
Binary file
|
|
Binary file
|
package/template/src/App.tsx
CHANGED
|
@@ -1,63 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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
|
-
<
|
|
31
|
-
|
|
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
|
+
});
|