feat(wallet): disconnect if wallet unreachable (#5501)

This commit is contained in:
Art 2023-12-18 11:39:18 +01:00 committed by GitHub
parent 4b208e90bf
commit 3beeb0140c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 220 additions and 6 deletions

View File

@ -24,7 +24,13 @@ import classNames from 'classnames';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
export const VegaWalletConnectButton = () => {
export const VegaWalletConnectButton = ({
intent = Intent.None,
onClick,
}: {
intent?: Intent;
onClick?: () => void;
}) => {
const t = useT();
const [dropdownOpen, setDropdownOpen] = useState(false);
const openVegaWalletDialog = useVegaWalletDialogStore(
@ -117,9 +123,12 @@ export const VegaWalletConnectButton = () => {
return (
<Button
data-testid="connect-vega-wallet"
onClick={openVegaWalletDialog}
onClick={() => {
onClick?.();
openVegaWalletDialog();
}}
size="small"
intent={Intent.None}
intent={intent}
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
>
<span className="whitespace-nowrap uppercase">

View File

@ -0,0 +1,62 @@
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]);
};

View File

@ -6,6 +6,7 @@ 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';
export const ToastsManager = () => {
useProposalToasts();
@ -16,6 +17,7 @@ export const ToastsManager = () => {
withdrawalsLink: Links.PORTFOLIO(),
});
useReferralToasts();
useWalletDisconnectedToasts();
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;

View File

@ -68,6 +68,19 @@ export class InjectedConnector implements VegaConnector {
return res.keys;
}
async isAlive() {
try {
const keys = await window.vega.listKeys();
if (keys.keys.length > 0) {
return true;
}
} catch (err) {
return false;
}
return false;
}
disconnect() {
clearConfig();
return window.vega.disconnectWallet();

View File

@ -123,12 +123,31 @@ export class JsonRpcConnector implements VegaConnector {
}
}
async isAlive() {
if (this.client) {
try {
const keys = await this.client.ListKeys();
if (keys.result.keys.length > 0) {
return true;
}
} catch (err) {
return false;
}
}
return false;
}
async disconnect() {
if (!this.client) {
throw ClientErrors.NO_CLIENT;
}
await this.client.DisconnectWallet();
try {
await this.client.DisconnectWallet();
} catch (err) {
// NOOP
}
clearConfig();
}

View File

@ -167,6 +167,19 @@ export class SnapConnector implements VegaConnector {
return res?.keys;
}
async isAlive() {
try {
const keys = await this.listKeys();
if (keys.keys.length > 0) {
return true;
}
} catch (err) {
return false;
}
return false;
}
async sendTx(pubKey: string, transaction: Transaction) {
if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;

View File

@ -543,4 +543,9 @@ export interface VegaConnector {
pubkey: string,
transaction: Transaction
) => Promise<TransactionResponse | null>;
/**
* Checks if the connection to the connector is alive.
*/
isAlive: () => Promise<boolean>;
}

View File

@ -39,6 +39,11 @@ export class ViewConnector implements VegaConnector {
},
]);
}
async isAlive() {
return true;
}
disconnect(): Promise<void> {
clearConfig();
this.pubkey = null;

View File

@ -56,6 +56,11 @@ export interface VegaWalletContextShape {
chromeExtensionUrl: string;
mozillaExtensionUrl: string;
};
/**
* A flag determining whether the current connection is alive.
*/
isAlive: boolean | null;
}
export const VegaWalletContext = createContext<

View File

@ -1,4 +1,4 @@
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import type { Transaction } from './connectors';
import { ViewConnector, JsonRpcConnector } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
@ -7,6 +7,7 @@ import { VegaWalletProvider } from './provider';
import { LocalStorage } from '@vegaprotocol/utils';
import type { ReactNode } from 'react';
import { WALLET_KEY } from './storage';
import { DEFAULT_KEEP_ALIVE } from './use-is-alive';
const jsonRpcConnector = new JsonRpcConnector();
const viewConnector = new ViewConnector();
@ -35,6 +36,7 @@ const setup = (config?: Partial<VegaWalletConfig>) => {
describe('VegaWalletProvider', () => {
afterAll(() => {
localStorage.clear();
jest.useRealTimers();
});
const mockPubKeys = [
@ -81,6 +83,7 @@ describe('VegaWalletProvider', () => {
browserList: expect.any(String),
...defaultConfig.links,
},
isAlive: null,
});
// Connect
@ -179,4 +182,27 @@ describe('VegaWalletProvider', () => {
});
expect(result.current.isReadOnly).toBe(true);
});
it('sets isAlive to false if connection lost', async () => {
// setup and connect
jest.useFakeTimers();
jest
.spyOn(jsonRpcConnector, 'isAlive')
.mockImplementation(() => Promise.resolve(true));
const { result } = setup();
await act(async () => {
result.current.connect(jsonRpcConnector);
});
expect(result.current.isAlive).toEqual(null);
// loose connection
jest
.spyOn(jsonRpcConnector, 'isAlive')
.mockImplementation(() => Promise.resolve(false));
jest.advanceTimersByTime(DEFAULT_KEEP_ALIVE);
await waitFor(() => {
expect(result.current.isAlive).toEqual(false);
});
});
});

View File

@ -1,5 +1,12 @@
import { LocalStorage } from '@vegaprotocol/utils';
import { useCallback, useMemo, useRef, useState, type ReactNode } from 'react';
import {
useCallback,
useMemo,
useRef,
useState,
type ReactNode,
useEffect,
} from 'react';
import { WalletClientError } from '@vegaprotocol/wallet-client';
import { type VegaWalletContextShape } from '.';
import {
@ -11,6 +18,7 @@ import { VegaWalletContext } from './context';
import { WALLET_KEY, WALLET_RISK_ACCEPTED_KEY } from './storage';
import { ViewConnector } from './connectors';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { DEFAULT_KEEP_ALIVE, useIsAlive } from './use-is-alive';
type Networks =
| 'MAINNET'
@ -33,6 +41,7 @@ export interface VegaWalletConfig {
vegaUrl: string;
vegaWalletServiceUrl: string;
links: VegaWalletLinks;
keepAlive?: number;
}
const ExternalLinks = {
@ -143,6 +152,19 @@ export const VegaWalletProvider = ({
const acknowledgeNeeded =
config.network === 'MAINNET' && riskAcceptedValue !== 'true';
const isAlive = useIsAlive(
connector.current && pubKey ? connector.current : null,
config.keepAlive != null ? config.keepAlive : DEFAULT_KEEP_ALIVE
);
/**
* Force disconnect if connected and wallet is unreachable.
*/
useEffect(() => {
if (isAlive === false) {
disconnect();
}
}, [disconnect, isAlive]);
const contextValue = useMemo<VegaWalletContextShape>(() => {
return {
vegaUrl: config.vegaUrl,
@ -165,6 +187,7 @@ export const VegaWalletProvider = ({
sendTx,
fetchPubKeys,
acknowledgeNeeded,
isAlive,
};
}, [
config,
@ -177,6 +200,7 @@ export const VegaWalletProvider = ({
sendTx,
fetchPubKeys,
acknowledgeNeeded,
isAlive,
]);
return (

View File

@ -0,0 +1,31 @@
import { useEffect, useState } from 'react';
import { type VegaConnector } from '.';
/**
* Determines the interval for checking if wallet connection is alive.
*/
export const DEFAULT_KEEP_ALIVE = 1000;
export const useIsAlive = (
connector: VegaConnector | null,
interval: number
) => {
const [alive, setAlive] = useState<boolean | null>(null);
useEffect(() => {
if (!connector) {
return;
}
const i = setInterval(() => {
connector.isAlive().then((isAlive) => {
if (alive !== isAlive) setAlive(isAlive);
});
}, interval);
return () => {
clearInterval(i);
};
}, [alive, connector, interval]);
return alive;
};