@miden-sdk/miden-wallet-adapter-react 0.13.0
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/MidenFiSignerProvider.tsx +732 -0
- package/WalletProvider.tsx +443 -0
- package/__tests__/MidenFiSignerProvider.test.tsx +426 -0
- package/dist/MidenFiSignerProvider.d.ts +113 -0
- package/dist/MidenFiSignerProvider.js +501 -0
- package/dist/MidenFiSignerProvider.js.map +1 -0
- package/dist/WalletProvider.d.ts +13 -0
- package/dist/WalletProvider.js +302 -0
- package/dist/WalletProvider.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/useLocalStorage.d.ts +2 -0
- package/dist/useLocalStorage.js +39 -0
- package/dist/useLocalStorage.js.map +1 -0
- package/dist/useWallet.d.ts +30 -0
- package/dist/useWallet.js +81 -0
- package/dist/useWallet.js.map +1 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +8 -0
- package/dist/vitest.config.js.map +1 -0
- package/docs/README.md +30 -0
- package/docs/functions/useLocalStorage.md +29 -0
- package/docs/functions/useMidenFiWallet.md +28 -0
- package/docs/functions/useWallet.md +13 -0
- package/docs/interfaces/MidenFiSignerProviderProps.md +87 -0
- package/docs/interfaces/Wallet.md +19 -0
- package/docs/interfaces/WalletContextState.md +235 -0
- package/docs/interfaces/WalletProviderProps.md +65 -0
- package/docs/variables/MidenFiSignerProvider.md +54 -0
- package/docs/variables/WalletContext.md +9 -0
- package/docs/variables/WalletProvider.md +9 -0
- package/index.ts +15 -0
- package/package.json +42 -0
- package/tsconfig.json +16 -0
- package/useLocalStorage.ts +39 -0
- package/useWallet.ts +197 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
type FC,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { SignerContext, type SignerContextValue } from '@miden-sdk/react';
|
|
13
|
+
import {
|
|
14
|
+
type Adapter,
|
|
15
|
+
AllowedPrivateData,
|
|
16
|
+
type MessageSignerWalletAdapterProps,
|
|
17
|
+
type MidenTransaction,
|
|
18
|
+
PrivateDataPermission,
|
|
19
|
+
SignKind,
|
|
20
|
+
WalletAdapterNetwork,
|
|
21
|
+
WalletError,
|
|
22
|
+
type WalletName,
|
|
23
|
+
WalletNotConnectedError,
|
|
24
|
+
WalletNotReadyError,
|
|
25
|
+
WalletNotSelectedError,
|
|
26
|
+
WalletReadyState,
|
|
27
|
+
type MidenSendTransaction,
|
|
28
|
+
type MidenConsumeTransaction,
|
|
29
|
+
type Asset,
|
|
30
|
+
type InputNoteDetails,
|
|
31
|
+
type TransactionOutput,
|
|
32
|
+
} from '@miden-sdk/miden-wallet-adapter-base';
|
|
33
|
+
import type { NoteFilterTypes } from '@miden-sdk/miden-sdk';
|
|
34
|
+
import { MidenWalletAdapter } from '@miden-sdk/miden-wallet-adapter-miden';
|
|
35
|
+
import { useLocalStorage } from './useLocalStorage';
|
|
36
|
+
|
|
37
|
+
// TYPES
|
|
38
|
+
// ================================================================================================
|
|
39
|
+
|
|
40
|
+
export interface Wallet {
|
|
41
|
+
adapter: Adapter;
|
|
42
|
+
readyState: WalletReadyState;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WalletContextState {
|
|
46
|
+
autoConnect: boolean;
|
|
47
|
+
wallets: Wallet[];
|
|
48
|
+
wallet: Wallet | null;
|
|
49
|
+
address: string | null;
|
|
50
|
+
publicKey: Uint8Array | null;
|
|
51
|
+
connected: boolean;
|
|
52
|
+
connecting: boolean;
|
|
53
|
+
disconnecting: boolean;
|
|
54
|
+
|
|
55
|
+
select(walletName: WalletName): void;
|
|
56
|
+
connect(): Promise<void>;
|
|
57
|
+
disconnect(): Promise<void>;
|
|
58
|
+
|
|
59
|
+
requestTransaction?: MessageSignerWalletAdapterProps['requestTransaction'];
|
|
60
|
+
requestAssets?: MessageSignerWalletAdapterProps['requestAssets'];
|
|
61
|
+
requestPrivateNotes?: MessageSignerWalletAdapterProps['requestPrivateNotes'];
|
|
62
|
+
signBytes?: MessageSignerWalletAdapterProps['signBytes'];
|
|
63
|
+
importPrivateNote?: MessageSignerWalletAdapterProps['importPrivateNote'];
|
|
64
|
+
requestConsumableNotes?: MessageSignerWalletAdapterProps['requestConsumableNotes'];
|
|
65
|
+
waitForTransaction?: MessageSignerWalletAdapterProps['waitForTransaction'];
|
|
66
|
+
requestSend?: MessageSignerWalletAdapterProps['requestSend'];
|
|
67
|
+
requestConsume?: MessageSignerWalletAdapterProps['requestConsume'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const WalletContext = createContext<WalletContextState>({} as WalletContextState);
|
|
71
|
+
|
|
72
|
+
// MIDENFI SIGNER PROVIDER
|
|
73
|
+
// ================================================================================================
|
|
74
|
+
|
|
75
|
+
export interface MidenFiSignerProviderProps {
|
|
76
|
+
children: ReactNode;
|
|
77
|
+
/** Wallet adapters to use. Defaults to [MidenWalletAdapter] */
|
|
78
|
+
wallets?: Adapter[];
|
|
79
|
+
/** App name passed to the default MidenWalletAdapter */
|
|
80
|
+
appName?: string;
|
|
81
|
+
/** Network to connect to */
|
|
82
|
+
network?: WalletAdapterNetwork;
|
|
83
|
+
/** Auto-connect to previously selected wallet on mount. Defaults to true */
|
|
84
|
+
autoConnect?: boolean;
|
|
85
|
+
/** Private data permission level */
|
|
86
|
+
privateDataPermission?: PrivateDataPermission;
|
|
87
|
+
/** Allowed private data types */
|
|
88
|
+
allowedPrivateData?: AllowedPrivateData;
|
|
89
|
+
/** Error handler */
|
|
90
|
+
onError?: (error: WalletError) => void;
|
|
91
|
+
/** LocalStorage key for persisting wallet selection */
|
|
92
|
+
localStorageKey?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const initialState: {
|
|
96
|
+
wallet: Wallet | null;
|
|
97
|
+
adapter: Adapter | null;
|
|
98
|
+
address: string | null;
|
|
99
|
+
publicKey: Uint8Array | null;
|
|
100
|
+
connected: boolean;
|
|
101
|
+
} = {
|
|
102
|
+
wallet: null,
|
|
103
|
+
adapter: null,
|
|
104
|
+
address: null,
|
|
105
|
+
publicKey: null,
|
|
106
|
+
connected: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* MidenFiSignerProvider bridges the MidenFi wallet with MidenProvider.
|
|
111
|
+
*
|
|
112
|
+
* This is a unified provider that handles both wallet connection and signer context.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* // Simplest usage - uses MidenWalletAdapter by default
|
|
117
|
+
* <MidenFiSignerProvider>
|
|
118
|
+
* <MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
119
|
+
* <App />
|
|
120
|
+
* </MidenProvider>
|
|
121
|
+
* </MidenFiSignerProvider>
|
|
122
|
+
*
|
|
123
|
+
* // With custom options
|
|
124
|
+
* <MidenFiSignerProvider
|
|
125
|
+
* appName="My DApp"
|
|
126
|
+
* network={WalletAdapterNetwork.Testnet}
|
|
127
|
+
* autoConnect={true}
|
|
128
|
+
* >
|
|
129
|
+
* <MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
130
|
+
* <App />
|
|
131
|
+
* </MidenProvider>
|
|
132
|
+
* </MidenFiSignerProvider>
|
|
133
|
+
*
|
|
134
|
+
* // With custom wallets
|
|
135
|
+
* <MidenFiSignerProvider wallets={[new CustomWalletAdapter()]}>
|
|
136
|
+
* <MidenProvider config={{ rpcUrl: "testnet" }}>
|
|
137
|
+
* <App />
|
|
138
|
+
* </MidenProvider>
|
|
139
|
+
* </MidenFiSignerProvider>
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* For wallet operations, use the useMidenFiWallet hook:
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* const { connected, connect, disconnect, select, wallets } = useMidenFiWallet();
|
|
147
|
+
*
|
|
148
|
+
* // If multiple wallets, select one first
|
|
149
|
+
* select(wallets[0].adapter.name);
|
|
150
|
+
*
|
|
151
|
+
* // Then connect
|
|
152
|
+
* await connect();
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export const MidenFiSignerProvider: FC<MidenFiSignerProviderProps> = ({
|
|
156
|
+
children,
|
|
157
|
+
wallets: walletsProp,
|
|
158
|
+
appName = 'Miden DApp',
|
|
159
|
+
network = WalletAdapterNetwork.Testnet,
|
|
160
|
+
autoConnect = true,
|
|
161
|
+
privateDataPermission = PrivateDataPermission.UponRequest,
|
|
162
|
+
allowedPrivateData = AllowedPrivateData.None,
|
|
163
|
+
onError,
|
|
164
|
+
localStorageKey = 'walletName',
|
|
165
|
+
}) => {
|
|
166
|
+
// Create default wallets if not provided
|
|
167
|
+
const adapters = useMemo(
|
|
168
|
+
() => walletsProp ?? [new MidenWalletAdapter({ appName })],
|
|
169
|
+
[walletsProp, appName]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const [name, setName] = useLocalStorage<WalletName | null>(
|
|
173
|
+
localStorageKey,
|
|
174
|
+
null
|
|
175
|
+
);
|
|
176
|
+
const [{ wallet, adapter, address, publicKey, connected }, setState] =
|
|
177
|
+
useState(initialState);
|
|
178
|
+
const readyState = adapter?.readyState || WalletReadyState.Unsupported;
|
|
179
|
+
const [connecting, setConnecting] = useState(false);
|
|
180
|
+
const [disconnecting, setDisconnecting] = useState(false);
|
|
181
|
+
const isConnecting = useRef(false);
|
|
182
|
+
const isDisconnecting = useRef(false);
|
|
183
|
+
const isUnloading = useRef(false);
|
|
184
|
+
|
|
185
|
+
// Wrap adapters to conform to the `Wallet` interface
|
|
186
|
+
const [wallets, setWallets] = useState(() =>
|
|
187
|
+
adapters.map((adapter) => ({
|
|
188
|
+
adapter,
|
|
189
|
+
readyState: adapter.readyState,
|
|
190
|
+
}))
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// When the adapters change, start to listen for changes to their `readyState`
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
setWallets((wallets) =>
|
|
196
|
+
adapters.map((adapter, index) => {
|
|
197
|
+
const wallet = wallets[index];
|
|
198
|
+
return wallet &&
|
|
199
|
+
wallet.adapter === adapter &&
|
|
200
|
+
wallet.readyState === adapter.readyState
|
|
201
|
+
? wallet
|
|
202
|
+
: {
|
|
203
|
+
adapter: adapter,
|
|
204
|
+
readyState: adapter.readyState,
|
|
205
|
+
};
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
function handleReadyStateChange(
|
|
210
|
+
this: Adapter,
|
|
211
|
+
readyState: WalletReadyState
|
|
212
|
+
) {
|
|
213
|
+
setWallets((prevWallets) => {
|
|
214
|
+
const index = prevWallets.findIndex(({ adapter }) => adapter === this);
|
|
215
|
+
if (index === -1) return prevWallets;
|
|
216
|
+
|
|
217
|
+
const { adapter } = prevWallets[index]!;
|
|
218
|
+
return [
|
|
219
|
+
...prevWallets.slice(0, index),
|
|
220
|
+
{ adapter, readyState },
|
|
221
|
+
...prevWallets.slice(index + 1),
|
|
222
|
+
];
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
adapters.forEach((adapter) =>
|
|
227
|
+
adapter.on('readyStateChange', handleReadyStateChange, adapter)
|
|
228
|
+
);
|
|
229
|
+
return () =>
|
|
230
|
+
adapters.forEach((adapter) =>
|
|
231
|
+
adapter.off('readyStateChange', handleReadyStateChange, adapter)
|
|
232
|
+
);
|
|
233
|
+
}, [adapters]);
|
|
234
|
+
|
|
235
|
+
// When the selected wallet changes, initialize the state.
|
|
236
|
+
// Use a functional update to bail out when nothing has actually changed,
|
|
237
|
+
// preventing unnecessary re-renders that cause WASM concurrency races.
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
const found = name && wallets.find(({ adapter }) => adapter.name === name);
|
|
240
|
+
if (found) {
|
|
241
|
+
setState((prev) => {
|
|
242
|
+
if (
|
|
243
|
+
prev.wallet === found &&
|
|
244
|
+
prev.adapter === found.adapter &&
|
|
245
|
+
prev.connected === found.adapter.connected &&
|
|
246
|
+
prev.address === found.adapter.address &&
|
|
247
|
+
prev.publicKey === found.adapter.publicKey
|
|
248
|
+
)
|
|
249
|
+
return prev;
|
|
250
|
+
return {
|
|
251
|
+
wallet: found,
|
|
252
|
+
adapter: found.adapter,
|
|
253
|
+
connected: found.adapter.connected,
|
|
254
|
+
address: found.adapter.address,
|
|
255
|
+
publicKey: found.adapter.publicKey,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
setState((prev) => (prev === initialState ? prev : initialState));
|
|
260
|
+
}
|
|
261
|
+
}, [name, wallets]);
|
|
262
|
+
|
|
263
|
+
// If the window is closing or reloading, ignore disconnect and error events
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
function listener() {
|
|
266
|
+
isUnloading.current = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
window.addEventListener('beforeunload', listener);
|
|
270
|
+
return () => window.removeEventListener('beforeunload', listener);
|
|
271
|
+
}, [isUnloading]);
|
|
272
|
+
|
|
273
|
+
// Handle the adapter's connect event.
|
|
274
|
+
// Functional update bails out when values haven't changed to prevent re-render loops.
|
|
275
|
+
const handleConnect = useCallback(() => {
|
|
276
|
+
if (!adapter) return;
|
|
277
|
+
setState((prev) => {
|
|
278
|
+
if (
|
|
279
|
+
prev.connected === adapter.connected &&
|
|
280
|
+
prev.address === adapter.address &&
|
|
281
|
+
prev.publicKey === adapter.publicKey
|
|
282
|
+
)
|
|
283
|
+
return prev;
|
|
284
|
+
return {
|
|
285
|
+
...prev,
|
|
286
|
+
connected: adapter.connected,
|
|
287
|
+
address: adapter.address,
|
|
288
|
+
publicKey: adapter.publicKey,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
}, [adapter]);
|
|
292
|
+
|
|
293
|
+
// Handle the adapter's disconnect event
|
|
294
|
+
const handleDisconnect = useCallback(() => {
|
|
295
|
+
if (!isUnloading.current) setName(null);
|
|
296
|
+
}, [isUnloading, setName]);
|
|
297
|
+
|
|
298
|
+
// Handle the adapter's error event
|
|
299
|
+
const handleError = useCallback(
|
|
300
|
+
(error: WalletError) => {
|
|
301
|
+
if (!isUnloading.current) (onError || console.error)(error);
|
|
302
|
+
return error;
|
|
303
|
+
},
|
|
304
|
+
[isUnloading, onError]
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Setup and teardown event listeners when the adapter changes
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (adapter) {
|
|
310
|
+
adapter.on('connect', handleConnect);
|
|
311
|
+
adapter.on('disconnect', handleDisconnect);
|
|
312
|
+
adapter.on('error', handleError);
|
|
313
|
+
return () => {
|
|
314
|
+
adapter.off('connect', handleConnect);
|
|
315
|
+
adapter.off('disconnect', handleDisconnect);
|
|
316
|
+
adapter.off('error', handleError);
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}, [adapter, handleConnect, handleDisconnect, handleError]);
|
|
320
|
+
|
|
321
|
+
// When the adapter changes, disconnect the old one
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
return () => {
|
|
324
|
+
adapter?.disconnect();
|
|
325
|
+
};
|
|
326
|
+
}, [adapter]);
|
|
327
|
+
|
|
328
|
+
// Auto-select the first wallet if only one is available and none selected
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (!name && wallets.length === 1) {
|
|
331
|
+
setName(wallets[0].adapter.name);
|
|
332
|
+
}
|
|
333
|
+
}, [name, wallets, setName]);
|
|
334
|
+
|
|
335
|
+
// If autoConnect is enabled, try to connect when the adapter changes and is ready
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
if (
|
|
338
|
+
isConnecting.current ||
|
|
339
|
+
connected ||
|
|
340
|
+
!autoConnect ||
|
|
341
|
+
!adapter ||
|
|
342
|
+
!(
|
|
343
|
+
readyState === WalletReadyState.Installed ||
|
|
344
|
+
readyState === WalletReadyState.Loadable
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
return;
|
|
348
|
+
|
|
349
|
+
(async function () {
|
|
350
|
+
isConnecting.current = true;
|
|
351
|
+
setConnecting(true);
|
|
352
|
+
try {
|
|
353
|
+
await adapter.connect(
|
|
354
|
+
privateDataPermission,
|
|
355
|
+
network,
|
|
356
|
+
allowedPrivateData
|
|
357
|
+
);
|
|
358
|
+
} catch (error: any) {
|
|
359
|
+
setName(null);
|
|
360
|
+
} finally {
|
|
361
|
+
setConnecting(false);
|
|
362
|
+
isConnecting.current = false;
|
|
363
|
+
}
|
|
364
|
+
})();
|
|
365
|
+
}, [isConnecting, connected, autoConnect, adapter, readyState, setName, privateDataPermission, network, allowedPrivateData]);
|
|
366
|
+
|
|
367
|
+
// Connect the adapter to the wallet
|
|
368
|
+
const connect = useCallback(async () => {
|
|
369
|
+
if (isConnecting.current || isDisconnecting.current || connected) return;
|
|
370
|
+
if (!adapter) throw handleError(new WalletNotSelectedError());
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
!(
|
|
374
|
+
readyState === WalletReadyState.Installed ||
|
|
375
|
+
readyState === WalletReadyState.Loadable
|
|
376
|
+
)
|
|
377
|
+
) {
|
|
378
|
+
setName(null);
|
|
379
|
+
|
|
380
|
+
if (typeof window !== 'undefined') {
|
|
381
|
+
window.open(adapter.url, '_blank');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
throw handleError(new WalletNotReadyError());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
isConnecting.current = true;
|
|
388
|
+
setConnecting(true);
|
|
389
|
+
try {
|
|
390
|
+
await adapter.connect(privateDataPermission, network, allowedPrivateData);
|
|
391
|
+
} catch (error: any) {
|
|
392
|
+
setName(null);
|
|
393
|
+
throw error;
|
|
394
|
+
} finally {
|
|
395
|
+
setConnecting(false);
|
|
396
|
+
isConnecting.current = false;
|
|
397
|
+
}
|
|
398
|
+
}, [
|
|
399
|
+
isConnecting,
|
|
400
|
+
isDisconnecting,
|
|
401
|
+
connected,
|
|
402
|
+
adapter,
|
|
403
|
+
readyState,
|
|
404
|
+
handleError,
|
|
405
|
+
setName,
|
|
406
|
+
privateDataPermission,
|
|
407
|
+
network,
|
|
408
|
+
allowedPrivateData,
|
|
409
|
+
]);
|
|
410
|
+
|
|
411
|
+
// Disconnect the adapter from the wallet
|
|
412
|
+
const disconnect = useCallback(async () => {
|
|
413
|
+
if (isDisconnecting.current) return;
|
|
414
|
+
if (!adapter) return setName(null);
|
|
415
|
+
|
|
416
|
+
isDisconnecting.current = true;
|
|
417
|
+
setDisconnecting(true);
|
|
418
|
+
try {
|
|
419
|
+
await adapter.disconnect();
|
|
420
|
+
} catch (error: any) {
|
|
421
|
+
setName(null);
|
|
422
|
+
throw error;
|
|
423
|
+
} finally {
|
|
424
|
+
setDisconnecting(false);
|
|
425
|
+
isDisconnecting.current = false;
|
|
426
|
+
}
|
|
427
|
+
}, [isDisconnecting, adapter, setName]);
|
|
428
|
+
|
|
429
|
+
// Request transaction
|
|
430
|
+
const requestTransaction:
|
|
431
|
+
| MessageSignerWalletAdapterProps['requestTransaction']
|
|
432
|
+
| undefined = useMemo(
|
|
433
|
+
() =>
|
|
434
|
+
adapter && 'requestTransaction' in adapter
|
|
435
|
+
? async (transaction: MidenTransaction) => {
|
|
436
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
437
|
+
return await adapter.requestTransaction(transaction);
|
|
438
|
+
}
|
|
439
|
+
: undefined,
|
|
440
|
+
[adapter, handleError, connected]
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Request assets
|
|
444
|
+
const requestAssets:
|
|
445
|
+
| MessageSignerWalletAdapterProps['requestAssets']
|
|
446
|
+
| undefined = useMemo(
|
|
447
|
+
() =>
|
|
448
|
+
adapter && 'requestAssets' in adapter
|
|
449
|
+
? async () => {
|
|
450
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
451
|
+
return await adapter.requestAssets();
|
|
452
|
+
}
|
|
453
|
+
: undefined,
|
|
454
|
+
[adapter, handleError, connected]
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Request private notes
|
|
458
|
+
const requestPrivateNotes:
|
|
459
|
+
| MessageSignerWalletAdapterProps['requestPrivateNotes']
|
|
460
|
+
| undefined = useMemo(
|
|
461
|
+
() =>
|
|
462
|
+
adapter && 'requestPrivateNotes' in adapter
|
|
463
|
+
? async (noteFilterType: NoteFilterTypes, noteIds?: string[]) => {
|
|
464
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
465
|
+
return await adapter.requestPrivateNotes(noteFilterType, noteIds);
|
|
466
|
+
}
|
|
467
|
+
: undefined,
|
|
468
|
+
[adapter, handleError, connected]
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const signBytes: MessageSignerWalletAdapterProps['signBytes'] | undefined =
|
|
472
|
+
useMemo(
|
|
473
|
+
() =>
|
|
474
|
+
adapter && 'signBytes' in adapter
|
|
475
|
+
? async (message: Uint8Array, kind: SignKind) => {
|
|
476
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
477
|
+
return await adapter.signBytes(message, kind);
|
|
478
|
+
}
|
|
479
|
+
: undefined,
|
|
480
|
+
[adapter, handleError, connected]
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const importPrivateNote:
|
|
484
|
+
| MessageSignerWalletAdapterProps['importPrivateNote']
|
|
485
|
+
| undefined = useMemo(
|
|
486
|
+
() =>
|
|
487
|
+
adapter && 'importPrivateNote' in adapter
|
|
488
|
+
? async (note: Uint8Array) => {
|
|
489
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
490
|
+
return await adapter.importPrivateNote(note);
|
|
491
|
+
}
|
|
492
|
+
: undefined,
|
|
493
|
+
[adapter, handleError, connected]
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const requestConsumableNotes:
|
|
497
|
+
| MessageSignerWalletAdapterProps['requestConsumableNotes']
|
|
498
|
+
| undefined = useMemo(
|
|
499
|
+
() =>
|
|
500
|
+
adapter && 'requestConsumableNotes' in adapter
|
|
501
|
+
? async () => {
|
|
502
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
503
|
+
return await adapter.requestConsumableNotes();
|
|
504
|
+
}
|
|
505
|
+
: undefined,
|
|
506
|
+
[adapter, handleError, connected]
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const waitForTransaction:
|
|
510
|
+
| MessageSignerWalletAdapterProps['waitForTransaction']
|
|
511
|
+
| undefined = useMemo(
|
|
512
|
+
() =>
|
|
513
|
+
adapter && 'waitForTransaction' in adapter
|
|
514
|
+
? async (txId: string, timeout?: number) => {
|
|
515
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
516
|
+
return await adapter.waitForTransaction(txId, timeout);
|
|
517
|
+
}
|
|
518
|
+
: undefined,
|
|
519
|
+
[adapter, handleError, connected]
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const requestSend:
|
|
523
|
+
| MessageSignerWalletAdapterProps['requestSend']
|
|
524
|
+
| undefined = useMemo(
|
|
525
|
+
() =>
|
|
526
|
+
adapter && 'requestSend' in adapter
|
|
527
|
+
? async (transaction) => {
|
|
528
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
529
|
+
return await adapter.requestSend(transaction);
|
|
530
|
+
}
|
|
531
|
+
: undefined,
|
|
532
|
+
[adapter, handleError, connected]
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const requestConsume:
|
|
536
|
+
| MessageSignerWalletAdapterProps['requestConsume']
|
|
537
|
+
| undefined = useMemo(
|
|
538
|
+
() =>
|
|
539
|
+
adapter && 'requestConsume' in adapter
|
|
540
|
+
? async (transaction) => {
|
|
541
|
+
if (!connected) throw handleError(new WalletNotConnectedError());
|
|
542
|
+
return await adapter.requestConsume(transaction);
|
|
543
|
+
}
|
|
544
|
+
: undefined,
|
|
545
|
+
[adapter, handleError, connected]
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// Build SignerContext value.
|
|
549
|
+
//
|
|
550
|
+
// CRITICAL: signerContext MUST be referentially stable. MidenProvider's init
|
|
551
|
+
// effect has signerContext in its deps — every new object re-triggers initClient
|
|
552
|
+
// which does async WASM work. Two concurrent initClient calls cause the
|
|
553
|
+
// "recursive use of an object" crash.
|
|
554
|
+
//
|
|
555
|
+
// We keep a single mutable ref and only call setSignerContext when the ref
|
|
556
|
+
// identity actually needs to change (disconnected ↔ connected, or address change).
|
|
557
|
+
//
|
|
558
|
+
// Initialise as {isConnected:false} rather than null so MidenProvider's init
|
|
559
|
+
// effect hits the early-return path instead of creating a local-keystore client
|
|
560
|
+
// that races with our buildContext's WASM operations.
|
|
561
|
+
|
|
562
|
+
// Keep signBytes in a ref so buildContext doesn't re-run when its identity changes.
|
|
563
|
+
const signBytesRef = useRef(signBytes);
|
|
564
|
+
useEffect(() => { signBytesRef.current = signBytes; }, [signBytes]);
|
|
565
|
+
const connectRef = useRef(connect);
|
|
566
|
+
useEffect(() => { connectRef.current = connect; }, [connect]);
|
|
567
|
+
const disconnectRef = useRef(disconnect);
|
|
568
|
+
useEffect(() => { disconnectRef.current = disconnect; }, [disconnect]);
|
|
569
|
+
|
|
570
|
+
const disconnectedCtx = useRef<SignerContextValue>({
|
|
571
|
+
signCb: async () => { throw new Error('MidenFi wallet not connected'); },
|
|
572
|
+
accountConfig: null as any,
|
|
573
|
+
storeName: '',
|
|
574
|
+
name: 'MidenFi',
|
|
575
|
+
isConnected: false,
|
|
576
|
+
connect: connectRef.current,
|
|
577
|
+
disconnect: disconnectRef.current,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// The connected context ref — reused across renders to maintain referential identity.
|
|
581
|
+
const connectedCtxRef = useRef<SignerContextValue | null>(null);
|
|
582
|
+
// Track which address the connected context was built for.
|
|
583
|
+
const connectedAddressRef = useRef<string | null>(null);
|
|
584
|
+
|
|
585
|
+
const [signerContext, setSignerContext] = useState<SignerContextValue>(
|
|
586
|
+
disconnectedCtx.current
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
let cancelled = false;
|
|
591
|
+
|
|
592
|
+
async function buildContext() {
|
|
593
|
+
if (!connected || !publicKey || !address || !signBytesRef.current) {
|
|
594
|
+
// Already disconnected — don't set state again (same ref = no re-render).
|
|
595
|
+
if (connectedCtxRef.current !== null) {
|
|
596
|
+
connectedCtxRef.current = null;
|
|
597
|
+
connectedAddressRef.current = null;
|
|
598
|
+
setSignerContext(disconnectedCtx.current);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Already built for this address — reuse existing context (same ref).
|
|
604
|
+
if (connectedCtxRef.current && connectedAddressRef.current === address) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
if (!cancelled) {
|
|
610
|
+
const { AccountStorageMode } = await import('@miden-sdk/miden-sdk');
|
|
611
|
+
|
|
612
|
+
const signCb = async (_: Uint8Array, signingInputs: Uint8Array) => {
|
|
613
|
+
const result = await signBytesRef.current!(signingInputs, 'signingInputs');
|
|
614
|
+
return result;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const ctx: SignerContextValue = {
|
|
618
|
+
signCb,
|
|
619
|
+
accountConfig: {
|
|
620
|
+
publicKeyCommitment: publicKey,
|
|
621
|
+
accountType: 'RegularAccountImmutableCode',
|
|
622
|
+
storageMode: AccountStorageMode.public(),
|
|
623
|
+
},
|
|
624
|
+
storeName: `midenfi_${address}`,
|
|
625
|
+
name: 'MidenFi',
|
|
626
|
+
isConnected: true,
|
|
627
|
+
connect: connectRef.current,
|
|
628
|
+
disconnect: disconnectRef.current,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
connectedCtxRef.current = ctx;
|
|
632
|
+
connectedAddressRef.current = address;
|
|
633
|
+
setSignerContext(ctx);
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error('Failed to build MidenFi signer context:', error);
|
|
637
|
+
if (!cancelled) {
|
|
638
|
+
connectedCtxRef.current = null;
|
|
639
|
+
connectedAddressRef.current = null;
|
|
640
|
+
setSignerContext(disconnectedCtx.current);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
buildContext();
|
|
646
|
+
return () => {
|
|
647
|
+
cancelled = true;
|
|
648
|
+
};
|
|
649
|
+
}, [connected, publicKey, address]);
|
|
650
|
+
|
|
651
|
+
const walletContextValue = useMemo(
|
|
652
|
+
() => ({
|
|
653
|
+
autoConnect,
|
|
654
|
+
wallets,
|
|
655
|
+
wallet,
|
|
656
|
+
address,
|
|
657
|
+
publicKey,
|
|
658
|
+
connected,
|
|
659
|
+
connecting,
|
|
660
|
+
disconnecting,
|
|
661
|
+
select: setName,
|
|
662
|
+
connect,
|
|
663
|
+
disconnect,
|
|
664
|
+
requestTransaction,
|
|
665
|
+
requestAssets,
|
|
666
|
+
requestPrivateNotes,
|
|
667
|
+
signBytes,
|
|
668
|
+
importPrivateNote,
|
|
669
|
+
requestConsumableNotes,
|
|
670
|
+
waitForTransaction,
|
|
671
|
+
requestSend,
|
|
672
|
+
requestConsume,
|
|
673
|
+
}),
|
|
674
|
+
[
|
|
675
|
+
autoConnect,
|
|
676
|
+
wallets,
|
|
677
|
+
wallet,
|
|
678
|
+
address,
|
|
679
|
+
publicKey,
|
|
680
|
+
connected,
|
|
681
|
+
connecting,
|
|
682
|
+
disconnecting,
|
|
683
|
+
setName,
|
|
684
|
+
connect,
|
|
685
|
+
disconnect,
|
|
686
|
+
requestTransaction,
|
|
687
|
+
requestAssets,
|
|
688
|
+
requestPrivateNotes,
|
|
689
|
+
signBytes,
|
|
690
|
+
importPrivateNote,
|
|
691
|
+
requestConsumableNotes,
|
|
692
|
+
waitForTransaction,
|
|
693
|
+
requestSend,
|
|
694
|
+
requestConsume,
|
|
695
|
+
]
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<WalletContext.Provider value={walletContextValue}>
|
|
700
|
+
<SignerContext.Provider value={signerContext}>
|
|
701
|
+
{children}
|
|
702
|
+
</SignerContext.Provider>
|
|
703
|
+
</WalletContext.Provider>
|
|
704
|
+
);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Hook for MidenFi wallet operations beyond the unified useSigner interface.
|
|
709
|
+
* Use this to access wallet-specific methods like requestTransaction, requestAssets, etc.
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```tsx
|
|
713
|
+
* const { connected, connect, disconnect, wallets, select } = useMidenFiWallet();
|
|
714
|
+
*
|
|
715
|
+
* // Connect
|
|
716
|
+
* await connect();
|
|
717
|
+
*
|
|
718
|
+
* // Request a transaction
|
|
719
|
+
* const txId = await requestTransaction({ ... });
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
export function useMidenFiWallet(): WalletContextState {
|
|
723
|
+
const context = useContext(WalletContext);
|
|
724
|
+
if (!context || Object.keys(context).length === 0) {
|
|
725
|
+
throw new Error('useMidenFiWallet must be used within MidenFiSignerProvider');
|
|
726
|
+
}
|
|
727
|
+
return context;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Re-export for backward compatibility
|
|
731
|
+
export { WalletContext };
|
|
732
|
+
export type { WalletContextState as MidenFiWalletContextState };
|