feat(trading,governance,wallet): browser wallet integration (#4121)
This commit is contained in:
parent
5692d4e74c
commit
6a55319e04
@ -78,7 +78,7 @@ context(
|
||||
cy.getByTestId('connector-jsonRpc')
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Connect Vega wallet');
|
||||
cy.getByTestId('connector-hosted')
|
||||
cy.getByTestId('connector-rest')
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Hosted Fairground wallet');
|
||||
});
|
||||
@ -94,7 +94,7 @@ context(
|
||||
describe('when rest connector form opened', function () {
|
||||
before('click hosted wallet app button', function () {
|
||||
cy.getByTestId(connectorsList).within(() => {
|
||||
cy.getByTestId('connector-hosted').click();
|
||||
cy.getByTestId('connector-rest').click();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,13 +2,8 @@ import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
|
||||
export const ConnectToVega = () => {
|
||||
const { appDispatch } = useAppState();
|
||||
const { t } = useTranslation();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
@ -16,10 +11,6 @@ export const ConnectToVega = () => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
data-testid="connect-to-vega-wallet-btn"
|
||||
|
@ -3,11 +3,6 @@ import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
|
||||
interface VegaWalletContainerProps {
|
||||
children: (key: string) => React.ReactElement;
|
||||
}
|
||||
@ -15,7 +10,6 @@ interface VegaWalletContainerProps {
|
||||
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { appDispatch } = useAppState();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
@ -25,10 +19,6 @@ export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
|
||||
<Button
|
||||
data-testid="connect-to-vega-wallet-btn"
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
>
|
||||
|
@ -13,12 +13,6 @@ export const VegaWalletDialogs = () => {
|
||||
<>
|
||||
<VegaConnectDialog
|
||||
connectors={Connectors}
|
||||
onChangeOpen={(open) =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: open,
|
||||
})
|
||||
}
|
||||
riskMessage={<RiskMessage />}
|
||||
/>
|
||||
|
||||
|
@ -71,7 +71,6 @@ export const VegaWallet = () => {
|
||||
|
||||
const VegaWalletNotConnected = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
@ -79,10 +78,6 @@ const VegaWalletNotConnected = () => {
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
fill={true}
|
||||
|
@ -28,9 +28,6 @@ export interface AppState {
|
||||
/** Total number of VEGA Tokens, both vesting and unlocked, associated for staking */
|
||||
totalAssociated: BigNumber;
|
||||
|
||||
/** Whether or not the connect to VEGA wallet overlay is open */
|
||||
vegaWalletOverlay: boolean;
|
||||
|
||||
/** Whether or not the manage VEGA wallet overlay is open */
|
||||
vegaWalletManageOverlay: boolean;
|
||||
|
||||
@ -52,9 +49,7 @@ export enum AppStateActionType {
|
||||
SET_TOKEN,
|
||||
SET_ALLOWANCE,
|
||||
REFRESH_BALANCES,
|
||||
SET_VEGA_WALLET_OVERLAY,
|
||||
SET_VEGA_WALLET_MANAGE_OVERLAY,
|
||||
SET_DRAWER,
|
||||
REFRESH_ASSOCIATED_BALANCES,
|
||||
SET_ASSOCIATION_BREAKDOWN,
|
||||
SET_TRANSACTION_OVERLAY,
|
||||
@ -69,18 +64,10 @@ export type AppStateAction =
|
||||
totalSupply: BigNumber;
|
||||
totalAssociated: BigNumber;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY;
|
||||
isOpen: boolean;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY;
|
||||
isOpen: boolean;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_DRAWER;
|
||||
isOpen: boolean;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_TRANSACTION_OVERLAY;
|
||||
isOpen: boolean;
|
||||
|
@ -14,7 +14,6 @@ const initialAppState: AppState = {
|
||||
totalAssociated: new BigNumber(0),
|
||||
decimals: 0,
|
||||
totalSupply: new BigNumber(0),
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
@ -31,23 +30,10 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
|
||||
totalAssociated: action.totalAssociated,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_VEGA_WALLET_OVERLAY: {
|
||||
return {
|
||||
...state,
|
||||
vegaWalletOverlay: action.isOpen,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY: {
|
||||
return {
|
||||
...state,
|
||||
vegaWalletManageOverlay: action.isOpen,
|
||||
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_DRAWER: {
|
||||
return {
|
||||
...state,
|
||||
vegaWalletOverlay: false,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_TRANSACTION_OVERLAY: {
|
||||
|
@ -2,15 +2,18 @@ import {
|
||||
RestConnector,
|
||||
JsonRpcConnector,
|
||||
ViewConnector,
|
||||
InjectedConnector,
|
||||
} from '@vegaprotocol/wallet';
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
export const injected = new InjectedConnector();
|
||||
export const rest = new RestConnector();
|
||||
export const jsonRpc = new JsonRpcConnector();
|
||||
export const view = new ViewConnector(urlParams.get('address'));
|
||||
|
||||
export const Connectors = {
|
||||
injected,
|
||||
rest,
|
||||
jsonRpc,
|
||||
view,
|
||||
|
@ -10,10 +10,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { addDecimal, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../../../contexts/app-state/app-state-context';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
import { DATE_FORMAT_LONG } from '../../../../lib/date-formats';
|
||||
import { VoteState } from './use-user-vote';
|
||||
@ -73,7 +70,6 @@ export const VoteButtons = ({
|
||||
dialog: Dialog,
|
||||
}: VoteButtonsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
@ -98,10 +94,6 @@ export const VoteButtons = ({
|
||||
<div data-testid="connect-wallet">
|
||||
<ButtonLink
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
>
|
||||
@ -142,7 +134,6 @@ export const VoteButtons = ({
|
||||
minVoterBalance,
|
||||
spamProtectionMinTokens,
|
||||
t,
|
||||
appDispatch,
|
||||
openVegaWalletDialog,
|
||||
]);
|
||||
|
||||
|
@ -14,7 +14,6 @@ const mockAppState: AppState = {
|
||||
totalAssociated: new BigNumber('50063005'),
|
||||
decimals: 18,
|
||||
totalSupply: mockTotalSupply,
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
|
@ -2,14 +2,9 @@ import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
import { SubHeading } from '../../components/heading';
|
||||
|
||||
export const ConnectToSeeRewards = () => {
|
||||
const { appDispatch } = useAppState();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
@ -26,10 +21,6 @@ export const ConnectToSeeRewards = () => {
|
||||
<Button
|
||||
data-testid="connect-to-vega-wallet-btn"
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
>
|
||||
|
@ -63,7 +63,7 @@ describe(
|
||||
cy.contains('Hosted Fairground wallet');
|
||||
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.find('[data-testid="connector-rest"]')
|
||||
.click();
|
||||
cy.getByTestId(form).find('#wallet').click().type('user');
|
||||
cy.getByTestId(form).find('#passphrase').click().type('pass');
|
||||
@ -89,7 +89,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.find('[data-testid="connector-rest"]')
|
||||
.click();
|
||||
cy.getByTestId(form).find('#wallet').click().type('invalid name');
|
||||
cy.getByTestId(form).find('#passphrase').click().type('invalid password');
|
||||
@ -100,7 +100,7 @@ describe(
|
||||
it('doesnt connect with empty fields', () => {
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.find('[data-testid="connector-rest"]')
|
||||
.click();
|
||||
|
||||
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
|
||||
|
@ -190,6 +190,9 @@ export const VegaWalletConnectButton = () => {
|
||||
>
|
||||
<DropdownMenuContent
|
||||
onInteractOutside={() => setDropdownOpen(false)}
|
||||
sideOffset={20}
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div className="min-w-[340px]" data-testid="keypair-list">
|
||||
<DropdownMenuRadioGroup
|
||||
|
@ -2,10 +2,12 @@ import {
|
||||
RestConnector,
|
||||
JsonRpcConnector,
|
||||
ViewConnector,
|
||||
InjectedConnector,
|
||||
} from '@vegaprotocol/wallet';
|
||||
|
||||
export const rest = new RestConnector();
|
||||
export const jsonRpc = new JsonRpcConnector();
|
||||
export const injected = new InjectedConnector();
|
||||
|
||||
let view: ViewConnector;
|
||||
if (typeof window !== 'undefined') {
|
||||
@ -16,6 +18,7 @@ if (typeof window !== 'undefined') {
|
||||
}
|
||||
|
||||
export const Connectors = {
|
||||
injected,
|
||||
rest,
|
||||
jsonRpc,
|
||||
view,
|
||||
|
@ -45,7 +45,7 @@ export function Dialog({
|
||||
'dark:bg-black bg-white dark:text-white',
|
||||
getIntentBorder(intent),
|
||||
{
|
||||
'w-[620px]': size === 'small',
|
||||
'w-[520px]': size === 'small',
|
||||
'w-[720px] lg:w-[940px]': size === 'medium',
|
||||
}
|
||||
);
|
||||
@ -77,7 +77,7 @@ export function Dialog({
|
||||
className="absolute p-2 top-0 right-0 md:top-2 md:right-2"
|
||||
data-testid="dialog-close"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CROSS} />
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||
</DialogPrimitives.Close>
|
||||
)}
|
||||
<div className="flex gap-4 max-w-full">
|
||||
|
@ -74,11 +74,11 @@ export const DropdownMenuContent = forwardRef<
|
||||
React.ComponentProps<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, ...contentProps }, forwardedRef) => (
|
||||
<DropdownMenuPrimitive.Content
|
||||
{...contentProps}
|
||||
ref={forwardedRef}
|
||||
sideOffset={10}
|
||||
className="min-w-[290px] bg-vega-light-100 dark:bg-vega-dark-100 p-2 rounded z-20 text-black dark:text-white border border-vega-light-200 dark:border-vega-dark-200"
|
||||
align="start"
|
||||
sideOffset={10}
|
||||
{...contentProps}
|
||||
/>
|
||||
));
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
export const IconChevronLeft = ({ size = 16 }: { size: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M10.38 1.62L11.13 2.38L5.5 8L11.13 13.62L10.38 14.38L4 8L10.38 1.62Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -2,6 +2,7 @@ import { IconArrowDown } from './svg-icons/icon-arrow-down';
|
||||
import { IconArrowRight } from './svg-icons/icon-arrow-right';
|
||||
import { IconBreakdown } from './svg-icons/icon-breakdown';
|
||||
import { IconChevronDown } from './svg-icons/icon-chevron-down';
|
||||
import { IconChevronLeft } from './svg-icons/icon-chevron-left';
|
||||
import { IconChevronUp } from './svg-icons/icon-chevron-up';
|
||||
import { IconCopy } from './svg-icons/icon-copy';
|
||||
import { IconCross } from './svg-icons/icon-cross';
|
||||
@ -26,6 +27,7 @@ export enum VegaIconNames {
|
||||
ARROW_RIGHT = 'arrow-right',
|
||||
BREAKDOWN = 'breakdown',
|
||||
CHEVRON_DOWN = 'chevron-down',
|
||||
CHEVRON_LEFT = 'chevron-left',
|
||||
CHEVRON_UP = 'chevron-up',
|
||||
COPY = 'copy',
|
||||
CROSS = 'cross',
|
||||
@ -53,6 +55,7 @@ export const VegaIconNameMap: Record<
|
||||
'arrow-down': IconArrowDown,
|
||||
'arrow-right': IconArrowRight,
|
||||
'chevron-down': IconChevronDown,
|
||||
'chevron-left': IconChevronLeft,
|
||||
'chevron-up': IconChevronUp,
|
||||
'open-external': IconOpenExternal,
|
||||
'question-mark': IconQuestionMark,
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { VegaConnector } from '../connectors';
|
||||
import { RestConnector } from '../connectors';
|
||||
|
||||
export const ConnectDialogTitle = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
@ -18,11 +21,35 @@ export const ConnectDialogContent = ({ children }: { children: ReactNode }) => {
|
||||
return <div>{children}</div>;
|
||||
};
|
||||
|
||||
export const ConnectDialogFooter = ({ children }: { children?: ReactNode }) => {
|
||||
export const ConnectDialogFooter = ({
|
||||
connector,
|
||||
}: {
|
||||
connector: VegaConnector | undefined;
|
||||
}) => {
|
||||
const wrapperClasses = classNames(
|
||||
'flex justify-center gap-4',
|
||||
'px-4 md:px-8 pt-4 md:pt-6',
|
||||
'border-t border-vega-light-200 dark:border-vega-dark-200',
|
||||
'text-vega-light-400 dark:text-vega-dark-400'
|
||||
);
|
||||
const isHostedWalletSelected = connector instanceof RestConnector;
|
||||
return (
|
||||
<footer className="flex justify-center gap-4 px-4 md:px-8 pt-4 md:pt-6 -mx-4 md:-mx-8 border-t border-neutral-500 text-neutral-500 dark:text-neutral-400 mt-6">
|
||||
{children ? (
|
||||
children
|
||||
<footer className={wrapperClasses}>
|
||||
{isHostedWalletSelected ? (
|
||||
<p className="text-center">
|
||||
{t('For demo purposes get a ')}
|
||||
<Link
|
||||
href={ExternalLinks.VEGA_WALLET_HOSTED_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('hosted wallet')}
|
||||
</Link>
|
||||
{t(', or for the real experience create a wallet in the ')}
|
||||
<Link href={ExternalLinks.VEGA_WALLET_URL}>
|
||||
{t('Vega wallet app')}
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<Link href={ExternalLinks.VEGA_WALLET_URL}>
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
import type { VegaConnectDialogProps } from '..';
|
||||
import {
|
||||
ClientErrors,
|
||||
InjectedConnector,
|
||||
JsonRpcConnector,
|
||||
RestConnector,
|
||||
ViewConnector,
|
||||
@ -24,6 +25,12 @@ import {
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { ChainIdQuery } from './__generated__/ChainId';
|
||||
import { ChainIdDocument } from './__generated__/ChainId';
|
||||
import {
|
||||
mockBrowserWallet,
|
||||
clearBrowserWallet,
|
||||
delayedReject,
|
||||
delayedResolve,
|
||||
} from '../test-helpers';
|
||||
|
||||
const mockUpdateDialogOpen = jest.fn();
|
||||
const mockCloseVegaDialog = jest.fn();
|
||||
@ -49,10 +56,12 @@ const INITIAL_KEY = 'some-key';
|
||||
const rest = new RestConnector();
|
||||
const jsonRpc = new JsonRpcConnector();
|
||||
const view = new ViewConnector(INITIAL_KEY);
|
||||
const injected = new InjectedConnector();
|
||||
const connectors = {
|
||||
rest,
|
||||
jsonRpc,
|
||||
view,
|
||||
injected,
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@ -105,7 +114,7 @@ describe('VegaConnectDialog', () => {
|
||||
expect(screen.getByTestId('connector-jsonRpc')).toHaveTextContent(
|
||||
'Connect Vega wallet'
|
||||
);
|
||||
expect(screen.getByTestId('connector-hosted')).toHaveTextContent(
|
||||
expect(screen.getByTestId('connector-rest')).toHaveTextContent(
|
||||
'Hosted Fairground wallet'
|
||||
);
|
||||
expect(screen.getByTestId('connector-view')).toHaveTextContent(
|
||||
@ -113,6 +122,17 @@ describe('VegaConnectDialog', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('displays browser wallet option if detected on window object', async () => {
|
||||
mockBrowserWallet();
|
||||
render(generateJSX());
|
||||
const list = await screen.findByTestId('connectors-list');
|
||||
expect(list.children).toHaveLength(4);
|
||||
expect(screen.getByTestId('connector-injected')).toHaveTextContent(
|
||||
'Connect Web wallet'
|
||||
);
|
||||
clearBrowserWallet();
|
||||
});
|
||||
|
||||
describe('RestConnector', () => {
|
||||
it('connects', async () => {
|
||||
const spy = jest
|
||||
@ -229,17 +249,19 @@ describe('VegaConnectDialog', () => {
|
||||
beforeEach(() => {
|
||||
spyOnCheckCompat = jest
|
||||
.spyOn(connectors.jsonRpc, 'checkCompat')
|
||||
.mockImplementation(() => delayedResolve(true));
|
||||
.mockImplementation(() => delayedResolve(true, delay));
|
||||
spyOnGetChainId = jest
|
||||
.spyOn(connectors.jsonRpc, 'getChainId')
|
||||
.mockImplementation(() => delayedResolve({ chainID: mockChainId }));
|
||||
.mockImplementation(() =>
|
||||
delayedResolve({ chainID: mockChainId }, delay)
|
||||
);
|
||||
spyOnConnectWallet = jest
|
||||
.spyOn(connectors.jsonRpc, 'connectWallet')
|
||||
.mockImplementation(() => delayedResolve(null));
|
||||
.mockImplementation(() => delayedResolve(null, delay));
|
||||
spyOnConnect = jest
|
||||
.spyOn(connectors.jsonRpc, 'connect')
|
||||
.mockImplementation(() =>
|
||||
delayedResolve([{ publicKey: 'pubkey', name: 'test key 1' }])
|
||||
delayedResolve([{ publicKey: 'pubkey', name: 'test key 1' }], delay)
|
||||
);
|
||||
});
|
||||
|
||||
@ -351,18 +373,6 @@ describe('VegaConnectDialog', () => {
|
||||
expect(screen.getByText('An unknown error occurred')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
function delayedResolve<T>(result: T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(result), delay);
|
||||
});
|
||||
}
|
||||
|
||||
function delayedReject<T>(result: T): Promise<T> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(result), delay);
|
||||
});
|
||||
}
|
||||
|
||||
async function selectJsonRpc() {
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
fireEvent.click(await screen.findByTestId('connector-jsonRpc'));
|
||||
@ -439,4 +449,109 @@ describe('VegaConnectDialog', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('InjectedConnector', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearBrowserWallet();
|
||||
});
|
||||
|
||||
it('connects', async () => {
|
||||
const delay = 100;
|
||||
const vegaWindow = {
|
||||
getChainId: jest.fn(() =>
|
||||
delayedResolve({ chainID: mockChainId }, 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);
|
||||
});
|
||||
|
||||
// Await user connect
|
||||
expect(screen.getByText('Connecting...')).toBeInTheDocument();
|
||||
expect(vegaWindow.connectWallet).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(delay);
|
||||
});
|
||||
|
||||
// Connect (list keys)
|
||||
expect(vegaWindow.listKeys).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(delay);
|
||||
});
|
||||
expect(screen.getByText('Successfully connected')).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(CLOSE_DELAY);
|
||||
});
|
||||
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'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -3,45 +3,50 @@ import {
|
||||
Button,
|
||||
Dialog,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
Link,
|
||||
Loader,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { VegaConnector } from '../connectors';
|
||||
import { InjectedConnector } from '../connectors';
|
||||
import { ViewConnector } from '../connectors';
|
||||
import { JsonRpcConnector, RestConnector } from '../connectors';
|
||||
import { RestConnectorForm } from './rest-connector-form';
|
||||
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
|
||||
import {
|
||||
Networks,
|
||||
useEnvironment,
|
||||
ExternalLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ConnectDialogContent,
|
||||
ConnectDialogFooter,
|
||||
ConnectDialogTitle,
|
||||
} from './connect-dialog-elements';
|
||||
import type { Status } from '../use-json-rpc-connect';
|
||||
import type { Status as JsonRpcStatus } from '../use-json-rpc-connect';
|
||||
import type { Status as InjectedStatus } from '../use-injected-connector';
|
||||
import { useJsonRpcConnect } from '../use-json-rpc-connect';
|
||||
import { ViewConnectorForm } from './view-connector-form';
|
||||
import { useChainIdQuery } from './__generated__/ChainId';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
import { useInjectedConnector } from '../use-injected-connector';
|
||||
import { InjectedConnectorForm } from './injected-connector-form';
|
||||
|
||||
export const CLOSE_DELAY = 1700;
|
||||
type Connectors = { [key: string]: VegaConnector };
|
||||
type WalletType = 'jsonRpc' | 'hosted' | 'view';
|
||||
export type WalletType = 'injected' | 'jsonRpc' | 'rest' | 'view';
|
||||
|
||||
export interface VegaConnectDialogProps {
|
||||
connectors: Connectors;
|
||||
onChangeOpen?: (open: boolean) => void;
|
||||
riskMessage?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface VegaWalletDialogStore {
|
||||
vegaWalletDialogOpen: boolean;
|
||||
updateVegaWalletDialog: (open: boolean) => void;
|
||||
openVegaWalletDialog: () => void;
|
||||
closeVegaWalletDialog: () => void;
|
||||
}
|
||||
|
||||
export const useVegaWalletDialogStore = create<VegaWalletDialogStore>()(
|
||||
(set) => ({
|
||||
vegaWalletDialogOpen: false,
|
||||
@ -52,32 +57,20 @@ export const useVegaWalletDialogStore = create<VegaWalletDialogStore>()(
|
||||
})
|
||||
);
|
||||
|
||||
export interface VegaWalletDialogStore {
|
||||
vegaWalletDialogOpen: boolean;
|
||||
updateVegaWalletDialog: (open: boolean) => void;
|
||||
openVegaWalletDialog: () => void;
|
||||
closeVegaWalletDialog: () => void;
|
||||
}
|
||||
|
||||
export const VegaConnectDialog = ({
|
||||
connectors,
|
||||
onChangeOpen,
|
||||
riskMessage,
|
||||
}: VegaConnectDialogProps) => {
|
||||
const { disconnect, acknowledgeNeeded } = useVegaWallet();
|
||||
const vegaWalletDialogOpen = useVegaWalletDialogStore(
|
||||
(store) => store.vegaWalletDialogOpen
|
||||
);
|
||||
const updateVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => (open: boolean) => {
|
||||
store.updateVegaWalletDialog(open);
|
||||
onChangeOpen?.(open);
|
||||
}
|
||||
);
|
||||
const closeVegaWalletDialog = useVegaWalletDialogStore((store) => () => {
|
||||
store.closeVegaWalletDialog();
|
||||
onChangeOpen?.(false);
|
||||
});
|
||||
const { disconnect, acknowledgeNeeded } = useVegaWallet();
|
||||
|
||||
const onVegaWalletDialogChange = useCallback(
|
||||
(open: boolean) => {
|
||||
updateVegaWalletDialog(open);
|
||||
@ -88,41 +81,9 @@ export const VegaConnectDialog = ({
|
||||
[updateVegaWalletDialog, acknowledgeNeeded, disconnect]
|
||||
);
|
||||
|
||||
const { data, error, loading } = useChainIdQuery();
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<ConnectDialogContent>
|
||||
<ConnectDialogTitle>
|
||||
{t('Could not retrieve chain id')}
|
||||
</ConnectDialogTitle>
|
||||
<ConnectDialogFooter />
|
||||
</ConnectDialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<ConnectDialogContent>
|
||||
<ConnectDialogTitle>{t('Fetching chain ID')}</ConnectDialogTitle>
|
||||
<div className="flex justify-center items-center my-6">
|
||||
<Loader />
|
||||
</div>
|
||||
<ConnectDialogFooter />
|
||||
</ConnectDialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectDialogContainer
|
||||
connectors={connectors}
|
||||
closeDialog={closeVegaWalletDialog}
|
||||
appChainId={data.statistics.chainId}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
// 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 { data } = useChainIdQuery();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -130,29 +91,35 @@ export const VegaConnectDialog = ({
|
||||
size="small"
|
||||
onChange={onVegaWalletDialogChange}
|
||||
>
|
||||
{renderContent()}
|
||||
{data && (
|
||||
<ConnectDialogContainer
|
||||
connectors={connectors}
|
||||
appChainId={data.statistics.chainId}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectDialogContainer = ({
|
||||
connectors,
|
||||
closeDialog,
|
||||
appChainId,
|
||||
riskMessage,
|
||||
}: {
|
||||
connectors: Connectors;
|
||||
closeDialog: () => void;
|
||||
appChainId: string;
|
||||
riskMessage?: React.ReactNode;
|
||||
}) => {
|
||||
const { VEGA_WALLET_URL, VEGA_ENV, HOSTED_WALLET_URL } = useEnvironment();
|
||||
const closeDialog = useVegaWalletDialogStore(
|
||||
(store) => store.closeVegaWalletDialog
|
||||
);
|
||||
const [selectedConnector, setSelectedConnector] = useState<VegaConnector>();
|
||||
const [walletUrl, setWalletUrl] = useState(VEGA_WALLET_URL || '');
|
||||
const [walletType, setWalletType] = useState<WalletType>();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setSelectedConnector(undefined);
|
||||
setWalletType(undefined);
|
||||
}, []);
|
||||
|
||||
const delayedOnConnect = useCallback(() => {
|
||||
@ -161,52 +128,59 @@ const ConnectDialogContainer = ({
|
||||
}, CLOSE_DELAY);
|
||||
}, [closeDialog]);
|
||||
|
||||
const { connect, ...jsonRpcState } = useJsonRpcConnect(delayedOnConnect);
|
||||
const { connect: jsonRpcConnect, ...jsonRpcState } =
|
||||
useJsonRpcConnect(delayedOnConnect);
|
||||
const { connect: injectedConnect, ...injectedState } =
|
||||
useInjectedConnector(delayedOnConnect);
|
||||
|
||||
const handleSelect = (type: WalletType, isHosted = false) => {
|
||||
let connector;
|
||||
const handleSelect = (type: WalletType) => {
|
||||
const connector = connectors[type];
|
||||
|
||||
if (isHosted) {
|
||||
// If the user has selected hosted wallet ensure that we are connecting to https://vega-hosted-wallet.on.fleek.co/
|
||||
// otherwise use the default walletUrl or what has been put in the input
|
||||
connector = connectors['rest'];
|
||||
connector.url = HOSTED_WALLET_URL || walletUrl;
|
||||
} else {
|
||||
connector = connectors[type];
|
||||
connector.url = walletUrl;
|
||||
}
|
||||
// If type is rest user has selected the hosted wallet option. So here
|
||||
// we ensure that we are connecting to https://vega-hosted-wallet.on.fleek.co/
|
||||
// otherwise use walletUrl which defaults to the localhost:1789
|
||||
connector.url = type === 'rest' ? HOSTED_WALLET_URL : walletUrl;
|
||||
|
||||
if (!connector) {
|
||||
// we should never get here unless connectors are not configured correctly
|
||||
throw new Error(`Connector type: ${type} not configured`);
|
||||
}
|
||||
|
||||
setSelectedConnector(connector);
|
||||
setWalletType(type);
|
||||
|
||||
// Immediately connect on selection if jsonRpc is selected, we can't do this
|
||||
// for rest because we need to show an authentication form
|
||||
if (connector instanceof JsonRpcConnector) {
|
||||
connect(connector, appChainId);
|
||||
jsonRpcConnect(connector, appChainId);
|
||||
} else if (connector instanceof InjectedConnector) {
|
||||
injectedConnect(connector, appChainId);
|
||||
}
|
||||
};
|
||||
|
||||
return selectedConnector !== undefined && walletType !== undefined ? (
|
||||
<SelectedForm
|
||||
type={walletType}
|
||||
connector={selectedConnector}
|
||||
jsonRpcState={jsonRpcState}
|
||||
onConnect={closeDialog}
|
||||
appChainId={appChainId}
|
||||
reset={reset}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
) : (
|
||||
<ConnectorList
|
||||
walletUrl={walletUrl}
|
||||
setWalletUrl={setWalletUrl}
|
||||
onSelect={handleSelect}
|
||||
isMainnet={VEGA_ENV === Networks.MAINNET}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogContent>
|
||||
{selectedConnector !== undefined ? (
|
||||
<SelectedForm
|
||||
connector={selectedConnector}
|
||||
jsonRpcState={jsonRpcState}
|
||||
injectedState={injectedState}
|
||||
onConnect={closeDialog}
|
||||
appChainId={appChainId}
|
||||
reset={reset}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
) : (
|
||||
<ConnectorList
|
||||
walletUrl={walletUrl}
|
||||
setWalletUrl={setWalletUrl}
|
||||
onSelect={handleSelect}
|
||||
isMainnet={VEGA_ENV === Networks.MAINNET}
|
||||
/>
|
||||
)}
|
||||
</ConnectDialogContent>
|
||||
<ConnectDialogFooter connector={selectedConnector} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -216,136 +190,129 @@ const ConnectorList = ({
|
||||
setWalletUrl,
|
||||
isMainnet,
|
||||
}: {
|
||||
onSelect: (type: WalletType, isHosted?: boolean) => void;
|
||||
onSelect: (type: WalletType) => void;
|
||||
walletUrl: string;
|
||||
setWalletUrl: (value: string) => void;
|
||||
isMainnet: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogContent>
|
||||
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
|
||||
<CustomUrlInput walletUrl={walletUrl} setWalletUrl={setWalletUrl} />
|
||||
<ul data-testid="connectors-list" className="mb-6">
|
||||
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
|
||||
<CustomUrlInput walletUrl={walletUrl} setWalletUrl={setWalletUrl} />
|
||||
<ul data-testid="connectors-list" className="mb-6">
|
||||
<li className="mb-4 last:mb-0">
|
||||
<ConnectionOption
|
||||
type="jsonRpc"
|
||||
text={t('Connect Vega wallet')}
|
||||
onClick={() => onSelect('jsonRpc')}
|
||||
/>
|
||||
</li>
|
||||
{'vega' in window && (
|
||||
<li className="mb-4 last:mb-0">
|
||||
<ConnectionOption
|
||||
type="jsonRpc"
|
||||
text={t('Connect Vega wallet')}
|
||||
onClick={() => onSelect('jsonRpc')}
|
||||
type="injected"
|
||||
text={t('Connect Web wallet')}
|
||||
onClick={() => onSelect('injected')}
|
||||
/>
|
||||
</li>
|
||||
{!isMainnet && (
|
||||
<li className="mb-4 last:mb-0">
|
||||
<ConnectionOption
|
||||
type="hosted"
|
||||
text={t('Hosted Fairground wallet')}
|
||||
onClick={() => onSelect('hosted', true)}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
)}
|
||||
{!isMainnet && (
|
||||
<li className="mb-4 last:mb-0">
|
||||
<div className="my-4 text-center text-vega-dark-400">{t('OR')}</div>
|
||||
<ConnectionOption
|
||||
type="view"
|
||||
text={t('View as vega user')}
|
||||
onClick={() => onSelect('view')}
|
||||
type="rest"
|
||||
text={t('Hosted Fairground wallet')}
|
||||
onClick={() => onSelect('rest')}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</ConnectDialogContent>
|
||||
<ConnectDialogFooter />
|
||||
)}
|
||||
<li className="mb-4 last:mb-0">
|
||||
<div className="my-4 text-center">{t('OR')}</div>
|
||||
<ConnectionOption
|
||||
type="view"
|
||||
text={t('View as vega user')}
|
||||
onClick={() => onSelect('view')}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectedForm = ({
|
||||
type,
|
||||
connector,
|
||||
appChainId,
|
||||
jsonRpcState,
|
||||
injectedState,
|
||||
reset,
|
||||
onConnect,
|
||||
riskMessage,
|
||||
}: {
|
||||
type: WalletType;
|
||||
connector: VegaConnector;
|
||||
appChainId: string;
|
||||
jsonRpcState: {
|
||||
status: Status;
|
||||
status: JsonRpcStatus;
|
||||
error: WalletClientError | null;
|
||||
};
|
||||
injectedState: {
|
||||
status: InjectedStatus;
|
||||
error: Error | null;
|
||||
};
|
||||
reset: () => void;
|
||||
onConnect: () => void;
|
||||
riskMessage?: React.ReactNode;
|
||||
}) => {
|
||||
if (connector instanceof InjectedConnector) {
|
||||
return (
|
||||
<InjectedConnectorForm
|
||||
status={injectedState.status}
|
||||
error={injectedState.error}
|
||||
onConnect={onConnect}
|
||||
appChainId={appChainId}
|
||||
reset={reset}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (connector instanceof RestConnector) {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogContent>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
|
||||
data-testid="back-button"
|
||||
>
|
||||
<Icon name={'chevron-left'} ariaLabel="back" size={4} />
|
||||
</button>
|
||||
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
|
||||
<div className="mb-2">
|
||||
<RestConnectorForm connector={connector} onConnect={onConnect} />
|
||||
</div>
|
||||
</ConnectDialogContent>
|
||||
{type === 'hosted' ? (
|
||||
<ConnectDialogFooter>
|
||||
<p className="text-center">
|
||||
{t('For demo purposes get a ')}
|
||||
<Link
|
||||
href={ExternalLinks.VEGA_WALLET_HOSTED_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('hosted wallet')}
|
||||
</Link>
|
||||
{t(', or for the real experience create a wallet in the ')}
|
||||
<Link href={ExternalLinks.VEGA_WALLET_URL}>
|
||||
{t('Vega wallet app')}
|
||||
</Link>
|
||||
</p>
|
||||
</ConnectDialogFooter>
|
||||
) : (
|
||||
<ConnectDialogFooter />
|
||||
)}
|
||||
<button
|
||||
onClick={reset}
|
||||
className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
|
||||
data-testid="back-button"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_LEFT} />
|
||||
</button>
|
||||
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
|
||||
<div className="mb-2">
|
||||
<RestConnectorForm connector={connector} onConnect={onConnect} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (connector instanceof JsonRpcConnector) {
|
||||
return (
|
||||
<ConnectDialogContent>
|
||||
<JsonRpcConnectorForm
|
||||
connector={connector}
|
||||
status={jsonRpcState.status}
|
||||
error={jsonRpcState.error}
|
||||
onConnect={onConnect}
|
||||
appChainId={appChainId}
|
||||
reset={reset}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
</ConnectDialogContent>
|
||||
<JsonRpcConnectorForm
|
||||
connector={connector}
|
||||
status={jsonRpcState.status}
|
||||
error={jsonRpcState.error}
|
||||
onConnect={onConnect}
|
||||
appChainId={appChainId}
|
||||
reset={reset}
|
||||
riskMessage={riskMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (connector instanceof ViewConnector) {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogContent>
|
||||
<ViewConnectorForm
|
||||
connector={connector}
|
||||
onConnect={onConnect}
|
||||
reset={reset}
|
||||
/>
|
||||
</ConnectDialogContent>
|
||||
<ConnectDialogFooter />
|
||||
</>
|
||||
<ViewConnectorForm
|
||||
connector={connector}
|
||||
onConnect={onConnect}
|
||||
reset={reset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -366,12 +333,12 @@ const ConnectionOption = ({
|
||||
onClick={onClick}
|
||||
size="lg"
|
||||
fill={true}
|
||||
variant={['hosted', 'view'].includes(type) ? 'default' : 'primary'}
|
||||
variant={['rest', 'view'].includes(type) ? 'default' : 'primary'}
|
||||
data-testid={`connector-${type}`}
|
||||
>
|
||||
<span className="-mx-6 flex text-left justify-between items-center">
|
||||
<span className="-mx-10 flex text-left justify-between items-center">
|
||||
{text}
|
||||
<Icon name="chevron-right" />
|
||||
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
@ -387,9 +354,7 @@ const CustomUrlInput = ({
|
||||
const [urlInputExpanded, setUrlInputExpanded] = useState(false);
|
||||
return urlInputExpanded ? (
|
||||
<>
|
||||
<p className="mb-2 text-neutral-600 dark:text-neutral-400">
|
||||
{t('Custom wallet location')}
|
||||
</p>
|
||||
<p className="mb-2">{t('Custom wallet location')}</p>
|
||||
<FormGroup
|
||||
labelFor="wallet-url"
|
||||
label={t('Custom wallet location')}
|
||||
@ -401,12 +366,10 @@ const CustomUrlInput = ({
|
||||
name="wallet-url"
|
||||
/>
|
||||
</FormGroup>
|
||||
<p className="mb-2 text-neutral-600 dark:text-neutral-400">
|
||||
{t('Choose wallet app to connect')}
|
||||
</p>
|
||||
<p className="mb-2">{t('Choose wallet app to connect')}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="mb-6 text-neutral-600 dark:text-neutral-400">
|
||||
<p className="mb-6">
|
||||
{t(
|
||||
'Choose wallet app to connect, or to change port or server URL enter a '
|
||||
)}
|
||||
|
153
libs/wallet/src/connect-dialog/injected-connector-form.tsx
Normal file
153
libs/wallet/src/connect-dialog/injected-connector-form.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Status } from '../use-injected-connector';
|
||||
import { ConnectDialogTitle } from './connect-dialog-elements';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
Diamond,
|
||||
Loader,
|
||||
Tick,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { setAcknowledged } from '../storage';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
|
||||
export const InjectedConnectorForm = ({
|
||||
status,
|
||||
onConnect,
|
||||
riskMessage,
|
||||
appChainId,
|
||||
reset,
|
||||
error,
|
||||
}: {
|
||||
// connector: JsonRpcConnector;
|
||||
appChainId: string;
|
||||
status: Status;
|
||||
error: Error | null;
|
||||
onConnect: () => void;
|
||||
reset: () => void;
|
||||
riskMessage?: React.ReactNode;
|
||||
}) => {
|
||||
const { disconnect } = useVegaWallet();
|
||||
|
||||
if (status === Status.Idle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === Status.Error) {
|
||||
return <Error error={error} appChainId={appChainId} onTryAgain={reset} />;
|
||||
}
|
||||
|
||||
if (status === Status.GettingChainId) {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{t('Verifying chain')}</ConnectDialogTitle>
|
||||
<Center>
|
||||
<Loader />
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === Status.Connected) {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{t('Successfully connected')}</ConnectDialogTitle>
|
||||
<Center>
|
||||
<Tick />
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === Status.Connecting) {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{t('Connecting...')}</ConnectDialogTitle>
|
||||
<Center>
|
||||
<Diamond />
|
||||
</Center>
|
||||
<p className="text-center">
|
||||
{t(
|
||||
"Approve the connection from your Vega wallet app. If you have multiple wallets you'll need to choose which to connect with."
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === Status.AcknowledgeNeeded) {
|
||||
const setConnection = () => {
|
||||
setAcknowledged();
|
||||
onConnect();
|
||||
};
|
||||
const handleDisagree = () => {
|
||||
disconnect();
|
||||
onConnect(); // this is dialog closing
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{t('Understand the risk')}</ConnectDialogTitle>
|
||||
{riskMessage}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div>
|
||||
<Button onClick={handleDisagree} fill>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={setConnection} variant="primary" fill>
|
||||
{t('I agree')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const Center = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<div className="flex justify-center items-center my-6">{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Error = ({
|
||||
error,
|
||||
appChainId,
|
||||
onTryAgain,
|
||||
}: {
|
||||
error: Error | null;
|
||||
appChainId: string;
|
||||
onTryAgain: () => void;
|
||||
}) => {
|
||||
let title = t('Something went wrong');
|
||||
let text: ReactNode | undefined = t('An unknown error occurred');
|
||||
const tryAgain: ReactNode | null = (
|
||||
<p className="text-center">
|
||||
<ButtonLink onClick={onTryAgain}>{t('Try again')}</ButtonLink>
|
||||
</p>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error.message === 'Invalid chain') {
|
||||
title = t('Wrong network');
|
||||
text = t(
|
||||
'To complete your wallet connection, set your wallet network in your app to "%s".',
|
||||
appChainId
|
||||
);
|
||||
} else if (error.message === 'window.vega not found') {
|
||||
title = t('No wallet detected');
|
||||
text = t('Vega browser extension not installed');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{title}</ConnectDialogTitle>
|
||||
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
|
||||
{tryAgain}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,21 +1,83 @@
|
||||
import type { VegaConnector } from './vega-connector';
|
||||
import { clearConfig, setConfig } from '../storage';
|
||||
import type { Transaction, VegaConnector } from './vega-connector';
|
||||
|
||||
declare global {
|
||||
interface Vega {
|
||||
getChainId: () => Promise<{ chainID: string }>;
|
||||
connectWallet: () => Promise<null>;
|
||||
disconnectWallet: () => Promise<void>;
|
||||
listKeys: () => Promise<{
|
||||
keys: Array<{ name: string; publicKey: string }>;
|
||||
}>;
|
||||
sendTransaction: (params: {
|
||||
publicKey: string;
|
||||
transaction: Transaction;
|
||||
sendingMode: 'TYPE_SYNC';
|
||||
}) => Promise<{
|
||||
receivedAt: string;
|
||||
sentAt: string;
|
||||
transaction: {
|
||||
from: {
|
||||
pubKey: string;
|
||||
};
|
||||
inputData: string;
|
||||
pow: {
|
||||
tid: string;
|
||||
nonce: string;
|
||||
};
|
||||
signature: {
|
||||
algo: string;
|
||||
value: string;
|
||||
version: number;
|
||||
};
|
||||
version: number;
|
||||
};
|
||||
transactionHash: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
vega: Vega;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy injected connector that we may use when browser wallet is implemented
|
||||
*/
|
||||
export class InjectedConnector implements VegaConnector {
|
||||
description = 'Connects using the Vega wallet browser extension';
|
||||
|
||||
async getChainId() {
|
||||
return window.vega.getChainId();
|
||||
}
|
||||
|
||||
connectWallet() {
|
||||
return window.vega.connectWallet();
|
||||
}
|
||||
|
||||
async connect() {
|
||||
return [{ publicKey: '0x123', name: 'text key' }];
|
||||
const res = await window.vega.listKeys();
|
||||
setConfig({
|
||||
connector: 'injected',
|
||||
token: null, // no token required for injected
|
||||
url: null, // no url for injected
|
||||
});
|
||||
return res.keys;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
return;
|
||||
disconnect() {
|
||||
clearConfig();
|
||||
return window.vega.disconnectWallet();
|
||||
}
|
||||
|
||||
// @ts-ignore injected connector is not implemented
|
||||
sendTx() {
|
||||
throw new Error('Not implemented');
|
||||
async sendTx(pubKey: string, transaction: Transaction) {
|
||||
const result = await window.vega.sendTransaction({
|
||||
publicKey: pubKey,
|
||||
transaction,
|
||||
sendingMode: 'TYPE_SYNC' as const,
|
||||
});
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
receivedAt: result.receivedAt,
|
||||
sentAt: result.sentAt,
|
||||
signature: result.transaction.signature.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -411,7 +411,7 @@ export interface PubKey {
|
||||
}
|
||||
|
||||
export interface VegaConnector {
|
||||
url: string | null;
|
||||
url?: string | null;
|
||||
|
||||
/** Connect to wallet and return keys */
|
||||
connect(): Promise<PubKey[] | null>;
|
||||
|
@ -106,7 +106,6 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
||||
if (!connector.current) {
|
||||
throw new Error('No connector');
|
||||
}
|
||||
|
||||
return connector.current.sendTx(pubkey, transaction);
|
||||
}, []);
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/utils';
|
||||
|
||||
interface ConnectorConfig {
|
||||
token: string | null;
|
||||
connector: 'rest' | 'jsonRpc' | 'view';
|
||||
connector: 'injected' | 'rest' | 'jsonRpc' | 'view';
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
|
39
libs/wallet/src/test-helpers.ts
Normal file
39
libs/wallet/src/test-helpers.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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
|
||||
.fn()
|
||||
.mockReturnValue({ keys: [{ name: 'test key', publicKey: '0x123' }] }),
|
||||
sendTransaction: jest.fn().mockReturnValue({
|
||||
code: 1,
|
||||
data: '',
|
||||
height: '1',
|
||||
log: '',
|
||||
success: true,
|
||||
txHash: '0x123',
|
||||
}),
|
||||
...overrides,
|
||||
};
|
||||
// @ts-ignore globalThis has no index signature
|
||||
globalThis.vega = vega;
|
||||
return vega;
|
||||
}
|
||||
|
||||
export function clearBrowserWallet() {
|
||||
// @ts-ignore no index signature on globalThis
|
||||
delete globalThis['vega'];
|
||||
}
|
||||
|
||||
export function delayedResolve<T>(result: T, delay = 0): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(result), delay);
|
||||
});
|
||||
}
|
||||
|
||||
export function delayedReject<T>(result: T, delay = 0): Promise<T> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(result), delay);
|
||||
});
|
||||
}
|
104
libs/wallet/src/use-injected-connector.spec.tsx
Normal file
104
libs/wallet/src/use-injected-connector.spec.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { Status, useInjectedConnector } from './use-injected-connector';
|
||||
import type { ReactNode } from 'react';
|
||||
import { VegaWalletProvider } from './provider';
|
||||
import { InjectedConnector } from './connectors';
|
||||
import { mockBrowserWallet } from './test-helpers';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Networks } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('@vegaprotocol/environment');
|
||||
|
||||
const setup = (callback = jest.fn()) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<VegaWalletProvider>{children}</VegaWalletProvider>
|
||||
);
|
||||
return renderHook(() => useInjectedConnector(callback), { wrapper });
|
||||
};
|
||||
|
||||
const injected = new InjectedConnector();
|
||||
|
||||
describe('useInjectedConnector', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore useEnvironment has been mocked
|
||||
useEnvironment.mockImplementation(() => ({ VEGA_ENV: Networks.TESTNET }));
|
||||
});
|
||||
it('attempts connection', async () => {
|
||||
const { result } = setup();
|
||||
expect(typeof result.current.connect).toBe('function');
|
||||
expect(result.current.status).toBe(Status.Idle);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
it('errors if vega not injected', async () => {
|
||||
const { result } = setup();
|
||||
await act(async () => {
|
||||
result.current.connect(injected, '1');
|
||||
});
|
||||
expect(result.current.error?.message).toBe('window.vega not found');
|
||||
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'),
|
||||
});
|
||||
const { result } = setup(callback);
|
||||
|
||||
await act(async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('connects', async () => {
|
||||
const callback = jest.fn();
|
||||
const vega = mockBrowserWallet();
|
||||
const { result } = setup(callback);
|
||||
|
||||
act(() => {
|
||||
result.current.connect(injected, '1'); // default mock chainId is '1'
|
||||
});
|
||||
expect(result.current.status).toBe(Status.GettingChainId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vega.connectWallet).toHaveBeenCalled();
|
||||
expect(vega.listKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(result.current.status).toBe(Status.Connected);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('connects when aknowledgement required', async () => {
|
||||
const callback = jest.fn();
|
||||
// @ts-ignore useEnvironment has been mocked
|
||||
useEnvironment.mockImplementation(() => ({ VEGA_ENV: Networks.MAINNET }));
|
||||
|
||||
const vega = mockBrowserWallet();
|
||||
const { result } = setup(callback);
|
||||
|
||||
act(() => {
|
||||
result.current.connect(injected, '1'); // default mock chainId is '1'
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vega.listKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(result.current.status).toBe(Status.AcknowledgeNeeded);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
61
libs/wallet/src/use-injected-connector.ts
Normal file
61
libs/wallet/src/use-injected-connector.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { InjectedConnector } from './connectors';
|
||||
import { useVegaWallet } from './use-vega-wallet';
|
||||
|
||||
export enum Status {
|
||||
Idle = 'Idle',
|
||||
GettingChainId = 'GettingChainId',
|
||||
Connecting = 'Connecting',
|
||||
Connected = 'Connected',
|
||||
Error = 'Error',
|
||||
AcknowledgeNeeded = 'AcknowledgeNeeded',
|
||||
}
|
||||
|
||||
export const useInjectedConnector = (onConnect: () => void) => {
|
||||
const { connect, acknowledgeNeeded } = useVegaWallet();
|
||||
const [status, setStatus] = useState(Status.Idle);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const attemptConnect = useCallback(
|
||||
async (connector: InjectedConnector, appChainId: string) => {
|
||||
try {
|
||||
if (!('vega' in window)) {
|
||||
throw new Error('window.vega not found');
|
||||
}
|
||||
|
||||
setStatus(Status.GettingChainId);
|
||||
|
||||
const { chainID } = await connector.getChainId();
|
||||
|
||||
if (chainID !== appChainId) {
|
||||
throw new Error('Invalid chain');
|
||||
}
|
||||
|
||||
setStatus(Status.Connecting);
|
||||
await connector.connectWallet(); // authorize wallet
|
||||
await connect(connector); // connect with keys
|
||||
|
||||
if (acknowledgeNeeded) {
|
||||
setStatus(Status.AcknowledgeNeeded);
|
||||
} else {
|
||||
setStatus(Status.Connected);
|
||||
onConnect();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err);
|
||||
} else {
|
||||
setError(new Error('injected connection failed'));
|
||||
}
|
||||
setStatus(Status.Error);
|
||||
}
|
||||
},
|
||||
[acknowledgeNeeded, connect, onConnect]
|
||||
);
|
||||
|
||||
return {
|
||||
status,
|
||||
error,
|
||||
connect: attemptConnect,
|
||||
};
|
||||
};
|
@ -11,7 +11,6 @@ export enum Status {
|
||||
GettingChainId = 'GettingChainId',
|
||||
Connecting = 'Connecting',
|
||||
GettingPerms = 'GettingPerms',
|
||||
ListingKeys = 'ListingKeys',
|
||||
Connected = 'Connected',
|
||||
Error = 'Error',
|
||||
AcknowledgeNeeded = 'AcknowledgeNeeded',
|
||||
|
Loading…
Reference in New Issue
Block a user