forked from Cylo-Traders/Agrocylo-PIP
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWalletContext.tsx
More file actions
200 lines (185 loc) · 5.31 KB
/
Copy pathWalletContext.tsx
File metadata and controls
200 lines (185 loc) · 5.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import type { ReactNode } from 'react';
import {
isConnected,
getAddress,
getNetworkDetails,
signTransaction as freighterSign,
} from '@stellar/freighter-api';
import { NETWORK_PASSPHRASE } from '../lib/soroban/config';
const STORAGE_KEY = 'agrocylo:wallet:address';
interface WalletState {
publicKey: string | null;
isConnected: boolean;
isConnecting: boolean;
error: string | null;
connect: () => Promise<void>;
disconnect: () => void;
clearError: () => void;
signTransaction: (xdr: string) => Promise<string>;
}
const WalletContext = createContext<WalletState | null>(null);
export function useWallet(): WalletState {
const ctx = useContext(WalletContext);
if (!ctx) {
throw new Error('useWallet must be used within a WalletProvider');
}
return ctx;
}
function truncateAddress(addr: string): string {
if (addr.length <= 12) return addr;
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
}
export { truncateAddress };
/**
* Compares the connected wallet's active network against the app's
* configured NETWORK_PASSPHRASE. Returns a human-readable warning if they
* differ, or null if they match (or if the check itself fails — Freighter's
* own signature request is the final guard in that case).
*/
async function describeNetworkMismatch(): Promise<string | null> {
try {
const details = await getNetworkDetails();
if (
details.networkPassphrase &&
details.networkPassphrase !== NETWORK_PASSPHRASE
) {
return `Your wallet is connected to "${details.network}" but this app is configured for a different network. Switch your wallet's network to continue.`;
}
return null;
} catch {
// If Freighter doesn't support the check (older versions) or it fails,
// don't block the user here — an actual mismatch will still be caught
// when Freighter is asked to sign against NETWORK_PASSPHRASE.
return null;
}
}
export function WalletProvider({ children }: { children: ReactNode }) {
const [publicKey, setPublicKey] = useState<string | null>(() => {
try {
return localStorage.getItem(STORAGE_KEY);
} catch {
return null;
}
});
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);
const connect = useCallback(async () => {
setError(null);
setIsConnecting(true);
try {
const connectedResult = await isConnected();
if (!connectedResult.isConnected) {
setError(
'No Stellar wallet detected. Please install Freighter or another supported wallet extension.',
);
return;
}
const addrResult = await getAddress();
setPublicKey(addrResult.address);
try {
localStorage.setItem(STORAGE_KEY, addrResult.address);
} catch {
// localStorage may be unavailable (SSR, private browsing, etc.)
}
const mismatchWarning = await describeNetworkMismatch();
if (mismatchWarning) {
setError(mismatchWarning);
}
} catch (err: unknown) {
if (err instanceof Error && err.message.includes('reject')) {
setError(
'Connection rejected. Please approve the prompt in your wallet.',
);
} else {
setError('Failed to connect wallet. Please try again.');
}
} finally {
setIsConnecting(false);
}
}, []);
const disconnect = useCallback(() => {
setPublicKey(null);
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
// Ignore storage errors.
}
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
const signTransaction = useCallback(
async (xdr: string): Promise<string> => {
if (!publicKey) {
throw new Error('Wallet not connected');
}
const result = await freighterSign(xdr, {
networkPassphrase: NETWORK_PASSPHRASE,
});
return result.signedTxXdr;
},
[publicKey],
);
// Validate persisted address on mount — silently disconnect if invalid.
// Mount-only: publicKey is read inside but intentionally not a trigger.
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
if (!publicKey) return;
let cancelled = false;
(async () => {
try {
const connectedResult = await isConnected();
if (!connectedResult.isConnected && !cancelled) {
setPublicKey(null);
localStorage.removeItem(STORAGE_KEY);
}
} catch {
// Freighter not available — clear persisted state.
if (!cancelled) {
setPublicKey(null);
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
// Ignore.
}
}
}
})();
return () => {
cancelled = true;
};
}, []);
/* eslint-enable react-hooks/exhaustive-deps */
const value = useMemo<WalletState>(
() => ({
publicKey,
isConnected: publicKey !== null,
isConnecting,
error,
connect,
disconnect,
clearError,
signTransaction,
}),
[
publicKey,
isConnecting,
error,
connect,
disconnect,
clearError,
signTransaction,
],
);
return (
<WalletContext.Provider value={value}>{children}</WalletContext.Provider>
);
}