@naculus/connect-appkit-react 0.1.2

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/api-extractor.json +21 -0
  3. package/dist/client.cjs +2 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.js +2 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/index.cjs +2 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +39 -0
  12. package/src/chain-abstraction/index.ts +8 -0
  13. package/src/chain-abstraction/useCompareCosts.ts +137 -0
  14. package/src/chain-abstraction/useExecuteRoute.ts +129 -0
  15. package/src/chain-abstraction/useRouteQuote.ts +168 -0
  16. package/src/client.test.ts +242 -0
  17. package/src/client.ts +265 -0
  18. package/src/hooks/useAccount.test.ts +67 -0
  19. package/src/hooks/useAccount.ts +37 -0
  20. package/src/hooks/useBalance.test.ts +459 -0
  21. package/src/hooks/useBalance.ts +170 -0
  22. package/src/hooks/useChain.test.ts +160 -0
  23. package/src/hooks/useChain.ts +53 -0
  24. package/src/hooks/useConnect.test.ts +112 -0
  25. package/src/hooks/useConnect.ts +29 -0
  26. package/src/hooks/useDisconnect.test.ts +75 -0
  27. package/src/hooks/useDisconnect.ts +32 -0
  28. package/src/hooks/useERC20Allowance.ts +93 -0
  29. package/src/hooks/useERC20Approve.ts +220 -0
  30. package/src/hooks/useERC20Transfer.ts +134 -0
  31. package/src/hooks/useERC20TransferSimulation.ts +147 -0
  32. package/src/hooks/useEmbeddedWallet.test.ts +266 -0
  33. package/src/hooks/useEmbeddedWallet.ts +181 -0
  34. package/src/hooks/useLastTx.test.ts +117 -0
  35. package/src/hooks/useLastTx.ts +73 -0
  36. package/src/hooks/useLookupAddress.ts +131 -0
  37. package/src/hooks/useNotification.ts +308 -0
  38. package/src/hooks/useResolveName.ts +128 -0
  39. package/src/hooks/useSIWxLogin.test.ts +307 -0
  40. package/src/hooks/useSIWxLogin.ts +163 -0
  41. package/src/hooks/useSIWxSession.test.ts +481 -0
  42. package/src/hooks/useSIWxSession.ts +290 -0
  43. package/src/hooks/useSendCalls.ts +103 -0
  44. package/src/hooks/useSendTransaction.test.ts +175 -0
  45. package/src/hooks/useSendTransaction.ts +80 -0
  46. package/src/hooks/useSendUserOperation.ts +313 -0
  47. package/src/hooks/useSession.ts +96 -0
  48. package/src/hooks/useSessionKeys.ts +356 -0
  49. package/src/hooks/useSignInWithEthereum.test.ts +224 -0
  50. package/src/hooks/useSignInWithEthereum.ts +46 -0
  51. package/src/hooks/useSignInWithX.test.ts +210 -0
  52. package/src/hooks/useSignInWithX.ts +141 -0
  53. package/src/hooks/useSignMessage.test.ts +151 -0
  54. package/src/hooks/useSignMessage.ts +65 -0
  55. package/src/hooks/useSimulateTransfer.ts +165 -0
  56. package/src/hooks/useSiwxAuthSession.test.ts +456 -0
  57. package/src/hooks/useSiwxAuthSession.ts +163 -0
  58. package/src/hooks/useSmartAccount.ts +194 -0
  59. package/src/hooks/useSwitchChain.ts +68 -0
  60. package/src/hooks/useTokenBalance.test.ts +337 -0
  61. package/src/hooks/useTokenBalance.ts +210 -0
  62. package/src/hooks/useTransactionSimulation.ts +401 -0
  63. package/src/hooks/useTxHistory.test.ts +108 -0
  64. package/src/hooks/useTxHistory.ts +83 -0
  65. package/src/hooks/useTxMonitor.test.ts +127 -0
  66. package/src/hooks/useTxMonitor.ts +178 -0
  67. package/src/hooks/useUserOpStatus.ts +245 -0
  68. package/src/hooks/useValidateDestination.test.ts +96 -0
  69. package/src/hooks/useValidateDestination.ts +72 -0
  70. package/src/hooks/useViemClient.test.ts +155 -0
  71. package/src/hooks/useViemClient.ts +65 -0
  72. package/src/hooks/useWallet.test.ts +133 -0
  73. package/src/hooks/useWallet.ts +39 -0
  74. package/src/hooks/useWeb3ErrorHandler.ts +102 -0
  75. package/src/index.ts +186 -0
  76. package/src/provider/Web3ConnectProvider.test.tsx +178 -0
  77. package/src/provider/Web3ConnectProvider.tsx +530 -0
  78. package/src/token-list/TokenSelector.tsx +351 -0
  79. package/src/token-list/defaults.ts +39 -0
  80. package/src/token-list/index.ts +14 -0
  81. package/src/token-list/useTokenList.ts +119 -0
  82. package/src/token-list/useTokenSearch.ts +99 -0
  83. package/src/types/index.ts +99 -0
  84. package/src/utils/chains.ts +115 -0
  85. package/src/utils/errorMessages.ts +142 -0
  86. package/src/utils/i18n.ts +90 -0
  87. package/src/wc-generated/components.ts +377 -0
  88. package/test/setup.ts +17 -0
  89. package/test/test-utils.tsx +78 -0
  90. package/tsup.config.ts +18 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * useValidateDestination — address blackhole prevention hook.
3
+ *
4
+ * Validates that a destination address is not a known burn/zero address
5
+ * before sending a transaction. Returns { isValid, warning, level }.
6
+ *
7
+ * Level: "safe" | "warning" | "blocked"
8
+ */
9
+
10
+ import { useMemo } from "react";
11
+ import { t } from "../utils/i18n";
12
+
13
+ // ── Known burn / zero addresses ─────────────────────────────────
14
+
15
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
16
+ const DEAD_ADDRESSES = new Set([
17
+ ZERO_ADDRESS,
18
+ "0x0000000000000000000000000000000000000001",
19
+ "0x000000000000000000000000000000000000dead",
20
+ "0x000000000000000000000000000000000000dEaD",
21
+ ]);
22
+
23
+ function isBurnAddress(addr: string): boolean {
24
+ const lower = addr.toLowerCase();
25
+ if (DEAD_ADDRESSES.has(lower)) return true;
26
+ return lower.includes("dead") && lower.length >= 40;
27
+ }
28
+
29
+ function isZeroAddress(addr: string): boolean {
30
+ return addr === ZERO_ADDRESS;
31
+ }
32
+
33
+ function isValidFormat(addr: string): boolean {
34
+ return /^0x[0-9a-fA-F]{40}$/.test(addr);
35
+ }
36
+
37
+ export type AddressValidationLevel = "safe" | "warning" | "blocked";
38
+
39
+ export interface AddressValidationResult {
40
+ isValid: boolean;
41
+ level: AddressValidationLevel;
42
+ warning: string | null;
43
+ }
44
+
45
+ export function validateDestination(address: string): AddressValidationResult {
46
+ if (!address) {
47
+ return { isValid: false, level: "blocked", warning: t("address.empty") };
48
+ }
49
+ if (!isValidFormat(address)) {
50
+ return { isValid: false, level: "blocked", warning: t("address.invalid_format") };
51
+ }
52
+ if (isZeroAddress(address)) {
53
+ return { isValid: false, level: "blocked", warning: t("address.zero_address") };
54
+ }
55
+ if (isBurnAddress(address)) {
56
+ return { isValid: false, level: "blocked", warning: t("address.burn_address") };
57
+ }
58
+ return { isValid: true, level: "safe", warning: null };
59
+ }
60
+
61
+ export interface UseValidateDestinationOptions {
62
+ address: string;
63
+ }
64
+
65
+ export interface UseValidateDestinationReturn {
66
+ validation: AddressValidationResult;
67
+ }
68
+
69
+ export function useValidateDestination({ address }: UseValidateDestinationOptions): UseValidateDestinationReturn {
70
+ const validation = useMemo(() => validateDestination(address), [address]);
71
+ return { validation };
72
+ }
@@ -0,0 +1,155 @@
1
+ /// <reference types="vitest" />
2
+ /// @vitest-environment jsdom
3
+
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { renderHook } from "@testing-library/react";
6
+
7
+ const mockUseAccount = vi.fn();
8
+ vi.mock("./useAccount", () => ({
9
+ useAccount: () => mockUseAccount(),
10
+ }));
11
+
12
+ const mockUseChain = vi.fn();
13
+ vi.mock("./useChain", () => ({
14
+ useChain: () => mockUseChain(),
15
+ }));
16
+
17
+ vi.mock("../utils/chains", () => ({
18
+ getDefaultChains: () => [
19
+ { id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" },
20
+ ],
21
+ }));
22
+
23
+ import { useViemClient } from "./useViemClient";
24
+
25
+ describe("useViemClient", () => {
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ it("should return null clients when not connected", () => {
31
+ mockUseAccount.mockReturnValue({
32
+ evmAccount: null,
33
+ isConnected: false,
34
+ });
35
+ mockUseChain.mockReturnValue({
36
+ currentChain: null,
37
+ chains: [{ id: 1, namespace: "eip155", name: "Ethereum", rpcUrl: "https://eth.llamarpc.com", token: "ETH" }],
38
+ });
39
+
40
+ const { result } = renderHook(() => useViemClient());
41
+
42
+ expect(result.current.publicClient).toBeNull();
43
+ expect(result.current.walletClient).toBeNull();
44
+ expect(result.current.chains).toHaveLength(1);
45
+ });
46
+
47
+ it("should create public client when chain has rpcUrl", () => {
48
+ mockUseAccount.mockReturnValue({
49
+ evmAccount: null,
50
+ isConnected: false,
51
+ });
52
+ mockUseChain.mockReturnValue({
53
+ currentChain: {
54
+ id: 1,
55
+ namespace: "eip155",
56
+ name: "Ethereum",
57
+ rpcUrl: "https://eth.llamarpc.com",
58
+ token: "ETH",
59
+ },
60
+ });
61
+
62
+ const { result } = renderHook(() => useViemClient());
63
+
64
+ expect(result.current.publicClient).not.toBeNull();
65
+ expect(result.current.publicClient?.chain?.id).toBe(1);
66
+ // walletClient should still be null since not connected
67
+ expect(result.current.walletClient).toBeNull();
68
+ });
69
+
70
+ it("should create both public and wallet client when connected with evm account", () => {
71
+ mockUseAccount.mockReturnValue({
72
+ evmAccount: "eip155:1:0x1234567890123456789012345678901234567890",
73
+ isConnected: true,
74
+ });
75
+ mockUseChain.mockReturnValue({
76
+ currentChain: {
77
+ id: 1,
78
+ namespace: "eip155",
79
+ name: "Ethereum",
80
+ rpcUrl: "https://eth.llamarpc.com",
81
+ token: "ETH",
82
+ },
83
+ });
84
+
85
+ const { result } = renderHook(() => useViemClient());
86
+
87
+ expect(result.current.publicClient).not.toBeNull();
88
+ expect(result.current.walletClient).not.toBeNull();
89
+ expect(result.current.walletClient?.account?.address).toBe("0x1234567890123456789012345678901234567890");
90
+ });
91
+
92
+ it("should handle plain address format (without eip155 prefix)", () => {
93
+ mockUseAccount.mockReturnValue({
94
+ evmAccount: "0xabcdef1234567890abcdef1234567890abcdef12",
95
+ isConnected: true,
96
+ });
97
+ mockUseChain.mockReturnValue({
98
+ currentChain: {
99
+ id: 137,
100
+ namespace: "eip155",
101
+ name: "Polygon",
102
+ rpcUrl: "https://polygon-rpc.com",
103
+ token: "MATIC",
104
+ },
105
+ });
106
+
107
+ const { result } = renderHook(() => useViemClient());
108
+
109
+ expect(result.current.publicClient).not.toBeNull();
110
+ expect(result.current.walletClient).not.toBeNull();
111
+ expect(result.current.walletClient?.account?.address).toBe("0xabcdef1234567890abcdef1234567890abcdef12");
112
+ });
113
+
114
+ it("should return null wallet client when not connected", () => {
115
+ mockUseAccount.mockReturnValue({
116
+ evmAccount: null,
117
+ isConnected: true,
118
+ });
119
+ mockUseChain.mockReturnValue({
120
+ currentChain: {
121
+ id: 1,
122
+ namespace: "eip155",
123
+ name: "Ethereum",
124
+ rpcUrl: "https://eth.llamarpc.com",
125
+ token: "ETH",
126
+ },
127
+ });
128
+
129
+ const { result } = renderHook(() => useViemClient());
130
+
131
+ expect(result.current.publicClient).not.toBeNull();
132
+ expect(result.current.walletClient).toBeNull();
133
+ });
134
+
135
+ it("should return null clients when no rpcUrl", () => {
136
+ mockUseAccount.mockReturnValue({
137
+ evmAccount: null,
138
+ isConnected: false,
139
+ });
140
+ mockUseChain.mockReturnValue({
141
+ currentChain: {
142
+ id: 1,
143
+ namespace: "eip155",
144
+ name: "Ethereum",
145
+ rpcUrl: undefined,
146
+ token: "ETH",
147
+ },
148
+ });
149
+
150
+ const { result } = renderHook(() => useViemClient());
151
+
152
+ expect(result.current.publicClient).toBeNull();
153
+ expect(result.current.walletClient).toBeNull();
154
+ });
155
+ });
@@ -0,0 +1,65 @@
1
+ import { useMemo } from "react";
2
+ import { createPublicClient, http, createWalletClient, type PublicClient, type WalletClient } from "viem";
3
+ import type { WalletChain } from "../types";
4
+ import { useAccount } from "./useAccount";
5
+ import { useChain } from "./useChain";
6
+
7
+ export function useViemClient(): {
8
+ publicClient: PublicClient | null;
9
+ walletClient: WalletClient | null;
10
+ chains: WalletChain[];
11
+ } {
12
+ const { evmAccount, isConnected } = useAccount();
13
+ const { currentChain, chains } = useChain();
14
+
15
+ const publicClient = useMemo<PublicClient | null>(() => {
16
+ if (!currentChain?.rpcUrl) return null;
17
+
18
+ return createPublicClient({
19
+ transport: http(currentChain.rpcUrl),
20
+ chain: {
21
+ id: currentChain.id,
22
+ name: currentChain.name,
23
+ nativeCurrency: {
24
+ name: currentChain.token ?? "ETH",
25
+ symbol: currentChain.token ?? "ETH",
26
+ decimals: 18
27
+ },
28
+ rpcUrls: {
29
+ default: { http: [currentChain.rpcUrl] },
30
+ public: { http: [currentChain.rpcUrl] }
31
+ }
32
+ }
33
+ });
34
+ }, [currentChain]);
35
+
36
+ const walletClient = useMemo<WalletClient | null>(() => {
37
+ if (!isConnected || !evmAccount || !currentChain?.rpcUrl) return null;
38
+
39
+ const address = evmAccount.includes(":") ? evmAccount.split(":").pop()! : evmAccount;
40
+
41
+ return createWalletClient({
42
+ transport: http(currentChain.rpcUrl),
43
+ chain: {
44
+ id: currentChain.id,
45
+ name: currentChain.name,
46
+ nativeCurrency: {
47
+ name: currentChain.token ?? "ETH",
48
+ symbol: currentChain.token ?? "ETH",
49
+ decimals: 18
50
+ },
51
+ rpcUrls: {
52
+ default: { http: [currentChain.rpcUrl] },
53
+ public: { http: [currentChain.rpcUrl] }
54
+ }
55
+ },
56
+ account: address as `0x${string}`
57
+ });
58
+ }, [isConnected, evmAccount, currentChain]);
59
+
60
+ return {
61
+ publicClient,
62
+ walletClient,
63
+ chains
64
+ };
65
+ }
@@ -0,0 +1,133 @@
1
+ /// <reference types="vitest" />
2
+ /// @vitest-environment jsdom
3
+
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { renderHook } from "@testing-library/react";
6
+
7
+ // Mock useWeb3 before importing hooks
8
+ const mockUseWeb3 = vi.fn();
9
+ vi.mock("../provider/Web3ConnectProvider", () => ({
10
+ useWeb3: () => mockUseWeb3(),
11
+ Web3ConnectProvider: ({ children }: { children: React.ReactNode }) => children,
12
+ }));
13
+
14
+ import { useWallet } from "./useWallet";
15
+ import type { Web3ContextValue } from "../provider/Web3ConnectProvider";
16
+
17
+ function createMockWeb3(overrides: Partial<Web3ContextValue> = {}): Web3ContextValue {
18
+ const base: Web3ContextValue = {
19
+ status: "disconnected",
20
+ session: null,
21
+ accounts: [],
22
+ chainId: null,
23
+ error: null,
24
+ isConnected: false,
25
+ chains: [],
26
+ sessionManager: null,
27
+ clearError: () => {},
28
+ connect: vi.fn(),
29
+ disconnect: vi.fn(),
30
+ reconnect: vi.fn(),
31
+ switchChain: vi.fn(),
32
+ startPairing: vi.fn(),
33
+ completePairing: vi.fn(),
34
+ connectInjected: vi.fn(),
35
+ connectEmbedded: vi.fn(),
36
+ connectPasskeys: vi.fn(),
37
+ ...overrides,
38
+ };
39
+ return base;
40
+ }
41
+
42
+ describe("useWallet", () => {
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ });
46
+
47
+ it("should return disconnected state by default", () => {
48
+ mockUseWeb3.mockReturnValue(createMockWeb3());
49
+ const { result } = renderHook(() => useWallet());
50
+
51
+ expect(result.current.isConnected).toBe(false);
52
+ expect(result.current.isConnecting).toBe(false);
53
+ expect(result.current.isReconnecting).toBe(false);
54
+ expect(result.current.status).toBe("disconnected");
55
+ expect(result.current.currentAccount).toBeNull();
56
+ });
57
+
58
+ it("should reflect connected state", () => {
59
+ mockUseWeb3.mockReturnValue(
60
+ createMockWeb3({
61
+ status: "connected",
62
+ isConnected: true,
63
+ accounts: ["eip155:1:0x1234"],
64
+ chainId: "eip155:1",
65
+ })
66
+ );
67
+ const { result } = renderHook(() => useWallet());
68
+
69
+ expect(result.current.isConnected).toBe(true);
70
+ expect(result.current.status).toBe("connected");
71
+ expect(result.current.currentAccount).toBe("eip155:1:0x1234");
72
+ expect(result.current.chainId).toBe("eip155:1");
73
+ });
74
+
75
+ it("should reflect connecting state", () => {
76
+ mockUseWeb3.mockReturnValue(
77
+ createMockWeb3({
78
+ status: "connecting",
79
+ })
80
+ );
81
+ const { result } = renderHook(() => useWallet());
82
+
83
+ expect(result.current.isConnecting).toBe(true);
84
+ expect(result.current.isConnected).toBe(false);
85
+ });
86
+
87
+ it("should reflect reconnecting state", () => {
88
+ mockUseWeb3.mockReturnValue(
89
+ createMockWeb3({
90
+ status: "reconnecting",
91
+ })
92
+ );
93
+ const { result } = renderHook(() => useWallet());
94
+
95
+ expect(result.current.isReconnecting).toBe(true);
96
+ });
97
+
98
+ it("should pass through connect function", () => {
99
+ const connect = vi.fn();
100
+ mockUseWeb3.mockReturnValue(createMockWeb3({ connect }));
101
+ const { result } = renderHook(() => useWallet());
102
+
103
+ result.current.connect();
104
+ expect(connect).toHaveBeenCalledTimes(1);
105
+ });
106
+
107
+ it("should pass through disconnect function", () => {
108
+ const disconnect = vi.fn();
109
+ mockUseWeb3.mockReturnValue(createMockWeb3({ disconnect }));
110
+ const { result } = renderHook(() => useWallet());
111
+
112
+ result.current.disconnect();
113
+ expect(disconnect).toHaveBeenCalledTimes(1);
114
+ });
115
+
116
+ it("should return null currentAccount when no accounts", () => {
117
+ mockUseWeb3.mockReturnValue(createMockWeb3({ accounts: [] }));
118
+ const { result } = renderHook(() => useWallet());
119
+
120
+ expect(result.current.currentAccount).toBeNull();
121
+ });
122
+
123
+ it("should use first account as currentAccount", () => {
124
+ mockUseWeb3.mockReturnValue(
125
+ createMockWeb3({
126
+ accounts: ["eip155:1:0xabc", "eip155:137:0xdef"],
127
+ })
128
+ );
129
+ const { result } = renderHook(() => useWallet());
130
+
131
+ expect(result.current.currentAccount).toBe("eip155:1:0xabc");
132
+ });
133
+ });
@@ -0,0 +1,39 @@
1
+ import { useWeb3 } from "../provider/Web3ConnectProvider";
2
+
3
+ export function useWallet() {
4
+ const {
5
+ status,
6
+ session,
7
+ accounts,
8
+ chainId,
9
+ error,
10
+ connect,
11
+ disconnect,
12
+ reconnect,
13
+ switchChain
14
+ } = useWeb3();
15
+
16
+ const isConnected = status === "connected";
17
+ const isConnecting = status === "connecting";
18
+ const isReconnecting = status === "reconnecting";
19
+
20
+ const currentAccount = accounts[0] ?? null;
21
+ const isDisconnected = status === "disconnected";
22
+
23
+ return {
24
+ status,
25
+ session,
26
+ accounts,
27
+ chainId,
28
+ error,
29
+ isConnected,
30
+ isConnecting,
31
+ isReconnecting,
32
+ isDisconnected,
33
+ currentAccount,
34
+ connect,
35
+ disconnect,
36
+ reconnect,
37
+ switchChain
38
+ };
39
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo } from "react";
4
+ import { useWeb3 } from "../provider/Web3ConnectProvider";
5
+ import {
6
+ getUserFriendlyError,
7
+ isRetryableError,
8
+ } from "../utils/errorMessages";
9
+
10
+ export interface UserFriendlyError {
11
+ title: string;
12
+ description: string;
13
+ code?: string;
14
+ }
15
+
16
+ export interface UseWeb3ErrorHandlerReturn {
17
+ /** Current error from the provider */
18
+ error: Error | null;
19
+ /** User-friendly representation of the current error */
20
+ friendlyError: UserFriendlyError | null;
21
+ /** Clear the current error */
22
+ clearError: () => void;
23
+ /** Check if current error is retryable */
24
+ isRetryable: boolean;
25
+ /** Get a user-friendly version of any error */
26
+ formatError: (error: unknown) => UserFriendlyError;
27
+ /** Wraps an async function with error formatting */
28
+ wrapAsync: <T>(fn: () => Promise<T>) => () => Promise<T>;
29
+ }
30
+
31
+ /**
32
+ * Hook that provides user-friendly error handling for web3 operations.
33
+ *
34
+ * Features:
35
+ * - Maps WalletError codes to human-readable messages
36
+ * - Provides retry detection for transient errors
37
+ * - Wraps async operations with consistent error formatting
38
+ * - Exposes the current error state from Web3ConnectProvider
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * const { friendlyError, clearError, isRetryable, wrapAsync } = useWeb3ErrorHandler();
43
+ *
44
+ * // Show error in UI
45
+ * if (friendlyError) {
46
+ * return <ErrorDisplay title={friendlyError.title} message={friendlyError.description} />;
47
+ * }
48
+ *
49
+ * // Wrap an operation
50
+ * const safeConnect = wrapAsync(() => connect());
51
+ * await safeConnect();
52
+ * ```
53
+ */
54
+ export function useWeb3ErrorHandler(): UseWeb3ErrorHandlerReturn {
55
+ const { error, clearError: providerClearError } = useWeb3();
56
+
57
+ const friendlyError: UserFriendlyError | null = useMemo(
58
+ () => (error ? getUserFriendlyError(error) : null),
59
+ [error]
60
+ );
61
+
62
+ const isRetryable = useMemo(
63
+ () => (error ? isRetryableError(error) : false),
64
+ [error]
65
+ );
66
+
67
+ const clearError = useCallback(() => {
68
+ providerClearError();
69
+ }, [providerClearError]);
70
+
71
+ const formatError = useCallback(
72
+ (err: unknown): UserFriendlyError => getUserFriendlyError(err),
73
+ []
74
+ );
75
+
76
+ const wrapAsync = useCallback(
77
+ <T,>(fn: () => Promise<T>): (() => Promise<T>) => {
78
+ return async () => {
79
+ try {
80
+ return await fn();
81
+ } catch (err) {
82
+ // Re-throw with user-friendly information attached
83
+ const friendly = getUserFriendlyError(err);
84
+ const enhanced = new Error(friendly.description) as unknown as Record<string, unknown>;
85
+ enhanced.title = friendly.title;
86
+ enhanced.code = friendly.code;
87
+ throw enhanced;
88
+ }
89
+ };
90
+ },
91
+ []
92
+ );
93
+
94
+ return {
95
+ error,
96
+ friendlyError,
97
+ clearError,
98
+ isRetryable,
99
+ formatError,
100
+ wrapAsync,
101
+ };
102
+ }