Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wallet connect #195

Merged
merged 13 commits into from
Jun 27, 2023
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
Loading