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,257 @@
|
|
|
1
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
2
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Stub fetch — `increment` fetches `/packages/increment_note.masp`. We never
|
|
5
|
+
// reach Note construction in any test below (the path either short-circuits
|
|
6
|
+
// on a missing wallet address or is short-circuited before `requestTransaction`
|
|
7
|
+
// completes), but the fetch itself runs once and we don't want jsdom to error
|
|
8
|
+
// out on an unhandled network request.
|
|
9
|
+
const mockFetch = vi.fn(async () => ({
|
|
10
|
+
arrayBuffer: async () => new ArrayBuffer(0),
|
|
11
|
+
}));
|
|
12
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
13
|
+
|
|
14
|
+
vi.mock("@miden-sdk/react", () => import("@/__tests__/mocks/miden-sdk-react"));
|
|
15
|
+
|
|
16
|
+
// `useIncrementCounter` reads `requestTransaction` and `address` directly from
|
|
17
|
+
// the hook return per `WalletContextState`. We mock the wallet-adapter-react
|
|
18
|
+
// package and provide a default disconnected stub. Individual tests override
|
|
19
|
+
// via `vi.mocked(useMidenFiWallet).mockReturnValue(...)`.
|
|
20
|
+
const defaultWallet = {
|
|
21
|
+
autoConnect: false,
|
|
22
|
+
wallets: [],
|
|
23
|
+
wallet: null,
|
|
24
|
+
address: null as string | null,
|
|
25
|
+
publicKey: null,
|
|
26
|
+
connected: false,
|
|
27
|
+
connecting: false,
|
|
28
|
+
disconnecting: false,
|
|
29
|
+
select: vi.fn(),
|
|
30
|
+
connect: vi.fn(async () => undefined),
|
|
31
|
+
disconnect: vi.fn(async () => undefined),
|
|
32
|
+
requestTransaction: vi.fn(async () => "0xtx"),
|
|
33
|
+
requestAssets: undefined,
|
|
34
|
+
requestPrivateNotes: undefined,
|
|
35
|
+
signBytes: undefined,
|
|
36
|
+
importPrivateNote: undefined,
|
|
37
|
+
requestConsumableNotes: undefined,
|
|
38
|
+
waitForTransaction: undefined,
|
|
39
|
+
requestSend: undefined,
|
|
40
|
+
requestConsume: undefined,
|
|
41
|
+
createAccount: undefined,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mockGetAccount = vi.fn(async () => null);
|
|
45
|
+
const mockImportAccountById = vi.fn(async () => undefined);
|
|
46
|
+
const mockSyncState = vi.fn(async () => undefined);
|
|
47
|
+
|
|
48
|
+
vi.mock("@miden-sdk/miden-wallet-adapter-react", () => ({
|
|
49
|
+
useMidenFiWallet: vi.fn(() => defaultWallet),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// `useIncrementCounter` calls into many SDK constructors (Word/Felt/AccountId,
|
|
53
|
+
// note builders, etc.) and chained builder methods like
|
|
54
|
+
// `TransactionRequestBuilder().withOwnOutputNotes(...).build()`. We don't need
|
|
55
|
+
// real values from these in unit tests — only that calls don't throw and the
|
|
56
|
+
// hook can reach the poll loop. A Proxy-backed stub satisfies all chained
|
|
57
|
+
// calls (`new X(...)`, `X.staticMethod(...)`, `instance.foo().bar(...)`) by
|
|
58
|
+
// returning another callable+constructable stub for any property access.
|
|
59
|
+
//
|
|
60
|
+
// The one shape we DO need real data from is `Word.toU64s()` (the storage-map
|
|
61
|
+
// value the hook reads to derive the count). The proxy intercepts that
|
|
62
|
+
// specific access and returns a 4-tuple BigUint64Array-like value.
|
|
63
|
+
vi.mock("@miden-sdk/miden-sdk", async () => {
|
|
64
|
+
const stub = (): object =>
|
|
65
|
+
new Proxy(function noop() {}, {
|
|
66
|
+
get: (_t, prop) => {
|
|
67
|
+
if (prop === "toU64s") return () => [0n, 0n, 0n, 0n];
|
|
68
|
+
// Avoid breaking Promise resolution / native Symbol checks
|
|
69
|
+
if (typeof prop === "symbol") return undefined;
|
|
70
|
+
return stub();
|
|
71
|
+
},
|
|
72
|
+
apply: () => stub(),
|
|
73
|
+
construct: () => stub(),
|
|
74
|
+
});
|
|
75
|
+
const exports: Record<string, unknown> = {};
|
|
76
|
+
for (const k of [
|
|
77
|
+
"TransactionRequestBuilder",
|
|
78
|
+
"Package",
|
|
79
|
+
"NoteScript",
|
|
80
|
+
"Note",
|
|
81
|
+
"NoteAssets",
|
|
82
|
+
"NoteMetadata",
|
|
83
|
+
"NoteRecipient",
|
|
84
|
+
"NoteStorage",
|
|
85
|
+
"NoteTag",
|
|
86
|
+
"NoteType",
|
|
87
|
+
"NoteAttachment",
|
|
88
|
+
"NoteExecutionHint",
|
|
89
|
+
"NoteArray",
|
|
90
|
+
"AccountId",
|
|
91
|
+
"Felt",
|
|
92
|
+
"FeltArray",
|
|
93
|
+
"Word",
|
|
94
|
+
]) {
|
|
95
|
+
exports[k] = stub();
|
|
96
|
+
}
|
|
97
|
+
return exports;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
vi.mock("@miden-sdk/miden-wallet-adapter-base", () => ({
|
|
101
|
+
Transaction: { createCustomTransaction: vi.fn(() => ({})) },
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
vi.mock("@/lib/miden", () => ({ randomWord: () => ({}) }));
|
|
105
|
+
|
|
106
|
+
import { useMiden, useMidenClient } from "@miden-sdk/react";
|
|
107
|
+
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
|
|
108
|
+
import { useIncrementCounter } from "../useIncrementCounter";
|
|
109
|
+
import {
|
|
110
|
+
NETWORK_POLL_INTERVAL_MS,
|
|
111
|
+
NETWORK_POLL_TIMEOUT_MS,
|
|
112
|
+
} from "@/config";
|
|
113
|
+
|
|
114
|
+
const COUNTER_ADDRESS = "mtst1aqmx7qv6h3y92sqsmunh8uht4ujmfy4j";
|
|
115
|
+
|
|
116
|
+
describe("useIncrementCounter", () => {
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
vi.useRealTimers();
|
|
119
|
+
vi.clearAllMocks();
|
|
120
|
+
mockGetAccount.mockReset();
|
|
121
|
+
mockImportAccountById.mockReset();
|
|
122
|
+
mockSyncState.mockReset();
|
|
123
|
+
mockFetch.mockClear();
|
|
124
|
+
|
|
125
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
126
|
+
client: null,
|
|
127
|
+
isReady: true,
|
|
128
|
+
isInitializing: false,
|
|
129
|
+
error: null,
|
|
130
|
+
sync: vi.fn(),
|
|
131
|
+
runExclusive: <T,>(fn: () => Promise<T>) => fn(),
|
|
132
|
+
prover: null,
|
|
133
|
+
signerAccountId: null,
|
|
134
|
+
signerConnected: null,
|
|
135
|
+
});
|
|
136
|
+
vi.mocked(useMidenClient).mockReturnValue({
|
|
137
|
+
getAccount: mockGetAccount,
|
|
138
|
+
importAccountById: mockImportAccountById,
|
|
139
|
+
syncState: mockSyncState,
|
|
140
|
+
} as unknown as ReturnType<typeof useMidenClient>);
|
|
141
|
+
vi.mocked(useMidenFiWallet).mockReturnValue(defaultWallet);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("surfaces an error when increment is called without a wallet address", async () => {
|
|
145
|
+
// Default stub has `address: null` (wallet not connected to an account).
|
|
146
|
+
// Make the mount-time loadCount succeed so its error path doesn't race
|
|
147
|
+
// with the one we're asserting against.
|
|
148
|
+
const fakeAccount = {
|
|
149
|
+
storage: () => ({ getMapItem: () => null }),
|
|
150
|
+
};
|
|
151
|
+
mockGetAccount.mockResolvedValue(fakeAccount as never);
|
|
152
|
+
|
|
153
|
+
const { result } = renderHook(() => useIncrementCounter(COUNTER_ADDRESS));
|
|
154
|
+
// Wait for mount-effect to finish (count resolves from null → 0).
|
|
155
|
+
await waitFor(() => expect(result.current.count).toBe(0));
|
|
156
|
+
|
|
157
|
+
await act(async () => {
|
|
158
|
+
await result.current.increment();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result.current.error).toMatch(/no wallet account available/i);
|
|
162
|
+
expect(result.current.isSubmitting).toBe(false);
|
|
163
|
+
// requestTransaction must NOT have been called — we short-circuit before
|
|
164
|
+
// touching the wallet.
|
|
165
|
+
expect(defaultWallet.requestTransaction).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("surfaces an error when the counter account is unreachable on-chain", async () => {
|
|
169
|
+
// import + retry both return null → should setError, not stay silent.
|
|
170
|
+
mockGetAccount.mockResolvedValue(null);
|
|
171
|
+
|
|
172
|
+
const { result } = renderHook(() => useIncrementCounter(COUNTER_ADDRESS));
|
|
173
|
+
|
|
174
|
+
await waitFor(() => {
|
|
175
|
+
expect(result.current.error).toMatch(/counter account not found/i);
|
|
176
|
+
});
|
|
177
|
+
expect(result.current.count).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not call useMiden().sync() during a poll iteration", async () => {
|
|
181
|
+
// Wallet must be connected so increment doesn't short-circuit before the
|
|
182
|
+
// poll loop. Provide a real address + a working `requestTransaction`.
|
|
183
|
+
const requestTransaction = vi.fn(async () => "0xtx");
|
|
184
|
+
vi.mocked(useMidenFiWallet).mockReturnValue({
|
|
185
|
+
...defaultWallet,
|
|
186
|
+
address: "mtst1arwk88k8smzcq5p30upr6eerw5npmnyz",
|
|
187
|
+
connected: true,
|
|
188
|
+
requestTransaction,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Spy on the hook-level `sync()`. The fix this test guards against would
|
|
192
|
+
// call this inside the poll loop; production code now does not.
|
|
193
|
+
const sync = vi.fn(async () => undefined);
|
|
194
|
+
vi.mocked(useMiden).mockReturnValue({
|
|
195
|
+
client: null,
|
|
196
|
+
isReady: true,
|
|
197
|
+
isInitializing: false,
|
|
198
|
+
error: null,
|
|
199
|
+
sync,
|
|
200
|
+
runExclusive: <T,>(fn: () => Promise<T>) => fn(),
|
|
201
|
+
prover: null,
|
|
202
|
+
signerAccountId: null,
|
|
203
|
+
signerConnected: null,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Storage-map value drives `count`. `Word.toU64s()` already resolves to
|
|
207
|
+
// [0n, 0n, 0n, 0n] via the SDK proxy stub above, so `loadCount()` always
|
|
208
|
+
// returns 0 → `previousCount === latest`. The loop will keep ticking
|
|
209
|
+
// until the deadline; we only need ONE iteration to elapse, then stop.
|
|
210
|
+
mockGetAccount.mockResolvedValue({
|
|
211
|
+
storage: () => ({ getMapItem: () => ({ toU64s: () => [0n, 0n, 0n, 0n] }) }),
|
|
212
|
+
} as never);
|
|
213
|
+
|
|
214
|
+
vi.useFakeTimers();
|
|
215
|
+
try {
|
|
216
|
+
const { result } = renderHook(() => useIncrementCounter(COUNTER_ADDRESS));
|
|
217
|
+
|
|
218
|
+
// Wait for mount-effect's loadCount to settle.
|
|
219
|
+
await vi.waitFor(() => {
|
|
220
|
+
expect(result.current.count).toBe(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Snapshot sync-call count before increment so we measure only what
|
|
224
|
+
// happens during the click + poll iteration.
|
|
225
|
+
expect(sync).not.toHaveBeenCalled();
|
|
226
|
+
|
|
227
|
+
// Trigger increment without awaiting; the call schedules the poll loop.
|
|
228
|
+
let incrementPromise: Promise<void> | undefined;
|
|
229
|
+
await act(async () => {
|
|
230
|
+
incrementPromise = result.current.increment();
|
|
231
|
+
// Let microtasks settle for the requestTransaction await.
|
|
232
|
+
await Promise.resolve();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Advance past one poll interval (2.5s) — exactly one iteration runs.
|
|
236
|
+
await act(async () => {
|
|
237
|
+
await vi.advanceTimersByTimeAsync(NETWORK_POLL_INTERVAL_MS + 50);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// The redundant `await sync()` was removed from the poll loop. After
|
|
241
|
+
// one iteration, `useMiden().sync` must not have been called.
|
|
242
|
+
expect(sync).not.toHaveBeenCalled();
|
|
243
|
+
|
|
244
|
+
// requestTransaction was reached — proves the loop actually ran.
|
|
245
|
+
expect(requestTransaction).toHaveBeenCalledTimes(1);
|
|
246
|
+
|
|
247
|
+
// Drain the rest of the deadline so the increment promise resolves
|
|
248
|
+
// cleanly and React unmount can finish.
|
|
249
|
+
await act(async () => {
|
|
250
|
+
await vi.advanceTimersByTimeAsync(NETWORK_POLL_TIMEOUT_MS);
|
|
251
|
+
await incrementPromise;
|
|
252
|
+
});
|
|
253
|
+
} finally {
|
|
254
|
+
vi.useRealTimers();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useMiden, useMidenClient } from "@miden-sdk/react";
|
|
3
|
+
// `useMidenFiWallet()` returns `WalletContextState` (see
|
|
4
|
+
// `@miden-sdk/miden-wallet-adapter-react/dist/MidenFiSignerProvider.d.ts`),
|
|
5
|
+
// which exposes `address`, `connected`, and `requestTransaction` directly
|
|
6
|
+
// at the top level — distinct from the inner `wallet` field on the same
|
|
7
|
+
// return (which is a `Wallet` adapter object, not the address).
|
|
8
|
+
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
|
|
9
|
+
import { Transaction } from "@miden-sdk/miden-wallet-adapter-base";
|
|
10
|
+
import {
|
|
11
|
+
TransactionRequestBuilder,
|
|
12
|
+
Package,
|
|
13
|
+
NoteScript,
|
|
14
|
+
Note,
|
|
15
|
+
NoteAssets,
|
|
16
|
+
NoteMetadata,
|
|
17
|
+
NoteRecipient,
|
|
18
|
+
NoteStorage,
|
|
19
|
+
NoteTag,
|
|
20
|
+
NoteType,
|
|
21
|
+
NoteAttachment,
|
|
22
|
+
NoteExecutionHint,
|
|
23
|
+
NoteArray,
|
|
24
|
+
AccountId,
|
|
25
|
+
Felt,
|
|
26
|
+
FeltArray,
|
|
27
|
+
Word,
|
|
28
|
+
} from "@miden-sdk/miden-sdk";
|
|
29
|
+
import { randomWord } from "@/lib/miden";
|
|
30
|
+
import {
|
|
31
|
+
COUNTER_SLOT_NAME,
|
|
32
|
+
EXPLORER_BASE_URL,
|
|
33
|
+
NETWORK_POLL_INTERVAL_MS,
|
|
34
|
+
NETWORK_POLL_TIMEOUT_MS,
|
|
35
|
+
} from "@/config";
|
|
36
|
+
|
|
37
|
+
export function useIncrementCounter(counterAddress: string) {
|
|
38
|
+
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
40
|
+
const [isWaiting, setIsWaiting] = useState(false);
|
|
41
|
+
const [count, setCount] = useState<number | null>(null);
|
|
42
|
+
|
|
43
|
+
const { runExclusive, isReady } = useMiden();
|
|
44
|
+
const {
|
|
45
|
+
address: walletAddress,
|
|
46
|
+
connected: walletConnected,
|
|
47
|
+
requestTransaction,
|
|
48
|
+
} = useMidenFiWallet();
|
|
49
|
+
const client = useMidenClient();
|
|
50
|
+
|
|
51
|
+
// Fetch the on-chain counter value. Imports the counter account on first
|
|
52
|
+
// call, syncs from the network, and reads the storage map. All WASM calls
|
|
53
|
+
// are serialized via runExclusive to avoid "recursive use of an object"
|
|
54
|
+
// errors. Returns the fetched count (or null if the account is unreachable)
|
|
55
|
+
// and also updates component state.
|
|
56
|
+
const loadCount = useCallback(async (): Promise<number | null> => {
|
|
57
|
+
if (!isReady || !counterAddress) return null;
|
|
58
|
+
return await runExclusive(async () => {
|
|
59
|
+
const counterAccountId = AccountId.fromBech32(counterAddress);
|
|
60
|
+
if (!(await client.getAccount(counterAccountId))) {
|
|
61
|
+
await client.importAccountById(counterAccountId);
|
|
62
|
+
}
|
|
63
|
+
await client.syncState();
|
|
64
|
+
|
|
65
|
+
const account = await client.getAccount(counterAccountId);
|
|
66
|
+
if (!account) {
|
|
67
|
+
setCount(null);
|
|
68
|
+
setError(
|
|
69
|
+
`Counter account not found on-chain (${counterAddress}). Check VITE_MIDEN_COUNTER_ADDRESS / src/config.ts and confirm the counter is deployed on the configured network.`,
|
|
70
|
+
);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const countKey = Word.newFromFelts([
|
|
74
|
+
new Felt(0n),
|
|
75
|
+
new Felt(0n),
|
|
76
|
+
new Felt(0n),
|
|
77
|
+
new Felt(1n),
|
|
78
|
+
]);
|
|
79
|
+
const value = account
|
|
80
|
+
.storage()
|
|
81
|
+
.getMapItem(COUNTER_SLOT_NAME, countKey);
|
|
82
|
+
// Storage map value is a Word whose first element holds the Felt count.
|
|
83
|
+
const newCount = value ? Number(value.toU64s()[0]) : 0;
|
|
84
|
+
setCount(newCount);
|
|
85
|
+
// Successful fetch — clear any prior unreachable/timeout error.
|
|
86
|
+
setError(null);
|
|
87
|
+
return newCount;
|
|
88
|
+
});
|
|
89
|
+
}, [isReady, client, runExclusive, counterAddress]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
loadCount().catch((err) => {
|
|
93
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
94
|
+
});
|
|
95
|
+
}, [loadCount]);
|
|
96
|
+
|
|
97
|
+
const increment = useCallback(async () => {
|
|
98
|
+
if (!walletAddress) {
|
|
99
|
+
setError(
|
|
100
|
+
"No wallet account available. Connect MidenFi to a testnet account before incrementing.",
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
setError(null);
|
|
105
|
+
setIsSubmitting(true);
|
|
106
|
+
try {
|
|
107
|
+
const buf = await fetch("/packages/increment_note.masp").then((r) =>
|
|
108
|
+
r.arrayBuffer(),
|
|
109
|
+
);
|
|
110
|
+
const pkg = Package.deserialize(new Uint8Array(buf));
|
|
111
|
+
const noteScript = NoteScript.fromPackage(pkg);
|
|
112
|
+
|
|
113
|
+
const counterAccountId = AccountId.fromBech32(counterAddress);
|
|
114
|
+
const walletAccountId = AccountId.fromBech32(walletAddress);
|
|
115
|
+
|
|
116
|
+
const serialNum = randomWord();
|
|
117
|
+
const storage = new NoteStorage(new FeltArray());
|
|
118
|
+
const recipient = new NoteRecipient(serialNum, noteScript, storage);
|
|
119
|
+
|
|
120
|
+
const tag = NoteTag.withAccountTarget(counterAccountId);
|
|
121
|
+
const attachment = NoteAttachment.newNetworkAccountTarget(
|
|
122
|
+
counterAccountId,
|
|
123
|
+
NoteExecutionHint.always(),
|
|
124
|
+
);
|
|
125
|
+
const metadata = new NoteMetadata(
|
|
126
|
+
walletAccountId,
|
|
127
|
+
NoteType.Public,
|
|
128
|
+
tag,
|
|
129
|
+
).withAttachment(attachment);
|
|
130
|
+
|
|
131
|
+
const note = new Note(new NoteAssets(), metadata, recipient);
|
|
132
|
+
const txRequest = new TransactionRequestBuilder()
|
|
133
|
+
.withOwnOutputNotes(new NoteArray([note]))
|
|
134
|
+
.build();
|
|
135
|
+
|
|
136
|
+
if (!requestTransaction) {
|
|
137
|
+
throw new Error("Wallet does not support requestTransaction");
|
|
138
|
+
}
|
|
139
|
+
const tx = Transaction.createCustomTransaction(
|
|
140
|
+
walletAddress,
|
|
141
|
+
counterAddress,
|
|
142
|
+
txRequest,
|
|
143
|
+
);
|
|
144
|
+
await requestTransaction(tx);
|
|
145
|
+
setIsSubmitting(false);
|
|
146
|
+
|
|
147
|
+
// Capture the pre-submission count so the poll loop knows what value
|
|
148
|
+
// to wait past. Not stale-safe because React batches state updates and
|
|
149
|
+
// `count` here is the closed-over value from the most recent render —
|
|
150
|
+
// which is exactly what we want: the value the user saw on the button.
|
|
151
|
+
const previousCount = count;
|
|
152
|
+
|
|
153
|
+
setIsWaiting(true);
|
|
154
|
+
// TODO(0xMiden/miden-client#2111): The React SDK exposes no hook to
|
|
155
|
+
// subscribe to account-state changes driven by the network operator
|
|
156
|
+
// (our counter is Network storage mode). `useWaitForCommit` only
|
|
157
|
+
// watches locally-submitted transactions, and this increment was
|
|
158
|
+
// submitted by the wallet. Until #2111 lands a subscription primitive,
|
|
159
|
+
// we poll the counter's storage map with a bounded timeout.
|
|
160
|
+
const deadline = Date.now() + NETWORK_POLL_TIMEOUT_MS;
|
|
161
|
+
let changed = false;
|
|
162
|
+
while (!changed && Date.now() < deadline) {
|
|
163
|
+
await new Promise((r) => setTimeout(r, NETWORK_POLL_INTERVAL_MS));
|
|
164
|
+
// `loadCount()` calls `client.syncState()` internally inside
|
|
165
|
+
// `runExclusive`; no need for a second `sync()` here.
|
|
166
|
+
const latest = await loadCount();
|
|
167
|
+
changed = latest !== null && latest !== previousCount;
|
|
168
|
+
}
|
|
169
|
+
setIsWaiting(false);
|
|
170
|
+
if (!changed) {
|
|
171
|
+
// The tx was submitted successfully but the counter hadn't updated
|
|
172
|
+
// by the timeout. This can happen when testnet is slow or the
|
|
173
|
+
// network operator hasn't picked up the note yet. Surface a
|
|
174
|
+
// non-fatal message so the user knows to refresh or retry sync.
|
|
175
|
+
setError(
|
|
176
|
+
`Transaction submitted, but counter update was not observed within ${Math.round(NETWORK_POLL_TIMEOUT_MS / 1000)}s. Refresh or retry sync.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
setIsSubmitting(false);
|
|
181
|
+
setIsWaiting(false);
|
|
182
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
183
|
+
}
|
|
184
|
+
}, [walletAddress, requestTransaction, counterAddress, loadCount, count]);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
increment,
|
|
188
|
+
count,
|
|
189
|
+
isSubmitting,
|
|
190
|
+
isWaiting,
|
|
191
|
+
error,
|
|
192
|
+
walletConnected,
|
|
193
|
+
explorerUrl: `${EXPLORER_BASE_URL}/account/${counterAddress}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
package/template/src/index.css
CHANGED
|
@@ -54,6 +54,13 @@ button:focus-visible {
|
|
|
54
54
|
outline: 4px auto -webkit-focus-ring-color;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
#root {
|
|
58
|
+
max-width: 1280px;
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
padding: 2rem;
|
|
61
|
+
text-align: center;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
@media (prefers-color-scheme: light) {
|
|
58
65
|
:root {
|
|
59
66
|
color: #213547;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Felt, Word } from "@miden-sdk/miden-sdk";
|
|
2
|
+
|
|
3
|
+
/** Generate a random 4-felt Word (used as note serial number). */
|
|
4
|
+
export function randomWord(): Word {
|
|
5
|
+
const felts = Array.from({ length: 4 }, () =>
|
|
6
|
+
new Felt(BigInt(Math.floor(Math.random() * 2 ** 32))),
|
|
7
|
+
);
|
|
8
|
+
return Word.newFromFelts(felts);
|
|
9
|
+
}
|
package/template/src/main.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { StrictMode } from
|
|
2
|
-
import { createRoot } from
|
|
3
|
-
import
|
|
4
|
-
import App from
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import "./index.css";
|
|
4
|
+
import App from "./App.tsx";
|
|
5
5
|
|
|
6
|
-
createRoot(document.getElementById(
|
|
6
|
+
createRoot(document.getElementById("root")!).render(
|
|
7
7
|
<StrictMode>
|
|
8
8
|
<App />
|
|
9
9
|
</StrictMode>,
|
|
10
|
-
)
|
|
10
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { MidenProvider } from "@miden-sdk/react";
|
|
3
|
+
import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react";
|
|
4
|
+
import { WalletAdapterNetwork } from "@miden-sdk/miden-wallet-adapter-base";
|
|
5
|
+
import { APP_NAME, MIDEN_RPC_URL, MIDEN_PROVER } from "@/config";
|
|
6
|
+
|
|
7
|
+
// MidenFiSignerProvider must wrap MidenProvider — MidenProvider reads
|
|
8
|
+
// SignerContext during initialization (to wire its external-keystore client),
|
|
9
|
+
// so the signer context has to exist before the provider mounts.
|
|
10
|
+
export function AppProviders({ children }: { children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<MidenFiSignerProvider
|
|
13
|
+
appName={APP_NAME}
|
|
14
|
+
network={WalletAdapterNetwork.Testnet}
|
|
15
|
+
autoConnect
|
|
16
|
+
>
|
|
17
|
+
<MidenProvider
|
|
18
|
+
config={{ rpcUrl: MIDEN_RPC_URL, prover: MIDEN_PROVER }}
|
|
19
|
+
loadingComponent={
|
|
20
|
+
<div className="loading">Loading Miden WASM...</div>
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</MidenProvider>
|
|
25
|
+
</MidenFiSignerProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"useDefineForClassFields": true,
|
|
6
6
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
7
|
"module": "ESNext",
|
|
8
|
-
"types": ["vite/client"],
|
|
8
|
+
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"],
|
|
9
9
|
"skipLibCheck": true,
|
|
10
10
|
|
|
11
11
|
/* Bundler mode */
|
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
"noEmit": true,
|
|
17
17
|
"jsx": "react-jsx",
|
|
18
18
|
|
|
19
|
+
/* Path aliases */
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
},
|
|
24
|
+
|
|
19
25
|
/* Linting */
|
|
20
26
|
"strict": true,
|
|
21
27
|
"noUnusedLocals": true,
|
|
22
28
|
"noUnusedParameters": true,
|
|
23
|
-
"
|
|
24
|
-
"noFallthroughCasesInSwitch": true,
|
|
25
|
-
"noUncheckedSideEffectImports": true
|
|
29
|
+
"noFallthroughCasesInSwitch": true
|
|
26
30
|
},
|
|
27
31
|
"include": ["src"]
|
|
28
32
|
}
|
|
@@ -18,9 +18,7 @@
|
|
|
18
18
|
"strict": true,
|
|
19
19
|
"noUnusedLocals": true,
|
|
20
20
|
"noUnusedParameters": true,
|
|
21
|
-
"
|
|
22
|
-
"noFallthroughCasesInSwitch": true,
|
|
23
|
-
"noUncheckedSideEffectImports": true
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
24
22
|
},
|
|
25
23
|
"include": ["vite.config.ts"]
|
|
26
24
|
}
|
package/template/vite.config.ts
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { defineConfig } from "vite";
|
|
2
3
|
import react from "@vitejs/plugin-react";
|
|
3
|
-
import
|
|
4
|
-
import topLevelAwait from "vite-plugin-top-level-await";
|
|
5
|
-
import path from "path";
|
|
4
|
+
import { midenVitePlugin } from "@miden-sdk/vite-plugin";
|
|
6
5
|
|
|
7
|
-
// https://vite.dev/config/
|
|
8
6
|
export default defineConfig({
|
|
9
|
-
plugins: [react(),
|
|
7
|
+
plugins: [react(), midenVitePlugin({ crossOriginIsolation: true })],
|
|
10
8
|
resolve: {
|
|
9
|
+
dedupe: ["react", "react-dom", "react/jsx-runtime"],
|
|
11
10
|
alias: {
|
|
12
|
-
|
|
13
|
-
dexie: path.resolve(__dirname, "node_modules/dexie"),
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
optimizeDeps: {
|
|
17
|
-
exclude: ["@miden-sdk/miden-sdk"],
|
|
18
|
-
include: ["dexie"],
|
|
19
|
-
},
|
|
20
|
-
server: {
|
|
21
|
-
headers: {
|
|
22
|
-
"Cross-Origin-Opener-Policy": "same-origin",
|
|
23
|
-
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
11
|
+
"@": path.resolve(__dirname, "./src"),
|
|
24
12
|
},
|
|
25
13
|
},
|
|
26
14
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
"@": path.resolve(__dirname, "./src"),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
test: {
|
|
13
|
+
environment: "jsdom",
|
|
14
|
+
globals: true,
|
|
15
|
+
setupFiles: ["./vitest.setup.ts"],
|
|
16
|
+
include: ["src/**/*.{test,spec}.{ts,tsx}"],
|
|
17
|
+
passWithNoTests: true,
|
|
18
|
+
server: {
|
|
19
|
+
deps: {
|
|
20
|
+
// Tests mock the wallet adapter at the module level, so externalizing is safe.
|
|
21
|
+
external: [/@miden-sdk\/miden-wallet-adapter-react/],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|