@hardkas/react 0.4.0-alpha
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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/index.d.ts +4722 -0
- package/dist/index.js +655 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
// src/provider.tsx
|
|
2
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { createPublicClient, http } from "viem";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var HardKasContext = createContext(void 0);
|
|
7
|
+
function HardKasProvider({ config, children, queryClient: externalQueryClient }) {
|
|
8
|
+
const queryClient = useMemo(() => externalQueryClient ?? new QueryClient(), [externalQueryClient]);
|
|
9
|
+
const [sseStatus, setSseStatus] = React.useState("disconnected");
|
|
10
|
+
const [lastEvent, setLastEvent] = React.useState(null);
|
|
11
|
+
const listeners = React.useRef(/* @__PURE__ */ new Set());
|
|
12
|
+
const eventSource = React.useRef(null);
|
|
13
|
+
const reconnectTimer = React.useRef(null);
|
|
14
|
+
const backoffMs = React.useRef(500);
|
|
15
|
+
const subscribe = React.useCallback((callback) => {
|
|
16
|
+
listeners.current.add(callback);
|
|
17
|
+
return () => listeners.current.delete(callback);
|
|
18
|
+
}, []);
|
|
19
|
+
const connect = React.useCallback(() => {
|
|
20
|
+
if (typeof window === "undefined") return;
|
|
21
|
+
if (!config.devServerUrl) {
|
|
22
|
+
setSseStatus("disconnected");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof EventSource === "undefined") {
|
|
26
|
+
setSseStatus("failed");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (eventSource.current) {
|
|
30
|
+
eventSource.current.close();
|
|
31
|
+
}
|
|
32
|
+
setSseStatus("connecting");
|
|
33
|
+
const baseUrl = config.devServerUrl;
|
|
34
|
+
const url = baseUrl.endsWith("/") ? `${baseUrl}api/stream` : `${baseUrl}/api/stream`;
|
|
35
|
+
const es = new EventSource(url);
|
|
36
|
+
eventSource.current = es;
|
|
37
|
+
es.onopen = () => {
|
|
38
|
+
setSseStatus("connected");
|
|
39
|
+
backoffMs.current = 500;
|
|
40
|
+
};
|
|
41
|
+
es.onerror = () => {
|
|
42
|
+
es.close();
|
|
43
|
+
eventSource.current = null;
|
|
44
|
+
setSseStatus("reconnecting");
|
|
45
|
+
const nextBackoff = Math.min(backoffMs.current * 2, 1e4);
|
|
46
|
+
backoffMs.current = nextBackoff;
|
|
47
|
+
reconnectTimer.current = setTimeout(() => {
|
|
48
|
+
connect();
|
|
49
|
+
}, nextBackoff);
|
|
50
|
+
};
|
|
51
|
+
es.onmessage = (e) => {
|
|
52
|
+
const event = {
|
|
53
|
+
type: "message",
|
|
54
|
+
payload: e.data,
|
|
55
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
56
|
+
};
|
|
57
|
+
setLastEvent(event);
|
|
58
|
+
listeners.current.forEach((l) => l(event));
|
|
59
|
+
};
|
|
60
|
+
const namedEvents = [
|
|
61
|
+
"session-changed",
|
|
62
|
+
"session-created",
|
|
63
|
+
"session-deleted",
|
|
64
|
+
"health-changed",
|
|
65
|
+
"sandbox-session-created",
|
|
66
|
+
"sandbox-session-paired",
|
|
67
|
+
"sandbox-session-expired",
|
|
68
|
+
"sandbox-session-disconnected"
|
|
69
|
+
];
|
|
70
|
+
namedEvents.forEach((type) => {
|
|
71
|
+
es.addEventListener(type, (e) => {
|
|
72
|
+
const event = {
|
|
73
|
+
type,
|
|
74
|
+
payload: e.data ? JSON.parse(e.data) : void 0,
|
|
75
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
76
|
+
};
|
|
77
|
+
setLastEvent(event);
|
|
78
|
+
listeners.current.forEach((l) => l(event));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}, [config.devServerUrl]);
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
connect();
|
|
84
|
+
return () => {
|
|
85
|
+
if (eventSource.current) {
|
|
86
|
+
eventSource.current.close();
|
|
87
|
+
eventSource.current = null;
|
|
88
|
+
}
|
|
89
|
+
if (reconnectTimer.current) {
|
|
90
|
+
clearTimeout(reconnectTimer.current);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}, [connect]);
|
|
94
|
+
const igraClient = useMemo(() => {
|
|
95
|
+
return createPublicClient({
|
|
96
|
+
chain: {
|
|
97
|
+
id: 19416,
|
|
98
|
+
// Igra Local Default
|
|
99
|
+
name: "Igra Local",
|
|
100
|
+
nativeCurrency: { name: "Igra Kaspa", symbol: "iKAS", decimals: 18 },
|
|
101
|
+
rpcUrls: {
|
|
102
|
+
default: { http: [config.igraRpcUrl || "http://127.0.0.1:8545"] }
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
transport: http(config.igraRpcUrl || "http://127.0.0.1:8545")
|
|
106
|
+
});
|
|
107
|
+
}, [config.igraRpcUrl]);
|
|
108
|
+
const value = useMemo(() => ({
|
|
109
|
+
config: {
|
|
110
|
+
...config,
|
|
111
|
+
localOnly: config.localOnly ?? true
|
|
112
|
+
},
|
|
113
|
+
igraClient,
|
|
114
|
+
queryClient,
|
|
115
|
+
sseStatus,
|
|
116
|
+
lastEvent,
|
|
117
|
+
subscribe
|
|
118
|
+
}), [config, igraClient, queryClient, sseStatus, lastEvent, subscribe]);
|
|
119
|
+
return /* @__PURE__ */ jsx(HardKasContext.Provider, { value, children: /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children }) });
|
|
120
|
+
}
|
|
121
|
+
function useHardKas() {
|
|
122
|
+
const context = useContext(HardKasContext);
|
|
123
|
+
if (!context) {
|
|
124
|
+
throw new Error("useHardKas must be used within a HardKasProvider");
|
|
125
|
+
}
|
|
126
|
+
return context;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/hooks/session.ts
|
|
130
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
131
|
+
import { useEffect } from "react";
|
|
132
|
+
function useHardKasSession(name) {
|
|
133
|
+
const { config, subscribe } = useHardKas();
|
|
134
|
+
const queryClient = useQueryClient();
|
|
135
|
+
const sessionToResolve = name || config.sessionName;
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const sync = () => queryClient.invalidateQueries({ queryKey: ["hardkas", "session"] });
|
|
138
|
+
return subscribe((event) => {
|
|
139
|
+
if (["session-changed", "session-created", "session-deleted"].includes(event.type)) {
|
|
140
|
+
sync();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}, [queryClient, subscribe]);
|
|
144
|
+
return useQuery({
|
|
145
|
+
queryKey: ["hardkas", "session", sessionToResolve || "active"],
|
|
146
|
+
queryFn: async () => {
|
|
147
|
+
try {
|
|
148
|
+
const baseUrl = config.devServerUrl || "";
|
|
149
|
+
const url = baseUrl ? baseUrl.endsWith("/") ? `${baseUrl}api/session` : `${baseUrl}/api/session` : "/api/session";
|
|
150
|
+
const response = await fetch(url);
|
|
151
|
+
const json = await response.json();
|
|
152
|
+
const active = json.active;
|
|
153
|
+
if (!active) return null;
|
|
154
|
+
return {
|
|
155
|
+
name: active.name,
|
|
156
|
+
l1: { wallet: active.l1?.wallet, address: active.l1?.address },
|
|
157
|
+
l2: { account: active.l2?.account, address: active.l2?.address },
|
|
158
|
+
bridge: { mode: active.bridge?.mode },
|
|
159
|
+
health: active.health || { isHealthy: true, warnings: [] },
|
|
160
|
+
diagnostics: json.diagnostics || []
|
|
161
|
+
};
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error("Failed to fetch session from dev server:", e);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
staleTime: 3e4
|
|
168
|
+
// Rely on SSE for updates
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/hooks/health.ts
|
|
173
|
+
import { useQuery as useQuery2, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
|
|
174
|
+
import { useEffect as useEffect2 } from "react";
|
|
175
|
+
function useHardKasHealth() {
|
|
176
|
+
const { config, subscribe } = useHardKas();
|
|
177
|
+
const queryClient = useQueryClient2();
|
|
178
|
+
useEffect2(() => {
|
|
179
|
+
const sync = () => queryClient.invalidateQueries({ queryKey: ["hardkas", "health"] });
|
|
180
|
+
return subscribe((event) => {
|
|
181
|
+
if (["health-changed", "session-changed"].includes(event.type)) {
|
|
182
|
+
sync();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}, [queryClient, subscribe]);
|
|
186
|
+
return useQuery2({
|
|
187
|
+
queryKey: ["hardkas", "health"],
|
|
188
|
+
queryFn: async () => {
|
|
189
|
+
try {
|
|
190
|
+
const baseUrl = config.devServerUrl || "";
|
|
191
|
+
const url = baseUrl ? baseUrl.endsWith("/") ? `${baseUrl}api/health` : `${baseUrl}/api/health` : "/api/health";
|
|
192
|
+
const res = await fetch(url);
|
|
193
|
+
if (!res || !res.ok) {
|
|
194
|
+
throw new Error("Failed to fetch health from dev server");
|
|
195
|
+
}
|
|
196
|
+
return await res.json();
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error("Failed to fetch health from dev server:", e);
|
|
199
|
+
throw e;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
refetchInterval: 3e4,
|
|
203
|
+
// Background poll as fallback
|
|
204
|
+
retry: 2
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/hooks/kaspa.ts
|
|
209
|
+
import { useQuery as useQuery3 } from "@tanstack/react-query";
|
|
210
|
+
function useKaspaWallet() {
|
|
211
|
+
const { data: session } = useHardKasSession();
|
|
212
|
+
return {
|
|
213
|
+
name: session?.l1.wallet,
|
|
214
|
+
address: session?.l1.address,
|
|
215
|
+
isLoading: !session
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function useKaspaBalance(options = {}) {
|
|
219
|
+
const { config } = useHardKas();
|
|
220
|
+
const { address, name } = useKaspaWallet();
|
|
221
|
+
return useQuery3({
|
|
222
|
+
queryKey: ["kaspa", "balance", address, config.kaspaRpcUrl, name],
|
|
223
|
+
queryFn: async () => {
|
|
224
|
+
if (!address) return 0n;
|
|
225
|
+
const url = config.kaspaRpcUrl || "http://127.0.0.1:16110";
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(url, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "Content-Type": "application/json" },
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
jsonrpc: "2.0",
|
|
232
|
+
id: 1,
|
|
233
|
+
method: "getBalanceByAddressRequest",
|
|
234
|
+
params: { address }
|
|
235
|
+
})
|
|
236
|
+
});
|
|
237
|
+
const json = await response.json();
|
|
238
|
+
return BigInt(json.result?.balance || 0);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
console.error("Failed to fetch Kaspa balance:", e);
|
|
241
|
+
return 0n;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
enabled: !!address,
|
|
245
|
+
refetchInterval: options.refetchInterval ?? false
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/hooks/igra.ts
|
|
250
|
+
import { useQuery as useQuery4 } from "@tanstack/react-query";
|
|
251
|
+
function useIgraAccount() {
|
|
252
|
+
const { data: session } = useHardKasSession();
|
|
253
|
+
return {
|
|
254
|
+
name: session?.l2.account,
|
|
255
|
+
address: session?.l2.address,
|
|
256
|
+
isLoading: !session
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function useIgraBalance(options = {}) {
|
|
260
|
+
const { igraClient } = useHardKas();
|
|
261
|
+
const { address } = useIgraAccount();
|
|
262
|
+
const { data: session } = useHardKasSession();
|
|
263
|
+
const { config } = useHardKas();
|
|
264
|
+
return useQuery4({
|
|
265
|
+
queryKey: ["igra", "balance", address, config.igraRpcUrl, session?.name],
|
|
266
|
+
queryFn: async () => {
|
|
267
|
+
if (!address) return 0n;
|
|
268
|
+
return await igraClient.getBalance({ address });
|
|
269
|
+
},
|
|
270
|
+
enabled: !!address,
|
|
271
|
+
refetchInterval: options.refetchInterval ?? false
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/hooks/bridge.ts
|
|
276
|
+
import { useQuery as useQuery5, useMutation } from "@tanstack/react-query";
|
|
277
|
+
import { planBridgeEntry, simulatePrefixMining } from "@hardkas/bridge-local";
|
|
278
|
+
function useBridgeLocalPlan(options) {
|
|
279
|
+
const { data: session } = useHardKasSession();
|
|
280
|
+
const { config } = useHardKas();
|
|
281
|
+
return useQuery5({
|
|
282
|
+
queryKey: ["bridge", "plan", session?.name, options?.amountSompi.toString(), options?.toIgra, config.localOnly],
|
|
283
|
+
queryFn: async () => {
|
|
284
|
+
if (!session || !options) return null;
|
|
285
|
+
const targetAddress = options.toIgra || session.l2.address;
|
|
286
|
+
if (!targetAddress) throw new Error("Target Igra address is required.");
|
|
287
|
+
return planBridgeEntry({
|
|
288
|
+
fromAddress: session.l1.address,
|
|
289
|
+
targetEvmAddress: targetAddress,
|
|
290
|
+
amountSompi: options.amountSompi,
|
|
291
|
+
networkId: "simnet",
|
|
292
|
+
// Fixed for local simulation
|
|
293
|
+
availableUtxos: [
|
|
294
|
+
{
|
|
295
|
+
outpoint: { transactionId: "mock-utxo", index: 0 },
|
|
296
|
+
address: session.l1.address,
|
|
297
|
+
amountSompi: options.amountSompi * 2n,
|
|
298
|
+
scriptPublicKey: "mock-script"
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
enabled: !!session && !!options
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function useBridgeLocalSimulation() {
|
|
307
|
+
return useMutation({
|
|
308
|
+
mutationFn: async (params) => {
|
|
309
|
+
return simulatePrefixMining(params.payloadBase, params.prefix, { timeoutMs: 1e4 });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/hooks/metamask.ts
|
|
315
|
+
import { useState, useEffect as useEffect3, useCallback } from "react";
|
|
316
|
+
import { createWalletClient, custom } from "viem";
|
|
317
|
+
function useMetaMaskLocal() {
|
|
318
|
+
const [state, setState] = useState({
|
|
319
|
+
installed: false,
|
|
320
|
+
connected: false,
|
|
321
|
+
supported: false,
|
|
322
|
+
localIgraDetected: false,
|
|
323
|
+
errors: []
|
|
324
|
+
});
|
|
325
|
+
const checkStatus = useCallback(async () => {
|
|
326
|
+
if (typeof window === "undefined" || !window.ethereum) {
|
|
327
|
+
setState((s) => ({ ...s, installed: false }));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const provider = window.ethereum;
|
|
332
|
+
const chainIdHex = await provider.request({ method: "eth_chainId" });
|
|
333
|
+
const chainId = parseInt(chainIdHex, 16);
|
|
334
|
+
const accounts = await provider.request({ method: "eth_accounts" });
|
|
335
|
+
setState({
|
|
336
|
+
installed: true,
|
|
337
|
+
connected: accounts.length > 0,
|
|
338
|
+
supported: true,
|
|
339
|
+
account: accounts[0],
|
|
340
|
+
chainId,
|
|
341
|
+
localIgraDetected: chainId === 19416,
|
|
342
|
+
errors: []
|
|
343
|
+
});
|
|
344
|
+
} catch (e) {
|
|
345
|
+
setState((s) => ({ ...s, errors: [e.message] }));
|
|
346
|
+
}
|
|
347
|
+
}, []);
|
|
348
|
+
useEffect3(() => {
|
|
349
|
+
if (typeof window === "undefined" || !window.ethereum) return;
|
|
350
|
+
const provider = window.ethereum;
|
|
351
|
+
checkStatus();
|
|
352
|
+
const handleChange = () => checkStatus();
|
|
353
|
+
provider.on("accountsChanged", handleChange);
|
|
354
|
+
provider.on("chainChanged", handleChange);
|
|
355
|
+
provider.on("disconnect", handleChange);
|
|
356
|
+
return () => {
|
|
357
|
+
if (provider.removeListener) {
|
|
358
|
+
provider.removeListener("accountsChanged", handleChange);
|
|
359
|
+
provider.removeListener("chainChanged", handleChange);
|
|
360
|
+
provider.removeListener("disconnect", handleChange);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}, [checkStatus]);
|
|
364
|
+
return { state, refresh: checkStatus };
|
|
365
|
+
}
|
|
366
|
+
function useSwitchToLocalIgra() {
|
|
367
|
+
const switchChain = async () => {
|
|
368
|
+
if (typeof window === "undefined" || !window.ethereum) return;
|
|
369
|
+
try {
|
|
370
|
+
await window.ethereum.request({
|
|
371
|
+
method: "wallet_switchEthereumChain",
|
|
372
|
+
params: [{ chainId: "0x4bd8" }]
|
|
373
|
+
// 19416
|
|
374
|
+
});
|
|
375
|
+
} catch (e) {
|
|
376
|
+
if (e.code === 4902) {
|
|
377
|
+
await window.ethereum.request({
|
|
378
|
+
method: "wallet_addEthereumChain",
|
|
379
|
+
params: [{
|
|
380
|
+
chainId: "0x4bd8",
|
|
381
|
+
chainName: "HardKas Igra Local",
|
|
382
|
+
rpcUrls: ["http://127.0.0.1:8545"],
|
|
383
|
+
nativeCurrency: { name: "iKAS", symbol: "iKAS", decimals: 18 }
|
|
384
|
+
}]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
return { switchChain };
|
|
390
|
+
}
|
|
391
|
+
function useIgraInjectedAccount(sessionL2Address) {
|
|
392
|
+
const { state } = useMetaMaskLocal();
|
|
393
|
+
const matches = !!state.account && !!sessionL2Address && state.account.toLowerCase() === sessionL2Address.toLowerCase();
|
|
394
|
+
return {
|
|
395
|
+
injectedAddress: state.account,
|
|
396
|
+
sessionAddress: sessionL2Address,
|
|
397
|
+
matches
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function useLocalIgraWalletClient() {
|
|
401
|
+
const { state } = useMetaMaskLocal();
|
|
402
|
+
const getClient = useCallback(() => {
|
|
403
|
+
if (!state.connected || !window.ethereum) return null;
|
|
404
|
+
return createWalletClient({
|
|
405
|
+
account: state.account,
|
|
406
|
+
transport: custom(window.ethereum)
|
|
407
|
+
});
|
|
408
|
+
}, [state.connected, state.account]);
|
|
409
|
+
return { getClient };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/hooks/kasware.ts
|
|
413
|
+
import { useState as useState2, useEffect as useEffect4, useCallback as useCallback2 } from "react";
|
|
414
|
+
function useKasWareLocal() {
|
|
415
|
+
const [state, setState] = useState2({
|
|
416
|
+
installed: false,
|
|
417
|
+
connected: false,
|
|
418
|
+
supported: false,
|
|
419
|
+
localNetworkDetected: false,
|
|
420
|
+
errors: []
|
|
421
|
+
});
|
|
422
|
+
const checkStatus = useCallback2(async () => {
|
|
423
|
+
if (typeof window === "undefined" || !window.kasware) {
|
|
424
|
+
setState((s) => ({ ...s, installed: false }));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const provider = window.kasware;
|
|
429
|
+
const accounts = await provider.getAccounts();
|
|
430
|
+
const network = await provider.getNetwork();
|
|
431
|
+
setState({
|
|
432
|
+
installed: true,
|
|
433
|
+
connected: accounts.length > 0,
|
|
434
|
+
supported: true,
|
|
435
|
+
address: accounts[0],
|
|
436
|
+
network,
|
|
437
|
+
// Common local/dev network strings for KasWare
|
|
438
|
+
localNetworkDetected: ["kasparegtest", "localnet", "simnet"].includes(network),
|
|
439
|
+
errors: []
|
|
440
|
+
});
|
|
441
|
+
} catch (e) {
|
|
442
|
+
setState((s) => ({ ...s, errors: [e.message] }));
|
|
443
|
+
}
|
|
444
|
+
}, []);
|
|
445
|
+
useEffect4(() => {
|
|
446
|
+
if (typeof window === "undefined" || !window.kasware) return;
|
|
447
|
+
const provider = window.kasware;
|
|
448
|
+
checkStatus();
|
|
449
|
+
const handleChange = () => checkStatus();
|
|
450
|
+
provider.on("accountsChanged", handleChange);
|
|
451
|
+
provider.on("networkChanged", handleChange);
|
|
452
|
+
provider.on("disconnect", handleChange);
|
|
453
|
+
return () => {
|
|
454
|
+
if (provider.removeListener) {
|
|
455
|
+
provider.removeListener("accountsChanged", handleChange);
|
|
456
|
+
provider.removeListener("networkChanged", handleChange);
|
|
457
|
+
provider.removeListener("disconnect", handleChange);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}, [checkStatus]);
|
|
461
|
+
return { state, refresh: checkStatus };
|
|
462
|
+
}
|
|
463
|
+
function useConnectKasWareLocal() {
|
|
464
|
+
const { refresh } = useKasWareLocal();
|
|
465
|
+
const connect = async () => {
|
|
466
|
+
if (typeof window === "undefined" || !window.kasware) return null;
|
|
467
|
+
try {
|
|
468
|
+
const accounts = await window.kasware.requestAccounts();
|
|
469
|
+
await refresh();
|
|
470
|
+
return accounts[0];
|
|
471
|
+
} catch (e) {
|
|
472
|
+
console.error("KasWare connection failed:", e);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
return { connect };
|
|
477
|
+
}
|
|
478
|
+
function useKasWareSessionMatch(sessionL1Address) {
|
|
479
|
+
const { state } = useKasWareLocal();
|
|
480
|
+
let matches = false;
|
|
481
|
+
let reason;
|
|
482
|
+
if (!state.installed) {
|
|
483
|
+
reason = "not-installed";
|
|
484
|
+
} else if (!state.connected) {
|
|
485
|
+
reason = "not-connected";
|
|
486
|
+
} else if (!sessionL1Address) {
|
|
487
|
+
reason = "no-session";
|
|
488
|
+
} else if (!state.localNetworkDetected) {
|
|
489
|
+
reason = "network-mismatch";
|
|
490
|
+
} else {
|
|
491
|
+
const normalizedWallet = state.address?.toLowerCase().trim();
|
|
492
|
+
const normalizedSession = sessionL1Address?.toLowerCase().trim();
|
|
493
|
+
matches = normalizedWallet === normalizedSession;
|
|
494
|
+
if (!matches) reason = "address-mismatch";
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
walletAddress: state.address,
|
|
498
|
+
sessionAddress: sessionL1Address || void 0,
|
|
499
|
+
matches,
|
|
500
|
+
reason
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/hooks/sandbox.ts
|
|
505
|
+
import { useQuery as useQuery6, useMutation as useMutation2, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
|
|
506
|
+
import { useEffect as useEffect5 } from "react";
|
|
507
|
+
function useSandboxSessions() {
|
|
508
|
+
const { config, subscribe } = useHardKas();
|
|
509
|
+
const queryClient = useQueryClient3();
|
|
510
|
+
useEffect5(() => {
|
|
511
|
+
const sync = () => queryClient.invalidateQueries({ queryKey: ["sandbox", "sessions"] });
|
|
512
|
+
return subscribe((event) => {
|
|
513
|
+
if ([
|
|
514
|
+
"sandbox-session-created",
|
|
515
|
+
"sandbox-session-paired",
|
|
516
|
+
"sandbox-session-expired",
|
|
517
|
+
"sandbox-session-disconnected"
|
|
518
|
+
].includes(event.type)) {
|
|
519
|
+
sync();
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}, [queryClient, subscribe]);
|
|
523
|
+
return useQuery6({
|
|
524
|
+
queryKey: ["sandbox", "sessions"],
|
|
525
|
+
queryFn: async () => {
|
|
526
|
+
const baseUrl = config.devServerUrl || "";
|
|
527
|
+
const url = baseUrl ? baseUrl.endsWith("/") ? `${baseUrl}api/walletconnect/sandbox/sessions` : `${baseUrl}/api/walletconnect/sandbox/sessions` : "/api/walletconnect/sandbox/sessions";
|
|
528
|
+
const res = await fetch(url);
|
|
529
|
+
const json = await res.json();
|
|
530
|
+
return json.sessions;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function useCreateSandboxSession() {
|
|
535
|
+
const { config } = useHardKas();
|
|
536
|
+
const queryClient = useQueryClient3();
|
|
537
|
+
return useMutation2({
|
|
538
|
+
mutationFn: async () => {
|
|
539
|
+
const baseUrl = config.devServerUrl || "";
|
|
540
|
+
const url = baseUrl ? baseUrl.endsWith("/") ? `${baseUrl}api/walletconnect/sandbox/create` : `${baseUrl}/api/walletconnect/sandbox/create` : "/api/walletconnect/sandbox/create";
|
|
541
|
+
const res = await fetch(url, { method: "POST" });
|
|
542
|
+
return await res.json();
|
|
543
|
+
},
|
|
544
|
+
onSuccess: () => {
|
|
545
|
+
queryClient.invalidateQueries({ queryKey: ["sandbox", "sessions"] });
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function usePairSandboxSession() {
|
|
550
|
+
const { config } = useHardKas();
|
|
551
|
+
const queryClient = useQueryClient3();
|
|
552
|
+
return useMutation2({
|
|
553
|
+
mutationFn: async (id) => {
|
|
554
|
+
const baseUrl = config.devServerUrl || "";
|
|
555
|
+
const url = baseUrl ? baseUrl.endsWith("/") ? `${baseUrl}api/walletconnect/sandbox/pair` : `${baseUrl}/api/walletconnect/sandbox/pair` : "/api/walletconnect/sandbox/pair";
|
|
556
|
+
const res = await fetch(url, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: { "Content-Type": "application/json" },
|
|
559
|
+
body: JSON.stringify({ id })
|
|
560
|
+
});
|
|
561
|
+
return await res.json();
|
|
562
|
+
},
|
|
563
|
+
onSuccess: () => {
|
|
564
|
+
queryClient.invalidateQueries({ queryKey: ["sandbox", "sessions"] });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function useDisconnectSandboxSession() {
|
|
569
|
+
const { config } = useHardKas();
|
|
570
|
+
const queryClient = useQueryClient3();
|
|
571
|
+
return useMutation2({
|
|
572
|
+
mutationFn: async (id) => {
|
|
573
|
+
const baseUrl = config.devServerUrl || "";
|
|
574
|
+
const url = baseUrl ? baseUrl.endsWith("/") ? `${baseUrl}api/walletconnect/sandbox/disconnect` : `${baseUrl}/api/walletconnect/sandbox/disconnect` : "/api/walletconnect/sandbox/disconnect";
|
|
575
|
+
const res = await fetch(url, {
|
|
576
|
+
method: "POST",
|
|
577
|
+
headers: { "Content-Type": "application/json" },
|
|
578
|
+
body: JSON.stringify({ id })
|
|
579
|
+
});
|
|
580
|
+
return await res.json();
|
|
581
|
+
},
|
|
582
|
+
onSuccess: () => {
|
|
583
|
+
queryClient.invalidateQueries({ queryKey: ["sandbox", "sessions"] });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/hooks/contracts.ts
|
|
589
|
+
import { useQuery as useQuery7, useMutation as useMutation3 } from "@tanstack/react-query";
|
|
590
|
+
function useIgraReadContract(options) {
|
|
591
|
+
const { igraClient, config } = useHardKas();
|
|
592
|
+
const { data: session } = useHardKasSession();
|
|
593
|
+
return useQuery7({
|
|
594
|
+
queryKey: ["igra", "read", options.address, options.functionName, options.args, config.igraRpcUrl, session?.name],
|
|
595
|
+
queryFn: async () => {
|
|
596
|
+
return await igraClient.readContract({
|
|
597
|
+
address: options.address,
|
|
598
|
+
abi: options.abi,
|
|
599
|
+
functionName: options.functionName,
|
|
600
|
+
args: options.args
|
|
601
|
+
});
|
|
602
|
+
},
|
|
603
|
+
enabled: options.enabled ?? true,
|
|
604
|
+
refetchInterval: options.refetchInterval ?? false
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function useIgraWriteContract() {
|
|
608
|
+
return useMutation3({
|
|
609
|
+
mutationFn: async (params) => {
|
|
610
|
+
if (!params.walletClient) {
|
|
611
|
+
throw new Error("A wallet client is required to write to a contract.");
|
|
612
|
+
}
|
|
613
|
+
return await params.walletClient.writeContract({
|
|
614
|
+
address: params.address,
|
|
615
|
+
abi: params.abi,
|
|
616
|
+
functionName: params.functionName,
|
|
617
|
+
args: params.args
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function useIgraWaitForReceipt() {
|
|
623
|
+
const { igraClient } = useHardKas();
|
|
624
|
+
return useMutation3({
|
|
625
|
+
mutationFn: async (hash) => {
|
|
626
|
+
return await igraClient.waitForTransactionReceipt({ hash });
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
export {
|
|
631
|
+
HardKasProvider,
|
|
632
|
+
useBridgeLocalPlan,
|
|
633
|
+
useBridgeLocalSimulation,
|
|
634
|
+
useConnectKasWareLocal,
|
|
635
|
+
useCreateSandboxSession,
|
|
636
|
+
useDisconnectSandboxSession,
|
|
637
|
+
useHardKas,
|
|
638
|
+
useHardKasHealth,
|
|
639
|
+
useHardKasSession,
|
|
640
|
+
useIgraAccount,
|
|
641
|
+
useIgraBalance,
|
|
642
|
+
useIgraInjectedAccount,
|
|
643
|
+
useIgraReadContract,
|
|
644
|
+
useIgraWaitForReceipt,
|
|
645
|
+
useIgraWriteContract,
|
|
646
|
+
useKasWareLocal,
|
|
647
|
+
useKasWareSessionMatch,
|
|
648
|
+
useKaspaBalance,
|
|
649
|
+
useKaspaWallet,
|
|
650
|
+
useLocalIgraWalletClient,
|
|
651
|
+
useMetaMaskLocal,
|
|
652
|
+
usePairSandboxSession,
|
|
653
|
+
useSandboxSessions,
|
|
654
|
+
useSwitchToLocalIgra
|
|
655
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hardkas/react",
|
|
3
|
+
"version": "0.4.0-alpha",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"react": ">=18",
|
|
12
|
+
"react-dom": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@tanstack/react-query": "^5.61.5",
|
|
16
|
+
"viem": "^2.21.51",
|
|
17
|
+
"@hardkas/bridge-local": "0.4.0-alpha",
|
|
18
|
+
"@hardkas/core": "0.4.0-alpha",
|
|
19
|
+
"@hardkas/kaspa-rpc": "0.4.0-alpha",
|
|
20
|
+
"@hardkas/sessions": "0.4.0-alpha",
|
|
21
|
+
"@hardkas/l2": "0.4.0-alpha"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@testing-library/dom": "^10.4.1",
|
|
25
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
26
|
+
"@testing-library/react": "^16.3.2",
|
|
27
|
+
"@types/react": "^18.3.12",
|
|
28
|
+
"@types/react-dom": "^18.3.1",
|
|
29
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
30
|
+
"jsdom": "^29.1.1",
|
|
31
|
+
"react": "^18.3.1",
|
|
32
|
+
"react-dom": "^18.3.1",
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.7.2",
|
|
35
|
+
"vite": "^5.0.0",
|
|
36
|
+
"vitest": "^2.1.8"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"author": "Javier Rodriguez",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
46
|
+
"test": "vitest run --passWithNoTests",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
}
|
|
49
|
+
}
|