feat(wallet): disconnect if wallet unreachable (#5501)
This commit is contained in:
parent
4b208e90bf
commit
3beeb0140c
@ -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">
|
||||
|
62
apps/trading/lib/hooks/use-wallet-disconnected-toasts.tsx
Normal file
62
apps/trading/lib/hooks/use-wallet-disconnected-toasts.tsx
Normal 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]);
|
||||
};
|
@ -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} />;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.DisconnectWallet();
|
||||
} catch (err) {
|
||||
// NOOP
|
||||
}
|
||||
clearConfig();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -39,6 +39,11 @@ export class ViewConnector implements VegaConnector {
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
async isAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
disconnect(): Promise<void> {
|
||||
clearConfig();
|
||||
this.pubkey = null;
|
||||
|
@ -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<
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
|
31
libs/wallet/src/use-is-alive.ts
Normal file
31
libs/wallet/src/use-is-alive.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user