on master help sign in
bridge/src/lib/useWalletConnect.ts
360 lines 9.0 kB history raw
1import { SignClient as Connector } from '@walletconnect/sign-client';
2import { SignClient } from '@walletconnect/sign-client/dist/types/client';
3import { Web3Modal } from '@web3modal/standalone';
4import { SessionTypes } from '@walletconnect/types';
6import { useEffect, useMemo, useState } from 'react';
7import { Just, Nothing } from 'folktale/maybe';
9import { useWallet } from 'store/wallet';
10import { getAuthToken } from './authToken';
11import { WALLET_TYPES } from './constants';
12import WalletConnectWallet from './types/WalletConnectWallet';
13import { isGoerli } from './flags';
14import { ITxData } from './types/ITxData';
15import { mayCreateHexString } from './utils/hex';
17type PeerMeta = {
18 description: string;
19 icons?: string[];
20 name: string;
21 url: string;
22};
24type PersonalSign = {
25 message: string;
26 address: string;
27};
29const getChain = () => {
30 return isGoerli ? 'eip155:5' : 'eip155:1';
31};
33export const useWalletConnect = () => {
34 const {
35 wallet,
36 setWallet,
37 setAuthToken,
38 setFakeToken,
39 skipLoginSigning,
40 resetWallet,
41 }: any = useWallet();
43 const [connector, setConnector] = useState<SignClient | null>(null);
44 const [address, setAddress] = useState<string | null>(null);
45 const [peerMeta, setPeerMeta] = useState<PeerMeta | null>(null);
46 const [session, setSession] = useState<SessionTypes.Struct | null>(null);
47 const [modal, setModal] = useState<Web3Modal | null>(null);
49 const resetSession = () => {
50 setSession(null);
51 setAddress(null);
52 setPeerMeta(null);
53 };
55 const updateSession = (_session: SessionTypes.Struct) => {
56 setSession(_session);
57 setAddress(_session.namespaces.eip155.accounts[0].slice(9));
58 setPeerMeta(_session.peer.metadata);
59 };
61 const initConnector = async () => {
62 if (connector) return;
64 const newConnector = await Connector.init({
65 projectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID,
66 });
68 setConnector(newConnector);
70 const newModal = new Web3Modal({
71 projectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID,
72 standaloneChains: [getChain()],
73 walletConnectVersion: 2,
74 });
76 setModal(newModal);
78 // restore state from cached connection
79 if (Just.hasInstance(wallet) && !session) {
80 const cached_sessions = [
81 ...newConnector.session.map,
82 ].map(([key, value]) => ({ key, value }));
84 var restored: SessionTypes.Struct | null = null;
86 // this will set restored to the last
87 // cached session with the same address
88 for (var key in cached_sessions) {
89 if (
90 cached_sessions[key].value.namespaces.eip155.accounts[0].slice(9) ===
91 wallet.value.address
92 ) {
93 restored = cached_sessions[key].value;
94 }
95 }
97 if (restored) {
98 updateSession(restored);
99 }
100 }
101 };
103 // Init connector
104 useEffect(() => {
105 initConnector();
106 // eslint-disable-next-line react-hooks/exhaustive-deps
107 }, []);
109 // Behavior
110 const connect = async () => {
111 if (!connector) {
112 return;
113 }
115 const proposalNamespace = {
116 eip155: {
117 chains: [getChain()],
118 methods: [
119 'eth_sendTransaction',
120 'eth_signTransaction',
121 'personal_sign',
122 ],
123 events: ['connect', 'disconnect'],
124 },
125 };
127 const { uri, approval } = await connector.connect({
128 requiredNamespaces: proposalNamespace,
129 });
131 if (uri) {
132 modal?.openModal({ uri });
133 const session = await approval();
134 updateSession(session);
135 modal?.closeModal();
136 }
137 };
139 const disconnect = async () => {
140 if (!connector || !session) {
141 return;
142 }
144 await connector.disconnect({
145 topic: session.topic,
146 reason: { code: 6000, message: 'User disconnected' },
147 });
149 resetSession();
150 };
152 const authenticate = async () => {
153 if (!(address && connector)) {
154 return;
155 }
157 const wallet: WalletConnectWallet = {
158 address,
159 };
160 setWallet(Just(wallet));
162 if (skipLoginSigning) {
163 setFakeToken();
164 return;
165 }
166 let authToken = Nothing();
168 try {
169 const token = await getAuthToken({
170 address: address,
171 walletType: WALLET_TYPES.WALLET_CONNECT,
172 signPersonalMessage: signPersonalMessage,
173 });
174 authToken = Just(token);
175 setAuthToken(authToken);
176 modal?.closeModal();
177 } catch (e) {
178 if (e.message === 'METHOD_NOT_SUPPORTED') {
179 console.warn(
180 'wallet does not support message signing. proceeding without auth token.'
181 );
182 } else {
183 throw e;
184 //TODO should errors with this *really* prevent login?
185 }
186 }
187 };
189 const isConnected = () => {
190 if (!connector || !session) {
191 return false;
192 }
194 return session?.acknowledged;
195 };
197 const peerIcon = useMemo(() => {
198 if (!peerMeta?.icons) {
199 return null;
200 }
202 // Some peers return a list of empty string(s) :)
203 const iconCandidates = peerMeta.icons.filter(pm => pm !== '');
205 return iconCandidates.length > 0 ? iconCandidates[0] : null;
206 }, [peerMeta]);
208 const signTransaction = async ({
209 from,
210 to,
211 gas,
212 gasPrice,
213 value,
214 data,
215 nonce,
216 }: ITxData): Promise<string> => {
217 return new Promise((resolve, reject) => {
218 if (!connector || !isConnected()) {
219 reject(new Error('No connected wallet available for signing'));
220 return;
221 }
223 // Ledger Live needs to use fakeSign instead since it sends the txn upon signing
224 if (peerMeta?.name === 'Ledger Wallet') {
225 reject(new Error('METHOD_NOT_SUPPORTED'));
226 return;
227 }
229 return connector
230 .request<string>({
231 topic: session?.topic!,
232 chainId: getChain(),
233 request: {
234 method: 'eth_signTransaction',
235 params: [
236 {
237 to,
238 from,
239 data: mayCreateHexString(data),
240 nonce: mayCreateHexString(nonce),
241 gasPrice: mayCreateHexString(gasPrice),
242 gasLimit: mayCreateHexString(gas),
243 value: mayCreateHexString(value),
244 },
245 ],
246 },
247 })
248 .then((signature: string) => resolve(signature))
249 .catch((error: Error) => reject(error));
250 });
251 };
253 const sendTransaction = async ({
254 from,
255 to,
256 gas,
257 gasPrice,
258 value,
259 data,
260 nonce,
261 }: ITxData): Promise<string> => {
262 return new Promise((resolve, reject) => {
263 if (!connector || !isConnected()) {
264 reject(new Error('No connected wallet available for sending'));
265 return;
266 }
268 return connector
269 .request<string>({
270 topic: session?.topic!,
271 chainId: getChain(),
272 request: {
273 method: 'eth_sendTransaction',
274 params: [
275 {
276 to,
277 from,
278 data: mayCreateHexString(data),
279 nonce: mayCreateHexString(nonce),
280 gasPrice: mayCreateHexString(gasPrice),
281 gasLimit: mayCreateHexString(gas),
282 value: mayCreateHexString(value),
283 },
284 ],
285 },
286 })
287 .then((txHash: string) => resolve(txHash))
288 .catch((error: Error) => reject(error));
289 });
290 };
292 const signPersonalMessage = async ({ message, address }: PersonalSign) => {
293 return new Promise((resolve, reject) => {
294 if (!connector || !isConnected()) {
295 reject(new Error('No connected wallet available for signing'));
296 return;
297 }
299 return connector
300 .request({
301 topic: session?.topic!,
302 chainId: getChain(),
303 request: {
304 method: 'personal_sign',
305 params: [mayCreateHexString(message), address],
306 },
307 })
308 .then(result => {
309 resolve(result);
310 })
311 .catch((error: Error) => reject(error));
312 });
313 };
315 // Event handlers
317 const onSessionDelete = () => {
318 setAddress(null);
319 setPeerMeta(null);
320 resetWallet();
321 };
323 const onSessionUpdate = ({ topic, params }: any) => {
324 const _session = connector?.session.get(topic);
325 if (_session) {
326 updateSession(_session);
327 }
328 };
330 // Init and clean up
331 useEffect(() => {
332 if (!connector) {
333 return;
334 }
335 connector.on('session_delete', onSessionDelete);
336 connector.on('session_update', onSessionUpdate);
338 return () => {
339 // Clean up listeners
340 connector.off('session_delete', onSessionDelete);
341 connector.off('session_update', onSessionUpdate);
342 };
343 // eslint-disable-next-line react-hooks/exhaustive-deps
344 }, [connector]);
346 return {
347 address,
348 authenticate,
349 connect,
350 connector,
351 disconnect,
352 isConnected,
353 peerIcon,
354 peerMeta,
355 initConnector,
356 signTransaction,
357 sendTransaction,
358 signPersonalMessage,
359 };
360};