feat: view apps as any pub key (#2548)
This commit is contained in:
parent
c533c584da
commit
f5b345636c
@ -17,6 +17,18 @@ import {
|
||||
import { useContracts } from './contexts/contracts/contracts-context';
|
||||
import { useRefreshAssociatedBalances } from './hooks/use-refresh-associated-balances';
|
||||
import { Connectors } from './lib/vega-connectors';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const useVegaWalletEagerConnect = () => {
|
||||
const vegaConnecting = useEagerConnect(Connectors);
|
||||
const { pubKey, connect } = useVegaWallet();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [query] = React.useState(searchParams.get('address'));
|
||||
if (query && !pubKey) {
|
||||
connect(Connectors['view']);
|
||||
}
|
||||
return vegaConnecting;
|
||||
};
|
||||
|
||||
export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
const { t } = useTranslation();
|
||||
@ -27,7 +39,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
const { token, staking, vesting } = useContracts();
|
||||
const setAssociatedBalances = useRefreshAssociatedBalances();
|
||||
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
|
||||
const vegaConnecting = useEagerConnect(Connectors);
|
||||
const vegaConnecting = useVegaWalletEagerConnect();
|
||||
|
||||
const loaded = balancesLoaded && !vegaConnecting;
|
||||
|
||||
|
@ -88,7 +88,7 @@ const Web3Container = ({
|
||||
<AppLoader>
|
||||
<BalanceManager>
|
||||
<>
|
||||
<div className="app w-full max-w-[1500px] mx-auto grid grid-rows-[min-content_1fr_min-content] min-h-full border-neutral-700 lg:border-l lg:border-r lg:text-body-large">
|
||||
<div className="app w-full max-w-[1500px] mx-auto grid grid-rows-[min-content_min-content_1fr_min-content] min-h-full border-neutral-700 lg:border-l lg:border-r lg:text-body-large">
|
||||
<TemplateSidebar sidebar={sideBar}>
|
||||
<AppRouter />
|
||||
</TemplateSidebar>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import React from 'react';
|
||||
|
||||
import { Nav } from '../nav';
|
||||
@ -10,9 +12,15 @@ export interface TemplateSidebarProps {
|
||||
|
||||
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
|
||||
return (
|
||||
<>
|
||||
<Nav navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
|
||||
{isReadOnly ? (
|
||||
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="w-full border-b border-neutral-700 lg:grid lg:grid-rows-[min-content_1fr] lg:grid-cols-[1fr_450px]">
|
||||
<main className="col-start-1 p-4">{children}</main>
|
||||
<aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700">
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { RestConnector, JsonRpcConnector } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
RestConnector,
|
||||
JsonRpcConnector,
|
||||
ViewConnector,
|
||||
} from '@vegaprotocol/wallet';
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
export const rest = new RestConnector();
|
||||
export const jsonRpc = new JsonRpcConnector();
|
||||
export const view = new ViewConnector(urlParams.get('address'));
|
||||
|
||||
export const Connectors = {
|
||||
rest,
|
||||
jsonRpc,
|
||||
view,
|
||||
};
|
||||
|
@ -55,6 +55,7 @@ describe('Vote buttons', () => {
|
||||
const mockWalletNoPubKeyContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
|
@ -43,7 +43,6 @@ export const ProposeFreeform = () => {
|
||||
NetworkParams.governance_proposal_freeform_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -85,7 +85,6 @@ export const ProposeNetworkParameter = () => {
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams();
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -55,7 +55,6 @@ export const ProposeNewAsset = () => {
|
||||
NetworkParams.governance_proposal_asset_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -53,7 +53,6 @@ export const ProposeNewMarket = () => {
|
||||
NetworkParams.governance_proposal_market_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -42,7 +42,6 @@ export const ProposeRaw = () => {
|
||||
NetworkParams.governance_proposal_freeform_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -53,7 +53,6 @@ export const ProposeUpdateAsset = () => {
|
||||
NetworkParams.governance_proposal_updateAsset_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -94,7 +94,6 @@ export const ProposeUpdateMarket = () => {
|
||||
const [selectedMarket, setSelectedMarket] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -11,6 +11,7 @@ export const mockPubkey: PubKey = {
|
||||
export const mockWalletContext = {
|
||||
pubKey: mockPubkey.publicKey,
|
||||
pubKeys: [mockPubkey],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
|
11
apps/trading/components/viewing-banner/index.tsx
Normal file
11
apps/trading/components/viewing-banner/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
export const ViewingBanner = () => {
|
||||
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
|
||||
return isReadOnly ? (
|
||||
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@ -1,9 +1,22 @@
|
||||
import { RestConnector, JsonRpcConnector } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
RestConnector,
|
||||
JsonRpcConnector,
|
||||
ViewConnector,
|
||||
} from '@vegaprotocol/wallet';
|
||||
|
||||
export const rest = new RestConnector();
|
||||
export const jsonRpc = new JsonRpcConnector();
|
||||
|
||||
let view: ViewConnector;
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.hash.split('?')[1]);
|
||||
view = new ViewConnector(urlParams.get('address'));
|
||||
} else {
|
||||
view = new ViewConnector();
|
||||
}
|
||||
|
||||
export const Connectors = {
|
||||
rest,
|
||||
jsonRpc,
|
||||
view,
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
VegaWalletProvider,
|
||||
useVegaTransactionManager,
|
||||
useVegaTransactionUpdater,
|
||||
useVegaWallet,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useEagerConnect as useEthereumEagerConnect,
|
||||
@ -29,8 +30,9 @@ import { Footer } from '../components/footer';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import DialogsContainer from './dialogs-container';
|
||||
import ToastsManager from './toasts-manager';
|
||||
import { HashRouter, useLocation } from 'react-router-dom';
|
||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@ -78,10 +80,11 @@ function AppBody({ Component }: AppProps) {
|
||||
<VegaWalletProvider>
|
||||
<AppLoader>
|
||||
<Web3Provider>
|
||||
<div className="h-full relative z-0 grid grid-rows-[min-content,1fr,min-content]">
|
||||
<div className="h-full relative z-0 grid grid-rows-[min-content,min-content,1fr,min-content]">
|
||||
<Navbar
|
||||
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
|
||||
/>
|
||||
<ViewingBanner />
|
||||
<main data-testid={location.pathname}>
|
||||
<Component />
|
||||
</main>
|
||||
@ -133,5 +136,12 @@ export default VegaTradingApp;
|
||||
const MaybeConnectEagerly = () => {
|
||||
useVegaEagerConnect(Connectors);
|
||||
useEthereumEagerConnect();
|
||||
|
||||
const { pubKey, connect } = useVegaWallet();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [query] = useState(searchParams.get('address'));
|
||||
if (query && !pubKey) {
|
||||
connect(Connectors['view']);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
isWithdrawTransaction,
|
||||
useVegaTransactionStore,
|
||||
VegaTxStatus,
|
||||
WalletError,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { VegaTransaction } from '../components/vega-transaction';
|
||||
import { VerificationStatus } from '@vegaprotocol/withdraws';
|
||||
@ -284,13 +285,17 @@ export const ToastsManager = () => {
|
||||
if (tx.status === VegaTxStatus.Error) {
|
||||
toast = {
|
||||
render: () => {
|
||||
const errorMessage = `${tx.error?.message} ${
|
||||
tx.error?.data ? `: ${tx.error?.data}` : ''
|
||||
const error = `${tx.error?.message} ${
|
||||
tx.error instanceof WalletError
|
||||
? tx.error?.data
|
||||
? `: ${tx.error?.data}`
|
||||
: ''
|
||||
: ''
|
||||
}`;
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Error occurred')}</h3>
|
||||
<p>{errorMessage}</p>
|
||||
<p>{error}</p>
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
|
@ -54,7 +54,8 @@ const ErrorContent = ({ transaction, reset }: ErrorContentProps) => {
|
||||
}
|
||||
return (
|
||||
<p data-testid={transaction.status}>
|
||||
{error.message}: {error.data}
|
||||
{error.message}{' '}
|
||||
{error instanceof WalletError ? `: ${error.data}` : null}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import * as Schema from '@vegaprotocol/types';
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
|
@ -14,6 +14,7 @@ import * as Schema from '@vegaprotocol/types';
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
|
@ -40,6 +40,7 @@ const defaultMarket = {
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
|
@ -17,6 +17,7 @@ const pubKey = 'test-pubkey';
|
||||
const defaultWalletContext = {
|
||||
pubKey,
|
||||
pubKeys: [{ publicKey: pubKey, name: 'test pubkey' }],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
|
@ -39,6 +39,7 @@ export * from './toggle';
|
||||
export * from './tooltip';
|
||||
export * from './vega-icons';
|
||||
export * from './vega-logo';
|
||||
export * from './viewing-as-user';
|
||||
export * from './traffic-light';
|
||||
export * from './toast';
|
||||
export * from './notification';
|
||||
|
31
libs/ui-toolkit/src/components/viewing-as-user/index.tsx
Normal file
31
libs/ui-toolkit/src/components/viewing-as-user/index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Button } from '../button';
|
||||
import React from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export function truncateMiddle(address: string) {
|
||||
if (address.length < 11) return address;
|
||||
return (
|
||||
address.slice(0, 6) +
|
||||
'\u2026' +
|
||||
address.slice(address.length - 4, address.length)
|
||||
);
|
||||
}
|
||||
|
||||
export interface ViewingAsBannerProps {
|
||||
pubKey: string | null;
|
||||
disconnect: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const ViewingAsBanner = ({
|
||||
pubKey,
|
||||
disconnect,
|
||||
}: ViewingAsBannerProps) => {
|
||||
return (
|
||||
<div className="w-full p-2 bg-neutral-800 flex justify-between text-neutral-400">
|
||||
<div className="text-base flex items-center justify-center">
|
||||
{t('Viewing as Vega user:')} {pubKey && truncateMiddle(pubKey)}
|
||||
</div>
|
||||
<Button onClick={disconnect}>{t('Exit view as')}</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -14,6 +14,7 @@ import {
|
||||
ClientErrors,
|
||||
JsonRpcConnector,
|
||||
RestConnector,
|
||||
ViewConnector,
|
||||
WalletError,
|
||||
} from '../connectors';
|
||||
import { EnvironmentProvider } from '@vegaprotocol/environment';
|
||||
@ -30,11 +31,15 @@ jest.mock('zustand', () => () => () => ({
|
||||
|
||||
let defaultProps: VegaConnectDialogProps;
|
||||
|
||||
const INITIAL_KEY = 'some-key';
|
||||
|
||||
const rest = new RestConnector();
|
||||
const jsonRpc = new JsonRpcConnector();
|
||||
const view = new ViewConnector(INITIAL_KEY);
|
||||
const connectors = {
|
||||
rest,
|
||||
jsonRpc,
|
||||
view,
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@ -89,13 +94,16 @@ describe('VegaConnectDialog', () => {
|
||||
rerender(generateJSX());
|
||||
const list = await screen.findByTestId('connectors-list');
|
||||
expect(list).toBeInTheDocument();
|
||||
expect(list.children).toHaveLength(2);
|
||||
expect(list.children).toHaveLength(3);
|
||||
expect(screen.getByTestId('connector-jsonRpc')).toHaveTextContent(
|
||||
'Connect Vega wallet'
|
||||
);
|
||||
expect(screen.getByTestId('connector-hosted')).toHaveTextContent(
|
||||
'Hosted Fairground wallet'
|
||||
);
|
||||
expect(screen.getByTestId('connector-view')).toHaveTextContent(
|
||||
'View as vega user'
|
||||
);
|
||||
});
|
||||
|
||||
describe('RestConnector', () => {
|
||||
@ -346,4 +354,75 @@ describe('VegaConnectDialog', () => {
|
||||
fireEvent.click(await screen.findByTestId('connector-jsonRpc'));
|
||||
}
|
||||
});
|
||||
|
||||
describe('ViewOnlyConnector', () => {
|
||||
const fillInForm = (address = '0'.repeat(64)) => {
|
||||
fireEvent.change(screen.getByTestId('address'), {
|
||||
target: { value: address },
|
||||
});
|
||||
return { address };
|
||||
};
|
||||
|
||||
it('connects', async () => {
|
||||
const spy = jest.spyOn(connectors.view, 'connect');
|
||||
|
||||
render(generateJSX());
|
||||
// Switches to view form
|
||||
fireEvent.click(await screen.findByText('View as vega user'));
|
||||
|
||||
// Client side validation
|
||||
fireEvent.submit(screen.getByTestId('view-connector-form'));
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Required')).toHaveLength(1);
|
||||
});
|
||||
|
||||
fillInForm();
|
||||
|
||||
// Wait for auth method to be called
|
||||
await act(async () => {
|
||||
fireEvent.submit(screen.getByTestId('view-connector-form'));
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
expect(mockCloseVegaDialog).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ensures pubkey is of correct length', async () => {
|
||||
render(generateJSX());
|
||||
// Switches to view form
|
||||
fireEvent.click(await screen.findByText('View as vega user'));
|
||||
|
||||
fillInForm('123');
|
||||
|
||||
// Wait for auth method to be called
|
||||
await act(async () => {
|
||||
fireEvent.submit(screen.getByTestId('view-connector-form'));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText('Pubkey must be 64 characters in length')
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures pubkey is of valid hex', async () => {
|
||||
render(generateJSX());
|
||||
// Switches to view form
|
||||
fireEvent.click(await screen.findByText('View as vega user'));
|
||||
|
||||
fillInForm('q'.repeat(64));
|
||||
|
||||
// Wait for auth method to be called
|
||||
await act(async () => {
|
||||
fireEvent.submit(screen.getByTestId('view-connector-form'));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Pubkey must be be valid hex')).toHaveLength(
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ExternalLinks, t, useChainIdQuery } from '@vegaprotocol/react-helpers';
|
||||
import type { VegaConnector, WalletError } from '../connectors';
|
||||
import { ViewConnector } from '../connectors';
|
||||
import { JsonRpcConnector, RestConnector } from '../connectors';
|
||||
import { RestConnectorForm } from './rest-connector-form';
|
||||
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
|
||||
@ -22,10 +23,11 @@ import {
|
||||
} from './connect-dialog-elements';
|
||||
import type { Status } from '../use-json-rpc-connect';
|
||||
import { useJsonRpcConnect } from '../use-json-rpc-connect';
|
||||
import { ViewConnectorForm } from './view-connector-form';
|
||||
|
||||
export const CLOSE_DELAY = 1700;
|
||||
type Connectors = { [key: string]: VegaConnector };
|
||||
type WalletType = 'jsonRpc' | 'hosted';
|
||||
type WalletType = 'jsonRpc' | 'hosted' | 'view';
|
||||
|
||||
export interface VegaConnectDialogProps {
|
||||
connectors: Connectors;
|
||||
@ -217,7 +219,7 @@ const ConnectorList = ({
|
||||
/>
|
||||
</li>
|
||||
{!isMainnet && (
|
||||
<li className="mb-0 border-t pt-4">
|
||||
<li className="mb-4 last:mb-0">
|
||||
<ConnectionOption
|
||||
type="hosted"
|
||||
text={t('Hosted Fairground wallet')}
|
||||
@ -225,6 +227,13 @@ const ConnectorList = ({
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
<li className="mb-4 last:mb-0">
|
||||
<ConnectionOption
|
||||
type="view"
|
||||
text={t('View as vega user')}
|
||||
onClick={() => onSelect('view')}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</ConnectDialogContent>
|
||||
<ConnectDialogFooter />
|
||||
@ -301,6 +310,17 @@ const SelectedForm = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (connector instanceof ViewConnector) {
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogContent>
|
||||
<ViewConnectorForm connector={connector} onConnect={onConnect} />
|
||||
</ConnectDialogContent>
|
||||
<ConnectDialogFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('No connector selected');
|
||||
};
|
||||
|
||||
@ -318,7 +338,7 @@ const ConnectionOption = ({
|
||||
onClick={onClick}
|
||||
size="lg"
|
||||
fill={true}
|
||||
variant={type === 'hosted' ? 'default' : 'primary'}
|
||||
variant={['hosted', 'view'].includes(type) ? 'default' : 'primary'}
|
||||
data-testid={`connector-${type}`}
|
||||
>
|
||||
<span className="-mx-6 flex text-left justify-between items-center">
|
||||
|
64
libs/wallet/src/connect-dialog/view-connector-form.tsx
Normal file
64
libs/wallet/src/connect-dialog/view-connector-form.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Button, FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { ViewConnector } from '../connectors';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
|
||||
interface FormFields {
|
||||
address: string;
|
||||
}
|
||||
|
||||
interface RestConnectorFormProps {
|
||||
connector: ViewConnector;
|
||||
onConnect: (connector: ViewConnector) => void;
|
||||
}
|
||||
|
||||
export function ViewConnectorForm({
|
||||
connector,
|
||||
onConnect,
|
||||
}: RestConnectorFormProps) {
|
||||
const { connect } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>();
|
||||
|
||||
const validatePubkey = (value: string) => {
|
||||
const number = +`0x${value}`;
|
||||
if (value.length !== 64) {
|
||||
return t('Pubkey must be 64 characters in length');
|
||||
} else if (Number.isNaN(number)) {
|
||||
return t('Pubkey must be be valid hex');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
async function onSubmit(fields: FormFields) {
|
||||
await connector.setPubkey(fields.address);
|
||||
await connect(connector);
|
||||
onConnect(connector);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} data-testid="view-connector-form">
|
||||
<FormGroup label={t('Vega Pubkey')} labelFor="address">
|
||||
<Input
|
||||
{...register('address', {
|
||||
required: t('Required'),
|
||||
validate: validatePubkey,
|
||||
})}
|
||||
id="address"
|
||||
data-testid="address"
|
||||
type="text"
|
||||
/>
|
||||
{errors.address?.message && (
|
||||
<InputError intent="danger">{errors.address.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button variant="primary" type="submit" fill={true}>
|
||||
{t('Connect')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -2,3 +2,4 @@ export * from './vega-connector';
|
||||
export * from './rest-connector';
|
||||
export * from './injected-connector';
|
||||
export * from './json-rpc-connector';
|
||||
export * from './view-connector';
|
||||
|
52
libs/wallet/src/connectors/view-connector.tsx
Normal file
52
libs/wallet/src/connectors/view-connector.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import type {
|
||||
PubKey,
|
||||
TransactionResponse,
|
||||
VegaConnector,
|
||||
} from './vega-connector';
|
||||
import { clearConfig, getConfig, setConfig } from '../storage';
|
||||
|
||||
export class ViewConnector implements VegaConnector {
|
||||
url: string | null;
|
||||
pubkey: string | null | undefined = null;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(pubkey?: string | null) {
|
||||
this.url = 'view-only';
|
||||
const cfg = getConfig();
|
||||
|
||||
if (pubkey || cfg?.token) {
|
||||
this.pubkey = pubkey || cfg?.token;
|
||||
}
|
||||
}
|
||||
setPubkey(pubkey: string) {
|
||||
this.pubkey = pubkey;
|
||||
}
|
||||
connect(): Promise<PubKey[] | null> {
|
||||
if (!this.pubkey) {
|
||||
throw new Error('Cannot connect until address is set first');
|
||||
}
|
||||
setConfig({
|
||||
token: this.pubkey,
|
||||
connector: 'view',
|
||||
url: this.url,
|
||||
});
|
||||
return Promise.resolve([
|
||||
{
|
||||
name: 'View only pubkey',
|
||||
publicKey: this.pubkey,
|
||||
},
|
||||
]);
|
||||
}
|
||||
disconnect(): Promise<void> {
|
||||
clearConfig();
|
||||
this.pubkey = null;
|
||||
return Promise.resolve();
|
||||
}
|
||||
sendTx(): Promise<TransactionResponse | null> {
|
||||
throw new Error(
|
||||
`You are connected in a view only state for public key: ${this.pubkey}. In order to send transactions you must connect to a real wallet.`
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ import type {
|
||||
} from './connectors';
|
||||
|
||||
export interface VegaWalletContextShape {
|
||||
/** If the current connector does not support signing transactions */
|
||||
isReadOnly: boolean;
|
||||
/** The current select public key */
|
||||
pubKey: string | null;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { Transaction } from './connectors';
|
||||
import { ViewConnector } from './connectors';
|
||||
import { RestConnector } from './connectors';
|
||||
import { useVegaWallet } from './use-vega-wallet';
|
||||
import { VegaWalletProvider } from './provider';
|
||||
@ -8,6 +9,7 @@ import type { ReactNode } from 'react';
|
||||
import { WALLET_KEY } from './storage';
|
||||
|
||||
const restConnector = new RestConnector();
|
||||
const viewConnector = new ViewConnector();
|
||||
|
||||
const setup = () => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
@ -43,6 +45,7 @@ describe('VegaWalletProvider', () => {
|
||||
expect(result.current).toEqual({
|
||||
pubKey: null,
|
||||
pubKeys: null,
|
||||
isReadOnly: false,
|
||||
selectPubKey: expect.any(Function),
|
||||
connect: expect.any(Function),
|
||||
disconnect: expect.any(Function),
|
||||
@ -92,4 +95,17 @@ describe('VegaWalletProvider', () => {
|
||||
expect(spyOnDisconnect).toHaveBeenCalled();
|
||||
expect(localStorage.getItem(WALLET_KEY)).toBe(null);
|
||||
});
|
||||
|
||||
it('sets isReadOnly to true if using view connector', async () => {
|
||||
jest
|
||||
.spyOn(viewConnector, 'connect')
|
||||
.mockImplementation(() => Promise.resolve(mockPubKeys));
|
||||
const { result } = setup();
|
||||
expect(result.current.pubKey).toBe(null);
|
||||
|
||||
await act(async () => {
|
||||
result.current.connect(viewConnector);
|
||||
});
|
||||
expect(result.current.isReadOnly).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
import { VegaWalletContext } from './context';
|
||||
import { WALLET_KEY } from './storage';
|
||||
import { WalletError } from './connectors/vega-connector';
|
||||
import { ViewConnector } from './connectors';
|
||||
|
||||
interface VegaWalletProviderProps {
|
||||
children: ReactNode;
|
||||
@ -18,6 +19,7 @@ interface VegaWalletProviderProps {
|
||||
export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
||||
// Current selected pubKey
|
||||
const [pubKey, setPubKey] = useState<string | null>(null);
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
|
||||
|
||||
// Arary of pubkeys retrieved from the connector
|
||||
const [pubKeys, setPubKeys] = useState<PubKey[] | null>(null);
|
||||
@ -37,7 +39,7 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
||||
|
||||
if (keys?.length) {
|
||||
setPubKeys(keys);
|
||||
|
||||
setIsReadOnly(connector.current instanceof ViewConnector);
|
||||
const lastUsedPubKey = LocalStorage.getItem(WALLET_KEY);
|
||||
const foundKey = keys.find((key) => key.publicKey === lastUsedPubKey);
|
||||
if (foundKey) {
|
||||
@ -65,6 +67,7 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
||||
// again as expected
|
||||
setPubKeys(null);
|
||||
setPubKey(null);
|
||||
setIsReadOnly(false);
|
||||
LocalStorage.removeItem(WALLET_KEY);
|
||||
try {
|
||||
await connector.current?.disconnect();
|
||||
@ -85,6 +88,7 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
||||
|
||||
const contextValue = useMemo<VegaWalletContextShape>(() => {
|
||||
return {
|
||||
isReadOnly,
|
||||
pubKey,
|
||||
pubKeys,
|
||||
selectPubKey,
|
||||
@ -92,7 +96,7 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
||||
disconnect,
|
||||
sendTx,
|
||||
};
|
||||
}, [pubKey, pubKeys, selectPubKey, connect, disconnect, sendTx]);
|
||||
}, [isReadOnly, pubKey, pubKeys, selectPubKey, connect, disconnect, sendTx]);
|
||||
|
||||
return (
|
||||
<VegaWalletContext.Provider value={contextValue}>
|
||||
|
@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface ConnectorConfig {
|
||||
token: string | null;
|
||||
connector: 'rest' | 'jsonRpc';
|
||||
connector: 'rest' | 'jsonRpc' | 'view';
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ export function useEagerConnect(Connectors: {
|
||||
useEffect(() => {
|
||||
const attemptConnect = async () => {
|
||||
const cfg = getConfig();
|
||||
|
||||
// No stored config, or config was malformed
|
||||
if (!cfg || !cfg.connector) {
|
||||
setConnecting(false);
|
||||
@ -31,7 +30,6 @@ export function useEagerConnect(Connectors: {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await connect(Connectors[cfg.connector]);
|
||||
} catch {
|
||||
|
@ -14,6 +14,7 @@ const mockPubKey = '0x123';
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
@ -52,7 +53,7 @@ describe('useVegaTransaction', () => {
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
});
|
||||
|
||||
it('handles a single error', () => {
|
||||
it('handles a single wallet error', () => {
|
||||
const error = new WalletError('test error', 1, 'test data');
|
||||
const mockSendTx = jest.fn(() => {
|
||||
throw error;
|
||||
@ -70,6 +71,22 @@ describe('useVegaTransaction', () => {
|
||||
expect(result.current.transaction.error).toHaveProperty('data', error.data);
|
||||
});
|
||||
|
||||
it('handles a single error', () => {
|
||||
const error = new Error('test error');
|
||||
const mockSendTx = jest.fn(() => {
|
||||
throw error;
|
||||
});
|
||||
const { result } = setup({ sendTx: mockSendTx });
|
||||
act(() => {
|
||||
result.current.send(mockPubKey, {} as Transaction);
|
||||
});
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error);
|
||||
expect(result.current.transaction.error).toHaveProperty(
|
||||
'message',
|
||||
error.message
|
||||
);
|
||||
});
|
||||
|
||||
it('handles an unkwown error', () => {
|
||||
const unknownThrow = { foo: 'bar' };
|
||||
const mockSendTx = jest.fn(() => {
|
||||
|
@ -25,7 +25,7 @@ export enum VegaTxStatus {
|
||||
|
||||
export interface VegaTxState {
|
||||
status: VegaTxStatus;
|
||||
error: WalletError | null;
|
||||
error: WalletError | Error | null;
|
||||
txHash: string | null;
|
||||
signature: string | null;
|
||||
dialogOpen: boolean;
|
||||
@ -89,8 +89,14 @@ export const useVegaTransaction = () => {
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
const error =
|
||||
err instanceof WalletError
|
||||
? err
|
||||
: err instanceof Error
|
||||
? err
|
||||
: ClientErrors.UNKNOWN;
|
||||
setTransaction({
|
||||
error: err instanceof WalletError ? err : ClientErrors.UNKNOWN,
|
||||
error: error,
|
||||
status: VegaTxStatus.Error,
|
||||
});
|
||||
return null;
|
||||
|
@ -2,6 +2,7 @@ import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { WalletError } from '../connectors';
|
||||
import type { VegaTxState } from '../use-vega-transaction';
|
||||
import { VegaTxStatus } from '../use-vega-transaction';
|
||||
|
||||
@ -108,11 +109,14 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||
if (transaction.status === VegaTxStatus.Error) {
|
||||
content = (
|
||||
<div data-testid={transaction.status}>
|
||||
{transaction.error && (
|
||||
{transaction.error instanceof WalletError && (
|
||||
<p>
|
||||
{transaction.error.message}: {transaction.error.data}
|
||||
</p>
|
||||
)}
|
||||
{transaction.error instanceof Error && (
|
||||
<p>{transaction.error.message}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user