From 5487296487c2cc52047993b5baa3e4f6e783e192 Mon Sep 17 00:00:00 2001 From: Nejc Date: Tue, 27 Jun 2023 23:16:13 +0800 Subject: [PATCH] feat: wallet connect (#195) * feat: wallet connect WIP * feat: wallet connect * refactor: wc * Update src/components/Wallet/WalletConnect/index.tsx Co-authored-by: Marcel Ebert * fix: add optional chains * refactor: config * fix: config bug * refactor: close modal on connect * feat: wallet connect sign transaction * Implement `signPayload()` for walletconnect This fixes the signing problems with walletconnect * feat: wallet connect persistance * fix: wallet storage issues --------- Co-authored-by: Marcel Ebert --- package.json | 4 +- src/GlobalStateProvider.tsx | 140 +-- src/app.tsx | 2 +- src/assets/AmplitudeLogo.tsx | 2 +- src/assets/wallet-connect.svg | 1 + src/components/ChainSelector.tsx | 10 +- src/components/Layout/Nav.tsx | 4 +- src/components/Layout/NetworkId.tsx | 2 +- src/components/Layout/index.tsx | 35 +- src/components/Layout/links.tsx | 5 +- src/components/Swap/useSwapComponent.ts | 2 - src/components/Wallet/WalletConnect/index.tsx | 67 ++ .../{OpenWallet.tsx => Wallet/index.tsx} | 42 +- src/config/index.ts | 4 + src/config/walletConnect.ts | 26 + src/hooks/useLocalStorage.ts | 44 +- src/index.css | 5 + src/main.tsx | 7 +- src/pages/bridge/Issue.tsx | 2 +- src/pages/bridge/Redeem.tsx | 2 +- src/pages/bridge/TransferDialog.tsx | 4 +- src/pages/bridge/Transfers.tsx | 4 +- src/pages/dashboard/Dashboard.tsx | 5 +- src/pages/stats/index.tsx | 2 +- src/services/storage/local.ts | 16 - src/services/storage/types.ts | 1 - src/services/walletConnect/index.ts | 68 ++ yarn.lock | 870 +++++++++++++++++- 28 files changed, 1156 insertions(+), 220 deletions(-) create mode 100644 src/assets/wallet-connect.svg create mode 100644 src/components/Wallet/WalletConnect/index.tsx rename src/components/{OpenWallet.tsx => Wallet/index.tsx} (64%) create mode 100644 src/config/walletConnect.ts create mode 100644 src/services/walletConnect/index.ts diff --git a/package.json b/package.json index c6b5af73..b6fccb18 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "@talismn/connect-wallets": "^1.2.3", "@tanstack/react-query": "^4.24.10", "@tanstack/react-table": "^8.7.9", + "@walletconnect/modal": "^2.4.7", + "@walletconnect/universal-provider": "^2.8.1", "big.js": "^6.2.1", "bn.js": "^5.2.1", "bs58": "^5.0.0", @@ -105,4 +107,4 @@ "npm": "please-use-yarn", "yarn": ">=1.22.19" } -} +} \ No newline at end of file diff --git a/src/GlobalStateProvider.tsx b/src/GlobalStateProvider.tsx index 7a1ff41c..cd1f38f0 100644 --- a/src/GlobalStateProvider.tsx +++ b/src/GlobalStateProvider.tsx @@ -1,6 +1,6 @@ import { WalletAccount, getWalletBySource } from '@talismn/connect-wallets'; import { createContext } from 'preact'; -import { StateUpdater, useCallback, useContext, useEffect, useMemo, useState } from 'preact/compat'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/compat'; import { useLocation } from 'react-router-dom'; import { config } from './config'; import { storageKeys } from './constants/localStorage'; @@ -8,103 +8,107 @@ import { useLocalStorage } from './hooks/useLocalStorage'; import { TenantName } from './models/Tenant'; import { ThemeName } from './models/Theme'; import { storageService } from './services/storage/local'; +import { walletConnectService } from './services/walletConnect'; +import { chainIds } from './config/walletConnect'; -export interface GlobalStateValues { +export interface GlobalState { + dAppName: string; tenantName: TenantName; tenantRPC?: string; - wallet?: WalletAccount; -} - -export interface GlobalState { - state: Partial; - setState: StateUpdater>; - walletAccount: WalletAccount | undefined; + walletAccount?: WalletAccount; setWalletAccount: (data: WalletAccount) => void; removeWalletAccount: () => void; getThemeName: () => ThemeName; - dAppName: string; } -export const defaultState: GlobalStateValues = { - tenantName: TenantName.Amplitude, - tenantRPC: undefined, -}; - +export const defaultTenant = TenantName.Pendulum; const GlobalStateContext = createContext(undefined); -const GlobalStateProvider = ({ - children, - value = defaultState, -}: { - children: ReactNode; - value?: Partial; -}) => { +const initTalisman = async (dAppName: string, selected?: string) => { + const name = storageService.get('@talisman-connect/selected-wallet-name'); + if (!name?.length) return; + const wallet = getWalletBySource(name); + if (!wallet) return; + await wallet.enable(dAppName); + const accounts = await wallet.getAccounts(); + const selectedWallet = accounts.find((a) => a.address === selected) || accounts[0]; + return selectedWallet; +}; +const initWalletConnect = async (chainId: string) => { + const provider = await walletConnectService.getProvider(); + //const pairings = provider.client.pairing.getAll({ active: true }); + if (!provider?.session) return; + return await walletConnectService.init(provider?.session, chainId); +}; + +const GlobalStateProvider = ({ children }: { children: ReactNode }) => { + const tenantRef = useRef(); + const [walletAccount, setWallet] = useState(undefined); const { pathname } = useLocation(); - const [state, setState] = useState(() => { - if (value) return value; - if (pathname) { - const [network] = pathname.split('/').filter(Boolean); - const tenantName = Object.values(TenantName).includes(network) - ? (network as TenantName) - : TenantName.Pendulum; - if (tenantName) { - return { - tenantName, - tenantRPC: config.tenants[tenantName].rpc, - }; - } - } - return defaultState; - }); - const dAppName = state.tenantName || TenantName.Amplitude; + const network = pathname.split('/').filter(Boolean)[0]?.toLowerCase(); + + const tenantName = useMemo(() => { + return network && Object.values(TenantName).includes(network) ? (network as TenantName) : defaultTenant; + }, [network]); + + const dAppName = tenantName; const getThemeName = useCallback( - () => (state.tenantName ? config.tenants[state.tenantName]?.theme || ThemeName.Amplitude : ThemeName.Amplitude), - [state?.tenantName], + () => (tenantName ? config.tenants[tenantName]?.theme || ThemeName.Amplitude : ThemeName.Amplitude), + [tenantName], ); const { - state: account, + state: storageAddress, set, clear, } = useLocalStorage({ - key: `${storageKeys.ACCOUNT}-${state.tenantName}`, + key: `${storageKeys.ACCOUNT}-${tenantName}`, + expire: 2 * 86400, // 2 days }); + const removeWalletAccount = useCallback(() => { + clear(); + setWallet(undefined); + }, [clear]); + + const setWalletAccount = useCallback( + (wallet: WalletAccount | undefined) => { + set(wallet?.address); + setWallet(wallet); + }, + [set], + ); + + const accountAddress = walletAccount?.address; useEffect(() => { const run = async () => { - storageService.removeExpired(); - if (!account) return; - const name = storageService.get('@talisman-connect/selected-wallet-name'); - if (!name) return; - const wallet = getWalletBySource(name); - if (!wallet) return; - // TODO: optimize this - make reusable as it's used in multiple places - await wallet.enable(dAppName || TenantName.Amplitude); - const selectedWallet = (await wallet.getAccounts()).find((a) => a.address === account); - if (!selectedWallet) return; - setState((prev) => ({ ...prev, wallet: selectedWallet })); + if (!storageAddress) { + removeWalletAccount(); + return; + } + // skip if tenant already initialized + if (tenantRef.current === tenantName || accountAddress) return; + tenantRef.current = tenantName; + const appName = dAppName || TenantName.Amplitude; + const selectedWallet = + (await initTalisman(appName, storageAddress)) || (await initWalletConnect(chainIds[tenantName])); + if (selectedWallet) setWallet(selectedWallet); }; run(); - }, [account, dAppName, state.tenantName]); + }, [storageAddress, removeWalletAccount, dAppName, tenantName, accountAddress]); - const providerValue = useMemo( + const providerValue = useMemo( () => ({ - state, - setState, - walletAccount: state.wallet, - setWalletAccount: (wallet: WalletAccount | undefined) => { - set(wallet?.address); - setState((prev) => ({ ...prev, wallet })); - }, - removeWalletAccount: () => { - clear(); - setState((prev) => ({ ...prev, wallet: undefined })); - }, + walletAccount, + tenantName: tenantName, + tenantRPC: config.tenants[tenantName].rpc, + setWalletAccount, + removeWalletAccount, getThemeName, dAppName, }), - [clear, dAppName, getThemeName, set, state], + [dAppName, getThemeName, removeWalletAccount, setWalletAccount, tenantName, walletAccount], ); return {children}; diff --git a/src/app.tsx b/src/app.tsx index 7a2af601..988ef252 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,12 +1,12 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; +import TermsAndConditions from './TermsAndConditions'; import Layout from './components/Layout'; import { defaultPageLoader } from './components/Loader/Page'; import { NotFound } from './components/NotFound'; import { SuspenseLoad } from './components/Suspense'; import { config } from './config'; -import TermsAndConditions from './TermsAndConditions'; /** * Components need to be default exports inside the file for suspense loading to work properly diff --git a/src/assets/AmplitudeLogo.tsx b/src/assets/AmplitudeLogo.tsx index 3cfd8f1c..27f2d9ee 100644 --- a/src/assets/AmplitudeLogo.tsx +++ b/src/assets/AmplitudeLogo.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from 'react'; +import { HTMLAttributes } from 'preact/compat'; interface Props extends HTMLAttributes { className?: string; diff --git a/src/assets/wallet-connect.svg b/src/assets/wallet-connect.svg new file mode 100644 index 00000000..d90457ad --- /dev/null +++ b/src/assets/wallet-connect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ChainSelector.tsx b/src/components/ChainSelector.tsx index b696ba15..871964ce 100644 --- a/src/components/ChainSelector.tsx +++ b/src/components/ChainSelector.tsx @@ -17,16 +17,16 @@ const ChainSelector = ({ tenantName }: { tenantName: TenantName | undefined }): {options.map((option, i) => ( diff --git a/src/components/Layout/Nav.tsx b/src/components/Layout/Nav.tsx index 2b833207..a324128b 100644 --- a/src/components/Layout/Nav.tsx +++ b/src/components/Layout/Nav.tsx @@ -17,7 +17,7 @@ const CollapseMenu = ({ children: JSX.Element | null; }) => { const { pathname } = useLocation(); - const { tenantName } = useGlobalState().state; + const { tenantName } = useGlobalState(); const isPendulum = tenantName === TenantName.Pendulum; const isActive = useMemo(() => { @@ -69,7 +69,7 @@ export type NavProps = { }; const Nav = memo(({ onClick }: NavProps) => { - const { state } = useGlobalState(); + const state = useGlobalState(); return (