on e5e005a help sign in
bridge/src/lib/useWalletConnect.ts
292 lines 6.1 kB history raw
1import WalletConnect from '@walletconnect/client';
2import QRCodeModal from '@walletconnect/qrcode-modal';
3import { ITxData } from '@walletconnect/types';
5import { useEffect, useMemo, useState } from 'react';
6import { Just, Nothing } from 'folktale/maybe';
8import { useWallet } from 'store/wallet';
9import { getAuthToken } from './authToken';
10import { WALLET_TYPES } from './constants';
11import WalletConnectWallet from './types/WalletConnectWallet';
13type PeerMeta = {
14 description: string;
15 icons?: string[];
16 name: string;
17 url: string;
18};
20type ConnectEvent = {
21 event: 'connect';
22 params: [
23 {
24 accounts: string[];
25 chainId: number;
26 peerId: string;
27 peerMeta: PeerMeta;
28 }
29 ];
30};
32export const useWalletConnect = () => {
33 const {
34 setWallet,
35 setAuthToken,
36 setFakeToken,
37 skipLoginSigning,
38 resetWallet,
39 }: any = useWallet();
40 const [connector, setConnector] = useState<WalletConnect | null>(null);
41 const [address, setAddress] = useState<string | null>(null);
42 const [peerMeta, setPeerMeta] = useState<PeerMeta | null>(null);
44 const resetConnector = () => {
45 const newConnector = new WalletConnect({
46 bridge: 'https://bridge.walletconnect.org',
47 qrcodeModal: QRCodeModal,
48 });
50 setConnector(newConnector);
52 // restore state from cached connection
53 if (newConnector.accounts.length > 0) {
54 setAddress(newConnector.accounts[0]);
55 }
57 if (newConnector.peerMeta) {
58 setPeerMeta(newConnector.peerMeta);
59 }
60 };
62 // Init connector
63 useEffect(() => {
64 resetConnector();
65 // eslint-disable-next-line react-hooks/exhaustive-deps
66 }, []);
68 // Behavior
69 const connect = async () => {
70 if (!connector || connector.connected) {
71 return;
72 }
74 await connector.createSession();
75 };
77 const disconnect = async () => {
78 if (!connector) {
79 return;
80 }
82 await connector.killSession();
83 };
85 const authenticate = async () => {
86 if (!(address && connector)) {
87 return;
88 }
90 const wallet: WalletConnectWallet = {
91 address,
92 };
93 setWallet(Just(wallet));
94 if (skipLoginSigning) {
95 setFakeToken();
96 return;
97 }
99 let authToken = Nothing();
100 try {
101 const token = await getAuthToken({
102 address,
103 connector,
104 walletType: WALLET_TYPES.WALLET_CONNECT,
105 });
106 authToken = Just(token);
107 setAuthToken(authToken);
108 } catch (e) {
109 if (e.message === 'METHOD_NOT_SUPPORTED') {
110 console.warn(
111 'wallet does not support message signing. proceeding without auth token.'
112 );
113 } else {
114 throw e;
115 //TODO should errors with this *really* prevent login?
116 }
117 }
118 };
120 const isConnected = () => {
121 if (!connector) {
122 return false;
123 }
125 return connector.connected;
126 };
128 const peerIcon = useMemo(() => {
129 if (!peerMeta?.icons) {
130 return null;
131 }
133 // Some peers return a list of empty string(s) :)
134 const iconCandidates = peerMeta.icons.filter(pm => pm !== '');
136 return iconCandidates.length > 0 ? iconCandidates[0] : null;
137 }, [peerMeta]);
139 const signTransaction = async ({
140 from,
141 to,
142 gas,
143 gasPrice,
144 value,
145 data,
146 nonce,
147 }: ITxData): Promise<string> => {
148 return new Promise((resolve, reject) => {
149 if (!connector || !isConnected()) {
150 reject(new Error('No connected wallet available for signing'));
151 return;
152 }
154 return connector
155 .signTransaction({
156 from,
157 to,
158 gas,
159 gasPrice,
160 value,
161 data,
162 nonce,
163 })
164 .then((signature: string) => resolve(signature))
165 .catch((error: Error) => reject(error));
166 });
167 };
169 const sendTransaction = async ({
170 from,
171 to,
172 gas,
173 gasPrice,
174 value,
175 data,
176 nonce,
177 }: ITxData): Promise<string> => {
178 return new Promise((resolve, reject) => {
179 if (!connector || !isConnected()) {
180 reject(new Error('No connected wallet available for sending'));
181 return;
182 }
184 //REVIEW .then path untested
185 return connector
186 .sendTransaction({
187 from,
188 to,
189 gas,
190 gasPrice,
191 value,
192 data,
193 nonce,
194 })
195 .then((txHash: string) => resolve(txHash))
196 .catch((error: Error) => reject(error));
197 });
198 };
200 // Event Handlers
201 const initConnectHandler = () => {
202 if (!connector) {
203 return;
204 }
206 connector.on('connect', async (error, payload: ConnectEvent) => {
207 if (error) {
208 throw error;
209 }
211 const address = payload.params[0].accounts[0];
212 setAddress(address);
213 setPeerMeta(payload.params[0].peerMeta);
214 });
215 };
217 const initDisconnectHandler = () => {
218 if (!connector) {
219 return;
220 }
222 connector.on('disconnect', (error, _payload) => {
223 if (error) {
224 throw error;
225 }
227 setAddress(null);
228 setPeerMeta(null);
229 resetWallet();
230 });
231 };
233 const initSessionUpdateHandler = () => {
234 if (!connector) {
235 return;
236 }
238 connector.on('session_update', (error, payload) => {
239 if (error) {
240 throw error;
241 }
243 disconnect();
244 });
245 };
247 const initModalClosedHandler = () => {
248 if (!connector) {
249 return;
250 }
252 connector.on('modal_closed', () => {
253 resetWallet();
254 resetConnector();
255 });
256 };
258 // Init and clean up
259 useEffect(() => {
260 if (!connector) {
261 return;
262 }
264 initConnectHandler();
265 initDisconnectHandler();
266 initSessionUpdateHandler();
267 initModalClosedHandler();
269 return () => {
270 // Clean up listeners
271 connector.off('connect');
272 connector.off('disconnect');
273 connector.off('session_update');
274 connector.off('modal_closed');
275 };
276 // eslint-disable-next-line react-hooks/exhaustive-deps
277 }, [connector]);
279 return {
280 address,
281 authenticate,
282 connect,
283 connector,
284 disconnect,
285 isConnected,
286 peerIcon,
287 peerMeta,
288 resetConnector,
289 signTransaction,
290 sendTransaction,
291 };
292};