on c8cd811 help sign in
bridge/src/lib/useWalletConnect.ts
354 lines 8.7 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 return connector
224 .request<string>({
225 topic: session?.topic!,
226 chainId: getChain(),
227 request: {
228 method: 'eth_signTransaction',
229 params: [
230 {
231 to,
232 from,
233 data: mayCreateHexString(data),
234 nonce: mayCreateHexString(nonce),
235 gasPrice: mayCreateHexString(gasPrice),
236 gasLimit: mayCreateHexString(gas),
237 value: mayCreateHexString(value),
238 },
239 ],
240 },
241 })
242 .then((signature: string) => resolve(signature))
243 .catch((error: Error) => reject(error));
244 });
245 };
247 const sendTransaction = async ({
248 from,
249 to,
250 gas,
251 gasPrice,
252 value,
253 data,
254 nonce,
255 }: ITxData): Promise<string> => {
256 return new Promise((resolve, reject) => {
257 if (!connector || !isConnected()) {
258 reject(new Error('No connected wallet available for sending'));
259 return;
260 }
262 return connector
263 .request<string>({
264 topic: session?.topic!,
265 chainId: getChain(),
266 request: {
267 method: 'eth_sendTransaction',
268 params: [
269 {
270 to,
271 from,
272 data: mayCreateHexString(data),
273 nonce: mayCreateHexString(nonce),
274 gasPrice: mayCreateHexString(gasPrice),
275 gasLimit: mayCreateHexString(gas),
276 value: mayCreateHexString(value),
277 },
278 ],
279 },
280 })
281 .then((txHash: string) => resolve(txHash))
282 .catch((error: Error) => reject(error));
283 });
284 };
286 const signPersonalMessage = async ({ message, address }: PersonalSign) => {
287 return new Promise((resolve, reject) => {
288 if (!connector || !isConnected()) {
289 reject(new Error('No connected wallet available for signing'));
290 return;
291 }
293 return connector
294 .request({
295 topic: session?.topic!,
296 chainId: getChain(),
297 request: {
298 method: 'personal_sign',
299 params: [mayCreateHexString(message), address],
300 },
301 })
302 .then(result => {
303 resolve(result);
304 })
305 .catch((error: Error) => reject(error));
306 });
307 };
309 // Event handlers
311 const onSessionDelete = () => {
312 setAddress(null);
313 setPeerMeta(null);
314 resetWallet();
315 };
317 const onSessionUpdate = ({ topic, params }: any) => {
318 const _session = connector?.session.get(topic);
319 if (_session) {
320 updateSession(_session);
321 }
322 };
324 // Init and clean up
325 useEffect(() => {
326 if (!connector) {
327 return;
328 }
329 connector.on('session_delete', onSessionDelete);
330 connector.on('session_update', onSessionUpdate);
332 return () => {
333 // Clean up listeners
334 connector.off('session_delete', onSessionDelete);
335 connector.off('session_update', onSessionUpdate);
336 };
337 // eslint-disable-next-line react-hooks/exhaustive-deps
338 }, [connector]);
340 return {
341 address,
342 authenticate,
343 connect,
344 connector,
345 disconnect,
346 isConnected,
347 peerIcon,
348 peerMeta,
349 initConnector,
350 signTransaction,
351 sendTransaction,
352 signPersonalMessage,
353 };
354};