feat(#1271): allow ethereum disconnect (#2325)

This commit is contained in:
Matthew Russell 2022-12-07 03:24:40 -06:00 committed by GitHub
parent 46a85b8e65
commit d1e0af0dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 491 additions and 288 deletions

View File

@ -1,3 +1,5 @@
import { connectEthereumWallet } from '../support/ethereum-wallet';
const assetSelectField = 'select[name="asset"]';
const toAddressField = 'input[name="to"]';
const amountField = 'input[name="amount"]';
@ -18,6 +20,7 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('unable to select assets not enabled', () => {
connectEthereumWallet();
cy.getByTestId('deposit-submit').click();
// Assets not enabled in mocks
cy.get(assetSelectField + ' option:contains(Asset 2)').should('not.exist');
@ -26,6 +29,7 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('handles empty fields', () => {
connectEthereumWallet();
// Submit form to trigger any empty validation messages
cy.getByTestId('deposit-submit').click();

View File

@ -1,4 +1,5 @@
import { aliasQuery, mockConnectWallet } from '@vegaprotocol/cypress';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
const connectEthWalletBtn = 'connect-eth-wallet-btn';
@ -102,31 +103,30 @@ describe('ethereum wallet', { tags: '@smoke' }, () => {
});
cy.mockGQLSubscription();
cy.get('main[data-testid="/portfolio"]').should('exist');
cy.connectVegaWallet();
cy.getByTestId('Withdrawals').click();
});
it('can connect', () => {
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-MetaMask').click();
cy.getByTestId('web3-connector-list').should('not.exist');
cy.getByTestId('tab-withdrawals').should('not.be.empty');
cy.getByTestId('tab-deposits').should('not.be.empty');
});
it('able to disconnect eth wallet', () => {
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.connectVegaWallet();
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.get('#ethereum-address').should('have.value', ethWalletAddress).click();
cy.getByTestId('dialog-content').within(() => {
cy.get('p').should('have.text', `Connected with ${ethWalletAddress}`);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect Ethereum Wallet')
.click();
});
cy.getByTestId('connect-eth-wallet-msg').should('exist');
connectEthereumWallet();
cy.get('#ethereum-address').should('have.value', ethWalletAddress);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
cy.getByTestId(connectEthWalletBtn).should('exist');
});
});

View File

@ -22,10 +22,11 @@ describe('withdraw form validation', { tags: '@smoke' }, () => {
// Withdraw page requires vega wallet connection
cy.connectVegaWallet();
cy.getByTestId('withdraw-dialog-button').click();
// It also requires connection Ethereum wallet
connectEthereumWallet();
cy.getByTestId('withdraw-dialog-button').click();
cy.wait('@Accounts');
cy.wait('@Assets');
});
@ -78,10 +79,10 @@ describe('withdraw actions', { tags: '@regression' }, () => {
// Withdraw page requires vega wallet connection
cy.connectVegaWallet();
// It also requires connection Ethereum wallet
cy.getByTestId('withdraw-dialog-button').click();
connectEthereumWallet();
cy.getByTestId('withdraw-dialog-button').click();
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();

View File

@ -7,51 +7,48 @@ import {
} from '@vegaprotocol/withdraws';
import { t } from '@vegaprotocol/react-helpers';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { Web3Container } from '@vegaprotocol/web3';
export const WithdrawalsContainer = () => {
const { pending, completed, loading, error } = useWithdrawals();
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
return (
<Web3Container>
<VegaWalletContainer>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full">
<AsyncRenderer
data={{ pending, completed }}
loading={loading}
error={error}
render={({ pending, completed }) => (
<>
{pending && pending.length > 0 && (
<>
<h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4>
<PendingWithdrawalsTable rowData={pending} />
</>
)}
{completed && completed.length > 0 && (
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
)}
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={completed}
/>
</>
)}
/>
</div>
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
onClick={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
</Button>
</div>
<VegaWalletContainer>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full">
<AsyncRenderer
data={{ pending, completed }}
loading={loading}
error={error}
render={({ pending, completed }) => (
<>
{pending && pending.length > 0 && (
<>
<h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4>
<PendingWithdrawalsTable rowData={pending} />
</>
)}
{completed && completed.length > 0 && (
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
)}
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={completed}
/>
</>
)}
/>
</div>
</VegaWalletContainer>
</Web3Container>
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
onClick={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
</Button>
</div>
</div>
</VegaWalletContainer>
);
};

View File

@ -1,9 +1,14 @@
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { useEagerConnect } from '@vegaprotocol/wallet';
import { NetworkLoader } from '@vegaprotocol/environment';
import { Connectors } from '../../lib/vega-connectors';
import { useEffect } from 'react';
import { NetworkLoader, useEnvironment } from '@vegaprotocol/environment';
import type { InMemoryCacheConfig } from '@apollo/client';
import {
useEthereumConfig,
createConnectors,
Web3Provider as Web3ProviderInternal,
useWeb3ConnectStore,
} from '@vegaprotocol/web3';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
interface AppLoaderProps {
children: ReactNode;
@ -14,57 +19,87 @@ interface AppLoaderProps {
* that must happen for it can be used
*/
export function AppLoader({ children }: AppLoaderProps) {
// Get keys from vega wallet immediately
useEagerConnect(Connectors);
const cache: InMemoryCacheConfig = useMemo(
() => ({
typePolicies: {
Account: {
keyFields: false,
fields: {
balanceFormatted: {},
},
},
Instrument: {
keyFields: false,
},
TradableInstrument: {
keyFields: ['instrument'],
},
Product: {
keyFields: ['settlementAsset', ['id']],
},
MarketData: {
keyFields: ['market', ['id']],
},
Node: {
keyFields: false,
},
Withdrawal: {
fields: {
pendingOnForeignChain: {
read: (isPending = false) => isPending,
},
},
},
ERC20: {
keyFields: ['contractAddress'],
},
PositionUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
Party: {
keyFields: false,
},
Fees: {
keyFields: false,
return <NetworkLoader cache={cacheConfig}>{children}</NetworkLoader>;
}
export const Web3Provider = ({ children }: { children: ReactNode }) => {
const { config, loading, error } = useEthereumConfig();
const { ETHEREUM_PROVIDER_URL } = useEnvironment();
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
store.connectors,
store.initialize,
]);
useEffect(() => {
if (config?.chain_id) {
return initializeConnectors(
createConnectors(ETHEREUM_PROVIDER_URL, Number(config?.chain_id)),
Number(config.chain_id)
);
}
}, [config?.chain_id, ETHEREUM_PROVIDER_URL, initializeConnectors]);
return (
<AsyncRenderer
loading={loading}
error={error}
data={connectors}
noDataCondition={(d) => {
if (!d) return true;
return d.length < 1;
}}
>
<Web3ProviderInternal connectors={connectors}>
<>{children}</>
</Web3ProviderInternal>
</AsyncRenderer>
);
};
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
Account: {
keyFields: false,
fields: {
balanceFormatted: {},
},
},
Instrument: {
keyFields: false,
},
TradableInstrument: {
keyFields: ['instrument'],
},
Product: {
keyFields: ['settlementAsset', ['id']],
},
MarketData: {
keyFields: ['market', ['id']],
},
Node: {
keyFields: false,
},
Withdrawal: {
fields: {
pendingOnForeignChain: {
read: (isPending = false) => isPending,
},
},
}),
[]
);
return <NetworkLoader cache={cache}>{children}</NetworkLoader>;
}
},
ERC20: {
keyFields: ['contractAddress'],
},
PositionUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
Party: {
keyFields: false,
},
Fees: {
keyFields: false,
},
},
};

View File

@ -2,20 +2,25 @@ import Head from 'next/head';
import type { AppProps } from 'next/app';
import { Navbar } from '../components/navbar';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import {
useEagerConnect as useVegaEagerConnect,
VegaWalletProvider,
} from '@vegaprotocol/wallet';
import {
EnvironmentProvider,
envTriggerMapping,
Networks,
useEnvironment,
} from '@vegaprotocol/environment';
import { AppLoader } from '../components/app-loader';
import { AppLoader, Web3Provider } from '../components/app-loader';
import './styles.css';
import { usePageTitleStore } from '../stores';
import { Footer } from '../components/footer';
import { useEffect, useMemo, useState } from 'react';
import DialogsContainer from './dialogs-container';
import { HashRouter, useLocation } from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { useEagerConnect as useEthereumEagerConnect } from '@vegaprotocol/web3';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@ -52,20 +57,25 @@ function AppBody({ Component }: AppProps) {
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Title />
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
<VegaWalletProvider>
<AppLoader>
<Navbar
theme={theme}
toggleTheme={toggleTheme}
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
/>
<main data-testid={location.pathname}>
<Component />
</main>
<Footer />
<DialogsContainer />
<Web3Provider>
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
<Navbar
theme={theme}
toggleTheme={toggleTheme}
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
/>
<main data-testid={location.pathname}>
<Component />
</main>
<Footer />
<DialogsContainer />
<MaybeConnectEagerly />
</div>
</Web3Provider>
</AppLoader>
</div>
</VegaWalletProvider>
</ThemeContext.Provider>
);
}
@ -85,12 +95,16 @@ function VegaTradingApp(props: AppProps) {
return (
<HashRouter>
<EnvironmentProvider>
<VegaWalletProvider>
<AppBody {...props} />
</VegaWalletProvider>
<AppBody {...props} />
</EnvironmentProvider>
</HashRouter>
);
}
export default VegaTradingApp;
const MaybeConnectEagerly = () => {
useVegaEagerConnect(Connectors);
useEthereumEagerConnect();
return null;
};

View File

@ -7,7 +7,6 @@ import { Connectors } from '../lib/vega-connectors';
import { RiskNoticeDialog } from '../components/risk-notice-dialog';
import { WithdrawalDialog } from '@vegaprotocol/withdraws';
import { DepositDialog } from '@vegaprotocol/deposits';
import { Web3Container } from '@vegaprotocol/web3';
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
import { WelcomeNoticeDialog } from '../components/welcome-notice';
@ -25,9 +24,7 @@ const DialogsContainer = () => {
<RiskNoticeDialog />
<DepositDialog />
<Web3ConnectUncontrolledDialog />
<Web3Container childrenOnly connectEagerly>
<WithdrawalDialog />
</Web3Container>
<WithdrawalDialog />
<WelcomeNoticeDialog />
</>
);

View File

@ -1,6 +1,5 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
import { Web3Container } from '@vegaprotocol/web3';
import { DepositManager } from './deposit-manager';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { enabledAssetsProvider } from '@vegaprotocol/assets';
@ -24,14 +23,12 @@ export const DepositContainer = ({
return (
<AsyncRenderer data={data} loading={loading} error={error}>
{data && data.length ? (
<Web3Container connectEagerly>
<DepositManager
assetId={assetId}
assets={data}
isFaucetable={VEGA_ENV !== Networks.MAINNET}
setDialogStyleProps={setDialogStyleProps}
/>
</Web3Container>
<DepositManager
assetId={assetId}
assets={data}
isFaucetable={VEGA_ENV !== Networks.MAINNET}
setDialogStyleProps={setDialogStyleProps}
/>
) : (
<Splash>
<p>{t('No assets on this network')}</p>

View File

@ -4,7 +4,7 @@ import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { useCallback, useState } from 'react';
import { DepositContainer } from './deposit-container';
import { useWeb3ConnectDialog } from '@vegaprotocol/web3';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
interface State {
isOpen: boolean;
@ -41,7 +41,7 @@ const DEFAULT_STYLE: DepositDialogStyleProps = {
export const DepositDialog = () => {
const { assetId, isOpen, open, close } = useDepositDialog();
const connectWalletDialogIsOpen = useWeb3ConnectDialog(
const connectWalletDialogIsOpen = useWeb3ConnectStore(
(state) => state.isOpen
);
const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE);

View File

@ -10,6 +10,8 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
jest.mock('@vegaprotocol/wallet');
jest.mock('@web3-react/core');
const mockConnector = { deactivate: jest.fn() };
function generateAsset(): AssetFieldsFragment {
return {
__typename: 'Asset',
@ -53,7 +55,11 @@ beforeEach(() => {
};
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null });
(useWeb3React as jest.Mock).mockReturnValue({ account: MOCK_ETH_ADDRESS });
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
connector: mockConnector,
});
});
describe('Deposit form', () => {
@ -192,7 +198,11 @@ describe('Deposit form', () => {
mockUseVegaWallet.mockReturnValue({ pubKey: null });
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account: undefined });
mockUseWeb3React.mockReturnValue({
account: MOCK_ETH_ADDRESS,
isActive: true,
connector: mockConnector,
});
render(
<DepositForm
@ -219,7 +229,11 @@ describe('Deposit form', () => {
const account = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account });
mockUseWeb3React.mockReturnValue({
account,
isActive: true,
connector: mockConnector,
});
const balance = new BigNumber(50);
const max = new BigNumber(20);

View File

@ -8,6 +8,7 @@ import {
minSafe,
maxSafe,
addDecimal,
useLocalStorage,
} from '@vegaprotocol/react-helpers';
import {
Button,
@ -19,13 +20,17 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import { Web3WalletInput } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
ETHEREUM_EAGER_CONNECT,
useWeb3ConnectStore,
ChainIdMap,
} from '@vegaprotocol/web3';
interface FormFields {
asset: string;
@ -137,10 +142,19 @@ export const DepositForm = ({
label={t('From (Ethereum address)')}
labelFor="ethereum-address"
>
<Web3WalletInput
inputProps={{
id: 'ethereum-address',
...register('from', { validate: { required, ethereumAddress } }),
<Input
id="ethereum-address"
{...register('from', {
validate: {
required,
ethereumAddress,
},
})}
/>
<EthereumButton
clearAddress={() => {
setValue('from', '');
clearErrors('from');
}}
/>
{errors.from?.message && (
@ -291,12 +305,38 @@ const FormButton = ({
allowance,
onApproveClick,
}: FormButtonProps) => {
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
open: store.open,
desiredChainId: store.desiredChainId,
}));
const { isActive, chainId } = useWeb3React();
const approved =
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
let button = null;
let message: ReactNode = '';
if (!selectedAsset) {
if (!isActive) {
button = (
<Button onClick={open} data-testid="connect-eth-wallet-btn">
{t('Connect Ethereum wallet')}
</Button>
);
} else if (chainId !== desiredChainId) {
console.log(chainId, desiredChainId);
const chainName = desiredChainId ? ChainIdMap[desiredChainId] : 'Unknown';
message = t(`This app only works on ${chainName}.`);
button = (
<Button
type="submit"
data-testid="deposit-submit"
variant="primary"
fill={true}
disabled={true}
>
{t('Deposit')}
</Button>
);
} else if (!selectedAsset) {
button = (
<Button
type="submit"
@ -346,19 +386,37 @@ const FormButton = ({
);
};
interface UseButtonProps {
children: ReactNode;
onClick: () => void;
}
type UseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
const UseButton = ({ children, onClick }: UseButtonProps) => {
const UseButton = (props: UseButtonProps) => {
return (
<button
{...props}
type="button"
className="ml-auto text-sm absolute top-0 right-0 underline"
onClick={onClick}
>
{children}
</button>
/>
);
};
const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
const openDialog = useWeb3ConnectStore((state) => state.open);
const { isActive, connector } = useWeb3React();
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
if (!isActive) {
return <UseButton onClick={openDialog}>{t('Connect')}</UseButton>;
}
return (
<UseButton
onClick={() => {
connector.deactivate();
clearAddress();
removeEagerConnector();
}}
data-testid="disconnect-ethereum-wallet"
>
{t('Disconnect')}
</UseButton>
);
};

View File

@ -1,5 +1,7 @@
export * from './lib/constants';
export * from './lib/ethereum-error';
export * from './lib/use-bridge-contract';
export * from './lib/use-eager-connect';
export * from './lib/use-token-contract';
export * from './lib/use-token-decimals';
export * from './lib/use-ethereum-config';
@ -7,6 +9,7 @@ export * from './lib/use-ethereum-read-contract';
export * from './lib/use-ethereum-transaction';
export * from './lib/ethereum-transaction-dialog';
export * from './lib/web3-provider';
export * from './lib/web3-connectors';
export * from './lib/web3-connect-dialog';
export * from './lib/web3-wallet-input';
export * from './lib/web3-connect-store';
export * from './lib/web3-container';

View File

@ -0,0 +1,6 @@
export const ChainIdMap: {
[id: number]: string;
} = {
11155111: 'Sepolia',
1: 'Mainnet',
};

View File

@ -0,0 +1,43 @@
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import type { Web3ReactHooks } from '@web3-react/core';
import { MetaMask } from '@web3-react/metamask';
import type { Connector } from '@web3-react/types';
import { WalletConnect } from '@web3-react/walletconnect';
import { useEffect, useRef } from 'react';
import { useWeb3ConnectStore } from './web3-connect-store';
export const ETHEREUM_EAGER_CONNECT = 'ethereum-eager-connect';
export const useEagerConnect = () => {
const connectors = useWeb3ConnectStore((store) => store.connectors);
const [eagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
const attemptedRef = useRef(false);
useEffect(() => {
if (attemptedRef.current || 'Cypress' in window) return;
const stored = getConnector(connectors, eagerConnector);
// found a valid connection option
if (stored && stored[0].connectEagerly) {
stored[0].connectEagerly();
}
attemptedRef.current = true;
}, [eagerConnector, connectors]);
};
const getConnector = (
connectors: [Connector, Web3ReactHooks][],
connectorName?: string | null
) => {
if (connectorName === 'MetaMask') {
return connectors.find(([c]) => c instanceof MetaMask);
}
if (connectorName === 'WalletConnect') {
return connectors.find(([c]) => c instanceof WalletConnect);
}
return null;
};

View File

@ -5,7 +5,9 @@ import { initializeConnector } from '@web3-react/core';
import { MetaMask } from '@web3-react/metamask';
import type { Connector } from '@web3-react/types';
const [foo, fooHooks] = initializeConnector((actions) => new MetaMask(actions));
const [foo, fooHooks] = initializeConnector((actions) => {
return new MetaMask(actions);
});
const connectors: [Connector, Web3ReactHooks][] = [[foo, fooHooks]];

View File

@ -1,34 +1,16 @@
import create from 'zustand';
import { t } from '@vegaprotocol/react-helpers';
import { t, useLocalStorage } from '@vegaprotocol/react-helpers';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { MetaMask } from '@web3-react/metamask';
import { WalletConnect } from '@web3-react/walletconnect';
import type { Web3ReactHooks } from '@web3-react/core';
import type { Connector } from '@web3-react/types';
interface State {
isOpen: boolean;
connectors: [Connector, Web3ReactHooks][];
desiredChainId?: number;
}
interface Actions {
open: (connectors: State['connectors'], desiredChainId?: number) => void;
close: () => void;
}
export const useWeb3ConnectDialog = create<State & Actions>((set) => ({
isOpen: false,
connectors: [],
open: (connectors, desiredChainId) =>
set(() => ({ isOpen: true, connectors, desiredChainId })),
close: () =>
set(() => ({ isOpen: false, connectors: [], desiredChainId: undefined })),
}));
import { ETHEREUM_EAGER_CONNECT } from './use-eager-connect';
import type { Web3ReactHooks } from '@web3-react/core';
import { useWeb3ConnectStore } from './web3-connect-store';
interface Web3ConnectDialogProps {
dialogOpen: boolean;
setDialogOpen: (isOpen: boolean) => void;
connectors: State['connectors'];
connectors: [Connector, Web3ReactHooks][];
desiredChainId?: number;
}
@ -38,6 +20,8 @@ export const Web3ConnectDialog = ({
connectors,
desiredChainId,
}: Web3ConnectDialogProps) => {
const [, setEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
return (
<Dialog
open={dialogOpen}
@ -55,6 +39,7 @@ export const Web3ConnectDialog = ({
data-testid={`web3-connector-${info.name}`}
onClick={async () => {
await connector.activate(desiredChainId);
setEagerConnector(info.name);
setDialogOpen(false);
}}
>
@ -69,17 +54,16 @@ export const Web3ConnectDialog = ({
};
export const Web3ConnectUncontrolledDialog = () => {
const { isOpen, connectors, desiredChainId, open, close } =
useWeb3ConnectDialog();
const onChange = (isOpen: boolean) =>
isOpen ? open(connectors, desiredChainId) : close();
const { isOpen, connectors, open, close, desiredChainId } =
useWeb3ConnectStore();
const onChange = (isOpen: boolean) => (isOpen ? open() : close());
return (
<Web3ConnectDialog
dialogOpen={isOpen}
setDialogOpen={onChange}
connectors={connectors}
desiredChainId={desiredChainId}
/>
);
};

View File

@ -0,0 +1,29 @@
import create from 'zustand';
import type { MetaMask } from '@web3-react/metamask';
import type { WalletConnect } from '@web3-react/walletconnect';
import type { Web3ReactHooks } from '@web3-react/core';
import type { Connector } from '@web3-react/types';
interface State {
isOpen: boolean;
connectors: [Connector, Web3ReactHooks][];
desiredChainId?: number;
}
interface Actions {
initialize: (
connectors: [MetaMask | WalletConnect, Web3ReactHooks][],
desiredChainId: number
) => void;
open: () => void;
close: () => void;
}
export const useWeb3ConnectStore = create<State & Actions>((set) => ({
isOpen: false,
connectors: [],
initialize: (connectors, desiredChainId) => {
set({ connectors, desiredChainId });
},
open: () => set(() => ({ isOpen: true })),
close: () => set(() => ({ isOpen: false })),
}));

View File

@ -6,7 +6,7 @@ import { t } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import { Web3Provider } from './web3-provider';
import { useEthereumConfig } from './use-ethereum-config';
import { useWeb3ConnectDialog } from './web3-connect-dialog';
import { useWeb3ConnectStore } from './web3-connect-store';
import { createConnectors } from './web3-connectors';
interface Web3ContainerProps {
@ -62,7 +62,7 @@ export const Web3Content = ({
connectors,
}: Web3ContentProps) => {
const { isActive, error, connector, chainId } = useWeb3React();
const openDialog = useWeb3ConnectDialog((state) => state.open);
const openDialog = useWeb3ConnectStore((state) => state.open);
useEffect(() => {
if (
@ -98,10 +98,7 @@ export const Web3Content = ({
<p data-testid="connect-eth-wallet-msg" className="mb-4">
{t('Connect your Ethereum wallet')}
</p>
<Button
onClick={() => openDialog(connectors, chainId)}
data-testid="connect-eth-wallet-btn"
>
<Button onClick={openDialog} data-testid="connect-eth-wallet-btn">
{t('Connect')}
</Button>
</SplashWrapper>

View File

@ -1,46 +0,0 @@
import type { ComponentProps } from 'react';
import { useState } from 'react';
import { useWeb3React } from '@web3-react/core';
import { t } from '@vegaprotocol/react-helpers';
import { Button, Input, Dialog } from '@vegaprotocol/ui-toolkit';
type Web3WalletInputProps = {
inputProps: Partial<
Omit<
ComponentProps<typeof Input>,
'appendIconName' | 'prependIconName' | 'appendElement' | 'prependElement'
>
>;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
export const Web3WalletInput = ({ inputProps }: Web3WalletInputProps) => {
const [isDialogOpen, setDialogOpen] = useState(false);
const { account, connector } = useWeb3React();
return (
<>
<Input
{...inputProps}
appendIconName="chevron-down"
className="cursor-pointer select-none"
onChange={noop}
onClick={() => setDialogOpen(true)}
/>
<Dialog open={isDialogOpen} onChange={setDialogOpen} size="small">
<p className="mb-2">
{t('Connected with ')}
<span className="font-mono">{account}</span>
</p>
<Button
onClick={() => connector.deactivate()}
data-testid="disconnect-ethereum-wallet"
>
{t('Disconnect Ethereum Wallet')}
</Button>
</Dialog>
</>
);
};

View File

@ -37,16 +37,18 @@ export const useWithdrawAsset = (
: new BigNumber(0);
// Query collateral bridge for threshold for selected asset
// and subsequent delay if withdrawal amount is larger than it
let threshold;
let delay;
let threshold = new BigNumber(0);
let delay = 0;
try {
const result = await Promise.all([getThreshold(asset), getDelay()]);
threshold = result[0];
delay = result[1];
update({ asset, balance, min, threshold, delay });
} catch (err) {
captureException(err);
}
update({ asset, balance, min, threshold, delay });
},
[accounts, assets, update, getThreshold, getDelay]
);

View File

@ -5,6 +5,7 @@ import {
t,
removeDecimal,
required,
useLocalStorage,
} from '@vegaprotocol/react-helpers';
import { isAssetTypeERC20 } from '@vegaprotocol/assets';
import {
@ -16,10 +17,14 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import type { ButtonHTMLAttributes } from 'react';
import { useForm, Controller, useWatch } from 'react-hook-form';
import type { WithdrawalArgs } from './use-create-withdraw';
import { WithdrawLimits } from './withdraw-limits';
import {
ETHEREUM_EAGER_CONNECT,
useWeb3ConnectStore,
} from '@vegaprotocol/web3';
interface FormFields {
asset: string;
@ -133,16 +138,12 @@ export const WithdrawForm = ({
label={t('To (Ethereum address)')}
labelFor="ethereum-address"
>
{address && (
<UseButton
onClick={() => {
setValue('to', address);
clearErrors('to');
}}
>
{t('Use connected')}
</UseButton>
)}
<EthereumButton
clearAddress={() => {
setValue('to', '');
clearErrors('to');
}}
/>
<Input
id="ethereum-address"
data-testid="eth-address-input"
@ -198,7 +199,12 @@ export const WithdrawForm = ({
</UseButton>
)}
</FormGroup>
<Button data-testid="submit-withdrawal" type="submit" variant="primary">
<Button
data-testid="submit-withdrawal"
type="submit"
variant="primary"
fill={true}
>
Release funds
</Button>
</form>
@ -206,18 +212,41 @@ export const WithdrawForm = ({
);
};
interface UseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
}
type UseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
const UseButton = ({ children, ...rest }: UseButtonProps) => {
const UseButton = (props: UseButtonProps) => {
return (
<button
{...rest}
{...props}
type="button"
className="ml-auto text-sm absolute top-0 right-0 underline"
>
{children}
</button>
/>
);
};
const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
const openDialog = useWeb3ConnectStore((state) => state.open);
const { isActive, connector } = useWeb3React();
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
if (!isActive) {
return (
<UseButton onClick={openDialog} data-testid="connect-eth-wallet-btn">
{t('Connect')}
</UseButton>
);
}
return (
<UseButton
onClick={() => {
connector.deactivate();
clearAddress();
removeEagerConnector();
}}
data-testid="disconnect-ethereum-wallet"
>
{t('Disconnect')}
</UseButton>
);
};

View File

@ -6,8 +6,7 @@ import { useCompleteWithdraw } from './use-complete-withdraw';
import { useCreateWithdraw } from './use-create-withdraw';
import { WithdrawFormContainer } from './withdraw-form-container';
import { WithdrawalFeedback } from './withdrawal-feedback';
import { Web3Container } from '@vegaprotocol/web3';
import { useWeb3ConnectDialog } from '@vegaprotocol/web3';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
interface State {
isOpen: boolean;
assetId?: string;
@ -30,7 +29,7 @@ export const WithdrawalDialog = () => {
const { pubKey } = useVegaWallet();
const createWithdraw = useCreateWithdraw();
const completeWithdraw = useCompleteWithdraw();
const connectWalletDialogIsOpen = useWeb3ConnectDialog(
const connectWalletDialogIsOpen = useWeb3ConnectStore(
(state) => state.isOpen
);
return (
@ -41,16 +40,14 @@ export const WithdrawalDialog = () => {
onChange={(isOpen) => (isOpen ? open() : close())}
size="small"
>
<Web3Container connectEagerly>
<WithdrawFormContainer
assetId={assetId}
partyId={pubKey ? pubKey : undefined}
submit={(args) => {
close();
createWithdraw.submit(args);
}}
/>
</Web3Container>
<WithdrawFormContainer
assetId={assetId}
partyId={pubKey ? pubKey : undefined}
submit={(args) => {
close();
createWithdraw.submit(args);
}}
/>
</Dialog>
<createWithdraw.Dialog
content={{

View File

@ -10,6 +10,8 @@ import {
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { ChainIdMap, useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import { formatDistanceToNow } from 'date-fns';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
@ -75,17 +77,7 @@ export const WithdrawalFeedback = ({
</KeyValueTable>
)}
{isAvailable ? (
<Button
disabled={withdrawal === null ? true : false}
data-testid="withdraw-funds"
onClick={() => {
if (withdrawal) {
submitWithdraw(withdrawal.id);
}
}}
>
{t('Withdraw funds')}
</Button>
<ActionButton withdrawal={withdrawal} submitWithdraw={submitWithdraw} />
) : (
<p className="text-danger">
{t(
@ -98,3 +90,51 @@ export const WithdrawalFeedback = ({
</div>
);
};
const ActionButton = ({
withdrawal,
submitWithdraw,
}: {
withdrawal: WithdrawalFieldsFragment | null;
submitWithdraw: (withdrawalId: string) => void;
}) => {
const { isActive, chainId } = useWeb3React();
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
open: store.open,
desiredChainId: store.desiredChainId,
}));
if (!isActive) {
return (
<Button onClick={() => open()}>
{t('Connect Ethereum wallet to complete')}
</Button>
);
}
if (chainId !== desiredChainId) {
const chainName = desiredChainId ? ChainIdMap[desiredChainId] : 'Unknown';
return (
<>
<p className="text-danger mb-2">
{t(`This app only works on ${chainName}. Please change chain.`)}
</p>
<Button disabled={true}>{t('Withdraw funds')}</Button>
</>
);
}
return (
<Button
disabled={withdrawal === null ? true : false}
data-testid="withdraw-funds"
onClick={() => {
if (withdrawal) {
submitWithdraw(withdrawal.id);
}
}}
>
{t('Withdraw funds')}
</Button>
);
};