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

View File

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

View File

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

View File

@ -7,51 +7,48 @@ import {
} from '@vegaprotocol/withdraws'; } from '@vegaprotocol/withdraws';
import { t } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers';
import { VegaWalletContainer } from '../../components/vega-wallet-container'; import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { Web3Container } from '@vegaprotocol/web3';
export const WithdrawalsContainer = () => { export const WithdrawalsContainer = () => {
const { pending, completed, loading, error } = useWithdrawals(); const { pending, completed, loading, error } = useWithdrawals();
const openWithdrawDialog = useWithdrawalDialog((state) => state.open); const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
return ( return (
<Web3Container> <VegaWalletContainer>
<VegaWalletContainer> <div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full relative grid grid-rows-[1fr,min-content]"> <div className="h-full">
<div className="h-full"> <AsyncRenderer
<AsyncRenderer data={{ pending, completed }}
data={{ pending, completed }} loading={loading}
loading={loading} error={error}
error={error} render={({ pending, completed }) => (
render={({ pending, completed }) => ( <>
<> {pending && pending.length > 0 && (
{pending && pending.length > 0 && ( <>
<> <h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4>
<h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4> <PendingWithdrawalsTable rowData={pending} />
<PendingWithdrawalsTable rowData={pending} /> </>
</> )}
)} {completed && completed.length > 0 && (
{completed && completed.length > 0 && ( <h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4> )}
)} <WithdrawalsTable
<WithdrawalsTable data-testid="withdrawals-history"
data-testid="withdrawals-history" rowData={completed}
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>
</div> </div>
</VegaWalletContainer> <div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
</Web3Container> <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 type { ReactNode } from 'react';
import { useMemo } from 'react'; import { useEffect } from 'react';
import { useEagerConnect } from '@vegaprotocol/wallet'; import { NetworkLoader, useEnvironment } from '@vegaprotocol/environment';
import { NetworkLoader } from '@vegaprotocol/environment';
import { Connectors } from '../../lib/vega-connectors';
import type { InMemoryCacheConfig } from '@apollo/client'; import type { InMemoryCacheConfig } from '@apollo/client';
import {
useEthereumConfig,
createConnectors,
Web3Provider as Web3ProviderInternal,
useWeb3ConnectStore,
} from '@vegaprotocol/web3';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
interface AppLoaderProps { interface AppLoaderProps {
children: ReactNode; children: ReactNode;
@ -14,57 +19,87 @@ interface AppLoaderProps {
* that must happen for it can be used * that must happen for it can be used
*/ */
export function AppLoader({ children }: AppLoaderProps) { export function AppLoader({ children }: AppLoaderProps) {
// Get keys from vega wallet immediately return <NetworkLoader cache={cacheConfig}>{children}</NetworkLoader>;
useEagerConnect(Connectors); }
const cache: InMemoryCacheConfig = useMemo(
() => ({ export const Web3Provider = ({ children }: { children: ReactNode }) => {
typePolicies: { const { config, loading, error } = useEthereumConfig();
Account: { const { ETHEREUM_PROVIDER_URL } = useEnvironment();
keyFields: false, const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
fields: { store.connectors,
balanceFormatted: {}, store.initialize,
}, ]);
},
Instrument: { useEffect(() => {
keyFields: false, if (config?.chain_id) {
}, return initializeConnectors(
TradableInstrument: { createConnectors(ETHEREUM_PROVIDER_URL, Number(config?.chain_id)),
keyFields: ['instrument'], Number(config.chain_id)
}, );
Product: { }
keyFields: ['settlementAsset', ['id']], }, [config?.chain_id, ETHEREUM_PROVIDER_URL, initializeConnectors]);
},
MarketData: { return (
keyFields: ['market', ['id']], <AsyncRenderer
}, loading={loading}
Node: { error={error}
keyFields: false, data={connectors}
}, noDataCondition={(d) => {
Withdrawal: { if (!d) return true;
fields: { return d.length < 1;
pendingOnForeignChain: { }}
read: (isPending = false) => isPending, >
}, <Web3ProviderInternal connectors={connectors}>
}, <>{children}</>
}, </Web3ProviderInternal>
ERC20: { </AsyncRenderer>
keyFields: ['contractAddress'], );
}, };
PositionUpdate: {
keyFields: false, const cacheConfig: InMemoryCacheConfig = {
}, typePolicies: {
AccountUpdate: { Account: {
keyFields: false, keyFields: false,
}, fields: {
Party: { balanceFormatted: {},
keyFields: false, },
}, },
Fees: { Instrument: {
keyFields: false, 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'],
return <NetworkLoader cache={cache}>{children}</NetworkLoader>; },
} 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 type { AppProps } from 'next/app';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers'; import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { VegaWalletProvider } from '@vegaprotocol/wallet'; import {
useEagerConnect as useVegaEagerConnect,
VegaWalletProvider,
} from '@vegaprotocol/wallet';
import { import {
EnvironmentProvider, EnvironmentProvider,
envTriggerMapping, envTriggerMapping,
Networks, Networks,
useEnvironment, useEnvironment,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import { AppLoader } from '../components/app-loader'; import { AppLoader, Web3Provider } from '../components/app-loader';
import './styles.css'; import './styles.css';
import { usePageTitleStore } from '../stores'; import { usePageTitleStore } from '../stores';
import { Footer } from '../components/footer'; import { Footer } from '../components/footer';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import DialogsContainer from './dialogs-container'; import DialogsContainer from './dialogs-container';
import { HashRouter, useLocation } from 'react-router-dom'; 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!'); 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" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</Head> </Head>
<Title /> <Title />
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]"> <VegaWalletProvider>
<AppLoader> <AppLoader>
<Navbar <Web3Provider>
theme={theme} <div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
toggleTheme={toggleTheme} <Navbar
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} theme={theme}
/> toggleTheme={toggleTheme}
<main data-testid={location.pathname}> navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
<Component /> />
</main> <main data-testid={location.pathname}>
<Footer /> <Component />
<DialogsContainer /> </main>
<Footer />
<DialogsContainer />
<MaybeConnectEagerly />
</div>
</Web3Provider>
</AppLoader> </AppLoader>
</div> </VegaWalletProvider>
</ThemeContext.Provider> </ThemeContext.Provider>
); );
} }
@ -85,12 +95,16 @@ function VegaTradingApp(props: AppProps) {
return ( return (
<HashRouter> <HashRouter>
<EnvironmentProvider> <EnvironmentProvider>
<VegaWalletProvider> <AppBody {...props} />
<AppBody {...props} />
</VegaWalletProvider>
</EnvironmentProvider> </EnvironmentProvider>
</HashRouter> </HashRouter>
); );
} }
export default VegaTradingApp; 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 { RiskNoticeDialog } from '../components/risk-notice-dialog';
import { WithdrawalDialog } from '@vegaprotocol/withdraws'; import { WithdrawalDialog } from '@vegaprotocol/withdraws';
import { DepositDialog } from '@vegaprotocol/deposits'; import { DepositDialog } from '@vegaprotocol/deposits';
import { Web3Container } from '@vegaprotocol/web3';
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3'; import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
import { WelcomeNoticeDialog } from '../components/welcome-notice'; import { WelcomeNoticeDialog } from '../components/welcome-notice';
@ -25,9 +24,7 @@ const DialogsContainer = () => {
<RiskNoticeDialog /> <RiskNoticeDialog />
<DepositDialog /> <DepositDialog />
<Web3ConnectUncontrolledDialog /> <Web3ConnectUncontrolledDialog />
<Web3Container childrenOnly connectEagerly> <WithdrawalDialog />
<WithdrawalDialog />
</Web3Container>
<WelcomeNoticeDialog /> <WelcomeNoticeDialog />
</> </>
); );

View File

@ -1,6 +1,5 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment'; import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
import { Web3Container } from '@vegaprotocol/web3';
import { DepositManager } from './deposit-manager'; import { DepositManager } from './deposit-manager';
import { t, useDataProvider } from '@vegaprotocol/react-helpers'; import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { enabledAssetsProvider } from '@vegaprotocol/assets'; import { enabledAssetsProvider } from '@vegaprotocol/assets';
@ -24,14 +23,12 @@ export const DepositContainer = ({
return ( return (
<AsyncRenderer data={data} loading={loading} error={error}> <AsyncRenderer data={data} loading={loading} error={error}>
{data && data.length ? ( {data && data.length ? (
<Web3Container connectEagerly> <DepositManager
<DepositManager assetId={assetId}
assetId={assetId} assets={data}
assets={data} isFaucetable={VEGA_ENV !== Networks.MAINNET}
isFaucetable={VEGA_ENV !== Networks.MAINNET} setDialogStyleProps={setDialogStyleProps}
setDialogStyleProps={setDialogStyleProps} />
/>
</Web3Container>
) : ( ) : (
<Splash> <Splash>
<p>{t('No assets on this network')}</p> <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 { Dialog } from '@vegaprotocol/ui-toolkit';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { DepositContainer } from './deposit-container'; import { DepositContainer } from './deposit-container';
import { useWeb3ConnectDialog } from '@vegaprotocol/web3'; import { useWeb3ConnectStore } from '@vegaprotocol/web3';
interface State { interface State {
isOpen: boolean; isOpen: boolean;
@ -41,7 +41,7 @@ const DEFAULT_STYLE: DepositDialogStyleProps = {
export const DepositDialog = () => { export const DepositDialog = () => {
const { assetId, isOpen, open, close } = useDepositDialog(); const { assetId, isOpen, open, close } = useDepositDialog();
const connectWalletDialogIsOpen = useWeb3ConnectDialog( const connectWalletDialogIsOpen = useWeb3ConnectStore(
(state) => state.isOpen (state) => state.isOpen
); );
const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE); const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE);

View File

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

View File

@ -8,6 +8,7 @@ import {
minSafe, minSafe,
maxSafe, maxSafe,
addDecimal, addDecimal,
useLocalStorage,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { import {
Button, Button,
@ -19,13 +20,17 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import { Web3WalletInput } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react'; import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form'; import { Controller, useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits'; import { DepositLimits } from './deposit-limits';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
ETHEREUM_EAGER_CONNECT,
useWeb3ConnectStore,
ChainIdMap,
} from '@vegaprotocol/web3';
interface FormFields { interface FormFields {
asset: string; asset: string;
@ -137,10 +142,19 @@ export const DepositForm = ({
label={t('From (Ethereum address)')} label={t('From (Ethereum address)')}
labelFor="ethereum-address" labelFor="ethereum-address"
> >
<Web3WalletInput <Input
inputProps={{ id="ethereum-address"
id: 'ethereum-address', {...register('from', {
...register('from', { validate: { required, ethereumAddress } }), validate: {
required,
ethereumAddress,
},
})}
/>
<EthereumButton
clearAddress={() => {
setValue('from', '');
clearErrors('from');
}} }}
/> />
{errors.from?.message && ( {errors.from?.message && (
@ -291,12 +305,38 @@ const FormButton = ({
allowance, allowance,
onApproveClick, onApproveClick,
}: FormButtonProps) => { }: FormButtonProps) => {
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
open: store.open,
desiredChainId: store.desiredChainId,
}));
const { isActive, chainId } = useWeb3React();
const approved = const approved =
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance); allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
let button = null; let button = null;
let message: ReactNode = ''; 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 = (
<Button <Button
type="submit" type="submit"
@ -346,19 +386,37 @@ const FormButton = ({
); );
}; };
interface UseButtonProps { type UseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
children: ReactNode;
onClick: () => void;
}
const UseButton = ({ children, onClick }: UseButtonProps) => { const UseButton = (props: UseButtonProps) => {
return ( return (
<button <button
{...props}
type="button" type="button"
className="ml-auto text-sm absolute top-0 right-0 underline" 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/ethereum-error';
export * from './lib/use-bridge-contract'; export * from './lib/use-bridge-contract';
export * from './lib/use-eager-connect';
export * from './lib/use-token-contract'; export * from './lib/use-token-contract';
export * from './lib/use-token-decimals'; export * from './lib/use-token-decimals';
export * from './lib/use-ethereum-config'; 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/use-ethereum-transaction';
export * from './lib/ethereum-transaction-dialog'; export * from './lib/ethereum-transaction-dialog';
export * from './lib/web3-provider'; export * from './lib/web3-provider';
export * from './lib/web3-connectors';
export * from './lib/web3-connect-dialog'; export * from './lib/web3-connect-dialog';
export * from './lib/web3-wallet-input'; export * from './lib/web3-connect-store';
export * from './lib/web3-container'; 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 { MetaMask } from '@web3-react/metamask';
import type { Connector } from '@web3-react/types'; 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]]; const connectors: [Connector, Web3ReactHooks][] = [[foo, fooHooks]];

View File

@ -1,34 +1,16 @@
import create from 'zustand'; import { t, useLocalStorage } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit'; import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { MetaMask } from '@web3-react/metamask'; import { MetaMask } from '@web3-react/metamask';
import { WalletConnect } from '@web3-react/walletconnect'; import { WalletConnect } from '@web3-react/walletconnect';
import type { Web3ReactHooks } from '@web3-react/core';
import type { Connector } from '@web3-react/types'; import type { Connector } from '@web3-react/types';
import { ETHEREUM_EAGER_CONNECT } from './use-eager-connect';
interface State { import type { Web3ReactHooks } from '@web3-react/core';
isOpen: boolean; import { useWeb3ConnectStore } from './web3-connect-store';
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 })),
}));
interface Web3ConnectDialogProps { interface Web3ConnectDialogProps {
dialogOpen: boolean; dialogOpen: boolean;
setDialogOpen: (isOpen: boolean) => void; setDialogOpen: (isOpen: boolean) => void;
connectors: State['connectors']; connectors: [Connector, Web3ReactHooks][];
desiredChainId?: number; desiredChainId?: number;
} }
@ -38,6 +20,8 @@ export const Web3ConnectDialog = ({
connectors, connectors,
desiredChainId, desiredChainId,
}: Web3ConnectDialogProps) => { }: Web3ConnectDialogProps) => {
const [, setEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
return ( return (
<Dialog <Dialog
open={dialogOpen} open={dialogOpen}
@ -55,6 +39,7 @@ export const Web3ConnectDialog = ({
data-testid={`web3-connector-${info.name}`} data-testid={`web3-connector-${info.name}`}
onClick={async () => { onClick={async () => {
await connector.activate(desiredChainId); await connector.activate(desiredChainId);
setEagerConnector(info.name);
setDialogOpen(false); setDialogOpen(false);
}} }}
> >
@ -69,17 +54,16 @@ export const Web3ConnectDialog = ({
}; };
export const Web3ConnectUncontrolledDialog = () => { export const Web3ConnectUncontrolledDialog = () => {
const { isOpen, connectors, desiredChainId, open, close } = const { isOpen, connectors, open, close, desiredChainId } =
useWeb3ConnectDialog(); useWeb3ConnectStore();
const onChange = (isOpen: boolean) => (isOpen ? open() : close());
const onChange = (isOpen: boolean) =>
isOpen ? open(connectors, desiredChainId) : close();
return ( return (
<Web3ConnectDialog <Web3ConnectDialog
dialogOpen={isOpen} dialogOpen={isOpen}
setDialogOpen={onChange} setDialogOpen={onChange}
connectors={connectors} 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 { useEnvironment } from '@vegaprotocol/environment';
import { Web3Provider } from './web3-provider'; import { Web3Provider } from './web3-provider';
import { useEthereumConfig } from './use-ethereum-config'; import { useEthereumConfig } from './use-ethereum-config';
import { useWeb3ConnectDialog } from './web3-connect-dialog'; import { useWeb3ConnectStore } from './web3-connect-store';
import { createConnectors } from './web3-connectors'; import { createConnectors } from './web3-connectors';
interface Web3ContainerProps { interface Web3ContainerProps {
@ -62,7 +62,7 @@ export const Web3Content = ({
connectors, connectors,
}: Web3ContentProps) => { }: Web3ContentProps) => {
const { isActive, error, connector, chainId } = useWeb3React(); const { isActive, error, connector, chainId } = useWeb3React();
const openDialog = useWeb3ConnectDialog((state) => state.open); const openDialog = useWeb3ConnectStore((state) => state.open);
useEffect(() => { useEffect(() => {
if ( if (
@ -98,10 +98,7 @@ export const Web3Content = ({
<p data-testid="connect-eth-wallet-msg" className="mb-4"> <p data-testid="connect-eth-wallet-msg" className="mb-4">
{t('Connect your Ethereum wallet')} {t('Connect your Ethereum wallet')}
</p> </p>
<Button <Button onClick={openDialog} data-testid="connect-eth-wallet-btn">
onClick={() => openDialog(connectors, chainId)}
data-testid="connect-eth-wallet-btn"
>
{t('Connect')} {t('Connect')}
</Button> </Button>
</SplashWrapper> </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); : new BigNumber(0);
// Query collateral bridge for threshold for selected asset // Query collateral bridge for threshold for selected asset
// and subsequent delay if withdrawal amount is larger than it // and subsequent delay if withdrawal amount is larger than it
let threshold; let threshold = new BigNumber(0);
let delay; let delay = 0;
try { try {
const result = await Promise.all([getThreshold(asset), getDelay()]); const result = await Promise.all([getThreshold(asset), getDelay()]);
threshold = result[0]; threshold = result[0];
delay = result[1]; delay = result[1];
update({ asset, balance, min, threshold, delay });
} catch (err) { } catch (err) {
captureException(err); captureException(err);
} }
update({ asset, balance, min, threshold, delay });
}, },
[accounts, assets, update, getThreshold, getDelay] [accounts, assets, update, getThreshold, getDelay]
); );

View File

@ -5,6 +5,7 @@ import {
t, t,
removeDecimal, removeDecimal,
required, required,
useLocalStorage,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { isAssetTypeERC20 } from '@vegaprotocol/assets'; import { isAssetTypeERC20 } from '@vegaprotocol/assets';
import { import {
@ -16,10 +17,14 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js'; 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 { useForm, Controller, useWatch } from 'react-hook-form';
import type { WithdrawalArgs } from './use-create-withdraw'; import type { WithdrawalArgs } from './use-create-withdraw';
import { WithdrawLimits } from './withdraw-limits'; import { WithdrawLimits } from './withdraw-limits';
import {
ETHEREUM_EAGER_CONNECT,
useWeb3ConnectStore,
} from '@vegaprotocol/web3';
interface FormFields { interface FormFields {
asset: string; asset: string;
@ -133,16 +138,12 @@ export const WithdrawForm = ({
label={t('To (Ethereum address)')} label={t('To (Ethereum address)')}
labelFor="ethereum-address" labelFor="ethereum-address"
> >
{address && ( <EthereumButton
<UseButton clearAddress={() => {
onClick={() => { setValue('to', '');
setValue('to', address); clearErrors('to');
clearErrors('to'); }}
}} />
>
{t('Use connected')}
</UseButton>
)}
<Input <Input
id="ethereum-address" id="ethereum-address"
data-testid="eth-address-input" data-testid="eth-address-input"
@ -198,7 +199,12 @@ export const WithdrawForm = ({
</UseButton> </UseButton>
)} )}
</FormGroup> </FormGroup>
<Button data-testid="submit-withdrawal" type="submit" variant="primary"> <Button
data-testid="submit-withdrawal"
type="submit"
variant="primary"
fill={true}
>
Release funds Release funds
</Button> </Button>
</form> </form>
@ -206,18 +212,41 @@ export const WithdrawForm = ({
); );
}; };
interface UseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { type UseButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
children: ReactNode;
}
const UseButton = ({ children, ...rest }: UseButtonProps) => { const UseButton = (props: UseButtonProps) => {
return ( return (
<button <button
{...rest} {...props}
type="button" type="button"
className="ml-auto text-sm absolute top-0 right-0 underline" 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 { useCreateWithdraw } from './use-create-withdraw';
import { WithdrawFormContainer } from './withdraw-form-container'; import { WithdrawFormContainer } from './withdraw-form-container';
import { WithdrawalFeedback } from './withdrawal-feedback'; import { WithdrawalFeedback } from './withdrawal-feedback';
import { Web3Container } from '@vegaprotocol/web3'; import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3ConnectDialog } from '@vegaprotocol/web3';
interface State { interface State {
isOpen: boolean; isOpen: boolean;
assetId?: string; assetId?: string;
@ -30,7 +29,7 @@ export const WithdrawalDialog = () => {
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const createWithdraw = useCreateWithdraw(); const createWithdraw = useCreateWithdraw();
const completeWithdraw = useCompleteWithdraw(); const completeWithdraw = useCompleteWithdraw();
const connectWalletDialogIsOpen = useWeb3ConnectDialog( const connectWalletDialogIsOpen = useWeb3ConnectStore(
(state) => state.isOpen (state) => state.isOpen
); );
return ( return (
@ -41,16 +40,14 @@ export const WithdrawalDialog = () => {
onChange={(isOpen) => (isOpen ? open() : close())} onChange={(isOpen) => (isOpen ? open() : close())}
size="small" size="small"
> >
<Web3Container connectEagerly> <WithdrawFormContainer
<WithdrawFormContainer assetId={assetId}
assetId={assetId} partyId={pubKey ? pubKey : undefined}
partyId={pubKey ? pubKey : undefined} submit={(args) => {
submit={(args) => { close();
close(); createWithdraw.submit(args);
createWithdraw.submit(args); }}
}} />
/>
</Web3Container>
</Dialog> </Dialog>
<createWithdraw.Dialog <createWithdraw.Dialog
content={{ content={{

View File

@ -10,6 +10,8 @@ import {
KeyValueTableRow, KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { VegaTxState } from '@vegaprotocol/wallet'; import type { VegaTxState } from '@vegaprotocol/wallet';
import { ChainIdMap, useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal'; import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
@ -75,17 +77,7 @@ export const WithdrawalFeedback = ({
</KeyValueTable> </KeyValueTable>
)} )}
{isAvailable ? ( {isAvailable ? (
<Button <ActionButton withdrawal={withdrawal} submitWithdraw={submitWithdraw} />
disabled={withdrawal === null ? true : false}
data-testid="withdraw-funds"
onClick={() => {
if (withdrawal) {
submitWithdraw(withdrawal.id);
}
}}
>
{t('Withdraw funds')}
</Button>
) : ( ) : (
<p className="text-danger"> <p className="text-danger">
{t( {t(
@ -98,3 +90,51 @@ export const WithdrawalFeedback = ({
</div> </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>
);
};