feat(wallet): new browser wallet connection model (#5572)
Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
parent
3dab5f3d9b
commit
39907f07db
@ -26,7 +26,7 @@ import {
|
||||
} from '@vegaprotocol/web3';
|
||||
import { Web3Provider } from '@vegaprotocol/web3';
|
||||
import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
|
||||
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletProvider, useChainId } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useVegaTransactionManager,
|
||||
useVegaTransactionUpdater,
|
||||
@ -96,7 +96,9 @@ const cache: InMemoryCacheConfig = {
|
||||
const Web3Container = ({
|
||||
chainId,
|
||||
}: {
|
||||
/** Ethereum chain id */
|
||||
chainId: number;
|
||||
/** Ethereum provider url */
|
||||
providerUrl: string;
|
||||
}) => {
|
||||
const InitializeHandlers = () => {
|
||||
@ -123,6 +125,9 @@ const Web3Container = ({
|
||||
MOZILLA_EXTENSION_URL,
|
||||
VEGA_WALLET_URL,
|
||||
} = useEnvironment();
|
||||
|
||||
const vegaChainId = useChainId(VEGA_URL);
|
||||
|
||||
useEffect(() => {
|
||||
if (chainId) {
|
||||
return initializeConnectors(
|
||||
@ -157,7 +162,8 @@ const Web3Container = ({
|
||||
!VEGA_EXPLORER_URL ||
|
||||
!DocsLinks ||
|
||||
!CHROME_EXTENSION_URL ||
|
||||
!MOZILLA_EXTENSION_URL
|
||||
!MOZILLA_EXTENSION_URL ||
|
||||
!vegaChainId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@ -169,6 +175,7 @@ const Web3Container = ({
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
chainId: vegaChainId,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
|
@ -48,6 +48,7 @@ const vegaWalletConfig: VegaWalletConfig = {
|
||||
chromeExtensionUrl: 'chrome',
|
||||
mozillaExtensionUrl: 'mozilla',
|
||||
},
|
||||
chainId: 'VEGA_CHAIN_ID',
|
||||
};
|
||||
|
||||
const renderComponent = (proposal: ProposalQuery['proposal']) => {
|
||||
|
@ -1,14 +1,50 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Intent,
|
||||
ToastsContainer,
|
||||
TradingButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
useToasts,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useEthereumTransactionToasts,
|
||||
useEthereumWithdrawApprovalsToasts,
|
||||
useVegaTransactionToasts,
|
||||
useWalletDisconnectToastActions,
|
||||
useWalletDisconnectedToasts,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const WalletDisconnectAdditionalContent = () => {
|
||||
const { t } = useTranslation();
|
||||
const { hideToast } = useWalletDisconnectToastActions();
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
return (
|
||||
<p className="mt-2">
|
||||
<TradingButton
|
||||
data-testid="connect-vega-wallet"
|
||||
onClick={() => {
|
||||
hideToast();
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
size="small"
|
||||
intent={Intent.Danger}
|
||||
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
|
||||
>
|
||||
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
|
||||
</TradingButton>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
|
||||
|
||||
const toasts = useToasts((store) => store.toasts);
|
||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||
|
@ -13,6 +13,7 @@ import type { ReactNode } from 'react';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { DataLoader } from './data-loader';
|
||||
import { useChainId } from '@vegaprotocol/wallet';
|
||||
|
||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
const t = useT();
|
||||
@ -26,13 +27,16 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
CHROME_EXTENSION_URL,
|
||||
} = useEnvironment();
|
||||
|
||||
const chainId = useChainId(VEGA_URL);
|
||||
|
||||
if (
|
||||
!VEGA_URL ||
|
||||
!VEGA_WALLET_URL ||
|
||||
!VEGA_EXPLORER_URL ||
|
||||
!CHROME_EXTENSION_URL ||
|
||||
!MOZILLA_EXTENSION_URL ||
|
||||
!DocsLinks
|
||||
!DocsLinks ||
|
||||
!chainId
|
||||
) {
|
||||
return <AppLoader />;
|
||||
}
|
||||
@ -72,6 +76,7 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
chainId,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
|
@ -1,62 +0,0 @@
|
||||
import {
|
||||
Intent,
|
||||
useToasts,
|
||||
ToastHeading,
|
||||
CLOSE_AFTER,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useT } from '../use-t';
|
||||
import { VegaWalletConnectButton } from '../../components/vega-wallet-connect-button';
|
||||
|
||||
const WALLET_DISCONNECTED_TOAST_ID = 'WALLET_DISCONNECTED_TOAST_ID';
|
||||
|
||||
export const useWalletDisconnectedToasts = () => {
|
||||
const t = useT();
|
||||
const [hasToast, setToast, updateToast] = useToasts((state) => [
|
||||
state.hasToast,
|
||||
state.setToast,
|
||||
state.update,
|
||||
]);
|
||||
const { isAlive } = useVegaWallet();
|
||||
|
||||
const toast = useMemo(
|
||||
() => ({
|
||||
id: WALLET_DISCONNECTED_TOAST_ID,
|
||||
intent: Intent.Danger,
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>{t('Wallet connection lost')}</ToastHeading>
|
||||
<p>{t('The connection to the Vega wallet has been lost.')}</p>
|
||||
<p className="mt-2">
|
||||
<VegaWalletConnectButton
|
||||
intent={Intent.Danger}
|
||||
onClick={() => {
|
||||
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
|
||||
hidden: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
onClose: () => {
|
||||
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
|
||||
hidden: true,
|
||||
});
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
}),
|
||||
[t, updateToast]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAlive === false) {
|
||||
if (hasToast(WALLET_DISCONNECTED_TOAST_ID)) {
|
||||
updateToast(WALLET_DISCONNECTED_TOAST_ID, { hidden: false });
|
||||
} else {
|
||||
setToast(toast);
|
||||
}
|
||||
}
|
||||
}, [hasToast, isAlive, setToast, t, toast, updateToast]);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useProposalToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
@ -6,7 +6,26 @@ import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
import { useReferralToasts } from '../client-pages/referrals/hooks/use-referral-toasts';
|
||||
import { useWalletDisconnectedToasts } from '../lib/hooks/use-wallet-disconnected-toasts';
|
||||
import {
|
||||
useWalletDisconnectToastActions,
|
||||
useWalletDisconnectedToasts,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button';
|
||||
|
||||
const WalletDisconnectAdditionalContent = () => {
|
||||
const { hideToast } = useWalletDisconnectToastActions();
|
||||
return (
|
||||
<p className="mt-2">
|
||||
<VegaWalletConnectButton
|
||||
intent={Intent.Danger}
|
||||
onClick={() => {
|
||||
// hide toast when clicked on `Connect`
|
||||
hideToast();
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useProposalToasts();
|
||||
@ -17,7 +36,7 @@ export const ToastsManager = () => {
|
||||
withdrawalsLink: Links.PORTFOLIO(),
|
||||
});
|
||||
useReferralToasts();
|
||||
useWalletDisconnectedToasts();
|
||||
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
|
||||
|
||||
const toasts = useToasts((store) => store.toasts);
|
||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||
|
@ -22,7 +22,7 @@ const mockUpdateDialogOpen = jest.fn();
|
||||
const mockCloseVegaDialog = jest.fn();
|
||||
|
||||
let mockIsDesktopRunning = true;
|
||||
const mockChainId = 'chain-id';
|
||||
const mockChainId = 'VEGA_CHAIN_ID';
|
||||
|
||||
jest.mock('../use-is-wallet-service-running', () => ({
|
||||
useIsWalletServiceRunning: jest
|
||||
@ -30,10 +30,6 @@ jest.mock('../use-is-wallet-service-running', () => ({
|
||||
.mockImplementation(() => mockIsDesktopRunning),
|
||||
}));
|
||||
|
||||
jest.mock('./use-chain-id', () => ({
|
||||
useChainId: jest.fn().mockImplementation(() => mockChainId),
|
||||
}));
|
||||
|
||||
let defaultProps: VegaConnectDialogProps;
|
||||
|
||||
const INITIAL_KEY = 'some-key';
|
||||
@ -71,6 +67,7 @@ const defaultConfig: VegaWalletConfig = {
|
||||
chromeExtensionUrl: 'chrome-link',
|
||||
mozillaExtensionUrl: 'mozilla-link',
|
||||
},
|
||||
chainId: 'VEGA_CHAIN_ID',
|
||||
};
|
||||
|
||||
function generateJSX(
|
||||
@ -214,7 +211,12 @@ describe('VegaConnectDialog', () => {
|
||||
.mockClear()
|
||||
.mockImplementation(() =>
|
||||
delayedReject(
|
||||
new WalletError('User error', 3001, 'The user rejected the request')
|
||||
new WalletError(
|
||||
'User error',
|
||||
3001,
|
||||
'The user rejected the request'
|
||||
),
|
||||
delay
|
||||
)
|
||||
);
|
||||
|
||||
@ -323,13 +325,6 @@ describe('VegaConnectDialog', () => {
|
||||
render(generateJSX());
|
||||
await selectInjected();
|
||||
|
||||
// Chain check
|
||||
expect(screen.getByText('Verifying chain')).toBeInTheDocument();
|
||||
expect(vegaWindow.getChainId).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(delay);
|
||||
});
|
||||
|
||||
// Await user connect
|
||||
expect(screen.getByText('Connecting...')).toBeInTheDocument();
|
||||
expect(vegaWindow.connectWallet).toHaveBeenCalled();
|
||||
@ -350,43 +345,6 @@ describe('VegaConnectDialog', () => {
|
||||
expect(mockCloseVegaDialog).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('handles invalid chain', async () => {
|
||||
const delay = 100;
|
||||
const invalidChain = 'invalid chain';
|
||||
const vegaWindow = {
|
||||
getChainId: jest.fn(() =>
|
||||
delayedResolve({ chainID: invalidChain }, delay)
|
||||
),
|
||||
connectWallet: jest.fn(() => delayedResolve(null, delay)),
|
||||
disconnectWallet: jest.fn(() => delayedResolve(undefined, delay)),
|
||||
listKeys: jest.fn(() =>
|
||||
delayedResolve(
|
||||
{
|
||||
keys: [{ name: 'test key', publicKey: '0x123' }],
|
||||
},
|
||||
100
|
||||
)
|
||||
),
|
||||
};
|
||||
mockBrowserWallet(vegaWindow);
|
||||
render(generateJSX());
|
||||
await selectInjected();
|
||||
|
||||
// Chain check
|
||||
expect(screen.getByText('Verifying chain')).toBeInTheDocument();
|
||||
expect(vegaWindow.getChainId).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(delay);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Wrong network')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
new RegExp(`set your wallet network in your app to "${mockChainId}"`)
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
async function selectInjected() {
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
fireEvent.click(await screen.findByTestId('connector-injected'));
|
||||
|
@ -44,7 +44,6 @@ import { isBrowserWalletInstalled } from '../utils';
|
||||
import { useIsWalletServiceRunning } from '../use-is-wallet-service-running';
|
||||
import { SnapStatus, useSnapStatus } from '../use-snap-status';
|
||||
import { useVegaWalletDialogStore } from './vega-wallet-dialog-store';
|
||||
import { useChainId } from './use-chain-id';
|
||||
import { useT } from '../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
@ -65,7 +64,7 @@ export const VegaConnectDialog = ({
|
||||
contentOnly,
|
||||
onClose,
|
||||
}: VegaConnectDialogProps) => {
|
||||
const { disconnect, acknowledgeNeeded } = useVegaWallet();
|
||||
const { chainId, disconnect, acknowledgeNeeded } = useVegaWallet();
|
||||
const vegaWalletDialogOpen = useVegaWalletDialogStore(
|
||||
(store) => store.vegaWalletDialogOpen
|
||||
);
|
||||
@ -85,10 +84,6 @@ export const VegaConnectDialog = ({
|
||||
[updateVegaWalletDialog, acknowledgeNeeded, disconnect]
|
||||
);
|
||||
|
||||
// Ensure we have a chain Id so we can compare with wallet chain id.
|
||||
// This value will already be in the cache, if it failed the app wont render
|
||||
const chainId = useChainId();
|
||||
|
||||
const content = chainId && (
|
||||
<ConnectDialogContainer
|
||||
connectors={connectors}
|
||||
|
@ -1,72 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
import { useChainId } from './use-chain-id';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
const mockFetch = global.fetch as jest.Mock;
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
return Promise.resolve({ ok: true });
|
||||
});
|
||||
|
||||
jest.mock('../use-vega-wallet', () => {
|
||||
const original = jest.requireActual('../use-vega-wallet');
|
||||
return {
|
||||
...original,
|
||||
useVegaWallet: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useChainId', () => {
|
||||
it('does not call fetch when statistics url could not be determined', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: '',
|
||||
});
|
||||
renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls fetch with correct statistics url', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: 'http://localhost:1234/graphql',
|
||||
});
|
||||
renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/statistics'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return chain id when chain id is not present in response', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: 'http://localhost:1234/graphql',
|
||||
});
|
||||
const { result } = renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns chain id when chain id is present in response', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: 'http://localhost:1234/graphql',
|
||||
});
|
||||
mockFetch.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => ({
|
||||
statistics: {
|
||||
chainId: '1234',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
const { result } = renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(result.current).not.toBeUndefined();
|
||||
expect(result.current).toEqual('1234');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,10 +1,11 @@
|
||||
import { clearConfig, setConfig } from '../storage';
|
||||
import type { Transaction, VegaConnector } from './vega-connector';
|
||||
|
||||
type VegaWalletEvent = 'client.disconnected';
|
||||
|
||||
declare global {
|
||||
interface Vega {
|
||||
getChainId: () => Promise<{ chainID: string }>;
|
||||
connectWallet: () => Promise<null>;
|
||||
connectWallet: (args: { chainId: string }) => Promise<null>;
|
||||
disconnectWallet: () => Promise<void>;
|
||||
listKeys: () => Promise<{
|
||||
keys: Array<{ name: string; publicKey: string }>;
|
||||
@ -34,6 +35,9 @@ declare global {
|
||||
};
|
||||
transactionHash: string;
|
||||
}>;
|
||||
|
||||
on: (event: VegaWalletEvent, callback: () => void) => void;
|
||||
isConnected?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
@ -47,15 +51,60 @@ export const InjectedConnectorErrors = {
|
||||
INVALID_CHAIN: new Error('Invalid chain'),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const wait = (ms: number) =>
|
||||
new Promise<boolean>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(false);
|
||||
}, ms);
|
||||
});
|
||||
|
||||
const INJECTED_CONNECTOR_TIMEOUT = 1000;
|
||||
|
||||
export class InjectedConnector implements VegaConnector {
|
||||
isConnected = false;
|
||||
chainId: string | null = null;
|
||||
description = 'Connects using the Vega wallet browser extension';
|
||||
alive: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
async getChainId() {
|
||||
return window.vega.getChainId();
|
||||
}
|
||||
async connectWallet(chainId: string) {
|
||||
this.chainId = chainId;
|
||||
try {
|
||||
await window.vega.connectWallet({ chainId });
|
||||
this.isConnected = true;
|
||||
window.vega.on('client.disconnected', () => {
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
connectWallet() {
|
||||
return window.vega.connectWallet();
|
||||
this.alive = setInterval(async () => {
|
||||
try {
|
||||
const connected = await Promise.race([
|
||||
// FIXME: All of the `window.vega` initiated promises are `pending`
|
||||
// while waiting for the user action when transaction is sent.
|
||||
// (Probably due to the FIFO queue of the `PortServer`?)
|
||||
// Because of that we cannot `wait` here as while waiting for the
|
||||
// user action in wallet this will `reject`. It'd be cool if the
|
||||
// `window.vega` was not blocking the api calls.
|
||||
|
||||
// wait(INJECTED_CONNECTOR_TIMEOUT),
|
||||
|
||||
// `isConnected` is only available in the newer versions
|
||||
// of the browser wallet
|
||||
'isConnected' in window.vega &&
|
||||
typeof window.vega.isConnected === 'function'
|
||||
? window.vega.isConnected()
|
||||
: window.vega.listKeys(),
|
||||
]);
|
||||
this.isConnected = Boolean(connected);
|
||||
} catch {
|
||||
this.isConnected = false;
|
||||
}
|
||||
}, INJECTED_CONNECTOR_TIMEOUT * 2);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`could not connect to the vega wallet on chain: ${chainId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
@ -69,19 +118,11 @@ export class InjectedConnector implements VegaConnector {
|
||||
}
|
||||
|
||||
async isAlive() {
|
||||
try {
|
||||
const keys = await window.vega.listKeys();
|
||||
if (keys.keys.length > 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
clearInterval(this.alive);
|
||||
clearConfig();
|
||||
return window.vega.disconnectWallet();
|
||||
}
|
||||
|
@ -13,6 +13,9 @@ export interface VegaWalletContextShape {
|
||||
/** Url of current connected node */
|
||||
vegaUrl: string;
|
||||
|
||||
/** Vega chain id */
|
||||
chainId: string;
|
||||
|
||||
/** Url of running wallet service */
|
||||
vegaWalletServiceUrl: string;
|
||||
|
||||
|
@ -7,3 +7,4 @@ export * from './provider';
|
||||
export * from './connect-dialog';
|
||||
export * from './utils';
|
||||
export * from './storage';
|
||||
export * from './use-chain-id';
|
||||
|
@ -22,6 +22,7 @@ const defaultConfig: VegaWalletConfig = {
|
||||
chromeExtensionUrl: 'chrome-link',
|
||||
mozillaExtensionUrl: 'mozilla-link',
|
||||
},
|
||||
chainId: 'VEGA_CHAIN_ID',
|
||||
};
|
||||
|
||||
const setup = (config?: Partial<VegaWalletConfig>) => {
|
||||
@ -68,6 +69,7 @@ describe('VegaWalletProvider', () => {
|
||||
expect(result.current).toEqual({
|
||||
network: defaultConfig.network,
|
||||
vegaUrl: defaultConfig.vegaUrl,
|
||||
chainId: defaultConfig.chainId,
|
||||
vegaWalletServiceUrl: defaultConfig.vegaWalletServiceUrl,
|
||||
acknowledgeNeeded: false,
|
||||
pubKey: null,
|
||||
|
@ -39,6 +39,7 @@ interface VegaWalletLinks {
|
||||
export interface VegaWalletConfig {
|
||||
network: Networks;
|
||||
vegaUrl: string;
|
||||
chainId: string;
|
||||
vegaWalletServiceUrl: string;
|
||||
links: VegaWalletLinks;
|
||||
keepAlive?: number;
|
||||
@ -168,6 +169,7 @@ export const VegaWalletProvider = ({
|
||||
const contextValue = useMemo<VegaWalletContextShape>(() => {
|
||||
return {
|
||||
vegaUrl: config.vegaUrl,
|
||||
chainId: config.chainId,
|
||||
vegaWalletServiceUrl: config.vegaWalletServiceUrl,
|
||||
network: config.network,
|
||||
links: {
|
||||
|
@ -1,6 +1,5 @@
|
||||
export function mockBrowserWallet(overrides?: Partial<Vega>) {
|
||||
const vega: Vega = {
|
||||
getChainId: jest.fn().mockReturnValue(Promise.resolve({ chainID: '1' })),
|
||||
connectWallet: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
disconnectWallet: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
listKeys: jest
|
||||
@ -14,6 +13,8 @@ export function mockBrowserWallet(overrides?: Partial<Vega>) {
|
||||
success: true,
|
||||
txHash: '0x123',
|
||||
}),
|
||||
on: jest.fn(),
|
||||
isConnected: jest.fn().mockRejectedValue(Promise.resolve(true)),
|
||||
...overrides,
|
||||
};
|
||||
// @ts-ignore globalThis has no index signature
|
||||
|
100
libs/wallet/src/use-chain-id.spec.ts
Normal file
100
libs/wallet/src/use-chain-id.spec.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { MAX_FETCH_ATTEMPTS, useChainId } from './use-chain-id';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
const mockFetch = global.fetch as jest.Mock;
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
return Promise.resolve({ ok: true });
|
||||
});
|
||||
|
||||
describe('useChainId', () => {
|
||||
it('does not call fetch when statistics url could not be determined', async () => {
|
||||
renderHook(() => useChainId(''));
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls fetch with correct statistics url', async () => {
|
||||
renderHook(() => useChainId('http://localhost:1234/graphql'));
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/statistics'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return chain id when chain id is not present in response', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useChainId('http://localhost:1234/graphql')
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns chain id when chain id is present in response', async () => {
|
||||
mockFetch.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => ({
|
||||
statistics: {
|
||||
chainId: '1234',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useChainId('http://localhost:1234/graphql')
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.current).not.toBeUndefined();
|
||||
expect(result.current).toEqual('1234');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns chain id when within max number of attempts', async () => {
|
||||
mockFetch.mockImplementation(() => {
|
||||
if (mockFetch.mock.calls.length < MAX_FETCH_ATTEMPTS) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => ({
|
||||
statistics: {
|
||||
chainId: '1234',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useChainId('http://localhost:1234/graphql')
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.current).not.toBeUndefined();
|
||||
expect(result.current).toEqual('1234');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return chain id when max number of attempts exceeded', async () => {
|
||||
mockFetch.mockImplementation(() => {
|
||||
if (mockFetch.mock.calls.length < MAX_FETCH_ATTEMPTS + 10) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => ({
|
||||
statistics: {
|
||||
chainId: '1234',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useChainId('http://localhost:5678/graphql')
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
|
||||
export const MAX_FETCH_ATTEMPTS = 3;
|
||||
|
||||
const cache: Record<string, string> = {};
|
||||
|
||||
@ -18,16 +19,17 @@ const getNodeStatisticsUrl = (vegaUrl: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const useChainId = () => {
|
||||
const { vegaUrl } = useVegaWallet();
|
||||
const [chainId, setChainId] = useState<undefined | string>(cache[vegaUrl]);
|
||||
const [fetchAttempt, setFetchAttempt] = useState(1);
|
||||
export const useChainId = (vegaUrl: string | undefined) => {
|
||||
const [chainId, setChainId] = useState<undefined | string>(
|
||||
vegaUrl ? cache[vegaUrl] : undefined
|
||||
);
|
||||
const [fetchAttempts, setFetchAttempts] = useState(1);
|
||||
|
||||
const statisticsUrl = getNodeStatisticsUrl(vegaUrl);
|
||||
const statisticsUrl = vegaUrl ? getNodeStatisticsUrl(vegaUrl) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
// abort when `/statistics` URL could not be determined
|
||||
if (!statisticsUrl) return;
|
||||
if (!statisticsUrl || !vegaUrl) return;
|
||||
let isCancelled = false;
|
||||
if (cache[vegaUrl]) {
|
||||
setChainId(cache[vegaUrl]);
|
||||
@ -42,16 +44,19 @@ export const useChainId = () => {
|
||||
if (!response?.statistics?.chainId) {
|
||||
throw new Error('statistics.chainId not present in fetched response');
|
||||
}
|
||||
setChainId(response?.statistics?.chainId);
|
||||
|
||||
const chainId = response.statistics.chainId;
|
||||
cache[vegaUrl] = chainId;
|
||||
setChainId(chainId);
|
||||
})
|
||||
.catch(() => {
|
||||
if (fetchAttempt < 3) {
|
||||
setFetchAttempt((value) => (value += 1));
|
||||
if (fetchAttempts < MAX_FETCH_ATTEMPTS) {
|
||||
setFetchAttempts((value) => (value += 1));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [fetchAttempt, statisticsUrl, vegaUrl]);
|
||||
}, [fetchAttempts, statisticsUrl, vegaUrl]);
|
||||
return chainId;
|
||||
};
|
@ -7,7 +7,7 @@ import { useVegaWallet } from './use-vega-wallet';
|
||||
|
||||
export function useEagerConnect(connectors: Connectors) {
|
||||
const [connecting, setConnecting] = useState(true);
|
||||
const { vegaUrl, connect, acknowledgeNeeded } = useVegaWallet();
|
||||
const { vegaUrl, chainId, connect, acknowledgeNeeded } = useVegaWallet();
|
||||
|
||||
useEffect(() => {
|
||||
const attemptConnect = async () => {
|
||||
@ -33,7 +33,7 @@ export function useEagerConnect(connectors: Connectors) {
|
||||
|
||||
try {
|
||||
if (connector instanceof InjectedConnector) {
|
||||
await connector.connectWallet();
|
||||
await connector.connectWallet(chainId);
|
||||
await connect(connector);
|
||||
} else if (connector instanceof SnapConnector) {
|
||||
connector.nodeAddress = new URL(vegaUrl).origin;
|
||||
@ -51,7 +51,7 @@ export function useEagerConnect(connectors: Connectors) {
|
||||
if (typeof window !== 'undefined') {
|
||||
attemptConnect();
|
||||
}
|
||||
}, [connect, connectors, acknowledgeNeeded, vegaUrl]);
|
||||
}, [connect, connectors, acknowledgeNeeded, vegaUrl, chainId]);
|
||||
|
||||
return connecting;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ const defaultConfig: VegaWalletConfig = {
|
||||
chromeExtensionUrl: 'chrome-link',
|
||||
mozillaExtensionUrl: 'mozilla-link',
|
||||
},
|
||||
chainId: 'VEGA_CHAIN_ID',
|
||||
};
|
||||
|
||||
const setup = (callback = jest.fn(), config?: Partial<VegaWalletConfig>) => {
|
||||
@ -46,20 +47,10 @@ describe('useInjectedConnector', () => {
|
||||
expect(result.current.status).toBe(Status.Error);
|
||||
});
|
||||
|
||||
it('errors if chain ids dont match', async () => {
|
||||
mockBrowserWallet();
|
||||
const { result } = setup();
|
||||
await act(async () => {
|
||||
result.current.connect(injected, '2'); // default mock chainId is '1'
|
||||
});
|
||||
expect(result.current.error?.message).toBe('Invalid chain');
|
||||
expect(result.current.status).toBe(Status.Error);
|
||||
});
|
||||
|
||||
it('errors if connection throws', async () => {
|
||||
const callback = jest.fn();
|
||||
mockBrowserWallet({
|
||||
getChainId: () => Promise.reject('failed'),
|
||||
connectWallet: jest.fn().mockReturnValue(Promise.reject()),
|
||||
});
|
||||
const { result } = setup(callback);
|
||||
|
||||
@ -67,7 +58,9 @@ describe('useInjectedConnector', () => {
|
||||
result.current.connect(injected, '1'); // default mock chainId is '1'
|
||||
});
|
||||
expect(result.current.status).toBe(Status.Error);
|
||||
expect(result.current.error?.message).toBe('injected connection failed');
|
||||
expect(result.current.error?.message).toBe(
|
||||
'could not connect to the vega wallet on chain: 1'
|
||||
);
|
||||
});
|
||||
|
||||
it('connects', async () => {
|
||||
@ -78,7 +71,6 @@ describe('useInjectedConnector', () => {
|
||||
act(() => {
|
||||
result.current.connect(injected, '1'); // default mock chainId is '1'
|
||||
});
|
||||
expect(result.current.status).toBe(Status.GettingChainId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vega.connectWallet).toHaveBeenCalled();
|
||||
|
@ -40,17 +40,19 @@ export const useInjectedConnector = (onConnect: () => void) => {
|
||||
connector.nodeAddress = new URL(vegaUrl).origin;
|
||||
}
|
||||
|
||||
setStatus(Status.GettingChainId);
|
||||
|
||||
const { chainID } = await connector.getChainId();
|
||||
if (chainID !== appChainId) {
|
||||
throw InjectedConnectorErrors.INVALID_CHAIN;
|
||||
// check the chain id for snap connector
|
||||
if (connector instanceof SnapConnector) {
|
||||
setStatus(Status.GettingChainId);
|
||||
const { chainID } = await connector.getChainId();
|
||||
if (chainID !== appChainId) {
|
||||
throw InjectedConnectorErrors.INVALID_CHAIN;
|
||||
}
|
||||
}
|
||||
|
||||
setStatus(Status.Connecting);
|
||||
if (connector instanceof InjectedConnector) {
|
||||
// extra step for injected connector - authorize wallet
|
||||
await connector.connectWallet();
|
||||
await connector.connectWallet(appChainId);
|
||||
}
|
||||
await connect(connector); // connect with keys
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
export * from './lib/__generated__/TransactionResult';
|
||||
export * from './lib/__generated__/WithdrawalApproval';
|
||||
export * from './lib/constants';
|
||||
export * from './lib/default-web3-provider';
|
||||
export * from './lib/eip-1193-custom-bridge';
|
||||
export * from './lib/ethereum-error';
|
||||
export * from './lib/ethereum-transaction-dialog';
|
||||
export * from './lib/types';
|
||||
export * from './lib/url-connector';
|
||||
export * from './lib/use-bridge-contract';
|
||||
export * from './lib/use-eager-connect';
|
||||
@ -19,7 +23,12 @@ export * from './lib/use-get-withdraw-delay';
|
||||
export * from './lib/use-get-withdraw-threshold';
|
||||
export * from './lib/use-token-contract';
|
||||
export * from './lib/use-token-decimals';
|
||||
export * from './lib/use-transaction-result';
|
||||
export * from './lib/use-vega-transaction-manager';
|
||||
export * from './lib/use-vega-transaction-store';
|
||||
export * from './lib/use-vega-transaction-toasts';
|
||||
export * from './lib/use-vega-transaction-updater';
|
||||
export * from './lib/use-wallet-disconnected-toasts';
|
||||
export * from './lib/use-web3-disconnect';
|
||||
export * from './lib/web3-connect-dialog';
|
||||
export * from './lib/web3-connect-store';
|
||||
@ -27,11 +36,3 @@ export * from './lib/web3-connectors';
|
||||
export * from './lib/web3-provider';
|
||||
export * from './lib/withdrawal-approval-dialog';
|
||||
export * from './lib/withdrawal-approval-status';
|
||||
export * from './lib/default-web3-provider';
|
||||
export * from './lib/use-vega-transaction-manager';
|
||||
export * from './lib/use-vega-transaction-store';
|
||||
export * from './lib/use-vega-transaction-updater';
|
||||
export * from './lib/use-transaction-result';
|
||||
export * from './lib/types';
|
||||
export * from './lib/__generated__/TransactionResult';
|
||||
export * from './lib/__generated__/WithdrawalApproval';
|
||||
|
78
libs/web3/src/lib/use-wallet-disconnected-toasts.tsx
Normal file
78
libs/web3/src/lib/use-wallet-disconnected-toasts.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import {
|
||||
Intent,
|
||||
useToasts,
|
||||
ToastHeading,
|
||||
CLOSE_AFTER,
|
||||
type Toast,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useT } from './use-t';
|
||||
import { usePrevious } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const WALLET_DISCONNECTED_TOAST_ID = 'WALLET_DISCONNECTED_TOAST_ID';
|
||||
|
||||
export const useWalletDisconnectToastActions = () => {
|
||||
const [hasToast, updateToast] = useToasts((state) => [
|
||||
state.hasToast,
|
||||
state.update,
|
||||
]);
|
||||
const hideToast = () => {
|
||||
if (!hasToast(WALLET_DISCONNECTED_TOAST_ID)) return;
|
||||
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
|
||||
hidden: true,
|
||||
});
|
||||
};
|
||||
const showToast = () => {
|
||||
if (!hasToast(WALLET_DISCONNECTED_TOAST_ID)) return;
|
||||
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
|
||||
hidden: false,
|
||||
});
|
||||
};
|
||||
return { showToast, hideToast };
|
||||
};
|
||||
|
||||
export const useWalletDisconnectedToasts = (
|
||||
additionalContent?: JSX.Element
|
||||
) => {
|
||||
const t = useT();
|
||||
const [hasToast, setToast] = useToasts((state) => [
|
||||
state.hasToast,
|
||||
state.setToast,
|
||||
state.update,
|
||||
]);
|
||||
const { showToast, hideToast } = useWalletDisconnectToastActions();
|
||||
|
||||
const { isAlive } = useVegaWallet();
|
||||
const wasAlive = usePrevious(isAlive);
|
||||
const disconnected = wasAlive && !isAlive;
|
||||
|
||||
const toast: Toast = useMemo(
|
||||
() => ({
|
||||
id: WALLET_DISCONNECTED_TOAST_ID,
|
||||
intent: Intent.Danger,
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>{t('Wallet connection lost')}</ToastHeading>
|
||||
<p>{t('The connection to the Vega wallet has been lost.')}</p>
|
||||
{additionalContent}
|
||||
</>
|
||||
),
|
||||
onClose: () => {
|
||||
hideToast();
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
}),
|
||||
[additionalContent, hideToast, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (disconnected) {
|
||||
if (hasToast(WALLET_DISCONNECTED_TOAST_ID)) {
|
||||
showToast();
|
||||
} else {
|
||||
setToast(toast);
|
||||
}
|
||||
}
|
||||
}, [disconnected, hasToast, isAlive, setToast, showToast, t, toast]);
|
||||
};
|
Loading…
Reference in New Issue
Block a user