@miden-sdk/miden-wallet-adapter-miden 0.16.2 → 0.16.3

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/conformance.ts DELETED
@@ -1,219 +0,0 @@
1
- // `EventEmitter` comes via the base package rather than `eventemitter3` directly:
2
- // base re-exports it and is a declared dependency here, eventemitter3 is not.
3
- import type { EventEmitter } from "@miden-sdk/miden-wallet-adapter-base";
4
-
5
- import type { MidenWallet, MidenWalletEvents } from "./adapter";
6
-
7
- /**
8
- * A conformance suite for `MidenWallet` implementations.
9
- *
10
- * ## Why this exists
11
- *
12
- * `createAccount` has been on the published `MidenWallet` interface since
13
- * adapter 0.13.2. No wallet provider implements it — not the browser
14
- * extension, not the mobile injection script, not the Tauri provider — and
15
- * there is no wire message for it. A dApp calling it gets
16
- * `TypeError: wallet.createAccount is not a function`.
17
- *
18
- * That shipped, and stayed shipped, because both sides of the contract were
19
- * green: this repo's tests inject a `vi.fn()` bag as `window.midenWallet`, and
20
- * the wallet's tests mock this package wholesale. Each side asserted against a
21
- * shape it invented.
22
- *
23
- * A suite written *here* cannot fix that. Whatever reference provider this
24
- * repo authors gets updated in the same commit that adds a method, so it can
25
- * never detect a missing one. The detection has to happen where the real
26
- * implementations live. So this module ships the *instrument*, and the wallet
27
- * runs it against its real providers.
28
- *
29
- * ## Two halves, deliberately separate
30
- *
31
- * {@link getSurfaceCases} asks only whether each method exists. It needs no
32
- * connection, no network and no state, so it is safe to run against providers
33
- * that are plain injected JavaScript — which is what the mobile and Tauri
34
- * providers are, and where the remaining gaps live.
35
- *
36
- * {@link getBehaviorCases} exercises round-trips and needs a live, connected
37
- * provider. Running it against an un-connected provider proves nothing.
38
- *
39
- * @packageDocumentation
40
- */
41
-
42
- /** Members `MidenWallet` inherits from EventEmitter rather than declaring. */
43
- type InheritedKeys = keyof EventEmitter<MidenWalletEvents>;
44
-
45
- /** Members `MidenWallet` declares itself. */
46
- type OwnKeys = Exclude<keyof MidenWallet, InheritedKeys>;
47
-
48
- /** The subset of {@link OwnKeys} that are callable — i.e. not `address`/`publicKey`. */
49
- type MethodKeys = {
50
- [K in OwnKeys]-?: NonNullable<MidenWallet[K]> extends (
51
- ...args: never[]
52
- ) => unknown
53
- ? K
54
- : never;
55
- }[OwnKeys];
56
-
57
- /**
58
- * Every method a `MidenWallet` must implement.
59
- *
60
- * Type-locked in both directions below: a method added to the interface
61
- * without being added here fails to compile, and a name here that is not on
62
- * the interface fails to compile too. The list cannot silently drift from the
63
- * contract it describes.
64
- */
65
- export const MIDEN_WALLET_METHODS = [
66
- "connect",
67
- "disconnect",
68
- "signBytes",
69
- "requestSend",
70
- "requestConsume",
71
- "requestTransaction",
72
- "requestAssets",
73
- "requestGuardianInfo",
74
- "requestPrivateNotes",
75
- "requestConsumableNotes",
76
- "importPrivateNote",
77
- "waitForTransaction",
78
- "createAccount",
79
- ] as const satisfies readonly MethodKeys[];
80
-
81
- /** Compile error if the interface gains a method this list does not name. */
82
- type Unlisted = Exclude<MethodKeys, (typeof MIDEN_WALLET_METHODS)[number]>;
83
- const _exhaustive: [Unlisted] extends [never]
84
- ? true
85
- : ["MIDEN_WALLET_METHODS is missing", Unlisted] = true;
86
- void _exhaustive;
87
-
88
- /**
89
- * Identifies this module as the real implementation.
90
- *
91
- * The wallet mocks this package in a dozen test files. If a conformance run
92
- * ever lands inside a suite that mocks it — or someone later adds a
93
- * `moduleNameMapper` — the imported suite would become a no-op that still
94
- * reports green: the identical failure mode this whole fixture exists to
95
- * prevent, one layer up. Assert on this before running any case.
96
- */
97
- export const CONFORMANCE_BUILD = {
98
- real: true,
99
- methodCount: MIDEN_WALLET_METHODS.length,
100
- } as const;
101
-
102
- /** One assertion, named so a failure says what is wrong without a stack trace. */
103
- export interface ConformanceCase {
104
- readonly name: string;
105
- /** Throws on failure; returns normally on success. */
106
- readonly run: () => void | Promise<void>;
107
- }
108
-
109
- /** Anything shaped enough to probe. Deliberately not `MidenWallet`: the point is to test providers that may not satisfy it. */
110
- export type ProviderLike = Record<string, unknown>;
111
-
112
- /**
113
- * Shape-only checks: does the provider expose every method?
114
- *
115
- * Safe against untyped, runtime-only providers — no connection or state
116
- * required. This is the half that catches a provider missing a method the
117
- * interface promises.
118
- */
119
- export function getSurfaceCases(provider: ProviderLike): ConformanceCase[] {
120
- return MIDEN_WALLET_METHODS.map((method) => ({
121
- name: `implements ${method}()`,
122
- run: () => {
123
- const value = provider[method];
124
- if (typeof value !== "function") {
125
- throw new Error(
126
- `MidenWallet.${method} is ${value === undefined ? "missing" : `a ${typeof value}`}. ` +
127
- `Every method on the published MidenWallet interface must exist on the provider; ` +
128
- `a dApp calling this one gets "TypeError: wallet.${method} is not a function".`
129
- );
130
- }
131
- },
132
- }));
133
- }
134
-
135
- /**
136
- * Round-trip checks against a live, connected provider.
137
- *
138
- * Unlike {@link getSurfaceCases} these need real state, so a provider obtained
139
- * by evaluating an injection script — with no wallet behind it — will fail
140
- * them for reasons that say nothing about conformance. Run the surface half
141
- * against those.
142
- */
143
- export function getBehaviorCases(provider: ProviderLike): ConformanceCase[] {
144
- const call = async <T>(method: string, ...args: unknown[]): Promise<T> => {
145
- const fn = provider[method];
146
- if (typeof fn !== "function") {
147
- throw new Error(`MidenWallet.${method} is not a function`);
148
- }
149
- return (await (fn as (...a: unknown[]) => Promise<T>).apply(
150
- provider,
151
- args
152
- )) as T;
153
- };
154
-
155
- return [
156
- {
157
- name: "requestAssets() resolves to { assets: [...] }",
158
- run: async () => {
159
- const result = await call<{ assets: unknown[] }>("requestAssets");
160
- if (!result || !Array.isArray(result.assets)) {
161
- throw new Error(
162
- `requestAssets() must resolve to { assets: Asset[] }, got ${JSON.stringify(result)}`
163
- );
164
- }
165
- },
166
- },
167
- {
168
- name: "requestConsumableNotes() resolves to { consumableNotes: [...] }",
169
- run: async () => {
170
- const result = await call<{ consumableNotes: unknown[] }>(
171
- "requestConsumableNotes"
172
- );
173
- if (!result || !Array.isArray(result.consumableNotes)) {
174
- throw new Error(
175
- `requestConsumableNotes() must resolve to { consumableNotes: InputNoteDetails[] }, got ${JSON.stringify(result)}`
176
- );
177
- }
178
- },
179
- },
180
- {
181
- name: "requestGuardianInfo() resolves to { guardianInfo }",
182
- run: async () => {
183
- const result = await call<{ guardianInfo: unknown }>(
184
- "requestGuardianInfo"
185
- );
186
- if (!result || result.guardianInfo === undefined) {
187
- throw new Error(
188
- `requestGuardianInfo() must resolve to { guardianInfo: GuardianInfo }, got ${JSON.stringify(result)}`
189
- );
190
- }
191
- },
192
- },
193
- ];
194
- }
195
-
196
- /**
197
- * Runs cases and collects every failure rather than stopping at the first.
198
- *
199
- * A provider missing four methods should report four failures, not one — the
200
- * point is to see the whole gap in a single run.
201
- */
202
- export async function runConformance(
203
- cases: ConformanceCase[]
204
- ): Promise<{ passed: string[]; failed: { name: string; error: string }[] }> {
205
- const passed: string[] = [];
206
- const failed: { name: string; error: string }[] = [];
207
- for (const testCase of cases) {
208
- try {
209
- await testCase.run();
210
- passed.push(testCase.name);
211
- } catch (error) {
212
- failed.push({
213
- name: testCase.name,
214
- error: error instanceof Error ? error.message : String(error),
215
- });
216
- }
217
- }
218
- return { passed, failed };
219
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,464 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { WalletNotConnectedError, WalletTransactionError, WalletReadyState, WalletNotReadyError, WalletConnectionError, PrivateDataPermission, WalletAdapterNetwork, } from "@miden-sdk/miden-wallet-adapter-base";
3
- // Mock @miden-sdk/miden-sdk before importing adapter
4
- vi.mock("@miden-sdk/miden-sdk", () => ({
5
- Note: {
6
- deserialize: vi.fn((bytes) => ({ mockNote: true, bytes })),
7
- },
8
- }));
9
- import { Note } from "@miden-sdk/miden-sdk";
10
- import { MidenWalletAdapter } from "../adapter";
11
- function createMockWallet(overrides = {}) {
12
- return {
13
- address: "0xmockaddress",
14
- publicKey: new Uint8Array([1, 2, 3]),
15
- connect: vi.fn(),
16
- disconnect: vi.fn(),
17
- requestSend: vi.fn().mockResolvedValue({ transactionId: "tx-send-1" }),
18
- requestConsume: vi
19
- .fn()
20
- .mockResolvedValue({ transactionId: "tx-consume-1" }),
21
- requestTransaction: vi
22
- .fn()
23
- .mockResolvedValue({ transactionId: "tx-custom-1" }),
24
- requestAssets: vi
25
- .fn()
26
- .mockResolvedValue({ assets: [{ faucetId: "f1", amount: "100" }] }),
27
- requestGuardianInfo: vi.fn().mockResolvedValue({
28
- guardianInfo: {
29
- isGuardianAccount: false,
30
- guardianEndpoint: null,
31
- guardianProvider: null,
32
- guardianSyncStatus: null,
33
- },
34
- }),
35
- requestPrivateNotes: vi
36
- .fn()
37
- .mockResolvedValue({ privateNotes: [{ noteId: "n1" }] }),
38
- signBytes: vi
39
- .fn()
40
- .mockResolvedValue({ signature: new Uint8Array([9, 8, 7]) }),
41
- importPrivateNote: vi.fn().mockResolvedValue({ noteId: "imported-note-1" }),
42
- requestConsumableNotes: vi
43
- .fn()
44
- .mockResolvedValue({ consumableNotes: [{ noteId: "cn1" }] }),
45
- createAccount: vi.fn().mockResolvedValue({ accountId: "account-123" }),
46
- waitForTransaction: vi.fn(),
47
- ...overrides,
48
- };
49
- }
50
- // Helper to set up a connected adapter by injecting mock wallet via window
51
- async function createConnectedAdapter() {
52
- const mockWallet = createMockWallet();
53
- window.midenWallet = mockWallet;
54
- const adapter = new MidenWalletAdapter();
55
- // Force readyState to Installed so connect works
56
- adapter.readyState = WalletReadyState.Installed;
57
- await adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
58
- return { adapter, mockWallet };
59
- }
60
- describe("MidenWalletAdapter", () => {
61
- beforeEach(() => {
62
- vi.clearAllMocks();
63
- delete window.midenWallet;
64
- delete window.miden;
65
- });
66
- describe("constructor", () => {
67
- it("initializes with null address and not connecting", () => {
68
- const adapter = new MidenWalletAdapter();
69
- expect(adapter.address).toBeNull();
70
- expect(adapter.publicKey).toBeNull();
71
- expect(adapter.connecting).toBe(false);
72
- expect(adapter.connected).toBe(false);
73
- });
74
- it("sets readyState to NotDetected when wallet is not present", () => {
75
- const adapter = new MidenWalletAdapter();
76
- expect(adapter.readyState).toBe(WalletReadyState.NotDetected);
77
- });
78
- });
79
- describe("not connected errors", () => {
80
- const methods = [
81
- {
82
- name: "requestSend",
83
- call: (a) => a.requestSend({
84
- senderAddress: "s",
85
- recipientAddress: "r",
86
- faucetId: "f",
87
- noteType: "public",
88
- amount: 1,
89
- }),
90
- },
91
- {
92
- name: "requestConsume",
93
- call: (a) => a.requestConsume({
94
- faucetId: "f",
95
- noteId: "n",
96
- noteType: "public",
97
- amount: 1,
98
- }),
99
- },
100
- {
101
- name: "requestTransaction",
102
- call: (a) => a.requestTransaction({ type: "send", payload: {} }),
103
- },
104
- {
105
- name: "requestAssets",
106
- call: (a) => a.requestAssets(),
107
- },
108
- {
109
- name: "requestGuardianInfo",
110
- call: (a) => a.requestGuardianInfo(),
111
- },
112
- {
113
- name: "requestPrivateNotes",
114
- call: (a) => a.requestPrivateNotes("All"),
115
- },
116
- {
117
- name: "signBytes",
118
- call: (a) => a.signBytes(new Uint8Array([1]), "word"),
119
- },
120
- {
121
- name: "importPrivateNote",
122
- call: (a) => a.importPrivateNote(new Uint8Array([1])),
123
- },
124
- {
125
- name: "requestConsumableNotes",
126
- call: (a) => a.requestConsumableNotes(),
127
- },
128
- {
129
- name: "waitForTransaction",
130
- call: (a) => a.waitForTransaction("tx-1"),
131
- },
132
- {
133
- name: "createAccount",
134
- call: (a) => a.createAccount(),
135
- },
136
- ];
137
- for (const { name, call } of methods) {
138
- it(`${name} throws WalletNotConnectedError when not connected`, async () => {
139
- const adapter = new MidenWalletAdapter();
140
- const errorHandler = vi.fn();
141
- adapter.on("error", errorHandler);
142
- await expect(call(adapter)).rejects.toThrow(WalletNotConnectedError);
143
- expect(errorHandler).toHaveBeenCalledWith(expect.any(WalletNotConnectedError));
144
- });
145
- }
146
- });
147
- describe("connected operations", () => {
148
- it("requestSend delegates to wallet and returns transactionId", async () => {
149
- const { adapter, mockWallet } = await createConnectedAdapter();
150
- const tx = {
151
- senderAddress: "s",
152
- recipientAddress: "r",
153
- faucetId: "f",
154
- noteType: "public",
155
- amount: 100,
156
- };
157
- const result = await adapter.requestSend(tx);
158
- expect(mockWallet.requestSend).toHaveBeenCalledWith(tx);
159
- expect(result).toBe("tx-send-1");
160
- });
161
- it("requestConsume delegates to wallet and returns transactionId", async () => {
162
- const { adapter, mockWallet } = await createConnectedAdapter();
163
- const tx = {
164
- faucetId: "f",
165
- noteId: "n",
166
- noteType: "public",
167
- amount: 50,
168
- };
169
- const result = await adapter.requestConsume(tx);
170
- expect(mockWallet.requestConsume).toHaveBeenCalledWith(tx);
171
- expect(result).toBe("tx-consume-1");
172
- });
173
- it("requestSend rejects (does not resolve undefined) when the wallet returns no transactionId (0xMiden/wallet#314)", async () => {
174
- const { adapter, mockWallet } = await createConnectedAdapter();
175
- // The wallet resolved "successfully" but without a transaction id — meaning
176
- // the transaction was never submitted; the dApp must see a rejection.
177
- mockWallet.requestSend.mockResolvedValue({});
178
- const errorHandler = vi.fn();
179
- adapter.on("error", errorHandler);
180
- await expect(adapter.requestSend({
181
- senderAddress: "s",
182
- recipientAddress: "r",
183
- faucetId: "f",
184
- noteType: "public",
185
- amount: 1,
186
- })).rejects.toThrow(WalletTransactionError);
187
- expect(errorHandler).toHaveBeenCalled();
188
- });
189
- it("requestConsume rejects when the wallet returns no transactionId (0xMiden/wallet#314)", async () => {
190
- const { adapter, mockWallet } = await createConnectedAdapter();
191
- mockWallet.requestConsume.mockResolvedValue({ transactionId: undefined });
192
- await expect(adapter.requestConsume({
193
- faucetId: "f",
194
- noteId: "n",
195
- noteType: "public",
196
- amount: 1,
197
- })).rejects.toThrow(WalletTransactionError);
198
- });
199
- it("requestTransaction (custom) rejects when the wallet returns no transactionId (0xMiden/wallet#314)", async () => {
200
- const { adapter, mockWallet } = await createConnectedAdapter();
201
- mockWallet.requestTransaction.mockResolvedValue({});
202
- await expect(adapter.requestTransaction({
203
- type: "custom",
204
- payload: {},
205
- })).rejects.toThrow(WalletTransactionError);
206
- expect(mockWallet.requestTransaction).toHaveBeenCalled();
207
- });
208
- it("requestTransaction routes a Consume transaction to wallet.requestConsume (issue #88)", async () => {
209
- const { adapter, mockWallet } = await createConnectedAdapter();
210
- const payload = {
211
- faucetId: "f",
212
- noteId: "n",
213
- noteType: "public",
214
- amount: 50,
215
- };
216
- const result = await adapter.requestTransaction({
217
- type: "consume",
218
- payload,
219
- });
220
- expect(mockWallet.requestConsume).toHaveBeenCalledWith(payload);
221
- expect(mockWallet.requestTransaction).not.toHaveBeenCalled();
222
- expect(result).toBe("tx-consume-1");
223
- });
224
- it("requestTransaction routes a Send transaction to wallet.requestSend", async () => {
225
- const { adapter, mockWallet } = await createConnectedAdapter();
226
- const payload = {
227
- senderAddress: "s",
228
- recipientAddress: "r",
229
- faucetId: "f",
230
- noteType: "public",
231
- amount: 100,
232
- };
233
- const result = await adapter.requestTransaction({
234
- type: "send",
235
- payload,
236
- });
237
- expect(mockWallet.requestSend).toHaveBeenCalledWith(payload);
238
- expect(mockWallet.requestTransaction).not.toHaveBeenCalled();
239
- expect(result).toBe("tx-send-1");
240
- });
241
- it("requestTransaction forwards a Custom transaction to wallet.requestTransaction", async () => {
242
- const { adapter, mockWallet } = await createConnectedAdapter();
243
- const tx = {
244
- type: "custom",
245
- payload: {
246
- address: "addr",
247
- recipientAddress: "r",
248
- transactionRequest: "base64-request",
249
- },
250
- };
251
- const result = await adapter.requestTransaction(tx);
252
- expect(mockWallet.requestTransaction).toHaveBeenCalledWith(tx);
253
- expect(result).toBe("tx-custom-1");
254
- });
255
- it("requestTransaction forwards a bare (typeless) payload to wallet.requestTransaction", async () => {
256
- const { adapter, mockWallet } = await createConnectedAdapter();
257
- const tx = {
258
- address: "addr",
259
- transactionRequest: "base64-request",
260
- };
261
- const result = await adapter.requestTransaction(tx);
262
- expect(mockWallet.requestTransaction).toHaveBeenCalledWith(tx);
263
- expect(result).toBe("tx-custom-1");
264
- });
265
- it("requestAssets returns assets array", async () => {
266
- const { adapter } = await createConnectedAdapter();
267
- const assets = await adapter.requestAssets();
268
- expect(assets).toEqual([{ faucetId: "f1", amount: "100" }]);
269
- });
270
- it("requestGuardianInfo unwraps the provider result", async () => {
271
- const { adapter, mockWallet } = await createConnectedAdapter();
272
- mockWallet.requestGuardianInfo.mockResolvedValue({
273
- guardianInfo: {
274
- isGuardianAccount: true,
275
- guardianEndpoint: "https://g",
276
- guardianProvider: "gateway",
277
- guardianSyncStatus: "in-sync",
278
- },
279
- });
280
- const guardianInfo = await adapter.requestGuardianInfo();
281
- expect(guardianInfo).toEqual({
282
- isGuardianAccount: true,
283
- guardianEndpoint: "https://g",
284
- guardianProvider: "gateway",
285
- guardianSyncStatus: "in-sync",
286
- });
287
- });
288
- it("requestGuardianInfo emits error and wraps failure", async () => {
289
- const { adapter, mockWallet } = await createConnectedAdapter();
290
- mockWallet.requestGuardianInfo.mockRejectedValue(new Error("network error"));
291
- const errorHandler = vi.fn();
292
- adapter.on("error", errorHandler);
293
- await expect(adapter.requestGuardianInfo()).rejects.toThrow(WalletTransactionError);
294
- expect(errorHandler).toHaveBeenCalledWith(expect.any(WalletTransactionError));
295
- });
296
- it("requestPrivateNotes returns notes array", async () => {
297
- const { adapter } = await createConnectedAdapter();
298
- const notes = await adapter.requestPrivateNotes("All");
299
- expect(notes).toEqual([{ noteId: "n1" }]);
300
- });
301
- it("signBytes returns signature", async () => {
302
- const { adapter } = await createConnectedAdapter();
303
- const sig = await adapter.signBytes(new Uint8Array([1, 2]), "word");
304
- expect(sig).toEqual(new Uint8Array([9, 8, 7]));
305
- });
306
- it("importPrivateNote returns noteId", async () => {
307
- const { adapter } = await createConnectedAdapter();
308
- const noteId = await adapter.importPrivateNote(new Uint8Array([1]));
309
- expect(noteId).toBe("imported-note-1");
310
- });
311
- it("requestConsumableNotes returns consumable notes", async () => {
312
- const { adapter } = await createConnectedAdapter();
313
- const notes = await adapter.requestConsumableNotes();
314
- expect(notes).toEqual([{ noteId: "cn1" }]);
315
- });
316
- it("createAccount returns accountId without params", async () => {
317
- const { adapter, mockWallet } = await createConnectedAdapter();
318
- const result = await adapter.createAccount();
319
- expect(mockWallet.createAccount).toHaveBeenCalledWith(undefined);
320
- expect(result).toBe("account-123");
321
- });
322
- it("createAccount returns accountId with params", async () => {
323
- const { adapter, mockWallet } = await createConnectedAdapter();
324
- const params = {
325
- accountType: "RegularAccountUpdatableCode",
326
- storageMode: "private",
327
- };
328
- const result = await adapter.createAccount(params);
329
- expect(mockWallet.createAccount).toHaveBeenCalledWith(params);
330
- expect(result).toBe("account-123");
331
- });
332
- it("createAccount passes customComponents to wallet", async () => {
333
- const { adapter, mockWallet } = await createConnectedAdapter();
334
- const component = new Uint8Array([1, 2, 3]);
335
- const params = { customComponents: [component] };
336
- const result = await adapter.createAccount(params);
337
- expect(mockWallet.createAccount).toHaveBeenCalledWith(params);
338
- expect(result).toBe("account-123");
339
- });
340
- });
341
- describe("waitForTransaction", () => {
342
- it("success path: deserializes notes and returns TransactionOutput", async () => {
343
- // base64 of [1,2,3] = "AQID"
344
- const { adapter, mockWallet } = await createConnectedAdapter();
345
- mockWallet.waitForTransaction.mockResolvedValue({
346
- txHash: "hash-123",
347
- outputNotes: ["AQID", "BAUG"],
348
- });
349
- const result = await adapter.waitForTransaction("tx-1");
350
- expect(result.txHash).toBe("hash-123");
351
- expect(result.outputNotes).toHaveLength(2);
352
- expect(Note.deserialize).toHaveBeenCalledTimes(2);
353
- });
354
- it("error path: throws WalletTransactionError on errorMessage", async () => {
355
- const { adapter, mockWallet } = await createConnectedAdapter();
356
- mockWallet.waitForTransaction.mockResolvedValue({
357
- errorMessage: "tx failed",
358
- });
359
- const errorHandler = vi.fn();
360
- adapter.on("error", errorHandler);
361
- await expect(adapter.waitForTransaction("tx-1")).rejects.toThrow(WalletTransactionError);
362
- expect(errorHandler).toHaveBeenCalled();
363
- });
364
- });
365
- describe("connect", () => {
366
- it("throws WalletNotReadyError if readyState is not Installed", async () => {
367
- const adapter = new MidenWalletAdapter();
368
- const errorHandler = vi.fn();
369
- adapter.on("error", errorHandler);
370
- await expect(adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet)).rejects.toThrow(WalletNotReadyError);
371
- });
372
- it("connects successfully and emits connect event", async () => {
373
- const mockWallet = createMockWallet();
374
- window.midenWallet = mockWallet;
375
- const adapter = new MidenWalletAdapter();
376
- adapter.readyState = WalletReadyState.Installed;
377
- const connectHandler = vi.fn();
378
- adapter.on("connect", connectHandler);
379
- await adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
380
- expect(adapter.address).toBe("0xmockaddress");
381
- expect(adapter.publicKey).toEqual(new Uint8Array([1, 2, 3]));
382
- expect(adapter.connected).toBe(true);
383
- expect(adapter.connecting).toBe(false);
384
- expect(connectHandler).toHaveBeenCalledWith("0xmockaddress");
385
- });
386
- it("throws WalletConnectionError if wallet has no address after connect", async () => {
387
- const mockWallet = createMockWallet({ address: undefined });
388
- window.midenWallet = mockWallet;
389
- const adapter = new MidenWalletAdapter();
390
- adapter.readyState = WalletReadyState.Installed;
391
- await expect(adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet)).rejects.toThrow(WalletConnectionError);
392
- });
393
- it("re-drives the handshake and re-emits `connect` when called while already connected, repopulating the current account (0xMiden/wallet#470)", async () => {
394
- const { adapter, mockWallet } = await createConnectedAdapter();
395
- const connectHandler = vi.fn();
396
- adapter.on("connect", connectHandler);
397
- // The in-app browser preserves this adapter's JS context across a
398
- // park/restore, so a re-opened dApp finds `connected === true`. The
399
- // old early-return made a second connect() a silent no-op, so a
400
- // re-mounted consumer that re-subscribed to events never got its
401
- // account back ("Connect Wallet works only once"). A repeat connect()
402
- // must re-drive the wallet handshake — which returns the current
403
- // account — and re-emit `connect` so the consumer repopulates.
404
- await adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
405
- expect(mockWallet.connect).toHaveBeenCalledTimes(2);
406
- expect(connectHandler).toHaveBeenCalledWith("0xmockaddress");
407
- expect(adapter.connected).toBe(true);
408
- expect(adapter.address).toBe("0xmockaddress");
409
- });
410
- it("ignores a concurrent connect() while one is still in flight", async () => {
411
- let releaseFirstConnect = () => { };
412
- const mockWallet = createMockWallet({
413
- connect: vi.fn(() => new Promise((resolve) => {
414
- releaseFirstConnect = resolve;
415
- })),
416
- });
417
- window.midenWallet = mockWallet;
418
- const adapter = new MidenWalletAdapter();
419
- adapter.readyState = WalletReadyState.Installed;
420
- // First connect stays in flight (its wallet.connect() promise is pending).
421
- const first = adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
422
- // A second call while `connecting` is true must be a guarded no-op.
423
- await adapter.connect(PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet);
424
- expect(mockWallet.connect).toHaveBeenCalledTimes(1);
425
- releaseFirstConnect();
426
- await first;
427
- expect(adapter.connected).toBe(true);
428
- });
429
- });
430
- describe("disconnect", () => {
431
- it("clears address, publicKey, and emits disconnect", async () => {
432
- const { adapter } = await createConnectedAdapter();
433
- const disconnectHandler = vi.fn();
434
- adapter.on("disconnect", disconnectHandler);
435
- await adapter.disconnect();
436
- expect(adapter.address).toBeNull();
437
- expect(adapter.publicKey).toBeNull();
438
- expect(adapter.connected).toBe(false);
439
- expect(disconnectHandler).toHaveBeenCalled();
440
- });
441
- it("emits error if wallet.disconnect fails", async () => {
442
- const { adapter, mockWallet } = await createConnectedAdapter();
443
- mockWallet.disconnect.mockRejectedValue(new Error("disconnect failed"));
444
- const errorHandler = vi.fn();
445
- adapter.on("error", errorHandler);
446
- await adapter.disconnect();
447
- expect(errorHandler).toHaveBeenCalled();
448
- });
449
- });
450
- describe("error wrapping", () => {
451
- it("wraps wallet method failures in WalletTransactionError", async () => {
452
- const { adapter, mockWallet } = await createConnectedAdapter();
453
- mockWallet.requestSend.mockRejectedValue(new Error("network error"));
454
- await expect(adapter.requestSend({
455
- senderAddress: "s",
456
- recipientAddress: "r",
457
- faucetId: "f",
458
- noteType: "public",
459
- amount: 1,
460
- })).rejects.toThrow(WalletTransactionError);
461
- });
462
- });
463
- });
464
- //# sourceMappingURL=adapter.test.js.map