Skip to content

Commit

Permalink
feat: wallet connect (#195)
Browse files Browse the repository at this point in the history
* feat: wallet connect WIP

* feat: wallet connect

* refactor: wc

* Update src/components/Wallet/WalletConnect/index.tsx

Co-authored-by: Marcel Ebert <[email protected]>

* 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 <[email protected]>
  • Loading branch information
nejcm and ebma authored Jun 27, 2023
1 parent 9d42e97 commit 5487296
Show file tree
Hide file tree
Showing 28 changed files with 1,156 additions and 220 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -105,4 +107,4 @@
"npm": "please-use-yarn",
"yarn": ">=1.22.19"
}
}
}
140 changes: 72 additions & 68 deletions src/GlobalStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,114 @@
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';
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<GlobalStateValues>;
setState: StateUpdater<Partial<GlobalStateValues>>;
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<GlobalState | undefined>(undefined);

const GlobalStateProvider = ({
children,
value = defaultState,
}: {
children: ReactNode;
value?: Partial<GlobalStateValues>;
}) => {
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<string>();
const [walletAccount, setWallet] = useState<WalletAccount | undefined>(undefined);
const { pathname } = useLocation();
const [state, setState] = useState(() => {
if (value) return value;
if (pathname) {
const [network] = pathname.split('/').filter(Boolean);
const tenantName = Object.values<string>(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<string>(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<string | undefined>({
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<GlobalState>(
() => ({
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 <GlobalStateContext.Provider value={providerValue}>{children}</GlobalStateContext.Provider>;
Expand Down
2 changes: 1 addition & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/assets/AmplitudeLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HTMLAttributes } from 'react';
import { HTMLAttributes } from 'preact/compat';

interface Props extends HTMLAttributes<SVGSVGElement> {
className?: string;
Expand Down
1 change: 1 addition & 0 deletions src/assets/wallet-connect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions src/components/ChainSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ const ChainSelector = ({ tenantName }: { tenantName: TenantName | undefined }):
<Button
size="sm"
color="ghost"
className="text-sm border-base-300 bg-base-200 min-h-[2.25rem] h-auto px-2 sm:px-3"
className="text-sm border-base-300 bg-base-200 min-h-[2.1rem] h-auto px-2 sm:px-3"
title={tenantName}
>
{tenantName === TenantName.Pendulum ? (
<PendulumLogo className="w-5 h-6 mr-1" />
<PendulumLogo className="w-4 h-5 mr-1" />
) : (
<AmplitudeLogo className="w-5 h-5 mr-1 " />
<AmplitudeLogo className="w-4 h-4 mr-1 " />
)}
<span className="text-sm mr-1 sm:mr-3">{tenantName ? toTitle(tenantName) : ''}</span>
<ChevronDownIcon className="w-3 h-3" stroke-width="2" />
<span className="text-sm mr-1 sm:mr-2">{tenantName ? toTitle(tenantName) : ''}</span>
<ChevronDownIcon className="w-4 h-4" stroke-width="2" />
</Button>
<Dropdown.Menu className="w-30 mt-1.5 p-1 text-sm border-base-300 border bg-base-200 rounded-xl shadow-none">
{options.map((option, i) => (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Layout/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -69,7 +69,7 @@ export type NavProps = {
};

const Nav = memo(({ onClick }: NavProps) => {
const { state } = useGlobalState();
const state = useGlobalState();

return (
<nav>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layout/NetworkId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useNodeInfoState } from '../../NodeInfoProvider';

const NetworkId: FC = memo(() => {
const lastBlockNumber = useNodeInfoState().state.bestNumberFinalize;
const { tenantRPC } = useGlobalState().state;
const { tenantRPC } = useGlobalState();
const encodedRPC = tenantRPC ? encodeURI(tenantRPC) : '';

return (
Expand Down
35 changes: 8 additions & 27 deletions src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Bars3Icon } from '@heroicons/react/20/solid';
import { memo, useEffect, useMemo, useState } from 'preact/compat';
import { Link, Outlet, useParams } from 'react-router-dom';
import { memo, useState } from 'preact/compat';
import { Outlet } from 'react-router-dom';
import { useGlobalState } from '../../GlobalStateProvider';
import AmplitudeLogo from '../../assets/amplitud-logo.svg';
import PendulumLogo from '../../assets/pendulum-logo.png';
import { config } from '../../config';
import { TenantName } from '../../models/Tenant';
import ChainSelector from '../ChainSelector';
import OpenWallet from '../OpenWallet';
import OpenWallet from '../Wallet';
import Nav from './Nav';
import NetworkId from './NetworkId';
import SocialAndTermLinks from './SocialAndTermLinks';
Expand All @@ -16,31 +15,13 @@ import './styles.sass';

export default function Layout(): JSX.Element | null {
const [visible, setVisible] = useState(false);
const params = useParams();
const { state, setState, dAppName } = useGlobalState();
const isPendulum = state.tenantName === TenantName.Pendulum;
const isTestnet = state.tenantName === TenantName.Foucoco;
const { tenantName, dAppName } = useGlobalState();
const isPendulum = tenantName === TenantName.Pendulum;
const isTestnet = tenantName === TenantName.Foucoco;
const sideBarLogo = isPendulum ? PendulumLogo : AmplitudeLogo;
const chevronColor = isPendulum ? 'white' : 'grey ';
const bgColor = isPendulum ? 'bg-white' : 'bg-black';

const network: TenantName = useMemo(() => {
return params.network && Object.values<string>(TenantName).includes(params.network)
? (params.network as TenantName)
: TenantName.Pendulum;
}, [params.network]);

useEffect(() => {
// Only change state if network is different
const n =
network && Object.values<string>(TenantName).includes(network) ? (network as TenantName) : TenantName.Pendulum;
setState((prevState) => ({
...prevState,
tenantName: n,
tenantRPC: config.tenants[n].rpc,
}));
}, [network, setState]);

const FooterLink = memo(() => {
return isPendulum ? (
<span onClick={() => (window.location.href = '/amplitude')}>Amplitude</span>
Expand Down Expand Up @@ -79,7 +60,7 @@ export default function Layout(): JSX.Element | null {
</div>
<Nav onClick={() => setVisible(false)} />
<div className="sidebar-footer">
<Versions tenantName={state.tenantName} />
<Versions tenantName={tenantName} />
<NetworkId />
<SocialAndTermLinks />
</div>
Expand All @@ -94,7 +75,7 @@ export default function Layout(): JSX.Element | null {
</button>
</div>
<OpenWallet dAppName={dAppName} />
<ChainSelector tenantName={state.tenantName} />
<ChainSelector tenantName={tenantName} />
<div className="dropdown dropdown-end mr-2 hidden">
<button className="flex space-x-2 items-center px-4 py-2 btn no-animation">
<span className={`${isPendulum ? 'text-white' : ''} text-md`}>
Expand Down
5 changes: 2 additions & 3 deletions src/components/Layout/links.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ChevronRightIcon } from '@heroicons/react/20/solid';
import { ComponentChildren } from 'preact';
import { HTMLAttributes } from 'preact/compat';
import { GlobalStateValues } from '../../GlobalStateProvider';
import BridgeIcon from '../../assets/bridge';
import { GlobalState } from '../../GlobalStateProvider';
import CollatorsIcon from '../../assets/collators';
import DashboardIcon from '../../assets/dashboard';
import GovernanceIcon from '../../assets/governance';
Expand All @@ -26,7 +25,7 @@ export type BaseLinkItem = {
export type LinkItem = BaseLinkItem & {
submenu?: BaseLinkItem[];
};
export type Links = (state: Partial<GlobalStateValues>) => LinkItem[];
export type Links = (state: Partial<GlobalState>) => LinkItem[];

const arrow = <ChevronRightIcon className="nav-arrow w-5 h-5" />;

Expand Down
2 changes: 0 additions & 2 deletions src/components/Swap/useSwapComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export interface UseSwapComponentProps {
export const defaults = config.swap.defaults;

export const useSwapComponent = ({ from, to, onChange }: UseSwapComponentProps) => {
console.log(from);
const { walletAccount } = useGlobalState();
const {
state: { api },
Expand All @@ -40,7 +39,6 @@ export const useSwapComponent = ({ from, to, onChange }: UseSwapComponentProps)
const storage = useLocalStorage<SwapSettings>({
key: storageKeys.SWAP_SETTINGS,
defaultValue: defaults,
parse: true,
debounce: 1000,
});
const { merge, state: storageState } = storage;
Expand Down
Loading

0 comments on commit 5487296

Please sign in to comment.