@naculus/connect-appkit-react 0.1.9 → 0.2.1
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/README.md +4 -4
- package/dist/client.cjs +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +144 -0
- package/dist/client.d.ts +144 -0
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2270 -0
- package/dist/index.d.ts +2270 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +30 -12
- package/api-extractor.json +0 -21
- package/src/chain-abstraction/index.ts +0 -8
- package/src/chain-abstraction/useCompareCosts.ts +0 -137
- package/src/chain-abstraction/useExecuteRoute.ts +0 -129
- package/src/chain-abstraction/useRouteQuote.ts +0 -168
- package/src/client.test.ts +0 -242
- package/src/client.ts +0 -265
- package/src/hooks/useAccount.test.ts +0 -67
- package/src/hooks/useAccount.ts +0 -37
- package/src/hooks/useBalance.test.ts +0 -459
- package/src/hooks/useBalance.ts +0 -170
- package/src/hooks/useChain.test.ts +0 -160
- package/src/hooks/useChain.ts +0 -53
- package/src/hooks/useConnect.test.ts +0 -112
- package/src/hooks/useConnect.ts +0 -29
- package/src/hooks/useDisconnect.test.ts +0 -75
- package/src/hooks/useDisconnect.ts +0 -32
- package/src/hooks/useERC20Allowance.ts +0 -93
- package/src/hooks/useERC20Approve.ts +0 -220
- package/src/hooks/useERC20Transfer.ts +0 -134
- package/src/hooks/useERC20TransferSimulation.ts +0 -147
- package/src/hooks/useEmbeddedWallet.test.ts +0 -266
- package/src/hooks/useEmbeddedWallet.ts +0 -181
- package/src/hooks/useLastTx.test.ts +0 -117
- package/src/hooks/useLastTx.ts +0 -73
- package/src/hooks/useLookupAddress.ts +0 -131
- package/src/hooks/useNotification.ts +0 -308
- package/src/hooks/useResolveName.ts +0 -128
- package/src/hooks/useSIWxLogin.test.ts +0 -307
- package/src/hooks/useSIWxLogin.ts +0 -163
- package/src/hooks/useSIWxSession.test.ts +0 -481
- package/src/hooks/useSIWxSession.ts +0 -290
- package/src/hooks/useSendCalls.ts +0 -103
- package/src/hooks/useSendTransaction.test.ts +0 -175
- package/src/hooks/useSendTransaction.ts +0 -80
- package/src/hooks/useSendUserOperation.ts +0 -313
- package/src/hooks/useSession.ts +0 -96
- package/src/hooks/useSessionKeys.ts +0 -356
- package/src/hooks/useSignInWithEthereum.test.ts +0 -224
- package/src/hooks/useSignInWithEthereum.ts +0 -46
- package/src/hooks/useSignInWithX.test.ts +0 -210
- package/src/hooks/useSignInWithX.ts +0 -141
- package/src/hooks/useSignMessage.test.ts +0 -151
- package/src/hooks/useSignMessage.ts +0 -65
- package/src/hooks/useSimulateTransfer.ts +0 -165
- package/src/hooks/useSiwxAuthSession.test.ts +0 -456
- package/src/hooks/useSiwxAuthSession.ts +0 -163
- package/src/hooks/useSmartAccount.ts +0 -194
- package/src/hooks/useSwitchChain.ts +0 -68
- package/src/hooks/useTokenBalance.test.ts +0 -337
- package/src/hooks/useTokenBalance.ts +0 -210
- package/src/hooks/useTransactionSimulation.ts +0 -401
- package/src/hooks/useTxHistory.test.ts +0 -108
- package/src/hooks/useTxHistory.ts +0 -83
- package/src/hooks/useTxMonitor.test.ts +0 -127
- package/src/hooks/useTxMonitor.ts +0 -178
- package/src/hooks/useUserOpStatus.ts +0 -245
- package/src/hooks/useValidateDestination.test.ts +0 -96
- package/src/hooks/useValidateDestination.ts +0 -72
- package/src/hooks/useViemClient.test.ts +0 -155
- package/src/hooks/useViemClient.ts +0 -65
- package/src/hooks/useWallet.test.ts +0 -133
- package/src/hooks/useWallet.ts +0 -39
- package/src/hooks/useWeb3ErrorHandler.ts +0 -102
- package/src/index.ts +0 -186
- package/src/provider/Web3ConnectProvider.test.tsx +0 -178
- package/src/provider/Web3ConnectProvider.tsx +0 -530
- package/src/token-list/TokenSelector.tsx +0 -351
- package/src/token-list/defaults.ts +0 -39
- package/src/token-list/index.ts +0 -14
- package/src/token-list/useTokenList.ts +0 -119
- package/src/token-list/useTokenSearch.ts +0 -99
- package/src/types/index.ts +0 -99
- package/src/utils/chains.ts +0 -115
- package/src/utils/errorMessages.ts +0 -142
- package/src/utils/i18n.ts +0 -90
- package/src/wc-generated/components.ts +0 -377
- package/test/setup.ts +0 -17
- package/test/test-utils.tsx +0 -78
- package/tsup.config.ts +0 -18
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
-
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
4
|
-
import { useTxMonitor, TxMonitorContext } from "./useTxMonitor";
|
|
5
|
-
import type { TxMonitorLike, TxStatusEntry } from "./useTxMonitor";
|
|
6
|
-
import React from "react";
|
|
7
|
-
|
|
8
|
-
class MockMonitor implements TxMonitorLike {
|
|
9
|
-
private entries = new Map<string, TxStatusEntry>();
|
|
10
|
-
private listeners = new Map<string, Set<Function>>();
|
|
11
|
-
|
|
12
|
-
setEntry(hash: string, chainId: number, entry: TxStatusEntry): void {
|
|
13
|
-
const key = `${chainId}:${hash}`;
|
|
14
|
-
this.entries.set(key, entry);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async watchTx(hash: string, chainId: number): Promise<TxStatusEntry> {
|
|
18
|
-
const key = `${chainId}:${hash}`;
|
|
19
|
-
const entry = this.entries.get(key) ?? {
|
|
20
|
-
hash,
|
|
21
|
-
chainId,
|
|
22
|
-
from: "",
|
|
23
|
-
to: "",
|
|
24
|
-
value: "0x0",
|
|
25
|
-
status: "unknown" as const,
|
|
26
|
-
createdAt: Date.now(),
|
|
27
|
-
updatedAt: Date.now(),
|
|
28
|
-
replacementCount: 0,
|
|
29
|
-
};
|
|
30
|
-
this.entries.set(key, entry);
|
|
31
|
-
this.emit("statusChange", entry);
|
|
32
|
-
return entry;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
stopWatching(hash: string, chainId?: number): void {
|
|
36
|
-
// no-op
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getTxStatus(hash: string, chainId?: number): TxStatusEntry | null {
|
|
40
|
-
if (chainId !== undefined) return this.entries.get(`${chainId}:${hash}`) ?? null;
|
|
41
|
-
for (const [, entry] of this.entries) {
|
|
42
|
-
if (entry.hash === hash) return entry;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async getTxHistory(): Promise<TxStatusEntry[]> {
|
|
48
|
-
return Array.from(this.entries.values());
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async refreshTx(): Promise<void> {
|
|
52
|
-
// no-op
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
on(event: string, listener: Function): this {
|
|
56
|
-
const set = this.listeners.get(event) ?? new Set();
|
|
57
|
-
set.add(listener);
|
|
58
|
-
this.listeners.set(event, set);
|
|
59
|
-
return this;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
off(event: string, listener: Function): this {
|
|
63
|
-
const set = this.listeners.get(event);
|
|
64
|
-
if (set) set.delete(listener);
|
|
65
|
-
return this;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private emit(event: string, ...args: any[]): void {
|
|
69
|
-
const set = this.listeners.get(event);
|
|
70
|
-
if (set) set.forEach(fn => fn(...args));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function Wrapper({ monitor, children }: { monitor: TxMonitorLike; children: React.ReactNode }) {
|
|
75
|
-
return React.createElement(TxMonitorContext.Provider, { value: monitor }, children);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
describe("useTxMonitor", () => {
|
|
79
|
-
let monitor: MockMonitor;
|
|
80
|
-
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
monitor = new MockMonitor();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("returns idle state when no hash provided", () => {
|
|
86
|
-
const { result } = renderHook(() => useTxMonitor(undefined, undefined, monitor));
|
|
87
|
-
expect(result.current.status).toBe("idle");
|
|
88
|
-
expect(result.current.entry).toBeNull();
|
|
89
|
-
expect(result.current.isWatching).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("starts watching when hash is provided", async () => {
|
|
93
|
-
const hash = "0x" + "a".repeat(64);
|
|
94
|
-
const entry: TxStatusEntry = {
|
|
95
|
-
hash, chainId: 1, from: "0x" + "b".repeat(40), to: "0x" + "c".repeat(40),
|
|
96
|
-
value: "0x0", status: "pending", createdAt: Date.now(), updatedAt: Date.now(),
|
|
97
|
-
replacementCount: 0,
|
|
98
|
-
};
|
|
99
|
-
monitor.setEntry(hash, 1, entry);
|
|
100
|
-
|
|
101
|
-
const { result } = renderHook(() => useTxMonitor(hash, 1, monitor));
|
|
102
|
-
|
|
103
|
-
await waitFor(() => {
|
|
104
|
-
expect(result.current.isLoading).toBe(false);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
expect(result.current.entry).not.toBeNull();
|
|
108
|
-
expect(result.current.entry!.hash).toBe(hash);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("provides refresh and stopWatching methods", () => {
|
|
112
|
-
const hash = "0x" + "d".repeat(64);
|
|
113
|
-
const { result } = renderHook(() => useTxMonitor(hash, 1, monitor));
|
|
114
|
-
|
|
115
|
-
expect(typeof result.current.refresh).toBe("function");
|
|
116
|
-
expect(typeof result.current.stopWatching).toBe("function");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("works with TxMonitorContext", () => {
|
|
120
|
-
const hash = "0x" + "e".repeat(64);
|
|
121
|
-
const { result } = renderHook(() => useTxMonitor(hash, 1), {
|
|
122
|
-
wrapper: ({ children }) => React.createElement(Wrapper, { monitor, children }, children),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(result.current.isWatching).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
|
|
3
|
-
// ── Types (standalone, to avoid dependency on wallet-engine) ──────
|
|
4
|
-
|
|
5
|
-
export type TxStatus =
|
|
6
|
-
| "pending"
|
|
7
|
-
| "mined"
|
|
8
|
-
| "confirmed"
|
|
9
|
-
| "failed"
|
|
10
|
-
| "unknown";
|
|
11
|
-
|
|
12
|
-
export interface TxStatusEntry {
|
|
13
|
-
hash: string;
|
|
14
|
-
chainId: number;
|
|
15
|
-
from: string;
|
|
16
|
-
to: string;
|
|
17
|
-
value: string;
|
|
18
|
-
data?: string;
|
|
19
|
-
nonce?: number;
|
|
20
|
-
status: TxStatus;
|
|
21
|
-
blockNumber?: number;
|
|
22
|
-
blockHash?: string;
|
|
23
|
-
gasUsed?: string;
|
|
24
|
-
effectiveGasPrice?: string;
|
|
25
|
-
createdAt: number;
|
|
26
|
-
confirmedAt?: number;
|
|
27
|
-
updatedAt: number;
|
|
28
|
-
error?: string;
|
|
29
|
-
label?: string;
|
|
30
|
-
memo?: string;
|
|
31
|
-
replacementCount?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Minimal TxMonitor interface for React hooks */
|
|
35
|
-
export interface TxMonitorLike {
|
|
36
|
-
watchTx(hash: string, chainId: number, options?: {
|
|
37
|
-
requiredConfirmations?: number;
|
|
38
|
-
pollInterval?: number;
|
|
39
|
-
label?: string;
|
|
40
|
-
memo?: string;
|
|
41
|
-
initialEntry?: Partial<TxStatusEntry>;
|
|
42
|
-
}): Promise<TxStatusEntry>;
|
|
43
|
-
|
|
44
|
-
stopWatching(hash: string, chainId?: number): void;
|
|
45
|
-
getTxStatus(hash: string, chainId?: number): TxStatusEntry | null;
|
|
46
|
-
getTxHistory(address?: string, chainId?: number): Promise<TxStatusEntry[]>;
|
|
47
|
-
refreshTx(hash: string, chainId?: number): Promise<void>;
|
|
48
|
-
|
|
49
|
-
on(event: "statusChange", listener: (entry: TxStatusEntry) => void): this;
|
|
50
|
-
on(event: "confirmed", listener: (entry: TxStatusEntry) => void): this;
|
|
51
|
-
on(event: "failed", listener: (entry: TxStatusEntry) => void): this;
|
|
52
|
-
off(event: string, listener: Function): this;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Context for providing TxMonitor ───────────────────────────────
|
|
56
|
-
|
|
57
|
-
import { createContext, useContext } from "react";
|
|
58
|
-
|
|
59
|
-
export const TxMonitorContext = createContext<TxMonitorLike | null>(null);
|
|
60
|
-
|
|
61
|
-
export function useTxMonitorProvider(): TxMonitorLike | null {
|
|
62
|
-
return useContext(TxMonitorContext);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── Hook ──────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
export interface UseTxMonitorResult {
|
|
68
|
-
entry: TxStatusEntry | null;
|
|
69
|
-
status: TxStatus | "idle";
|
|
70
|
-
isLoading: boolean;
|
|
71
|
-
isWatching: boolean;
|
|
72
|
-
error: Error | null;
|
|
73
|
-
refresh: () => Promise<void>;
|
|
74
|
-
stopWatching: () => void;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Monitor a single transaction's lifecycle.
|
|
79
|
-
* When hash changes, automatically stops old watch and starts new one.
|
|
80
|
-
*/
|
|
81
|
-
export function useTxMonitor(
|
|
82
|
-
hash?: string,
|
|
83
|
-
chainId?: number,
|
|
84
|
-
monitor?: TxMonitorLike,
|
|
85
|
-
): UseTxMonitorResult {
|
|
86
|
-
const ctxMonitor = useTxMonitorProvider();
|
|
87
|
-
const effectiveMonitor = monitor ?? ctxMonitor;
|
|
88
|
-
|
|
89
|
-
const [entry, setEntry] = useState<TxStatusEntry | null>(null);
|
|
90
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
91
|
-
const [isWatching, setIsWatching] = useState(false);
|
|
92
|
-
const [error, setError] = useState<Error | null>(null);
|
|
93
|
-
const prevHashRef = useRef<string | undefined>(undefined);
|
|
94
|
-
|
|
95
|
-
const refresh = useCallback(async () => {
|
|
96
|
-
if (!effectiveMonitor || !hash) return;
|
|
97
|
-
setIsLoading(true);
|
|
98
|
-
setError(null);
|
|
99
|
-
try {
|
|
100
|
-
await effectiveMonitor.refreshTx(hash, chainId);
|
|
101
|
-
const current = effectiveMonitor.getTxStatus(hash, chainId);
|
|
102
|
-
if (current) setEntry(current);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
105
|
-
} finally {
|
|
106
|
-
setIsLoading(false);
|
|
107
|
-
}
|
|
108
|
-
}, [effectiveMonitor, hash, chainId]);
|
|
109
|
-
|
|
110
|
-
const stopWatching = useCallback(() => {
|
|
111
|
-
if (!effectiveMonitor || !hash) return;
|
|
112
|
-
effectiveMonitor.stopWatching(hash, chainId);
|
|
113
|
-
setIsWatching(false);
|
|
114
|
-
}, [effectiveMonitor, hash, chainId]);
|
|
115
|
-
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
if (!effectiveMonitor || !hash || !chainId) {
|
|
118
|
-
setEntry(null);
|
|
119
|
-
setIsWatching(false);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Stop previous watch if hash changed
|
|
124
|
-
if (prevHashRef.current && prevHashRef.current !== hash) {
|
|
125
|
-
effectiveMonitor.stopWatching(prevHashRef.current);
|
|
126
|
-
}
|
|
127
|
-
prevHashRef.current = hash;
|
|
128
|
-
|
|
129
|
-
let cancelled = false;
|
|
130
|
-
setIsWatching(true);
|
|
131
|
-
setIsLoading(true);
|
|
132
|
-
setError(null);
|
|
133
|
-
|
|
134
|
-
// Check if we already have status
|
|
135
|
-
const current = effectiveMonitor.getTxStatus(hash, chainId);
|
|
136
|
-
if (current) {
|
|
137
|
-
setEntry(current);
|
|
138
|
-
setIsLoading(false);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Start watching
|
|
142
|
-
effectiveMonitor.watchTx(hash, chainId, {}).then((initial) => {
|
|
143
|
-
if (!cancelled) {
|
|
144
|
-
setEntry(initial);
|
|
145
|
-
setIsLoading(false);
|
|
146
|
-
}
|
|
147
|
-
}).catch((err) => {
|
|
148
|
-
if (!cancelled) {
|
|
149
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
150
|
-
setIsLoading(false);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Listen for status changes
|
|
155
|
-
const onStatusChange = (updated: TxStatusEntry) => {
|
|
156
|
-
if (!cancelled) setEntry({ ...updated });
|
|
157
|
-
};
|
|
158
|
-
effectiveMonitor.on("statusChange", onStatusChange);
|
|
159
|
-
|
|
160
|
-
return () => {
|
|
161
|
-
cancelled = true;
|
|
162
|
-
setIsWatching(false);
|
|
163
|
-
effectiveMonitor.off("statusChange", onStatusChange);
|
|
164
|
-
};
|
|
165
|
-
}, [effectiveMonitor, hash, chainId]);
|
|
166
|
-
|
|
167
|
-
const status: TxStatus | "idle" = entry?.status ?? "idle";
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
entry,
|
|
171
|
-
status,
|
|
172
|
-
isLoading,
|
|
173
|
-
isWatching,
|
|
174
|
-
error,
|
|
175
|
-
refresh,
|
|
176
|
-
stopWatching,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useUserOpStatus
|
|
3
|
-
*
|
|
4
|
-
* React hook for tracking ERC-4337 UserOperation lifecycle.
|
|
5
|
-
* Polls the bundler for UserOperation receipt and reports status transitions.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```tsx
|
|
9
|
-
* const { userOpHash, status, receipt, error } = useUserOpStatus({
|
|
10
|
-
* userOpHash: "0x...",
|
|
11
|
-
* bundlerUrl: "https://api.pimlico.io/v2/1/rpc?apikey=...",
|
|
12
|
-
* pollInterval: 2000,
|
|
13
|
-
* });
|
|
14
|
-
*
|
|
15
|
-
* if (status === "confirmed") {
|
|
16
|
-
* console.log("UserOp included in block!");
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { useState, useEffect, useRef, useCallback } from "react";
|
|
22
|
-
import type {
|
|
23
|
-
UserOperationReceipt,
|
|
24
|
-
Address,
|
|
25
|
-
Hex,
|
|
26
|
-
} from "@naculus/connect-core";
|
|
27
|
-
|
|
28
|
-
export type UserOpStatus = "idle" | "pending" | "confirmed" | "failed" | "not_found";
|
|
29
|
-
|
|
30
|
-
export interface UseUserOpStatusOptions {
|
|
31
|
-
/** The UserOperation hash to track */
|
|
32
|
-
userOpHash?: Hex | null;
|
|
33
|
-
/** Bundler RPC URL */
|
|
34
|
-
bundlerUrl?: string;
|
|
35
|
-
/** Polling interval in milliseconds (default: 2000) */
|
|
36
|
-
pollInterval?: number;
|
|
37
|
-
/** Maximum number of retries (default: 30) */
|
|
38
|
-
maxRetries?: number;
|
|
39
|
-
/** Auto-start polling when userOpHash is provided */
|
|
40
|
-
autoStart?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface UseUserOpStatusReturn {
|
|
44
|
-
/** Current status of the UserOperation */
|
|
45
|
-
status: UserOpStatus;
|
|
46
|
-
/** UserOperation receipt (available when confirmed or failed) */
|
|
47
|
-
receipt: UserOperationReceipt | null;
|
|
48
|
-
/** The userOpHash being tracked */
|
|
49
|
-
userOpHash: Hex | null;
|
|
50
|
-
/** Error state */
|
|
51
|
-
error: Error | null;
|
|
52
|
-
/** Whether polling is active */
|
|
53
|
-
isPolling: boolean;
|
|
54
|
-
/** Time elapsed since tracking started (ms) */
|
|
55
|
-
elapsedMs: number;
|
|
56
|
-
/** Number of poll attempts made */
|
|
57
|
-
attempts: number;
|
|
58
|
-
/** Start tracking a UserOperation */
|
|
59
|
-
start: (hash: Hex) => void;
|
|
60
|
-
/** Stop tracking */
|
|
61
|
-
stop: () => void;
|
|
62
|
-
/** Reset state */
|
|
63
|
-
reset: () => void;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Hook for tracking the lifecycle of an ERC-4337 UserOperation.
|
|
68
|
-
*
|
|
69
|
-
* @param options - Configuration for polling behavior
|
|
70
|
-
* @returns UserOperation status, receipt, and lifecycle controls
|
|
71
|
-
*/
|
|
72
|
-
export function useUserOpStatus(
|
|
73
|
-
options?: UseUserOpStatusOptions,
|
|
74
|
-
): UseUserOpStatusReturn {
|
|
75
|
-
const bundlerUrl = options?.bundlerUrl;
|
|
76
|
-
const pollInterval = options?.pollInterval ?? 2000;
|
|
77
|
-
const maxRetries = options?.maxRetries ?? 30;
|
|
78
|
-
|
|
79
|
-
const [status, setStatus] = useState<UserOpStatus>("idle");
|
|
80
|
-
const [receipt, setReceipt] = useState<UserOperationReceipt | null>(null);
|
|
81
|
-
const [userOpHash, setUserOpHash] = useState<Hex | null>(options?.userOpHash ?? null);
|
|
82
|
-
const [error, setError] = useState<Error | null>(null);
|
|
83
|
-
const [isPolling, setIsPolling] = useState(false);
|
|
84
|
-
const [attempts, setAttempts] = useState(0);
|
|
85
|
-
|
|
86
|
-
const startTimeRef = useRef<number>(0);
|
|
87
|
-
const [elapsedMs, setElapsedMs] = useState(0);
|
|
88
|
-
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
89
|
-
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
90
|
-
|
|
91
|
-
// Clear all timers
|
|
92
|
-
const clearTimers = useCallback(() => {
|
|
93
|
-
if (intervalRef.current) {
|
|
94
|
-
clearInterval(intervalRef.current);
|
|
95
|
-
intervalRef.current = null;
|
|
96
|
-
}
|
|
97
|
-
if (pollTimerRef.current) {
|
|
98
|
-
clearTimeout(pollTimerRef.current);
|
|
99
|
-
pollTimerRef.current = null;
|
|
100
|
-
}
|
|
101
|
-
}, []);
|
|
102
|
-
|
|
103
|
-
// Poll the bundler for receipt
|
|
104
|
-
const poll = useCallback(async (hash: Hex) => {
|
|
105
|
-
if (!bundlerUrl) return;
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const response = await fetch(bundlerUrl, {
|
|
109
|
-
method: "POST",
|
|
110
|
-
headers: { "Content-Type": "application/json" },
|
|
111
|
-
body: JSON.stringify({
|
|
112
|
-
jsonrpc: "2.0",
|
|
113
|
-
id: 1,
|
|
114
|
-
method: "eth_getUserOperationReceipt",
|
|
115
|
-
params: [hash],
|
|
116
|
-
}),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (!response.ok) return;
|
|
120
|
-
|
|
121
|
-
const json = await response.json() as {
|
|
122
|
-
result?: {
|
|
123
|
-
userOpHash: Hex;
|
|
124
|
-
entryPoint: Address;
|
|
125
|
-
sender: Address;
|
|
126
|
-
nonce: string;
|
|
127
|
-
paymaster?: Address;
|
|
128
|
-
actualGasUsed: string;
|
|
129
|
-
actualGasCost: string;
|
|
130
|
-
success: boolean;
|
|
131
|
-
transactionHash: Hex;
|
|
132
|
-
logs: Array<{ address: Address; topics: Hex[]; data: Hex }>;
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
if (json.result) {
|
|
137
|
-
const receiptData: UserOperationReceipt = {
|
|
138
|
-
userOpHash: json.result.userOpHash,
|
|
139
|
-
entryPoint: json.result.entryPoint,
|
|
140
|
-
sender: json.result.sender,
|
|
141
|
-
nonce: BigInt(json.result.nonce),
|
|
142
|
-
paymaster: json.result.paymaster,
|
|
143
|
-
actualGasUsed: BigInt(json.result.actualGasUsed),
|
|
144
|
-
actualGasCost: BigInt(json.result.actualGasCost),
|
|
145
|
-
success: json.result.success,
|
|
146
|
-
transactionHash: json.result.transactionHash,
|
|
147
|
-
logs: json.result.logs,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
setReceipt(receiptData);
|
|
151
|
-
setStatus(json.result.success ? "confirmed" : "failed");
|
|
152
|
-
setIsPolling(false);
|
|
153
|
-
clearTimers();
|
|
154
|
-
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// No receipt yet; schedule next poll
|
|
159
|
-
setAttempts(prev => prev + 1);
|
|
160
|
-
setStatus("pending");
|
|
161
|
-
|
|
162
|
-
if (attempts < maxRetries) {
|
|
163
|
-
pollTimerRef.current = setTimeout(() => poll(hash), pollInterval);
|
|
164
|
-
} else {
|
|
165
|
-
setStatus("not_found");
|
|
166
|
-
setIsPolling(false);
|
|
167
|
-
clearTimers();
|
|
168
|
-
setError(new Error("UserOperation not included after maximum retries"));
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
// Network error; retry
|
|
172
|
-
setAttempts(prev => prev + 1);
|
|
173
|
-
if (attempts < maxRetries) {
|
|
174
|
-
pollTimerRef.current = setTimeout(() => poll(hash), pollInterval);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}, [bundlerUrl, pollInterval, maxRetries, attempts, clearTimers]);
|
|
178
|
-
|
|
179
|
-
// Start tracking
|
|
180
|
-
const start = useCallback((hash: Hex) => {
|
|
181
|
-
clearTimers();
|
|
182
|
-
setUserOpHash(hash);
|
|
183
|
-
setStatus("pending");
|
|
184
|
-
setReceipt(null);
|
|
185
|
-
setError(null);
|
|
186
|
-
setAttempts(0);
|
|
187
|
-
setIsPolling(true);
|
|
188
|
-
startTimeRef.current = Date.now();
|
|
189
|
-
|
|
190
|
-
// Start elapsed timer
|
|
191
|
-
intervalRef.current = setInterval(() => {
|
|
192
|
-
setElapsedMs(Date.now() - startTimeRef.current);
|
|
193
|
-
}, 1000);
|
|
194
|
-
|
|
195
|
-
// Start polling
|
|
196
|
-
poll(hash);
|
|
197
|
-
}, [clearTimers, poll]);
|
|
198
|
-
|
|
199
|
-
// Stop tracking
|
|
200
|
-
const stop = useCallback(() => {
|
|
201
|
-
clearTimers();
|
|
202
|
-
setIsPolling(false);
|
|
203
|
-
}, [clearTimers]);
|
|
204
|
-
|
|
205
|
-
// Reset state
|
|
206
|
-
const reset = useCallback(() => {
|
|
207
|
-
clearTimers();
|
|
208
|
-
setUserOpHash(null);
|
|
209
|
-
setStatus("idle");
|
|
210
|
-
setReceipt(null);
|
|
211
|
-
setError(null);
|
|
212
|
-
setIsPolling(false);
|
|
213
|
-
setAttempts(0);
|
|
214
|
-
setElapsedMs(0);
|
|
215
|
-
}, [clearTimers]);
|
|
216
|
-
|
|
217
|
-
// Auto-start if userOpHash provided on mount
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (options?.userOpHash && options?.autoStart !== false) {
|
|
220
|
-
start(options.userOpHash);
|
|
221
|
-
}
|
|
222
|
-
// Only run on mount
|
|
223
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
-
}, []);
|
|
225
|
-
|
|
226
|
-
// Cleanup on unmount
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
return () => {
|
|
229
|
-
clearTimers();
|
|
230
|
-
};
|
|
231
|
-
}, [clearTimers]);
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
status,
|
|
235
|
-
receipt,
|
|
236
|
-
userOpHash,
|
|
237
|
-
error,
|
|
238
|
-
isPolling,
|
|
239
|
-
elapsedMs,
|
|
240
|
-
attempts,
|
|
241
|
-
start,
|
|
242
|
-
stop,
|
|
243
|
-
reset,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/// <reference types="vitest" />
|
|
2
|
-
|
|
3
|
-
// @vitest-environment jsdom
|
|
4
|
-
import { describe, it, expect, vi } from "vitest";
|
|
5
|
-
import { validateDestination, useValidateDestination } from "./useValidateDestination";
|
|
6
|
-
import { renderHook } from "@testing-library/react";
|
|
7
|
-
|
|
8
|
-
describe("validateDestination", () => {
|
|
9
|
-
// Valid addresses
|
|
10
|
-
it("accepts valid EVM addresses", () => {
|
|
11
|
-
expect(validateDestination("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").isValid).toBe(true);
|
|
12
|
-
expect(validateDestination("0x1234567890abcdef1234567890abcdef12345678").isValid).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("rejects empty address", () => {
|
|
16
|
-
expect(validateDestination("").isValid).toBe(false);
|
|
17
|
-
expect(validateDestination("").level).toBe("blocked");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("rejects zero address", () => {
|
|
21
|
-
expect(validateDestination("0x0000000000000000000000000000000000000000").isValid).toBe(false);
|
|
22
|
-
expect(validateDestination("0x0000000000000000000000000000000000000000").level).toBe("blocked");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("rejects burn addresses", () => {
|
|
26
|
-
expect(validateDestination("0x000000000000000000000000000000000000dead").isValid).toBe(false);
|
|
27
|
-
expect(validateDestination("0x0000000000000000000000000000000000000001").isValid).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("rejects invalid format", () => {
|
|
31
|
-
expect(validateDestination("not-an-address").isValid).toBe(false);
|
|
32
|
-
expect(validateDestination("0x123").isValid).toBe(false);
|
|
33
|
-
expect(validateDestination("0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG").isValid).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Property-based: fuzz variant with many generated addresses
|
|
37
|
-
it("correctly validates 50 random-like hex strings", () => {
|
|
38
|
-
for (let i = 0; i < 50; i++) {
|
|
39
|
-
// Generate random 40-char hex
|
|
40
|
-
const hex = "0x" + Array.from({ length: 40 }, () =>
|
|
41
|
-
Math.floor(Math.random() * 16).toString(16)
|
|
42
|
-
).join("");
|
|
43
|
-
|
|
44
|
-
const result = validateDestination(hex);
|
|
45
|
-
|
|
46
|
-
// All properly formatted EVM addresses should be valid unless they're zero/burn
|
|
47
|
-
if (hex === "0x0000000000000000000000000000000000000000" ||
|
|
48
|
-
hex.toLowerCase().includes("dead") && hex.length === 42) {
|
|
49
|
-
expect(result.isValid).toBe(false);
|
|
50
|
-
} else {
|
|
51
|
-
expect(result.isValid).toBe(true);
|
|
52
|
-
expect(result.level).toBe("safe");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Edge cases
|
|
58
|
-
it("handles uppercase addresses", () => {
|
|
59
|
-
expect(validateDestination("0xABCDEF1234567890ABCDEF1234567890ABCDEF12").isValid).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("handles mixed case checksum addresses", () => {
|
|
63
|
-
expect(validateDestination("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").isValid).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("useValidateDestination", () => {
|
|
68
|
-
it("returns safe validation for valid address", () => {
|
|
69
|
-
const { result } = renderHook(() =>
|
|
70
|
-
useValidateDestination({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
|
|
71
|
-
);
|
|
72
|
-
expect(result.current.validation.isValid).toBe(true);
|
|
73
|
-
expect(result.current.validation.level).toBe("safe");
|
|
74
|
-
expect(result.current.validation.warning).toBe(null);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("returns blocked for zero address", () => {
|
|
78
|
-
const { result } = renderHook(() =>
|
|
79
|
-
useValidateDestination({ address: "0x0000000000000000000000000000000000000000" })
|
|
80
|
-
);
|
|
81
|
-
expect(result.current.validation.isValid).toBe(false);
|
|
82
|
-
expect(result.current.validation.level).toBe("blocked");
|
|
83
|
-
expect(result.current.validation.warning).toBeTruthy();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("updates when address changes", () => {
|
|
87
|
-
const { result, rerender } = renderHook(
|
|
88
|
-
({ address }) => useValidateDestination({ address }),
|
|
89
|
-
{ initialProps: { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" } }
|
|
90
|
-
);
|
|
91
|
-
expect(result.current.validation.isValid).toBe(true);
|
|
92
|
-
|
|
93
|
-
rerender({ address: "0x0000000000000000000000000000000000000000" });
|
|
94
|
-
expect(result.current.validation.isValid).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
});
|