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: token approval #211

Merged
merged 34 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5b06069
feat: balance query
nejcm Jul 4, 2023
a80b64e
refactor: swap component
nejcm Jul 6, 2023
7665c23
Merge branch 'staging' into 111-fetch-wallet-token-balances-for-amber…
nejcm Jul 6, 2023
299fe64
feat: token balance hook
nejcm Jul 6, 2023
26226ca
refactor: ui
nejcm Jul 9, 2023
b3c24f3
fix: balance
nejcm Jul 10, 2023
6294f9d
fix: theme ui
nejcm Jul 10, 2023
80e369e
fix: lint
nejcm Jul 10, 2023
b4169ae
feat: WIP
nejcm Jul 10, 2023
c3eb58d
feat: contract
nejcm Jul 11, 2023
a181809
feat: token allowance
nejcm Jul 14, 2023
8e0407e
feat: token approval
nejcm Jul 14, 2023
0066c23
feat: token approval
nejcm Jul 14, 2023
cd4a8eb
feat: add and remove liquidity from swap pool
nejcm Jul 19, 2023
721fbe1
feat: add and remove liquidity from swap pool
nejcm Jul 19, 2023
0e9a3ce
feat: backstop pools
nejcm Jul 21, 2023
ee1bb64
Merge branch 'staging' into feat/token-approval
nejcm Jul 21, 2023
a9a47fb
fix: lint issues
nejcm Jul 21, 2023
7aee261
feat: swap, progress update
nejcm Jul 23, 2023
ea6c3cd
feat: apps, apps provider
nejcm Jul 27, 2023
59e4361
fix: imports
nejcm Jul 27, 2023
64ecb6a
fix: backstop ui
nejcm Jul 27, 2023
57e6497
fix: balance
nejcm Jul 27, 2023
263d609
feat: added dev
nejcm Jul 27, 2023
20e321c
style: updated scroll-bar
nejcm Aug 1, 2023
bd58d09
feat: tokens
nejcm Aug 1, 2023
870b574
fix: add liquidity info
nejcm Aug 2, 2023
e8361b2
feat: WIP
nejcm Aug 4, 2023
34fc486
fix: contract write
nejcm Aug 6, 2023
297410e
fix: contract write
nejcm Aug 7, 2023
be7c8bd
refactor: shared hooks
nejcm Aug 8, 2023
0939b5c
refactor: shared hooks
nejcm Aug 8, 2023
66707be
refactor: updates
nejcm Aug 9, 2023
46a2958
Merge remote-tracking branch 'origin/staging' into feat/token-approval
nejcm Aug 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/GlobalStateProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WalletAccount, getWalletBySource } from '@talismn/connect-wallets';
import { createContext } from 'preact';
import { ComponentChildren, createContext } from 'preact';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/compat';
import { useLocation } from 'react-router-dom';
import { config } from './config';
Expand Down Expand Up @@ -41,7 +41,7 @@ const initWalletConnect = async (chainId: string) => {
return await walletConnectService.init(provider?.session, chainId);
};

const GlobalStateProvider = ({ children }: { children: ReactNode }) => {
const GlobalStateProvider = ({ children }: { children: ComponentChildren }) => {
const tenantRef = useRef<string>();
const [walletAccount, setWallet] = useState<WalletAccount | undefined>(undefined);
const { pathname } = useLocation();
Expand Down
17 changes: 17 additions & 0 deletions src/SharedProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ComponentChildren } from 'preact';
import { useGlobalState } from './GlobalStateProvider';
import { useNodeInfoState } from './NodeInfoProvider';
import { SharedStateProvider } from './shared/Provider';

const SharedProvider = ({ children }: { children: ComponentChildren }) => {
const { api } = useNodeInfoState().state;
const { signer, address } = useGlobalState().walletAccount || {};

return (
<SharedStateProvider api={api} signer={signer} address={address}>
{children}
</SharedStateProvider>
);
};

export default SharedProvider;
5 changes: 4 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import TermsAndConditions from './TermsAndConditions';
import AppsProvider from './components/Apps/provider';
import Layout from './components/Layout';
import { defaultPageLoader } from './components/Loader/Page';
import { NotFound } from './components/NotFound';
Expand All @@ -21,6 +22,7 @@ const TransfersPage = <SuspenseLoad importFn={() => import('./pages/bridge/Trans
const BackstopPoolsPage = (
<SuspenseLoad importFn={() => import('./pages/nabla/backstop-pools')} fallback={defaultPageLoader} />
);
const DevPage = <SuspenseLoad importFn={() => import('./pages/nabla/dev')} fallback={defaultPageLoader} />;
const Bridge = <SuspenseLoad importFn={() => import('./pages/bridge')} fallback={defaultPageLoader} />;
const Staking = <SuspenseLoad importFn={() => import('./pages/collators/Collators')} fallback={defaultPageLoader} />;

Expand All @@ -38,11 +40,12 @@ export function App() {
<Route path="bridge" element={Bridge} />
<Route path="transfers" element={TransfersPage} />
</Route>
<Route path="nabla">
<Route path="nabla" Component={() => <AppsProvider app="nabla" />}>
<Route path="" element={NablaPage} />
<Route path="swap" element={SwapPage} />
<Route path="swap-pools" element={SwapPoolsPage} />
<Route path="backstop-pools" element={BackstopPoolsPage} />
{config.isDev && <Route path="dev" element={DevPage} />}
<Route path="*" element={<NotFound />} />
</Route>
<Route path="staking" element={Staking} />
Expand Down
8 changes: 0 additions & 8 deletions src/assets/amplitude-icon.svg

This file was deleted.

41 changes: 41 additions & 0 deletions src/components/Apps/Unsupported/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Button } from 'react-daisyui';
import { useLocation, useNavigate } from 'react-router-dom';
import { Apps } from '../../../config/apps';
import { buildTenantPath } from '../../../helpers/url';
import { TenantName } from '../../../models/Tenant';

export interface UnsupportedProps {
app: Apps;
tenant: TenantName;
supportedTenants: TenantName[];
}

const Unsupported = ({ app, tenant, supportedTenants }: UnsupportedProps): JSX.Element | null => {
const navigateTo = useNavigate();
const location = useLocation().pathname;
return (
<div className="text-center py-8">
<h2 className="text-xl mb-6">
<span className="capitalize">{app}</span> is not supported on <span className="capitalize">{tenant}</span>.
Switch to:
</h2>
<div className="flex justify-center items-center flex-wrap gap-3">
{supportedTenants.map((st) => (
<Button
key={st}
className="capitalize text-lg"
variant="secondary"
type="button"
onClick={() => {
navigateTo(buildTenantPath(tenant, st, location));
}}
>
{st}
</Button>
))}
</div>
</div>
);
};

export default Unsupported;
22 changes: 22 additions & 0 deletions src/components/Apps/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Outlet } from 'react-router-dom';
import { useGlobalState } from '../../GlobalStateProvider';
import { Apps, appsConfigs } from '../../config/apps';
import { TenantName } from '../../models/Tenant';
import Unsupported from './Unsupported';

export type AppsProviderProps = {
app: Apps;
};

const AppsProvider = ({ app }: AppsProviderProps): JSX.Element | null => {
const tenant = useGlobalState().tenantName;
const supportedTenants = appsConfigs[app].tenants;
if (!(supportedTenants as TenantName[]).includes(tenant)) {
return <Unsupported app={app} tenant={tenant} supportedTenants={supportedTenants} />;
}
return <Outlet />;
};

AppsProvider.displayName = 'AppsProvider';

export default AppsProvider;
45 changes: 45 additions & 0 deletions src/components/Asset/Approval/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Button, ButtonProps } from 'react-daisyui';
import { ApprovalState, useTokenApproval } from '../../../shared/useTokenApproval';

export type TokenApprovalProps = ButtonProps & {
token: string | undefined;
amount: number;
/** contract address (eg. router address) */
spender?: string;
enabled?: boolean;
children: ReactNode;
};

const TokenApproval = ({
amount,
token,
spender,
enabled = true,
children,
className = '',
...rest
}: TokenApprovalProps): JSX.Element | null => {
const approval = useTokenApproval({
amount,
token,
spender,
enabled,
});

if (approval[0] === ApprovalState.APPROVED || !enabled) return <>{children}</>;
const isPending = approval[0] === ApprovalState.PENDING;
const isLoading = approval[0] === ApprovalState.LOADING;
return (
<Button
className={`w-full${isPending || isLoading ? ' loading' : ''} ${className}`}
color="primary"
{...rest}
type="button"
disabled={isPending}
onClick={isPending ? undefined : () => approval[1].mutate()}
>
{isPending ? 'Approving' : isLoading ? 'Loading' : 'Approve'}
</Button>
);
};
export default TokenApproval;
33 changes: 18 additions & 15 deletions src/components/Asset/Selector/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { CheckIcon } from '@heroicons/react/20/solid';
import { matchSorter } from 'match-sorter';
import { ChangeEvent, useMemo, useState } from 'preact/compat';
import { Avatar, Button, Input, Modal, ModalProps } from 'react-daisyui';
import { repeat } from '../../../../helpers/general';
import { Asset } from '../../../../models/Asset';
import ModalCloseButton from '../../../Button/ModalClose';
import { Skeleton } from '../../../Skeleton';

export interface AssetListProps {
Expand Down Expand Up @@ -31,21 +33,24 @@ const AssetList = ({ assets, onSelect, selected }: AssetListProps): JSX.Element
{filteredTokens?.map((token) => (
<Button
type="button"
size="md"
variant="ghost"
size="lg"
variant="secondary"
key={token.address}
onClick={() => onSelect(token)}
className={`items-center w-full gap-4 text-base${selected === token.address ? ' bg-neutral-100' : ''}`}
className="w-full items-center justify-start gap-4 px-3 py-1 border-0 bg-[rgba(0,0,0,.2)] text-left hover:opacity-80 dark:bg-[rgba(255,255,255,.12)]"
>
<div>
<Avatar size="xs" letters={token.symbol} shape="circle" className="text-xs" />
</div>
<div>
<p>
<strong>{token.name}</strong>
</p>
</div>
<div className="ml-auto text-sm font-normal">{'Amount'}</div>
<span className="relative">
<Avatar size="xs" letters={token.symbol} src={token.logoURI} shape="circle" className="text-xs" />
{selected == token.address && (
<CheckIcon className="absolute -right-1 -top-1 w-5 h-5 p-[3px] text-white bg-green-600 rounded-full" />
)}
</span>
<span className="flex flex-col">
<span className="text-lg leading-5">
<strong>{token.symbol}</strong>
</span>
<span className="text-sm leading-5">{token.name}</span>
</span>
</Button>
))}
</div>
Expand All @@ -70,9 +75,7 @@ export const AssetSelectorModal = ({
return (
<Modal {...rest}>
<Modal.Header className="mb-0">
<Button size="sm" shape="circle" className="absolute right-2 top-2" onClick={onClose} type="button">
</Button>
<ModalCloseButton onClick={onClose} />
<h3 className="text-2xl font-normal">Select a token</h3>
</Modal.Header>
<Modal.Body>
Expand Down
24 changes: 15 additions & 9 deletions src/components/Balance/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { ComponentChildren } from 'preact';
import { QueryOptions } from '../../constants/cache';
import { useBalance } from '../../hooks/useBalance';
import { Skeleton } from '../Skeleton';
import { useContractBalance } from '../../shared/useContractBalance';
import { numberLoader } from '../Loader';

export type BalanceProps = {
address: string;
address?: string;
fallback?: string | number;
loader?: boolean;
options?: QueryOptions;
children?: ComponentChildren;
};

const Balance = ({ address, fallback = 0, loader = true, options }: BalanceProps): JSX.Element | null => {
const { isLoading, formatted } = useBalance(address, options);
if (isLoading) {
return loader ? <Skeleton className="inline-flex">10000</Skeleton> : null;
}
return <span title={formatted}>{formatted ?? fallback ?? null}</span>;
const Balance = ({ address, fallback = 0, loader = true, options, children }: BalanceProps): JSX.Element | null => {
const { isLoading, formatted, enabled } = useContractBalance({ contractAddress: address }, options);
if (!address || !enabled) return <>{fallback ?? null}</>;
if (isLoading) return loader ? numberLoader : null;
return (
<span title={formatted}>
{children}
{formatted ?? fallback ?? null}
</span>
);
};
export default Balance;
8 changes: 8 additions & 0 deletions src/components/Button/ModalClose/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Button, ButtonProps } from 'react-daisyui';

const ModalCloseButton = (props: ButtonProps): JSX.Element | null => (
<Button size="sm" color="secondary" shape="circle" className="absolute right-2 top-2" type="button" {...props}>
</Button>
);
export default ModalCloseButton;
7 changes: 2 additions & 5 deletions src/components/ChainSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import AmplitudeLogo from '../assets/AmplitudeLogo';
import PendulumLogo from '../assets/PendulumLogo';
import { toTitle } from '../helpers/string';
import { buildTenantPath } from '../helpers/url';
import { TenantName } from '../models/Tenant';

const options = [TenantName.Pendulum, TenantName.Amplitude, TenantName.Foucoco];
Expand Down Expand Up @@ -33,7 +34,7 @@ const ChainSelector = ({ tenantName }: { tenantName: TenantName | undefined }):
<Dropdown.Item
key={i}
onClick={() => {
navigateTo(buildPath(tenantName, option, location));
navigateTo(buildTenantPath(tenantName, option, location));
window.location.reload();
}}
>
Expand All @@ -51,8 +52,4 @@ const ChainSelector = ({ tenantName }: { tenantName: TenantName | undefined }):
);
};

function buildPath(current: TenantName | undefined, next: TenantName, location: string) {
return current ? location.replace(current, next) : location;
}

export default ChainSelector;
18 changes: 12 additions & 6 deletions src/components/Loader/Page/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import logoLoader from '../../../assets/pendulum-icon-loading.svg';
import { Skeleton } from '../../Skeleton';

export const defaultPageLoader = (
<div className="flex items-center justify-center w-full h-full p-8">
<img src={logoLoader} width="120px" height="120px" alt="Pendulum" />
export const PageLoader = ({ className = '' }: { className?: string }) => (
<div className={className}>
<Skeleton className="w-full h-[300px] mb-8" />
<Skeleton className="w-[50%] h-[42px] mb-4" />
<Skeleton className="w-[80%] h-[22px] mb-2" />
<Skeleton className="w-[45%] h-[22px] mb-2" />
<Skeleton className="w-[55%] h-[22px] mb-2" />
</div>
);

export const PageLoader = (props: { className?: string }) => (
<img src={logoLoader} width="120px" height="120px" alt="Pendulum" {...props} />
export const defaultPageLoader = (
<div className="flex justify-center my-8">
<PageLoader className="w-full max-w-[800px]" />
</div>
);
3 changes: 3 additions & 0 deletions src/components/Loader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Skeleton } from '../Skeleton';

export const numberLoader = <Skeleton className="inline-block">10000</Skeleton>;
Loading
Loading