@silentswap/react 0.1.52 → 0.1.53
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/dist/contexts/AuthContext.d.ts +50 -0
- package/dist/contexts/AuthContext.js +49 -0
- package/dist/contexts/PrivateWalletContext.d.ts +46 -0
- package/dist/contexts/PrivateWalletContext.js +63 -0
- package/dist/contexts/SilentSwapContext.d.ts +6 -0
- package/dist/contexts/SilentSwapContext.js +18 -31
- package/dist/hooks/silent/useAuth.js +77 -13
- package/dist/hooks/silent/useWallet.js +35 -24
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +3 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Connector } from 'wagmi';
|
|
3
|
+
import type { WalletClient } from 'viem';
|
|
4
|
+
import type { AuthResponse, SilentSwapClient } from '@silentswap/sdk';
|
|
5
|
+
/**
|
|
6
|
+
* Shape of the auth context exposed to consumers. Mirrors the subset of
|
|
7
|
+
* `useAuth` that callers actually need — keeping the surface narrow so
|
|
8
|
+
* the context doesn't have to track internal SDK changes.
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthContextValue {
|
|
11
|
+
/** Current auth response (null when not signed in). */
|
|
12
|
+
auth: AuthResponse | null;
|
|
13
|
+
/** True while a sign-in (including auto-auth retries) is in flight. */
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
/** Last sign-in error. */
|
|
16
|
+
error: Error | null;
|
|
17
|
+
/** True when SIWE auto-authentication exhausted all retries. */
|
|
18
|
+
authExhausted: boolean;
|
|
19
|
+
/** Imperatively start / retry SIWE sign-in. */
|
|
20
|
+
signIn: () => Promise<AuthResponse | null>;
|
|
21
|
+
/** Alias of {@link signIn} kept for callers that prefer the full name. */
|
|
22
|
+
authenticate: () => Promise<AuthResponse | null>;
|
|
23
|
+
/** Clear auth state for the current address. */
|
|
24
|
+
signOut: () => void;
|
|
25
|
+
/** True iff auth is present and not yet expired. */
|
|
26
|
+
isAuthenticated: () => boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function useAuthContext(): AuthContextValue;
|
|
29
|
+
export interface AuthProviderProps {
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
client?: SilentSwapClient;
|
|
32
|
+
address?: `0x${string}`;
|
|
33
|
+
walletClient?: WalletClient;
|
|
34
|
+
connector?: Connector;
|
|
35
|
+
/** Whether to auto-authenticate as soon as the prerequisites are available. */
|
|
36
|
+
autoAuthenticate?: boolean;
|
|
37
|
+
/** Status callback for sign-in progress (e.g. to show toasts). */
|
|
38
|
+
onStatus?: (status: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Wraps {@link useAuth} in a React context so multiple components can read
|
|
42
|
+
* the same auth state without each remounting its own hook instance.
|
|
43
|
+
*
|
|
44
|
+
* Two independent `useAuth()` callers race each other on mount — they each
|
|
45
|
+
* try to fire the same signMessage prompt, which is the origin of the
|
|
46
|
+
* "double sign-in dialog" bug on the hidden swap + order recovery pages.
|
|
47
|
+
* Centralising the hook behind this provider guarantees a single in-flight
|
|
48
|
+
* auth attempt per address.
|
|
49
|
+
*/
|
|
50
|
+
export declare function AuthProvider({ children, client, address, walletClient, connector, autoAuthenticate, onStatus, }: AuthProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import { useAuth } from '../hooks/silent/useAuth.js';
|
|
5
|
+
const DEFAULT_AUTH_VALUE = {
|
|
6
|
+
auth: null,
|
|
7
|
+
isLoading: false,
|
|
8
|
+
error: null,
|
|
9
|
+
authExhausted: false,
|
|
10
|
+
signIn: async () => null,
|
|
11
|
+
authenticate: async () => null,
|
|
12
|
+
signOut: () => { },
|
|
13
|
+
isAuthenticated: () => false,
|
|
14
|
+
};
|
|
15
|
+
const AuthContext = createContext(DEFAULT_AUTH_VALUE);
|
|
16
|
+
export function useAuthContext() {
|
|
17
|
+
return useContext(AuthContext);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps {@link useAuth} in a React context so multiple components can read
|
|
21
|
+
* the same auth state without each remounting its own hook instance.
|
|
22
|
+
*
|
|
23
|
+
* Two independent `useAuth()` callers race each other on mount — they each
|
|
24
|
+
* try to fire the same signMessage prompt, which is the origin of the
|
|
25
|
+
* "double sign-in dialog" bug on the hidden swap + order recovery pages.
|
|
26
|
+
* Centralising the hook behind this provider guarantees a single in-flight
|
|
27
|
+
* auth attempt per address.
|
|
28
|
+
*/
|
|
29
|
+
export function AuthProvider({ children, client, address, walletClient, connector, autoAuthenticate = true, onStatus, }) {
|
|
30
|
+
const { auth, isLoading, error, authExhausted, signIn, authenticate, signOut, isAuthenticated, } = useAuth({
|
|
31
|
+
client,
|
|
32
|
+
address,
|
|
33
|
+
walletClient,
|
|
34
|
+
connector,
|
|
35
|
+
autoAuthenticate,
|
|
36
|
+
onStatus,
|
|
37
|
+
});
|
|
38
|
+
const value = {
|
|
39
|
+
auth,
|
|
40
|
+
isLoading,
|
|
41
|
+
error,
|
|
42
|
+
authExhausted,
|
|
43
|
+
signIn,
|
|
44
|
+
authenticate,
|
|
45
|
+
signOut,
|
|
46
|
+
isAuthenticated,
|
|
47
|
+
};
|
|
48
|
+
return _jsx(AuthContext.Provider, { value: value, children: children });
|
|
49
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type SilentSwapWallet } from '../hooks/silent/useWallet.js';
|
|
3
|
+
import type { Connector } from 'wagmi';
|
|
4
|
+
import type { WalletClient } from 'viem';
|
|
5
|
+
import type { SilentSwapClient } from '@silentswap/sdk';
|
|
6
|
+
/**
|
|
7
|
+
* Shape of the private-wallet context exposed to consumers. Intentionally
|
|
8
|
+
* narrow — anything beyond this (entropy, facilitator groups, cache
|
|
9
|
+
* management) stays an implementation detail of {@link useWallet}.
|
|
10
|
+
*/
|
|
11
|
+
export interface PrivateWalletContextValue {
|
|
12
|
+
/** Generated SilentSwap facilitator wallet (null until ready). */
|
|
13
|
+
wallet: SilentSwapWallet | null;
|
|
14
|
+
/** True while entropy generation / signTypedData is in flight. */
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
/** Last generation error (null when healthy). */
|
|
17
|
+
error: Error | null;
|
|
18
|
+
/** Imperatively (re)generate the private wallet. */
|
|
19
|
+
generateWallet: () => Promise<void>;
|
|
20
|
+
/** User-initiated retry: clears cache + regenerates, bypassing the in-flight guard. */
|
|
21
|
+
retryWallet: () => Promise<void>;
|
|
22
|
+
/** Drop the cached wallet for the current address. */
|
|
23
|
+
clearWallet: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function usePrivateWalletContext(): PrivateWalletContextValue;
|
|
26
|
+
export interface PrivateWalletProviderProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
client: SilentSwapClient;
|
|
29
|
+
address?: `0x${string}`;
|
|
30
|
+
walletClient?: WalletClient;
|
|
31
|
+
connector?: Connector;
|
|
32
|
+
/** If true, includes all historical deposit nonces when regenerating. */
|
|
33
|
+
allDeposits?: boolean;
|
|
34
|
+
/** If true (default), auto-generates the wallet once auth is available. */
|
|
35
|
+
autoGenerate?: boolean;
|
|
36
|
+
/** Status callback for generation progress (e.g. to show toasts). */
|
|
37
|
+
onStatus?: (status: string) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Wraps {@link useWallet} in a React context and auto-triggers generation
|
|
41
|
+
* once the upstream {@link AuthContext} reports a valid auth. Using a
|
|
42
|
+
* provider here (instead of re-calling the hook in every consumer) keeps
|
|
43
|
+
* exactly one in-flight signTypedData prompt per session — mirrors the
|
|
44
|
+
* rationale behind {@link AuthProvider}.
|
|
45
|
+
*/
|
|
46
|
+
export declare function PrivateWalletProvider({ children, client, address, walletClient, connector, allDeposits, autoGenerate, onStatus, }: PrivateWalletProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useRef } from 'react';
|
|
4
|
+
import { useWallet } from '../hooks/silent/useWallet.js';
|
|
5
|
+
import { useAuthContext } from './AuthContext.js';
|
|
6
|
+
const DEFAULT_WALLET_VALUE = {
|
|
7
|
+
wallet: null,
|
|
8
|
+
isLoading: false,
|
|
9
|
+
error: null,
|
|
10
|
+
generateWallet: async () => { },
|
|
11
|
+
retryWallet: async () => { },
|
|
12
|
+
clearWallet: () => { },
|
|
13
|
+
};
|
|
14
|
+
const PrivateWalletContext = createContext(DEFAULT_WALLET_VALUE);
|
|
15
|
+
export function usePrivateWalletContext() {
|
|
16
|
+
return useContext(PrivateWalletContext);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wraps {@link useWallet} in a React context and auto-triggers generation
|
|
20
|
+
* once the upstream {@link AuthContext} reports a valid auth. Using a
|
|
21
|
+
* provider here (instead of re-calling the hook in every consumer) keeps
|
|
22
|
+
* exactly one in-flight signTypedData prompt per session — mirrors the
|
|
23
|
+
* rationale behind {@link AuthProvider}.
|
|
24
|
+
*/
|
|
25
|
+
export function PrivateWalletProvider({ children, client, address, walletClient, connector, allDeposits = true, autoGenerate = true, onStatus, }) {
|
|
26
|
+
const { auth } = useAuthContext();
|
|
27
|
+
const { wallet, isLoading, error, generateWallet, clearWallet, refreshWallet, } = useWallet({
|
|
28
|
+
client,
|
|
29
|
+
address: address,
|
|
30
|
+
auth: auth || undefined,
|
|
31
|
+
walletClient,
|
|
32
|
+
connector,
|
|
33
|
+
allDeposits,
|
|
34
|
+
onStatus,
|
|
35
|
+
});
|
|
36
|
+
// Keep generateWallet in a ref so the auto-trigger effect below doesn't
|
|
37
|
+
// re-fire when useWallet rebuilds the callback (e.g. after ensureChain
|
|
38
|
+
// swaps the walletClient identity). Re-firing would race the in-flight
|
|
39
|
+
// signTypedData prompt — exactly the bug we fixed in the old inner
|
|
40
|
+
// provider that used to live in SilentSwapContext.
|
|
41
|
+
const generateWalletRef = useRef(generateWallet);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
generateWalletRef.current = generateWallet;
|
|
44
|
+
}, [generateWallet]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!autoGenerate)
|
|
47
|
+
return;
|
|
48
|
+
if (auth && walletClient && connector && !wallet && !isLoading && !error) {
|
|
49
|
+
generateWalletRef.current().catch((err) => {
|
|
50
|
+
console.error('Failed to generate private wallet:', err);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}, [autoGenerate, auth, walletClient, connector, wallet, isLoading, error]);
|
|
54
|
+
const value = {
|
|
55
|
+
wallet,
|
|
56
|
+
isLoading,
|
|
57
|
+
error,
|
|
58
|
+
generateWallet,
|
|
59
|
+
retryWallet: refreshWallet,
|
|
60
|
+
clearWallet,
|
|
61
|
+
};
|
|
62
|
+
return _jsx(PrivateWalletContext.Provider, { value: value, children: children });
|
|
63
|
+
}
|
|
@@ -45,10 +45,16 @@ export interface SilentSwapContextType {
|
|
|
45
45
|
config: SilentSwapClientConfig;
|
|
46
46
|
wallet: SilentSwapWallet | null;
|
|
47
47
|
walletLoading: boolean;
|
|
48
|
+
/** Last error from private wallet generation (null when healthy) */
|
|
49
|
+
walletError: Error | null;
|
|
50
|
+
/** Manual trigger to retry wallet generation (e.g. after silent failure) */
|
|
51
|
+
retryWallet: () => Promise<void>;
|
|
48
52
|
auth: AuthResponse | null;
|
|
49
53
|
authLoading: boolean;
|
|
50
54
|
/** True when SIWE auto-authentication exhausted all retries (WalletConnect connection lost) */
|
|
51
55
|
authExhausted: boolean;
|
|
56
|
+
/** Manual trigger to start / retry SIWE sign-in */
|
|
57
|
+
signIn: () => Promise<AuthResponse | null>;
|
|
52
58
|
executeSwap: (params: ExecuteSwapParams) => Promise<SwapResult | null>;
|
|
53
59
|
swapLoading: boolean;
|
|
54
60
|
isSwapping: boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AuthProvider, useAuthContext } from './AuthContext.js';
|
|
5
|
+
import { PrivateWalletProvider, usePrivateWalletContext } from './PrivateWalletContext.js';
|
|
6
6
|
import { useSilentQuote } from '../hooks/silent/useSilentQuote.js';
|
|
7
7
|
import { useOrderTracking } from '../hooks/silent/useOrderTracking.js';
|
|
8
8
|
import { usePrices } from '../hooks/usePrices.js';
|
|
@@ -27,9 +27,12 @@ const DEFAULT_CONTEXT = {
|
|
|
27
27
|
},
|
|
28
28
|
wallet: null,
|
|
29
29
|
walletLoading: false,
|
|
30
|
+
walletError: null,
|
|
31
|
+
retryWallet: async () => { },
|
|
30
32
|
auth: null,
|
|
31
33
|
authLoading: false,
|
|
32
34
|
authExhausted: false,
|
|
35
|
+
signIn: async () => null,
|
|
33
36
|
executeSwap: async () => null,
|
|
34
37
|
swapLoading: false,
|
|
35
38
|
isSwapping: false,
|
|
@@ -81,26 +84,14 @@ export function useSilentSwap() {
|
|
|
81
84
|
}
|
|
82
85
|
return context;
|
|
83
86
|
}
|
|
84
|
-
function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, bitcoinAddress, tronAddress, solanaConnector, solanaConnection, bitcoinConnector, bitcoinConnection, tronConnector, tronConnection, environment, config, solanaRpcUrl, connector, isConnected = false, walletClient, proId, requestWalletConnect, forceBridgeProvider,
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
onStatus: onAuthStatus,
|
|
93
|
-
});
|
|
94
|
-
// Wallet management hook
|
|
95
|
-
const { wallet, generateWallet, refreshWallet, isLoading: walletLoading, error: walletError, } = useWallet({
|
|
96
|
-
client,
|
|
97
|
-
address: evmAddress,
|
|
98
|
-
auth: auth || undefined,
|
|
99
|
-
walletClient: walletClient,
|
|
100
|
-
connector: connector,
|
|
101
|
-
allDeposits: true,
|
|
102
|
-
onStatus: onWalletStatus,
|
|
103
|
-
});
|
|
87
|
+
function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, bitcoinAddress, tronAddress, solanaConnector, solanaConnection, bitcoinConnector, bitcoinConnection, tronConnector, tronConnection, environment, config, solanaRpcUrl, connector, isConnected = false, walletClient, proId, requestWalletConnect, forceBridgeProvider, }) {
|
|
88
|
+
// Auth + private-wallet state now come from their own dedicated contexts
|
|
89
|
+
// (see AuthProvider / PrivateWalletProvider wrapping this inner provider
|
|
90
|
+
// in SilentSwapProvider below). This keeps exactly one useAuth + one
|
|
91
|
+
// useWallet instance per session, so two consumers on the same page can
|
|
92
|
+
// never race each other into duplicate signMessage / signTypedData prompts.
|
|
93
|
+
const { auth, isLoading: authLoading, authenticate, signIn, authExhausted, } = useAuthContext();
|
|
94
|
+
const { wallet, generateWallet, retryWallet: refreshWallet, isLoading: walletLoading, error: walletError, } = usePrivateWalletContext();
|
|
104
95
|
const { setFacilitatorGroups } = useOrdersContext();
|
|
105
96
|
useWalletFacilitatorGroups(wallet, setFacilitatorGroups);
|
|
106
97
|
const tokenIn = useSwap((state) => state.tokenIn);
|
|
@@ -128,14 +119,7 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, bit
|
|
|
128
119
|
overheadUsd,
|
|
129
120
|
setInputAmount, // Pass setInputAmount for reverse calculation
|
|
130
121
|
});
|
|
131
|
-
|
|
132
|
-
console.log('[SilentSwapInnerProvider] useEffect', { auth, walletClient, connector, wallet, walletLoading });
|
|
133
|
-
if (auth && walletClient && connector && !wallet && !walletLoading) {
|
|
134
|
-
generateWallet().catch((err) => {
|
|
135
|
-
console.error('Failed to generate wallet:', err);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}, [auth, walletClient, connector, wallet, walletLoading, generateWallet]);
|
|
122
|
+
// (Auto-trigger for generateWallet now lives inside PrivateWalletProvider.)
|
|
139
123
|
const { executeSwap, isLoading: swapLoading, isSwapping, currentStep, error: swapError, quote, orderId, viewingAuth, clearQuote, depositAmountUsdc, loadingAmounts, } = useSilentQuote({
|
|
140
124
|
client,
|
|
141
125
|
address: effectiveQuoteAddress,
|
|
@@ -222,9 +206,12 @@ function SilentSwapInnerProvider({ children, client, evmAddress, solAddress, bit
|
|
|
222
206
|
config,
|
|
223
207
|
wallet,
|
|
224
208
|
walletLoading,
|
|
209
|
+
walletError,
|
|
210
|
+
retryWallet: refreshWallet,
|
|
225
211
|
auth,
|
|
226
212
|
authLoading,
|
|
227
213
|
authExhausted,
|
|
214
|
+
signIn,
|
|
228
215
|
executeSwap,
|
|
229
216
|
swapLoading,
|
|
230
217
|
isSwapping,
|
|
@@ -278,5 +265,5 @@ export function SilentSwapProvider({ children, client, evmAddress, solAddress, c
|
|
|
278
265
|
baseUrl: computedBaseUrl,
|
|
279
266
|
};
|
|
280
267
|
}, [environment, baseUrl]);
|
|
281
|
-
return (_jsx(AssetsProvider, { children: _jsx(PricesProvider, { children: _jsx(BalancesProvider, { evmAddress: evmAddress, solAddress: solAddress, solanaRpcUrl: solanaRpcUrl, bitcoinAddress: bitcoinAddress, bitcoinRpcUrl: bitcoinRpcUrl, tronAddress: tronAddress, tronRpcUrl: tronRpcUrl, children: _jsx(OrdersProvider, { baseUrl: config.baseUrl, children: _jsx(SilentSwapInnerProvider, { client: client, connector: connector, isConnected: isConnected, evmAddress: evmAddress, solAddress: solAddress, bitcoinAddress: bitcoinAddress, tronAddress: tronAddress, solanaConnector: solanaConnector, solanaConnection: solanaConnection, bitcoinConnector: bitcoinConnector, bitcoinConnection: bitcoinConnection, tronConnector: tronConnector, tronConnection: tronConnection, environment: environment, config: config, solanaRpcUrl: solanaRpcUrl, walletClient: walletClient, proId: proId, requestWalletConnect: requestWalletConnect, forceBridgeProvider: forceBridgeProvider,
|
|
268
|
+
return (_jsx(AssetsProvider, { children: _jsx(PricesProvider, { children: _jsx(BalancesProvider, { evmAddress: evmAddress, solAddress: solAddress, solanaRpcUrl: solanaRpcUrl, bitcoinAddress: bitcoinAddress, bitcoinRpcUrl: bitcoinRpcUrl, tronAddress: tronAddress, tronRpcUrl: tronRpcUrl, children: _jsx(OrdersProvider, { baseUrl: config.baseUrl, children: _jsx(AuthProvider, { client: client, address: evmAddress, walletClient: walletClient, connector: connector, autoAuthenticate: true, onStatus: onAuthStatus, children: _jsx(PrivateWalletProvider, { client: client, address: evmAddress, walletClient: walletClient, connector: connector, allDeposits: true, autoGenerate: true, onStatus: onWalletStatus, children: _jsx(SilentSwapInnerProvider, { client: client, connector: connector, isConnected: isConnected, evmAddress: evmAddress, solAddress: solAddress, bitcoinAddress: bitcoinAddress, tronAddress: tronAddress, solanaConnector: solanaConnector, solanaConnection: solanaConnection, bitcoinConnector: bitcoinConnector, bitcoinConnection: bitcoinConnection, tronConnector: tronConnector, tronConnection: tronConnection, environment: environment, config: config, solanaRpcUrl: solanaRpcUrl, walletClient: walletClient, proId: proId, requestWalletConnect: requestWalletConnect, forceBridgeProvider: forceBridgeProvider, children: children }) }) }) }) }) }) }));
|
|
282
269
|
}
|
|
@@ -68,6 +68,12 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
68
68
|
const hasAutoAuthenticatedRef = useRef(false);
|
|
69
69
|
// Track retry count for auto-authentication (WalletConnect relay can drop requests)
|
|
70
70
|
const autoAuthRetryCountRef = useRef(0);
|
|
71
|
+
// Track the pending retry timer so we can clear it on unmount / signOut
|
|
72
|
+
const retryTimerRef = useRef(null);
|
|
73
|
+
// Synchronous guard to prevent concurrent authenticate calls.
|
|
74
|
+
// React state (isLoading) is async/batched, so Strict Mode double-fire or
|
|
75
|
+
// rapid effect re-runs can trigger multiple signMessage prompts.
|
|
76
|
+
const inFlightRef = useRef(false);
|
|
71
77
|
const AUTO_AUTH_MAX_RETRIES = 6;
|
|
72
78
|
const AUTO_AUTH_RETRY_DELAY_MS = 5000;
|
|
73
79
|
// Normalize address
|
|
@@ -136,6 +142,12 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
136
142
|
* Perform SIWE authentication
|
|
137
143
|
*/
|
|
138
144
|
const authenticate = useCallback(async () => {
|
|
145
|
+
// Synchronous guard — blocks concurrent callers in the same tick.
|
|
146
|
+
// Must be checked and set before any `await` to beat React's state batching.
|
|
147
|
+
if (inFlightRef.current) {
|
|
148
|
+
console.log('[useAuth:authenticate] skipped, already in flight');
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
139
151
|
if (!client) {
|
|
140
152
|
throw new Error('Client required for authentication');
|
|
141
153
|
}
|
|
@@ -145,6 +157,7 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
145
157
|
if (!walletClient) {
|
|
146
158
|
throw new Error('Wallet client required for authentication');
|
|
147
159
|
}
|
|
160
|
+
inFlightRef.current = true;
|
|
148
161
|
setIsLoading(true);
|
|
149
162
|
setError(null);
|
|
150
163
|
try {
|
|
@@ -169,34 +182,51 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
169
182
|
// the ping above tests relay connectivity, but a real RPC round-trip catches
|
|
170
183
|
// cases where the relay is alive but the peer wallet is unresponsive.
|
|
171
184
|
const LIVENESS_TIMEOUT_MS = 5_000;
|
|
185
|
+
let livenessTimeout;
|
|
172
186
|
try {
|
|
173
187
|
await Promise.race([
|
|
174
188
|
walletClient.request({ method: 'eth_chainId' }),
|
|
175
|
-
new Promise((_, reject) =>
|
|
189
|
+
new Promise((_, reject) => {
|
|
190
|
+
livenessTimeout = setTimeout(() => reject(new Error('Wallet liveness check timed out')), LIVENESS_TIMEOUT_MS);
|
|
191
|
+
}),
|
|
176
192
|
]);
|
|
177
193
|
}
|
|
178
194
|
catch (livenessErr) {
|
|
179
195
|
throw new Error(`Wallet not responding to requests: ${livenessErr.message}`);
|
|
180
196
|
}
|
|
197
|
+
finally {
|
|
198
|
+
if (livenessTimeout)
|
|
199
|
+
clearTimeout(livenessTimeout);
|
|
200
|
+
}
|
|
181
201
|
// Create SIWE message
|
|
182
202
|
const siweMessage = createSignInMessageCallback(normalizedAddress, nonceValue, domain);
|
|
183
203
|
// Notify consumer that a wallet signature is about to be requested
|
|
184
204
|
onStatus?.('Please check your wallet and approve the sign-in request. This uses to load orders your orders from blockchain.');
|
|
185
205
|
// Sign the message with timeout — WalletConnect relay can silently drop requests.
|
|
186
206
|
const SIGN_TIMEOUT_MS = 30_000;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
let signTimeout;
|
|
208
|
+
let signature;
|
|
209
|
+
try {
|
|
210
|
+
signature = (await Promise.race([
|
|
211
|
+
walletClient.signMessage({
|
|
212
|
+
account: normalizedAddress,
|
|
213
|
+
message: siweMessage.message,
|
|
214
|
+
}),
|
|
215
|
+
new Promise((_, reject) => {
|
|
216
|
+
signTimeout = setTimeout(() => reject(new Error('Wallet did not respond to sign-in request. The request may not have reached your wallet.')), SIGN_TIMEOUT_MS);
|
|
217
|
+
}),
|
|
218
|
+
]));
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
if (signTimeout)
|
|
222
|
+
clearTimeout(signTimeout);
|
|
223
|
+
}
|
|
194
224
|
onStatus?.('Sign-in approved. Authenticating...');
|
|
195
225
|
// Authenticate with server
|
|
196
226
|
const [authError, authResponse] = await client.authenticate({
|
|
197
227
|
siwe: {
|
|
198
228
|
message: siweMessage.message,
|
|
199
|
-
signature
|
|
229
|
+
signature,
|
|
200
230
|
},
|
|
201
231
|
});
|
|
202
232
|
if (authError || !authResponse) {
|
|
@@ -214,9 +244,10 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
214
244
|
return null;
|
|
215
245
|
}
|
|
216
246
|
finally {
|
|
247
|
+
inFlightRef.current = false;
|
|
217
248
|
setIsLoading(false);
|
|
218
249
|
}
|
|
219
|
-
}, [client, normalizedAddress, walletClient, nonce, getNonce, createSignInMessageCallback, domain, saveAuthCallback]);
|
|
250
|
+
}, [client, normalizedAddress, walletClient, connector, nonce, getNonce, createSignInMessageCallback, domain, saveAuthCallback, onStatus]);
|
|
220
251
|
/**
|
|
221
252
|
* Sign in (alias for authenticate for convenience)
|
|
222
253
|
*/
|
|
@@ -234,8 +265,14 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
234
265
|
setNonce(null);
|
|
235
266
|
setError(null);
|
|
236
267
|
setIsAutoAuthenticating(false);
|
|
237
|
-
|
|
268
|
+
setAuthExhausted(false);
|
|
269
|
+
// Reset auto-authentication flags + pending retry
|
|
238
270
|
hasAutoAuthenticatedRef.current = false;
|
|
271
|
+
autoAuthRetryCountRef.current = 0;
|
|
272
|
+
if (retryTimerRef.current) {
|
|
273
|
+
clearTimeout(retryTimerRef.current);
|
|
274
|
+
retryTimerRef.current = null;
|
|
275
|
+
}
|
|
239
276
|
}, [normalizedAddress]);
|
|
240
277
|
/**
|
|
241
278
|
* Check if user is authenticated
|
|
@@ -254,6 +291,10 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
254
291
|
hasAutoAuthenticatedRef.current = false;
|
|
255
292
|
autoAuthRetryCountRef.current = 0;
|
|
256
293
|
setAuthExhausted(false);
|
|
294
|
+
if (retryTimerRef.current) {
|
|
295
|
+
clearTimeout(retryTimerRef.current);
|
|
296
|
+
retryTimerRef.current = null;
|
|
297
|
+
}
|
|
257
298
|
}
|
|
258
299
|
prevAddressRef.current = normalizedAddress;
|
|
259
300
|
}, [normalizedAddress]);
|
|
@@ -269,6 +310,10 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
269
310
|
hasAutoAuthenticatedRef.current = false;
|
|
270
311
|
autoAuthRetryCountRef.current = 0;
|
|
271
312
|
setAuthExhausted(false);
|
|
313
|
+
if (retryTimerRef.current) {
|
|
314
|
+
clearTimeout(retryTimerRef.current);
|
|
315
|
+
retryTimerRef.current = null;
|
|
316
|
+
}
|
|
272
317
|
}
|
|
273
318
|
}
|
|
274
319
|
prevWalletClientRef.current = walletClient;
|
|
@@ -317,8 +362,11 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
317
362
|
const attemptNumber = autoAuthRetryCountRef.current + 1;
|
|
318
363
|
console.log(`[useAuth] auto-authenticate attempt ${attemptNumber}/${AUTO_AUTH_MAX_RETRIES}`);
|
|
319
364
|
setIsAutoAuthenticating(true);
|
|
365
|
+
let cancelled = false;
|
|
320
366
|
authenticate()
|
|
321
367
|
.then((result) => {
|
|
368
|
+
if (cancelled)
|
|
369
|
+
return;
|
|
322
370
|
// authenticate() swallows errors and returns null on failure,
|
|
323
371
|
// so only mark as completed if it actually succeeded.
|
|
324
372
|
if (result) {
|
|
@@ -330,7 +378,10 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
330
378
|
// Retry after a delay by clearing error to re-trigger the effect.
|
|
331
379
|
autoAuthRetryCountRef.current += 1;
|
|
332
380
|
console.log(`[useAuth] auto-auth failed, scheduling retry ${autoAuthRetryCountRef.current + 1}/${AUTO_AUTH_MAX_RETRIES} in ${AUTO_AUTH_RETRY_DELAY_MS}ms`);
|
|
333
|
-
|
|
381
|
+
if (retryTimerRef.current)
|
|
382
|
+
clearTimeout(retryTimerRef.current);
|
|
383
|
+
retryTimerRef.current = setTimeout(() => {
|
|
384
|
+
retryTimerRef.current = null;
|
|
334
385
|
setError(null);
|
|
335
386
|
}, AUTO_AUTH_RETRY_DELAY_MS);
|
|
336
387
|
}
|
|
@@ -344,9 +395,22 @@ export function useAuth({ client, address, walletClient, connector, domain = typ
|
|
|
344
395
|
console.error('Auto-authentication failed:', err);
|
|
345
396
|
})
|
|
346
397
|
.finally(() => {
|
|
347
|
-
|
|
398
|
+
if (!cancelled)
|
|
399
|
+
setIsAutoAuthenticating(false);
|
|
348
400
|
});
|
|
401
|
+
return () => {
|
|
402
|
+
cancelled = true;
|
|
403
|
+
};
|
|
349
404
|
}, [autoAuthenticate, normalizedAddress, walletClient, auth, isLoading, isAutoAuthenticating, error, authenticate, loadCachedAuthCallback]);
|
|
405
|
+
// Clear pending retry timer on unmount
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
return () => {
|
|
408
|
+
if (retryTimerRef.current) {
|
|
409
|
+
clearTimeout(retryTimerRef.current);
|
|
410
|
+
retryTimerRef.current = null;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}, []);
|
|
350
414
|
return {
|
|
351
415
|
// State
|
|
352
416
|
auth,
|
|
@@ -47,8 +47,10 @@ export function useWallet({ client, address, auth, walletClient, connector, allD
|
|
|
47
47
|
const [isLoading, setIsLoading] = useState(false);
|
|
48
48
|
const [error, setError] = useState(null);
|
|
49
49
|
// Synchronous guard to prevent concurrent generateWallet calls.
|
|
50
|
-
// React state (isLoading) is async/batched, so multiple callers
|
|
50
|
+
// React state (isLoading) is async/batched, so multiple callers (Strict Mode
|
|
51
|
+
// double-fire, effect re-runs from callback identity changes) can see
|
|
51
52
|
// isLoading=false in the same tick and both trigger signTypedData prompts.
|
|
53
|
+
const inFlightRef = useRef(false);
|
|
52
54
|
// Normalize address
|
|
53
55
|
const normalizedAddress = address ? getAddress(address) : null;
|
|
54
56
|
/**
|
|
@@ -158,31 +160,41 @@ export function useWallet({ client, address, auth, walletClient, connector, allD
|
|
|
158
160
|
// the closure's walletClient which may be scoped to the pre-switch chain.
|
|
159
161
|
// Race against a timeout: WalletConnect can silently drop signTypedData requests
|
|
160
162
|
// (e.g. TrustWallet when the chain scope doesn't match), leaving users stuck forever.
|
|
161
|
-
console.log('[useWallet:generateEntropy] calling signTypedData ...', {
|
|
162
|
-
account: signingClient.account?.address,
|
|
163
|
-
domainChainId: String(eip712Doc.domain?.chainId),
|
|
164
|
-
});
|
|
165
163
|
const SIGN_TIMEOUT_MS = 60_000;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
164
|
+
let timeoutHandle;
|
|
165
|
+
try {
|
|
166
|
+
const signature = await Promise.race([
|
|
167
|
+
signingClient.signTypedData({
|
|
168
|
+
...eip712Doc,
|
|
169
|
+
account: signingClient.account,
|
|
170
|
+
}),
|
|
171
|
+
new Promise((_, reject) => {
|
|
172
|
+
timeoutHandle = setTimeout(() => reject(new Error('Wallet did not respond to the signature request. '
|
|
173
|
+
+ 'Please check your wallet app — you may need to manually switch to Ethereum mainnet and retry.')), SIGN_TIMEOUT_MS);
|
|
174
|
+
}),
|
|
175
|
+
]);
|
|
176
|
+
onStatus?.('Signature approved. Setting up wallet...');
|
|
177
|
+
return hexToBytes(signature);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
if (timeoutHandle)
|
|
181
|
+
clearTimeout(timeoutHandle);
|
|
182
|
+
}
|
|
177
183
|
}, [auth, walletClient, connector, address, onStatus]);
|
|
178
184
|
/**
|
|
179
185
|
* Generate new wallet with facilitator accounts
|
|
180
186
|
*/
|
|
181
187
|
const generateWallet = useCallback(async () => {
|
|
182
|
-
|
|
188
|
+
// Synchronous guard — blocks concurrent callers in the same tick.
|
|
189
|
+
// Must be checked and set before any `await` to beat React's state batching.
|
|
190
|
+
if (inFlightRef.current) {
|
|
191
|
+
console.log('[useWallet:generateWallet] skipped, already in flight');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
183
194
|
if (!normalizedAddress) {
|
|
184
195
|
throw new Error('Address required for wallet generation');
|
|
185
196
|
}
|
|
197
|
+
inFlightRef.current = true;
|
|
186
198
|
setIsLoading(true);
|
|
187
199
|
setError(null);
|
|
188
200
|
try {
|
|
@@ -246,6 +258,7 @@ export function useWallet({ client, address, auth, walletClient, connector, allD
|
|
|
246
258
|
throw error;
|
|
247
259
|
}
|
|
248
260
|
finally {
|
|
261
|
+
inFlightRef.current = false;
|
|
249
262
|
setIsLoading(false);
|
|
250
263
|
}
|
|
251
264
|
}, [normalizedAddress, getCachedWallet, generateEntropy, allDeposits, client.s0xGatewayAddress]);
|
|
@@ -259,18 +272,16 @@ export function useWallet({ client, address, auth, walletClient, connector, allD
|
|
|
259
272
|
setWallet(null);
|
|
260
273
|
}, [normalizedAddress]);
|
|
261
274
|
/**
|
|
262
|
-
* Refresh wallet (regenerate if needed)
|
|
275
|
+
* Refresh wallet (regenerate if needed).
|
|
276
|
+
* User-initiated retry — bypasses the in-flight guard so a stuck
|
|
277
|
+
* signTypedData request (e.g. dropped silently by an in-app browser)
|
|
278
|
+
* doesn't permanently block future attempts.
|
|
263
279
|
*/
|
|
264
280
|
const refreshWallet = useCallback(async () => {
|
|
281
|
+
inFlightRef.current = false;
|
|
265
282
|
clearWallet();
|
|
266
283
|
await generateWallet();
|
|
267
284
|
}, [clearWallet, generateWallet]);
|
|
268
|
-
// When walletClient or connector changes (e.g. WalletConnect chain switch causes
|
|
269
|
-
// wagmi to recreate the walletClient), reset the generation guard. Any in-flight
|
|
270
|
-
// generation using stale WalletConnect references will hang on signTypedData,
|
|
271
|
-
// permanently blocking future calls.
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
}, [walletClient, connector]);
|
|
274
285
|
// Clear wallet when address changes (e.g. user switched from MetaMask to Phantom)
|
|
275
286
|
const prevAddressRef = useRef(normalizedAddress);
|
|
276
287
|
useEffect(() => {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export { OrdersProvider, useOrdersContext, useWalletFacilitatorGroups } from './
|
|
|
11
11
|
export type { OrdersContextOrderMetadata, OrdersContextOrder, OrderDestination, FacilitatorGroup, OrdersContextType } from './contexts/OrdersContext.js';
|
|
12
12
|
export { SilentSwapProvider, useSilentSwap } from './contexts/SilentSwapContext.js';
|
|
13
13
|
export type { SilentSwapContextType, SilentSwapStatusData, SilentSwapFees, SilentSwapOrderTracking, SilentSwapEstimates, } from './contexts/SilentSwapContext.js';
|
|
14
|
+
export { AuthProvider, useAuthContext } from './contexts/AuthContext.js';
|
|
15
|
+
export type { AuthContextValue, AuthProviderProps } from './contexts/AuthContext.js';
|
|
16
|
+
export { PrivateWalletProvider, usePrivateWalletContext } from './contexts/PrivateWalletContext.js';
|
|
17
|
+
export type { PrivateWalletContextValue, PrivateWalletProviderProps } from './contexts/PrivateWalletContext.js';
|
|
14
18
|
export { useSilentOrders } from './hooks/silent/useSilentOrders.js';
|
|
15
19
|
export { useAuth } from './hooks/silent/useAuth.js';
|
|
16
20
|
export { useWallet } from './hooks/silent/useWallet.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ export { AssetsProvider, useAssetsContext } from './contexts/AssetsContext.js';
|
|
|
5
5
|
export { BalancesProvider, useBalancesContext } from './contexts/BalancesContext.js';
|
|
6
6
|
export { OrdersProvider, useOrdersContext, useWalletFacilitatorGroups } from './contexts/OrdersContext.js';
|
|
7
7
|
export { SilentSwapProvider, useSilentSwap } from './contexts/SilentSwapContext.js';
|
|
8
|
+
export { AuthProvider, useAuthContext } from './contexts/AuthContext.js';
|
|
9
|
+
export { PrivateWalletProvider, usePrivateWalletContext } from './contexts/PrivateWalletContext.js';
|
|
8
10
|
export { useSilentOrders } from './hooks/silent/useSilentOrders.js';
|
|
9
11
|
export { useAuth } from './hooks/silent/useAuth.js';
|
|
10
12
|
export { useWallet } from './hooks/silent/useWallet.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silentswap/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.53",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@bigmi/core": "^0.6.5",
|
|
26
26
|
"@ensdomains/ensjs": "^4.2.0",
|
|
27
|
-
"@silentswap/sdk": "0.1.
|
|
28
|
-
"@silentswap/ui-kit": "0.1.
|
|
27
|
+
"@silentswap/sdk": "0.1.53",
|
|
28
|
+
"@silentswap/ui-kit": "0.1.53",
|
|
29
29
|
"@solana/codecs-strings": "^5.1.0",
|
|
30
30
|
"@solana/kit": "^5.1.0",
|
|
31
31
|
"@solana/rpc": "^5.1.0",
|