feat(wallet): 4370 new connect wallet modal (#4492)

Co-authored-by: asiaznik <artur@vegaprotocol.io>
This commit is contained in:
Maciek 2023-08-10 13:03:53 +02:00 committed by GitHub
parent 6894fe1264
commit 069b57d4ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 959 additions and 955 deletions

View File

@ -25,6 +25,9 @@ import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
@ -55,6 +58,10 @@ context(
function () {
// 1002-STKE-002, 1002-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/');
ethereumWalletConnect();
cy.connectVegaWallet();
@ -65,6 +72,10 @@ context(
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.clearLocalStorage();
turnTelemetryOff();
// Go to homepage to allow wallet teardown without epoch timer refreshing page

View File

@ -6,6 +6,8 @@ import {
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { depositAsset } from '../../support/wallet-functions';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const withdraw = 'withdraw';
const withdrawalForm = 'withdraw-form';
@ -42,12 +44,20 @@ context(
{ tags: '@slow' },
function () {
before('visit withdrawals and connect vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/');
ethereumWalletConnect();
depositAsset(usdcEthAddress, '1000', 5);
});
beforeEach('Navigate to withdrawal page', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.clearLocalStorage();
turnTelemetryOff();
cy.reload();

View File

@ -11,6 +11,7 @@ import {
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { previousEpochData } from '../../fixtures/mocks/previous-epoch';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const guideLink = 'staking-guide-link';
const validatorTitle = 'validator-node-title';
@ -33,11 +34,22 @@ const overstakedPenaltyToolTip = 'overstaked-penalty-tooltip';
const multisigPenaltyToolTip = 'multisig-error-tooltip';
const epochCountDown = 'epoch-countdown';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
const txTimeout = Cypress.env('txTimeout');
context('Validators Page - verify elements on page', function () {
before('navigate to validators page', function () {
before('navigate to validators page', () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/validators');
});
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
it('Should have validators tab highlighted', function () {
@ -177,6 +189,11 @@ context('Validators Page - verify elements on page', function () {
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/validators');
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
@ -263,7 +280,7 @@ context('Validators Page - verify elements on page', function () {
cy.getByTestId(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
cy.get(nextEpochInfo, txTimeout).should('contain.text', 'Next epoch');
});
});
}

View File

@ -4,6 +4,8 @@ import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
} from '../../support/wallet-functions';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
@ -14,10 +16,6 @@ const dialogHeader = 'dialog-title';
const walletDialogHeader = 'wallet-dialog-title';
const connectorsList = 'connectors-list';
const dialogCloseBtn = 'dialog-close';
const restConnectorForm = 'rest-connector-form';
const restWallet = '#wallet';
const restPassphrase = '#passphrase';
const restConnectBtn = '[type="submit"]';
const accountNo = 'vega-account-truncated';
const currencyTitle = 'currency-title';
const currencyValue = 'currency-value';
@ -69,7 +67,7 @@ context(
cy.get(dialog).within(() => {
cy.getByTestId(walletDialogHeader)
.should('be.visible')
.and('have.text', 'Connect');
.and('have.text', 'Get a Vega wallet');
});
});
@ -77,10 +75,7 @@ context(
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-jsonRpc')
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.getByTestId('connector-rest')
.should('be.visible')
.and('have.text', 'Hosted Fairground wallet');
.and('have.text', 'Use the Desktop App/CLI');
});
});
@ -91,48 +86,14 @@ context(
});
});
describe('when rest connector form opened', function () {
before('click hosted wallet app button', function () {
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-rest').click();
});
});
// 0002-WCON-002
it('should have wallet field visible', function () {
cy.getByTestId(restConnectorForm).within(() => {
cy.get(restWallet).should('be.visible');
});
});
it('should have password field visible', function () {
cy.getByTestId(restConnectorForm).within(() => {
cy.get(restPassphrase).should('be.visible');
});
});
it('should have connect button visible', function () {
cy.getByTestId(restConnectorForm).within(() => {
cy.get(restConnectBtn)
.should('be.visible')
.and('have.text', 'Connect');
});
});
it('should have close button visible', function () {
cy.get(dialog).within(() => {
cy.getByTestId(dialogCloseBtn).should('be.visible');
});
});
after('close dialog', function () {
cy.getByTestId(dialogCloseBtn).click().should('not.exist');
});
});
describe('when vega wallet connected', function () {
before('connect vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
cy.visit('/');
cy.wait('@ChainId');
cy.connectVegaWallet();
vegaWalletTeardown();
});
@ -315,6 +276,10 @@ context(
];
before('faucet assets to connected vega wallet', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
for (const { id, amount } of assets) {
vegaWalletFaucetAssetsWithoutCheck(id, amount, vegaWalletPublicKey);
}

View File

@ -21,9 +21,12 @@ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
#Test configuration variables
CYPRESS_FAIRGROUND=false
LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=true

View File

@ -15,6 +15,7 @@ export const VegaWalletPrompt = () => {
</Link>
<ButtonLink
className="text-neutral-500"
data-testid="view-as-user"
onClick={() => setViewAsDialog(true)}
>
{t('viewAsParty')}

View File

@ -1,5 +1,4 @@
import {
RestConnector,
JsonRpcConnector,
ViewConnector,
InjectedConnector,
@ -8,13 +7,11 @@ import {
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,
};

View File

@ -169,14 +169,13 @@ describe('home', { tags: '@regression' }, () => {
it('click get started button should open connect dialog', () => {
cy.getByTestId('welcome-dialog').should('be.visible');
cy.getByTestId('get-started-button').click();
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
cy.window().then((window) => {
expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
'true'
);
// @ts-ignore stub it out just for test case
window.vega = {};
cy.getByTestId('get-started-button').click();
cy.getByTestId('wallet-dialog-title').should('contain.text', 'Connect');
});
cy.getByTestId('wallet-dialog-title').should('contain.text', 'Connect');
});
});

View File

@ -5,118 +5,8 @@ import {
const connectVegaBtn = 'connect-vega-wallet';
const manageVegaBtn = 'manage-vega-wallet';
const form = 'rest-connector-form';
const dialogContent = 'dialog-content';
describe(
'connect hosted wallet',
{ tags: '@smoke', testIsolation: true },
() => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
cy.visit('/#/portfolio');
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
});
it('can connect', () => {
// 0002-WCON-002
// 0002-WCON-003
// 0002-WCON-039
// 0002-WCON-017
// 0002-WCON-018
// 0002-WCON-019
// Mock authentication
cy.intercept(
'POST',
'https://wallet.testnet.vega.xyz/api/v1/auth/token',
{
body: {
token: 'test-token',
},
}
);
// Mock getting keys from wallet
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
body: {
keys: [
{
algorithm: {
name: 'algo',
version: 1,
},
index: 0,
meta: [],
pub: 'HOSTED_PUBKEY',
tainted: false,
},
],
},
});
cy.getByTestId(connectVegaBtn).click();
cy.contains(
'Choose wallet app to connect, or to change port or server URL enter a custom wallet location first'
);
cy.contains('Connect Vega wallet');
cy.contains('Hosted Fairground wallet');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('user');
cy.getByTestId(form).find('#passphrase').click().type('pass');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId(manageVegaBtn).should('exist');
cy.getByTestId('manage-vega-wallet').click();
cy.getByTestId('keypair-list').should('exist');
});
it('doesnt connect with invalid credentials', () => {
// 0002-WCON-020
// Mock incorrect username/password
cy.intercept(
'POST',
'https://wallet.testnet.vega.xyz/api/v1/auth/token',
{
body: {
error: 'No wallet',
},
statusCode: 403, // 403 forbidden invalid crednetials
}
);
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.click();
cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
});
it('doesnt connect with empty fields', () => {
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-rest"]')
.click();
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId(form)
.find('#wallet')
.next('[data-testid="input-error-text"]')
.should('have.text', 'Required');
cy.getByTestId(form)
.find('#passphrase')
.next('[data-testid="input-error-text"]')
.should('have.text', 'Required');
});
}
);
describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
@ -196,8 +86,6 @@ describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('connect-vega-wallet').should('exist');
cy.getByTestId('manage-vega-wallet').should('not.exist');
cy.getByTestId('connect-vega-wallet').click();
cy.contains(
'Choose wallet app to connect, or to change port or server URL enter a custom wallet location first'
);
cy.contains('Enter a custom wallet location');
});
});

View File

@ -13,9 +13,11 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS

View File

@ -411,24 +411,26 @@ describe('Closed', () => {
},
},
};
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
await act(() => {
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
});
await waitFor(() => {
expect(

View File

@ -1,11 +1,9 @@
import {
RestConnector,
JsonRpcConnector,
ViewConnector,
InjectedConnector,
} from '@vegaprotocol/wallet';
export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
@ -19,7 +17,6 @@ if (typeof window !== 'undefined') {
export const Connectors = {
injected,
rest,
jsonRpc,
view,
};

View File

@ -10,14 +10,11 @@ declare global {
export const addConnectPublicKey = () => {
Cypress.Commands.add('connectPublicKey', (publicKey) => {
const connectVegaWaletBtn = Cypress.$(
`[data-testid="connect-vega-wallet"]`
);
const connectVegaWaletBtn = Cypress.$(`[data-testid="view-as-user"]`);
if (connectVegaWaletBtn.length > 0) {
cy.get('aside [data-testid="connect-vega-wallet"]').click();
cy.getByTestId('connector-view').should('be.visible').click();
cy.getByTestId('address').click();
cy.getByTestId('address').type(publicKey);
cy.get('aside button').contains('View as party').click();
cy.getByTestId('address').should('be.visible').focus();
cy.getByTestId('address').type(publicKey, { delay: 50 });
cy.getByTestId('connect').click();
}
});

View File

@ -29,7 +29,7 @@ export const mockConnectWallet = () => {
export const mockConnectWalletWithUserError = () => {
cy.mockWallet((req) => {
aliasWalletConnectWithUserError(req);
aliasWalletConnectWithUserError(req, Cypress.env('VEGA_WALLET_API_TOKEN'));
});
};

View File

@ -62,10 +62,27 @@ export const aliasWalletConnectQuery = (
},
});
}
if (hasMethod(req, 'client.get_chain_id')) {
req.reply({
statusCode: 200,
headers: {
'Access-Control-Expose-Headers': 'Authorization',
Authorization: `VWT ${token}`,
},
body: {
jsonrpc: '2.0',
result: {
chainID: 'test-id',
},
id: '1',
},
});
}
};
export const aliasWalletConnectWithUserError = (
req: CyHttpMessages.IncomingHttpRequest
req: CyHttpMessages.IncomingHttpRequest,
token: string
) => {
if (hasMethod(req, 'client.connect_wallet')) {
req.alias = 'client.connect_wallet';
@ -82,4 +99,20 @@ export const aliasWalletConnectWithUserError = (
},
});
}
if (hasMethod(req, 'client.get_chain_id')) {
req.reply({
statusCode: 200,
headers: {
'Access-Control-Expose-Headers': 'Authorization',
Authorization: `VWT ${token}`,
},
body: {
jsonrpc: '2.0',
result: {
chainID: 'test-id',
},
id: '1',
},
});
}
};

View File

@ -371,6 +371,14 @@ function compileEnvVars() {
'NX_TENDERMINT_WEBSOCKET_URL',
process.env['NX_TENDERMINT_WEBSOCKET_URL']
),
CHROME_EXTENSION_URL: windowOrDefault(
'NX_CHROME_EXTENSION_URL',
process.env['NX_CHROME_EXTENSION_URL']
),
MOZILLA_EXTENSION_URL: windowOrDefault(
'NX_MOZILLA_EXTENSION_URL',
process.env['NX_MOZILLA_EXTENSION_URL']
),
};
return env;

View File

@ -160,7 +160,9 @@ export const ExternalLinks = {
MARGIN_CREDIT_RISK:
'https://vega.xyz/papers/margins-and-credit-risk.pdf#page=7',
VEGA_WALLET_URL: 'https://vega.xyz/wallet',
VEGA_WALLET_URL_ABOUT: 'https://vega.xyz/wallet/#overview',
VEGA_WALLET_HOSTED_URL: 'https://vega-hosted-wallet.on.fleek.co/',
VEGA_WALLET_BROWSER_LIST: '',
BLOG: 'https://blog.vega.xyz/',
};

View File

@ -59,6 +59,8 @@ export const envSchema = z
SENTRY_DSN: z.optional(z.string()),
TENDERMINT_URL: z.optional(z.string()),
TENDERMINT_WEBSOCKET_URL: z.optional(z.string()),
CHROME_EXTENSION_URL: z.optional(z.string()),
MOZILLA_EXTENSION_URL: z.optional(z.string()),
})
.refine(
(data) => {

View File

@ -0,0 +1,19 @@
export const IconArrowLeft = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path
d="M 3.63,7.47
C 3.63,7.47 8.72,2.37 8.72,2.37
8.72,2.37 7.98,1.63 7.98,1.63
7.98,1.63 1.60,8.00 1.60,8.00
1.60,8.00 7.98,14.37 7.98,14.37
7.98,14.37 8.72,13.63 8.72,13.63
8.72,13.63 3.63,8.53 3.63,8.53
3.63,8.53 14.35,8.53 14.35,8.53
14.35,8.53 14.35,7.47 14.35,7.47
14.35,7.47 3.63,7.47 3.63,7.47 Z
M 35.00,44.00"
/>
</svg>
);
};

View File

@ -0,0 +1,19 @@
export const IconArrowTopRight = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 14 14">
<path
d="M 10.47,4.28
C 10.47,4.28 10.47,11.49 10.47,11.49
10.47,11.49 11.53,11.49 11.53,11.49
11.53,11.49 11.53,2.47 11.53,2.47
11.53,2.47 2.51,2.47 2.51,2.47
2.51,2.47 2.51,3.53 2.51,3.53
2.51,3.53 9.72,3.53 9.72,3.53
9.72,3.53 2.14,11.12 2.14,11.12
2.14,11.12 2.88,11.86 2.88,11.86
2.88,11.86 10.47,4.28 10.47,4.28 Z
M -37.55,0.64"
/>
</svg>
);
};

View File

@ -1,6 +1,8 @@
import { IconArrowDown } from './svg-icons/icon-arrow-down';
import { IconArrowLeft } from './svg-icons/icon-arrow-left';
import { IconArrowUp } from './svg-icons/icon-arrow-up';
import { IconArrowRight } from './svg-icons/icon-arrow-right';
import { IconArrowTopRight } from './svg-icons/icon-arrow-top-right';
import { IconBreakdown } from './svg-icons/icon-breakdown';
import { IconBullet } from './svg-icons/icon-bullet';
import { IconChevronDown } from './svg-icons/icon-chevron-down';
@ -36,8 +38,10 @@ import { IconSearch } from './svg-icons/icon-search';
export enum VegaIconNames {
ARROW_DOWN = 'arrow-down',
ARROW_LEFT = 'arrow-left',
ARROW_UP = 'arrow-up',
ARROW_RIGHT = 'arrow-right',
ARROW_TOP_RIGHT = 'arrow-top-right',
BREAKDOWN = 'breakdown',
BULLET = 'bullet',
CHEVRON_DOWN = 'chevron-down',
@ -77,8 +81,10 @@ export const VegaIconNameMap: Record<
({ size }: { size: number }) => JSX.Element
> = {
'arrow-down': IconArrowDown,
'arrow-left': IconArrowLeft,
'arrow-up': IconArrowUp,
'arrow-right': IconArrowRight,
'arrow-top-right': IconArrowTopRight,
breakdown: IconBreakdown,
bullet: IconBullet,
'chevron-down': IconChevronDown,

View File

@ -54,3 +54,4 @@ export * from './traffic-light';
export * from './vega-icons';
export * from './vega-logo';
export * from './viewing-as-user';
export * from './pill';

View File

@ -0,0 +1 @@
export * from './pill';

View File

@ -0,0 +1,48 @@
import type { Story, Meta } from '@storybook/react';
import { Pill } from './pill';
import { Intent } from '../../utils/intent';
export default {
component: Pill,
title: 'Pill',
} as Meta;
const Template: Story = (args) => <Pill {...args}>Pill</Pill>;
export const Default = Template.bind({ intent: Intent.Primary, size: 'md' });
export const None = Template.bind({});
None.args = {
intent: Intent.None,
size: 'md',
};
export const Info = Template.bind({});
Info.args = {
intent: Intent.Info,
size: 'md',
};
export const Primary = Template.bind({});
Primary.args = {
intent: Intent.Primary,
size: 'md',
};
export const Success = Template.bind({});
Success.args = {
intent: Intent.Success,
size: 'md',
};
export const Warning = Template.bind({});
Warning.args = {
intent: Intent.Warning,
size: 'md',
};
export const Danger = Template.bind({});
Danger.args = {
intent: Intent.Danger,
size: 'md',
};

View File

@ -0,0 +1,44 @@
import type { ReactNode } from 'react';
import { Intent } from '../../utils/intent';
import classNames from 'classnames';
type Size = 'lg' | 'md' | 'sm' | 'xs' | 'xxs';
interface Props {
children: ReactNode;
intent?: Intent;
size?: Size;
className?: string;
}
const getClasses = (size: Size, intent?: Intent, className?: string) => {
return classNames(
['rounded-md', 'leading-none', 'font-alpha', 'py-1 px-2'],
{
'bg-vega-yellow dark:bg-vega-yellow': intent === Intent.Primary,
'bg-vega-clight-500 dark:bg-vega-cdark-500': intent === Intent.None,
'bg-vega-blue-500 dark:bg-vega-blue-500': intent === Intent.Info,
'bg-vega-orange-350 dark:bg-vega-orange-650': intent === Intent.Warning,
'bg-vega-red-350 dark:bg-vega-red-650': intent === Intent.Danger,
'bg-vega-green-350 dark:bg-vega-green-650': intent === Intent.Success,
'text-vega-clight-50 dark:text-vega-cdark-50': intent !== Intent.Primary,
'text-vega-clight-900 dark:text-vega-cdark-900':
intent === Intent.Primary,
},
{
'text-lg': size === 'lg',
'text-base': size === 'md',
'text-sma': size === 'sm',
'text-xs': size === 'xs',
'text-[10px]': size === 'xxs',
},
className
);
};
export const Pill = ({ intent, size, className, children }: Props) => {
return (
<span className={getClasses(size || 'md', intent, className)}>
{children}
</span>
);
};

View File

@ -13,6 +13,7 @@ type TradingButtonProps = {
children?: ReactNode;
icon?: ReactNode;
subLabel?: ReactNode;
fill?: boolean;
};
const getClassName = (
@ -20,7 +21,8 @@ const getClassName = (
size,
subLabel,
intent,
}: Pick<TradingButtonProps, 'size' | 'subLabel' | 'intent'>,
fill,
}: Pick<TradingButtonProps, 'size' | 'subLabel' | 'intent' | 'fill'>,
className?: string
) =>
classNames(
@ -61,6 +63,7 @@ const getClassName = (
intent === Intent.Primary,
'[&_[data-sub-label]]:text-vega-clight-100': intent === Intent.Primary,
},
{ 'w-full': fill },
className
);
@ -99,6 +102,7 @@ export const TradingButton = forwardRef<
children,
className,
subLabel,
fill,
...props
},
ref
@ -107,7 +111,7 @@ export const TradingButton = forwardRef<
ref={ref}
type={type}
data-trading-button
className={getClassName({ size, subLabel, intent }, className)}
className={getClassName({ size, subLabel, intent, fill }, className)}
{...props}
>
<Content icon={icon} subLabel={subLabel} children={children} />

View File

@ -1,16 +1,18 @@
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
import { ExternalLinks, useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { Link } from '@vegaprotocol/ui-toolkit';
import {
ExternalLink,
VegaIcon,
VegaIconNames,
} 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 (
<h1
data-testid="wallet-dialog-title"
className="text-2xl uppercase mb-6 text-center font-alpha calt"
className="text-2xl uppercase mb-6 font-alpha calt"
>
{children}
</h1>
@ -21,48 +23,353 @@ export const ConnectDialogContent = ({ children }: { children: ReactNode }) => {
return <div>{children}</div>;
};
export const ConnectDialogFooter = ({
connector,
}: {
connector: VegaConnector | undefined;
}) => {
export const ConnectDialogFooter = () => {
const wrapperClasses = classNames(
'flex justify-center gap-4',
'flex justify-center gap-4 mt-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'
'text-vega-light-400 dark:text-vega-dark-400 text-sm'
);
const isHostedWalletSelected = connector instanceof RestConnector;
return (
<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>
) : (
<ExternalLink
href={ExternalLinks.VEGA_WALLET_URL_ABOUT}
className="underline"
>
{t('About the Vega wallet')}{' '}
<VegaIcon name={VegaIconNames.ARROW_TOP_RIGHT} />
</ExternalLink>
{ExternalLinks.VEGA_WALLET_BROWSER_LIST && (
<>
<Link href={ExternalLinks.VEGA_WALLET_URL}>
{t('Get a Vega Wallet')}
</Link>
{' | '}
{DocsLinks && (
<Link href={DocsLinks.VEGA_WALLET_CONCEPTS_URL}>
{t('Having trouble?')}
</Link>
)}
<ExternalLink
href={ExternalLinks.VEGA_WALLET_BROWSER_LIST}
className="underline"
>
{t('Supported browsers')}{' '}
<VegaIcon name={VegaIconNames.ARROW_TOP_RIGHT} />
</ExternalLink>
</>
)}
</footer>
);
};
export const ChromeIcon = () => {
return (
<svg
width="28"
height="28"
viewBox="0 0 32 32"
fill="none"
data-testid="chrome-logo"
>
<g clipPath="url(#clip0_3681_24659)">
<path
d="M15.9987 9.99963L26.3893 9.99964C25.3364 8.17534 23.8217 6.6604 21.9976 5.60716C20.1735 4.55391 18.1042 3.99949 15.9979 3.99964C13.8915 3.99979 11.8223 4.5545 9.99837 5.608C8.1744 6.66149 6.65995 8.17664 5.6073 10.0011L10.8026 18.9996L10.8072 18.9984C10.2787 18.0871 9.99984 17.0525 9.99865 15.999C9.99747 14.9454 10.274 13.9102 10.8005 12.9977C11.3269 12.0851 12.0847 11.3275 12.9973 10.8011C13.9099 10.2748 14.9451 9.99832 15.9987 9.99963Z"
fill="url(#paint0_linear_3681_24659)"
/>
<path
d="M21.1974 18.9989L16.0021 27.9974C18.1084 27.9977 20.1777 27.4435 22.0019 26.3904C23.8261 25.3373 25.3409 23.8224 26.3939 21.9982C27.447 20.174 28.0012 18.1047 28.0008 15.9983C28.0004 13.892 27.4455 11.8228 26.3918 9.99898L16.0012 9.99899L15.9999 10.0036C17.0534 10.0016 18.0889 10.2773 19.0018 10.8031C19.9148 11.3288 20.673 12.0859 21.2001 12.9981C21.7272 13.9103 22.0044 14.9454 22.004 15.9989C22.0035 17.0524 21.7253 18.0872 21.1974 18.9989Z"
fill="url(#paint1_linear_3681_24659)"
/>
<path
d="M10.8044 19.0016L5.60914 10.0031C4.5557 11.8271 4.00106 13.8963 4.00098 16.0026C4.0009 18.109 4.55539 20.1782 5.60869 22.0023C6.66199 23.8264 8.17698 25.341 10.0013 26.3938C11.8257 27.4467 13.895 28.0007 16.0014 28.0001L21.1967 19.0015L21.1933 18.9981C20.6683 19.9115 19.9118 20.6703 19 21.1981C18.0882 21.7259 17.0534 22.004 15.9999 22.0043C14.9464 22.0047 13.9114 21.7273 12.9992 21.2001C12.0871 20.6729 11.3301 19.9146 10.8044 19.0016Z"
fill="url(#paint2_linear_3681_24659)"
/>
<path
d="M16 22C19.3137 22 22 19.3137 22 16C22 12.6863 19.3137 10 16 10C12.6863 10 10 12.6863 10 16C10 19.3137 12.6863 22 16 22Z"
fill="white"
/>
<path
d="M16 20.75C18.6234 20.75 20.75 18.6234 20.75 16C20.75 13.3766 18.6234 11.25 16 11.25C13.3766 11.25 11.25 13.3766 11.25 16C11.25 18.6234 13.3766 20.75 16 20.75Z"
fill="#1A73E8"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_3681_24659"
x1="25.093"
y1="9.25084"
x2="14.702"
y2="27.2485"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#D93025" />
<stop offset="1" stopColor="#EA4335" />
</linearGradient>
<linearGradient
id="paint1_linear_3681_24659"
x1="27.073"
y1="11.4997"
x2="6.29104"
y2="11.4997"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FCC934" />
<stop offset="1" stopColor="#FBBC04" />
</linearGradient>
<linearGradient
id="paint2_linear_3681_24659"
x1="17.2992"
y1="27.2508"
x2="6.90819"
y2="9.25305"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#1E8E3E" />
<stop offset="1" stopColor="#34A853" />
</linearGradient>
<clipPath id="clip0_3681_24659">
<rect
width="24"
height="24"
fill="white"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
);
};
export const MozillaIcon = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
data-testid="mozilla-logo"
>
<g clipPath="url(#clip0_3681_24667)">
<path
d="M22.4398 7.79786C21.9502 6.62017 20.9585 5.34873 20.1798 4.94687C20.8136 6.1893 21.1804 7.43561 21.3205 8.36575C21.3205 8.36758 21.3212 8.37212 21.3227 8.3845C20.0489 5.20951 17.889 3.92926 16.1252 1.1417C16.0361 1.00075 15.9468 0.85942 15.8598 0.710452C15.8155 0.634372 15.7742 0.556628 15.7358 0.477389C15.6625 0.335913 15.606 0.186371 15.5674 0.0317953C15.5676 0.0244774 15.5652 0.017323 15.5604 0.0117374C15.5557 0.00615177 15.549 0.00253868 15.5418 0.00160783C15.5349 -0.000373182 15.5275 -0.000373182 15.5206 0.00160783C15.519 0.00217033 15.5167 0.00399845 15.515 0.0046547C15.5125 0.00563908 15.5094 0.00788908 15.5068 0.0093422C15.508 0.0076547 15.5107 0.00385783 15.5115 0.0029672C12.6816 1.66028 11.7216 4.72614 11.6334 6.26003C10.5034 6.33772 9.42293 6.75406 8.53298 7.45478C8.43981 7.37608 8.3424 7.30253 8.24119 7.23447C7.98442 6.33615 7.97351 5.38538 8.20959 4.4814C7.05234 5.00833 6.15225 5.84125 5.49787 6.57672H5.49267C5.04609 6.01108 5.07759 4.14517 5.10305 3.75559C5.0977 3.73145 4.76991 3.92575 4.72697 3.95505C4.3329 4.23633 3.96449 4.55194 3.62606 4.89817C3.24095 5.28871 2.88907 5.71069 2.57409 6.15972C2.57409 6.16028 2.57377 6.16094 2.57358 6.1615C2.57358 6.16089 2.57391 6.16028 2.57409 6.15972C1.8497 7.18625 1.33595 8.34618 1.06252 9.57245C1.05712 9.59687 1.05258 9.62219 1.04733 9.6468C1.02614 9.74598 0.949828 10.2421 0.936469 10.3499C0.935438 10.3582 0.934969 10.3662 0.933984 10.3745C0.835324 10.8874 0.774224 11.4069 0.751172 11.9287C0.751172 11.9479 0.75 11.967 0.75 11.9862C0.750187 18.2072 5.79394 23.2501 12.0154 23.2501C17.5872 23.2501 22.2135 19.2053 23.1192 13.8924C23.1383 13.7482 23.1536 13.6033 23.1704 13.4578C23.3943 11.5261 23.1456 9.49572 22.4398 7.79786ZM9.45562 16.6148C9.50831 16.6399 9.55781 16.6675 9.61191 16.6916C9.61416 16.6931 9.61725 16.6949 9.61955 16.6963C9.56449 16.67 9.50984 16.6428 9.45562 16.6148ZM21.3236 8.38726L21.3221 8.37634C21.3227 8.38033 21.3234 8.3845 21.324 8.38848L21.3236 8.38726Z"
fill="url(#paint0_linear_3681_24667)"
/>
<path
d="M22.4397 7.79776C21.9501 6.62007 20.9584 5.34864 20.1797 4.94678C20.8135 6.1892 21.1803 7.43551 21.3204 8.36565C21.3204 8.36293 21.321 8.3679 21.3221 8.37625C21.3227 8.38023 21.3234 8.3844 21.324 8.38839C22.3869 11.2698 21.8078 14.1999 20.9734 15.9903C19.6825 18.7607 16.5571 21.5999 11.6652 21.4614C6.37978 21.3117 1.7235 17.39 0.854297 12.2536C0.695906 11.4436 0.854297 11.0323 0.933984 10.3746C0.836906 10.8816 0.799922 11.0281 0.751172 11.9289C0.751172 11.9481 0.75 11.9671 0.75 11.9864C0.750094 18.2071 5.79384 23.25 12.0153 23.25C17.5871 23.25 22.2134 19.2052 23.1191 13.8923C23.1382 13.7481 23.1535 13.6032 23.1703 13.4577C23.3942 11.526 23.1455 9.49562 22.4397 7.79776Z"
fill="url(#paint1_radial_3681_24667)"
/>
<path
d="M22.4397 7.79776C21.9501 6.62007 20.9584 5.34864 20.1797 4.94678C20.8135 6.1892 21.1803 7.43551 21.3204 8.36565C21.3204 8.36293 21.321 8.3679 21.3221 8.37625C21.3227 8.38023 21.3234 8.3844 21.324 8.38839C22.3869 11.2698 21.8078 14.1999 20.9734 15.9903C19.6825 18.7607 16.5571 21.5999 11.6652 21.4614C6.37978 21.3117 1.7235 17.39 0.854297 12.2536C0.695906 11.4436 0.854297 11.0323 0.933984 10.3746C0.836906 10.8816 0.799922 11.0281 0.751172 11.9289C0.751172 11.9481 0.75 11.9671 0.75 11.9864C0.750094 18.2071 5.79384 23.25 12.0153 23.25C17.5871 23.25 22.2134 19.2052 23.1191 13.8923C23.1382 13.7481 23.1535 13.6032 23.1703 13.4577C23.3942 11.526 23.1455 9.49562 22.4397 7.79776Z"
fill="url(#paint2_radial_3681_24667)"
/>
<path
d="M16.965 9.12184C16.9896 9.13909 17.0119 9.15625 17.035 9.1734C16.7523 8.67164 16.4002 8.21224 15.9892 7.80878C12.4874 4.3074 15.071 0.216811 15.5067 0.00906055C15.5079 0.00737305 15.5106 0.00357617 15.5114 0.00268555C12.6815 1.66 11.7215 4.72586 11.6333 6.25975C11.7646 6.25065 11.8954 6.23964 12.029 6.23964C14.1408 6.23964 15.9801 7.40073 16.965 9.12184Z"
fill="url(#paint3_radial_3681_24667)"
/>
<path
d="M12.0361 9.82097C12.0176 10.1012 11.0276 11.0675 10.6814 11.0675C7.47799 11.0675 6.95801 13.0051 6.95801 13.0051C7.0999 14.6368 8.23587 15.9805 9.61165 16.6915C9.67441 16.7239 9.73793 16.7532 9.80149 16.7822C9.91047 16.8304 10.0208 16.8756 10.1324 16.9175C10.6041 17.0845 11.0982 17.1798 11.5982 17.2002C17.2129 17.4635 18.3007 10.488 14.2488 8.4623C15.2864 8.28183 16.3635 8.69916 16.965 9.12169C15.9801 7.40072 14.1408 6.23962 12.0291 6.23962C11.8955 6.23962 11.7647 6.25064 11.6334 6.25973C10.5033 6.33742 9.42291 6.75376 8.53296 7.45448C8.70471 7.5998 8.89859 7.79405 9.30705 8.19642C10.0712 8.94956 12.0319 9.72947 12.0361 9.82097Z"
fill="url(#paint4_radial_3681_24667)"
/>
<path
d="M12.0361 9.82097C12.0176 10.1012 11.0276 11.0675 10.6814 11.0675C7.47799 11.0675 6.95801 13.0051 6.95801 13.0051C7.0999 14.6368 8.23587 15.9805 9.61165 16.6915C9.67441 16.7239 9.73793 16.7532 9.80149 16.7822C9.91047 16.8304 10.0208 16.8756 10.1324 16.9175C10.6041 17.0845 11.0982 17.1798 11.5982 17.2002C17.2129 17.4635 18.3007 10.488 14.2488 8.4623C15.2864 8.28183 16.3635 8.69916 16.965 9.12169C15.9801 7.40072 14.1408 6.23962 12.0291 6.23962C11.8955 6.23962 11.7647 6.25064 11.6334 6.25973C10.5033 6.33742 9.42291 6.75376 8.53296 7.45448C8.70471 7.5998 8.89859 7.79405 9.30705 8.19642C10.0712 8.94956 12.0319 9.72947 12.0361 9.82097Z"
fill="url(#paint5_radial_3681_24667)"
/>
<path
d="M8.00739 7.07982C8.08584 7.13043 8.16368 7.18199 8.24087 7.23451C7.98411 6.33619 7.97319 5.38542 8.20928 4.48145C7.05203 5.00837 6.15193 5.84129 5.49756 6.57676C5.5517 6.57521 7.18571 6.54582 8.00739 7.07982Z"
fill="url(#paint6_radial_3681_24667)"
/>
<path
d="M0.853976 12.2536C1.72318 17.3901 6.37946 21.3118 11.6649 21.4614C16.5568 21.5999 19.6822 18.7605 20.9731 15.9904C21.8075 14.1997 22.3866 11.2701 21.3237 8.38842L21.3233 8.3872L21.3218 8.37628C21.3206 8.36793 21.3199 8.36296 21.3201 8.36568C21.3201 8.36751 21.3208 8.37206 21.3223 8.38443C21.7219 10.9935 20.3947 13.5212 18.3199 15.2302L18.3137 15.2449C14.271 18.5366 10.4024 17.2309 9.61913 16.6964C9.56408 16.67 9.50939 16.6427 9.45507 16.6147C7.0981 15.4884 6.12441 13.3411 6.3332 11.4996C4.34302 11.4996 3.66441 9.82101 3.66441 9.82101C3.66441 9.82101 5.45124 8.54699 7.8062 9.65503C9.98729 10.6813 12.0356 9.8211 12.0359 9.82101C12.0317 9.72951 10.071 8.9496 9.30667 8.19651C8.89824 7.79414 8.70432 7.60012 8.53257 7.45457C8.4394 7.37587 8.34198 7.30232 8.24077 7.23426C8.16349 7.18188 8.08566 7.13031 8.00729 7.07957C7.18566 6.54557 5.5516 6.57496 5.49746 6.57637H5.49226C5.04568 6.01073 5.07718 4.14482 5.10263 3.75524C5.09729 3.7311 4.76949 3.9254 4.72655 3.9547C4.33248 4.23598 3.96408 4.55159 3.62565 4.89782C3.24052 5.28846 2.88865 5.71053 2.57368 6.15965C2.57368 6.16021 2.57335 6.16087 2.57316 6.16143C2.57316 6.16082 2.57349 6.16021 2.57368 6.15965C1.84929 7.18619 1.33553 8.34611 1.0621 9.57238C1.05671 9.59681 0.656632 11.3462 0.853976 12.2536Z"
fill="url(#paint7_radial_3681_24667)"
/>
<path
d="M15.9894 7.80883C16.4004 8.21229 16.7525 8.67169 17.0352 9.17345C17.0972 9.22005 17.1552 9.2665 17.2043 9.31183C19.7582 11.665 18.4201 14.9931 18.3203 15.2303C20.395 13.5212 21.7222 10.9936 21.3227 8.38445C20.0489 5.20951 17.8889 3.92926 16.1251 1.1417C16.0361 1.00075 15.9468 0.85942 15.8598 0.710452C15.8155 0.634372 15.7741 0.556628 15.7357 0.477389C15.6625 0.335913 15.6059 0.186371 15.5673 0.0317953C15.5676 0.0244774 15.5651 0.017323 15.5604 0.0117374C15.5556 0.00615177 15.549 0.00253868 15.5417 0.00160783C15.5348 -0.000373182 15.5275 -0.000373182 15.5206 0.00160783C15.519 0.00217033 15.5166 0.00399845 15.515 0.0046547C15.5125 0.00563908 15.5093 0.00788908 15.5067 0.0093422C15.0712 0.216905 12.4876 4.3075 15.9894 7.80883Z"
fill="url(#paint8_radial_3681_24667)"
/>
<path
d="M17.2043 9.31181C17.1551 9.26648 17.0972 9.22003 17.0352 9.17343C17.0123 9.15618 16.9898 9.13903 16.9652 9.12187C16.3637 8.69934 15.2866 8.28201 14.2489 8.46248C18.3008 10.4881 17.2131 17.4637 11.5984 17.2004C11.0984 17.18 10.6043 17.0847 10.1326 16.9177C10.021 16.8757 9.91066 16.8306 9.80166 16.7824C9.7381 16.7534 9.67458 16.7241 9.61182 16.6917C9.61407 16.6932 9.61716 16.6949 9.61946 16.6964C10.4027 17.2307 14.2713 18.5365 18.314 15.2448L18.3202 15.2302C18.42 14.9932 19.7581 11.665 17.2043 9.31181Z"
fill="url(#paint9_radial_3681_24667)"
/>
<path
d="M6.95794 13.0051C6.95794 13.0051 7.47793 11.0675 10.6813 11.0675C11.0276 11.0675 12.0177 10.1012 12.036 9.82096C12.0543 9.54074 9.98756 10.6812 7.80633 9.65497C5.45138 8.54694 3.66455 9.82096 3.66455 9.82096C3.66455 9.82096 4.34316 11.4995 6.33333 11.4995C6.1246 13.341 7.09828 15.4886 9.45521 16.6147C9.50789 16.6399 9.55739 16.6674 9.61149 16.6915C8.2358 15.9805 7.09983 14.6368 6.95794 13.0051Z"
fill="url(#paint10_radial_3681_24667)"
/>
<path
d="M22.4396 7.79786C21.95 6.62017 20.9583 5.34873 20.1796 4.94687C20.8134 6.1893 21.1802 7.43561 21.3203 8.36575C21.3203 8.36758 21.321 8.37212 21.3225 8.3845C20.0487 5.20951 17.8888 3.92926 16.125 1.1417C16.0359 1.00075 15.9466 0.85942 15.8596 0.710452C15.8153 0.634372 15.774 0.556628 15.7356 0.477389C15.6623 0.335913 15.6058 0.186371 15.5672 0.0317953C15.5675 0.0244774 15.565 0.017323 15.5602 0.0117374C15.5555 0.00615177 15.5489 0.00253868 15.5416 0.00160783C15.5347 -0.000373182 15.5274 -0.000373182 15.5205 0.00160783C15.5189 0.00217033 15.5165 0.00399845 15.5148 0.0046547C15.5123 0.00563908 15.5092 0.00788908 15.5066 0.0093422C15.5078 0.0076547 15.5105 0.00385783 15.5113 0.0029672C12.6814 1.66028 11.7214 4.72614 11.6332 6.26003C11.7645 6.25094 11.8953 6.23992 12.0289 6.23992C14.1408 6.23992 15.9801 7.40101 16.9649 9.12198C16.3634 8.69945 15.2863 8.28212 14.2486 8.46259C18.3005 10.4882 17.2127 17.4638 11.598 17.2005C11.098 17.1801 10.6039 17.0848 10.1322 16.9178C10.0207 16.8758 9.91032 16.8307 9.80133 16.7825C9.73777 16.7535 9.67425 16.7242 9.61148 16.6918C9.61373 16.6933 9.61683 16.6951 9.61912 16.6965C9.56407 16.67 9.50938 16.6427 9.45506 16.6148C9.50775 16.6399 9.55725 16.6675 9.61134 16.6916C8.23556 15.9806 7.09959 14.6369 6.9577 13.0052C6.9577 13.0052 7.47769 11.0676 10.6811 11.0676C11.0274 11.0676 12.0174 10.1013 12.0358 9.82108C12.0316 9.72958 10.0709 8.94967 9.30656 8.19658C8.89814 7.7942 8.70422 7.60019 8.53247 7.45464C8.43929 7.37594 8.34188 7.30239 8.24067 7.23433C7.98391 6.33601 7.97299 5.38524 8.20908 4.48126C7.05183 5.00819 6.15173 5.84111 5.49736 6.57658H5.49216C5.04558 6.01094 5.07708 4.14503 5.10253 3.75545C5.09719 3.73131 4.76939 3.92561 4.72645 3.9549C4.33238 4.23619 3.96398 4.5518 3.62555 4.89803C3.24054 5.28863 2.88878 5.71066 2.57391 6.15972C2.57391 6.16028 2.57358 6.16094 2.57339 6.1615C2.57339 6.16089 2.57372 6.16028 2.57391 6.15972C1.84952 7.18625 1.33576 8.34618 1.06233 9.57245C1.05694 9.59687 1.05239 9.62219 1.04714 9.6468C1.02595 9.74598 0.930609 10.2493 0.917297 10.3572C0.916266 10.3655 0.918281 10.349 0.917297 10.3572C0.830351 10.8773 0.774875 11.4022 0.751172 11.929C0.751172 11.9482 0.75 11.9672 0.75 11.9865C0.75 18.2072 5.79375 23.2501 12.0152 23.2501C17.587 23.2501 22.2133 19.2053 23.119 13.8924C23.1381 13.7482 23.1534 13.6033 23.1702 13.4578C23.3941 11.5261 23.1454 9.49572 22.4396 7.79786ZM21.322 8.37634C21.3226 8.38033 21.3233 8.3845 21.3239 8.38848L21.3235 8.38726L21.322 8.37634Z"
fill="url(#paint11_linear_3681_24667)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_3681_24667"
x1="20.3814"
y1="3.60386"
x2="2.29295"
y2="21.0528"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.05" stopColor="#FFF44F" />
<stop offset="0.37" stopColor="#FF980E" />
<stop offset="0.53" stopColor="#FF3647" />
<stop offset="0.7" stopColor="#E31587" />
</linearGradient>
<radialGradient
id="paint1_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(16.3404 2.59171) scale(23.0401 23.4281)"
>
<stop offset="0.13" stopColor="#FFBD4F" />
<stop offset="0.28" stopColor="#FF980E" />
<stop offset="0.47" stopColor="#FF3750" />
<stop offset="0.78" stopColor="#EB0878" />
<stop offset="0.86" stopColor="#E50080" />
</radialGradient>
<radialGradient
id="paint2_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(9.65968 12.2681) scale(23.6161 23.4281)"
>
<stop offset="0.3" stopColor="#960E18" />
<stop offset="0.35" stopColor="#B11927" stopOpacity="0.74" />
<stop offset="0.43" stopColor="#DB293D" stopOpacity="0.34" />
<stop offset="0.5" stopColor="#F5334B" stopOpacity="0.09" />
<stop offset="0.53" stopColor="#FF3750" stopOpacity="0" />
</radialGradient>
<radialGradient
id="paint3_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(14.2261 -1.0978) scale(7.56236 12.839)"
>
<stop offset="0.13" stopColor="#FFF44F" />
<stop offset="0.53" stopColor="#FF980E" />
</radialGradient>
<radialGradient
id="paint4_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(9.2356 18.3163) scale(10.007 10.9678)"
>
<stop offset="0.35" stopColor="#3A8EE6" />
<stop offset="0.67" stopColor="#9059FF" />
<stop offset="1" stopColor="#C139E6" />
</radialGradient>
<radialGradient
id="paint5_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(10.9455 9.859) scale(5.31373 6.47101)"
>
<stop offset="0.21" stopColor="#9059FF" stopOpacity="0" />
<stop offset="0.97" stopColor="#6E008B" stopOpacity="0.6" />
</radialGradient>
<radialGradient
id="paint6_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(11.2585 1.72838) scale(7.95561 7.98388)"
>
<stop offset="0.1" stopColor="#FFE226" />
<stop offset="0.79" stopColor="#FF7139" />
</radialGradient>
<radialGradient
id="paint7_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(18.5242 -3.50921) scale(37.9818 31.8836)"
>
<stop offset="0.11" stopColor="#FFF44F" />
<stop offset="0.46" stopColor="#FF980E" />
<stop offset="0.72" stopColor="#FF3647" />
<stop offset="0.9" stopColor="#E31587" />
</radialGradient>
<radialGradient
id="paint8_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4.41888 7.02007) rotate(77.3946) scale(12.0503 52.1278)"
>
<stop stopColor="#FFF44F" />
<stop offset="0.3" stopColor="#FF980E" />
<stop offset="0.57" stopColor="#FF3647" />
<stop offset="0.74" stopColor="#E31587" />
</radialGradient>
<radialGradient
id="paint9_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(11.3407 4.60001) scale(21.8071 21.424)"
>
<stop offset="0.14" stopColor="#FFF44F" />
<stop offset="0.48" stopColor="#FF980E" />
<stop offset="0.66" stopColor="#FF3647" />
<stop offset="0.9" stopColor="#E31587" />
</radialGradient>
<radialGradient
id="paint10_radial_3681_24667"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(17.0005 5.8529) scale(26.2114 23.4492)"
>
<stop offset="0.09" stopColor="#FFF44F" />
<stop offset="0.63" stopColor="#FF980E" />
</radialGradient>
<linearGradient
id="paint11_linear_3681_24667"
x1="18.75"
y1="3.25511"
x2="4.28552"
y2="19.0592"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.17" stopColor="#FFF44F" stopOpacity="0.8" />
<stop offset="0.6" stopColor="#FFF44F" stopOpacity="0" />
</linearGradient>
<clipPath id="clip0_3681_24667">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export const BrowserIcon = () => {
const { MOZILLA_EXTENSION_URL, CHROME_EXTENSION_URL } = useEnvironment();
const isItChrome = window.navigator.userAgent.includes('Chrome');
const isItMozilla =
window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
return (
<div className="absolute right-1 top-0 h-8 flex items-center">
{!isItChrome && !isItMozilla ? (
<>
<a href={MOZILLA_EXTENSION_URL} target="_blank" rel="noreferrer">
<MozillaIcon />
</a>{' '}
<a href={CHROME_EXTENSION_URL} target="_blank" rel="noreferrer">
<ChromeIcon />
</a>
</>
) : (
<>
{isItChrome && <ChromeIcon />}
{isItMozilla && <MozillaIcon />}
</>
)}
</div>
);
};

View File

@ -1,10 +1,4 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '../provider';
@ -18,7 +12,6 @@ import {
ClientErrors,
InjectedConnector,
JsonRpcConnector,
RestConnector,
ViewConnector,
WalletError,
} from '../connectors';
@ -36,6 +29,12 @@ const mockUpdateDialogOpen = jest.fn();
const mockCloseVegaDialog = jest.fn();
jest.mock('@vegaprotocol/environment');
let mockIsDesktopRunning = true;
jest.mock('../use-is-wallet-service-running', () => ({
useIsWalletServiceRunning: jest
.fn()
.mockImplementation(() => mockIsDesktopRunning),
}));
// @ts-ignore ignore mock implementation
useEnvironment.mockImplementation(() => ({
@ -53,12 +52,10 @@ let defaultProps: VegaConnectDialogProps;
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,
@ -104,6 +101,11 @@ function generateJSX(props?: Partial<VegaConnectDialogProps>) {
}
describe('VegaConnectDialog', () => {
let navigatorGetter: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
navigatorGetter = jest.spyOn(window.navigator, 'userAgent', 'get');
});
it('displays a list of connection options', async () => {
const { container, rerender } = render(generateJSX());
expect(container).toBeEmptyDOMElement();
@ -112,133 +114,23 @@ describe('VegaConnectDialog', () => {
expect(list).toBeInTheDocument();
expect(list.children).toHaveLength(3);
expect(screen.getByTestId('connector-jsonRpc')).toHaveTextContent(
'Connect Vega wallet'
);
expect(screen.getByTestId('connector-rest')).toHaveTextContent(
'Hosted Fairground wallet'
);
expect(screen.getByTestId('connector-view')).toHaveTextContent(
'View as vega user'
'Use the Desktop App/CLI'
);
});
it('displays browser wallet option if detected on window object', async () => {
navigatorGetter.mockReturnValue('Chrome');
mockBrowserWallet();
render(generateJSX());
const list = await screen.findByTestId('connectors-list');
expect(list.children).toHaveLength(4);
expect(list.children).toHaveLength(3);
expect(screen.getByTestId('connector-injected')).toHaveTextContent(
'Connect Web wallet'
'Connect'
);
clearBrowserWallet();
});
describe('RestConnector', () => {
it('connects', async () => {
const spy = jest
.spyOn(connectors.rest, 'authenticate')
.mockImplementation(() =>
Promise.resolve({ success: true, error: null })
);
jest
.spyOn(connectors.rest, 'connect')
.mockImplementation(() =>
Promise.resolve([{ publicKey: 'pubkey', name: 'test key 1' }])
);
render(generateJSX());
// Switches to rest form
fireEvent.click(await screen.findByText('Hosted Fairground wallet'));
// Client side validation
fireEvent.submit(screen.getByTestId('rest-connector-form'));
expect(spy).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getAllByText('Required')).toHaveLength(2);
});
const fields = fillInForm();
// Wait for auth method to be called
await act(async () => {
fireEvent.submit(screen.getByTestId('rest-connector-form'));
});
await waitFor(() => {
expect(spy).toHaveBeenCalledWith(fields);
expect(mockCloseVegaDialog).toHaveBeenCalled();
});
});
it('handles failed connection', async () => {
const errMessage = 'Error message';
// Error from service
let spy = jest
.spyOn(connectors.rest, 'authenticate')
.mockImplementation(() =>
Promise.resolve({ success: false, error: errMessage })
);
render(generateJSX());
// Switches to rest form
fireEvent.click(await screen.findByText('Hosted Fairground wallet'));
const fields = fillInForm();
fireEvent.submit(screen.getByTestId('rest-connector-form'));
// Wait for auth method to be called
await act(async () => {
fireEvent.submit(screen.getByTestId('rest-connector-form'));
});
expect(spy).toHaveBeenCalledWith(fields);
expect(screen.getByTestId('form-error')).toHaveTextContent(errMessage);
expect(mockUpdateDialogOpen).not.toHaveBeenCalled();
// Fetch failed due to wallet not running
spy = jest
.spyOn(connectors.rest, 'authenticate')
// @ts-ignore test fetch failed with typeerror
.mockImplementation(() =>
Promise.reject(new TypeError('fetch failed'))
);
await act(async () => {
fireEvent.submit(screen.getByTestId('rest-connector-form'));
});
expect(screen.getByTestId('form-error')).toHaveTextContent(
`Wallet not running at ${mockHostedWalletUrl}`
);
// Reject eg non 200 results
spy = jest
.spyOn(connectors.rest, 'authenticate')
// @ts-ignore test fetch failed with typeerror
.mockImplementation(() => Promise.reject(new Error('Error!')));
await act(async () => {
fireEvent.submit(screen.getByTestId('rest-connector-form'));
});
expect(screen.getByTestId('form-error')).toHaveTextContent(
'Authentication failed'
);
});
const fillInForm = () => {
const walletValue = 'test-wallet';
fireEvent.change(screen.getByTestId('rest-wallet'), {
target: { value: walletValue },
});
const passphraseValue = 'test-passphrase';
fireEvent.change(screen.getByTestId('rest-passphrase'), {
target: { value: passphraseValue },
});
return { wallet: walletValue, passphrase: passphraseValue };
};
});
describe('JsonRpcConnector', () => {
const delay = 100;
let spyOnCheckCompat: jest.SpyInstance;
@ -373,83 +265,36 @@ describe('VegaConnectDialog', () => {
expect(screen.getByText('An unknown error occurred')).toBeInTheDocument();
});
it('handles wallet running is not detected', async () => {
mockIsDesktopRunning = false;
render(generateJSX());
expect(await screen.findByTestId('connector-jsonRpc')).toBeDisabled();
});
it('Mozilla logo should be rendered', async () => {
navigatorGetter.mockReturnValue('Firefox');
render(generateJSX());
expect(await screen.findByTestId('mozilla-logo')).toBeInTheDocument();
});
it('Chrome logo should be rendered', async () => {
navigatorGetter.mockReturnValue('Chrome');
render(generateJSX());
expect(await screen.findByTestId('chrome-logo')).toBeInTheDocument();
});
it('Chrome and Firefox logo should be rendered', async () => {
navigatorGetter.mockReturnValue('Safari');
render(generateJSX());
expect(await screen.findByTestId('mozilla-logo')).toBeInTheDocument();
expect(await screen.findByTestId('chrome-logo')).toBeInTheDocument();
});
async function selectJsonRpc() {
expect(await screen.findByRole('dialog')).toBeInTheDocument();
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
);
});
});
});
describe('InjectedConnector', () => {
beforeAll(() => {
jest.useFakeTimers();

View File

@ -1,43 +1,51 @@
import classNames from 'classnames';
import { create } from 'zustand';
import {
Button,
Dialog,
FormGroup,
Input,
Intent,
Pill,
TradingButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
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 } from '@vegaprotocol/environment';
import {
InjectedConnector,
JsonRpcConnector,
ViewConnector,
} from '../connectors';
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
import { ViewConnectorForm } from './view-connector-form';
import { useEnvironment } from '@vegaprotocol/environment';
import {
BrowserIcon,
ConnectDialogContent,
ConnectDialogFooter,
ConnectDialogTitle,
} from './connect-dialog-elements';
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 type { Status as InjectedStatus } from '../use-injected-connector';
import { useInjectedConnector } from '../use-injected-connector';
import { useChainIdQuery } from './__generated__/ChainId';
import { useVegaWallet } from '../use-vega-wallet';
import { useInjectedConnector } from '../use-injected-connector';
import { InjectedConnectorForm } from './injected-connector-form';
import { isBrowserWalletInstalled } from '../utils';
import { useIsWalletServiceRunning } from '../use-is-wallet-service-running';
export const CLOSE_DELAY = 1700;
type Connectors = { [key: string]: VegaConnector };
export type WalletType = 'injected' | 'jsonRpc' | 'rest' | 'view';
export type WalletType = 'injected' | 'jsonRpc' | 'view';
export interface VegaConnectDialogProps {
connectors: Connectors;
riskMessage?: React.ReactNode;
riskMessage?: ReactNode;
}
export interface VegaWalletDialogStore {
@ -109,9 +117,9 @@ const ConnectDialogContainer = ({
}: {
connectors: Connectors;
appChainId: string;
riskMessage?: React.ReactNode;
riskMessage?: ReactNode;
}) => {
const { VEGA_WALLET_URL, VEGA_ENV, HOSTED_WALLET_URL } = useEnvironment();
const { VEGA_WALLET_URL } = useEnvironment();
const closeDialog = useVegaWalletDialogStore(
(store) => store.closeVegaWalletDialog
);
@ -135,11 +143,7 @@ const ConnectDialogContainer = ({
const handleSelect = (type: WalletType) => {
const connector = connectors[type];
// 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;
connector.url = walletUrl;
if (!connector) {
// we should never get here unless connectors are not configured correctly
@ -156,6 +160,11 @@ const ConnectDialogContainer = ({
injectedConnect(connector, appChainId);
}
};
const isDesktopWalletRunning = useIsWalletServiceRunning(
walletUrl,
connectors,
appChainId
);
return (
<>
@ -175,11 +184,11 @@ const ConnectDialogContainer = ({
walletUrl={walletUrl}
setWalletUrl={setWalletUrl}
onSelect={handleSelect}
isMainnet={VEGA_ENV === Networks.MAINNET}
isDesktopWalletRunning={isDesktopWalletRunning}
/>
)}
</ConnectDialogContent>
<ConnectDialogFooter connector={selectedConnector} />
<ConnectDialogFooter />
</>
);
};
@ -188,52 +197,62 @@ const ConnectorList = ({
onSelect,
walletUrl,
setWalletUrl,
isMainnet,
isDesktopWalletRunning,
}: {
onSelect: (type: WalletType) => void;
walletUrl: string;
setWalletUrl: (value: string) => void;
isMainnet: boolean;
isDesktopWalletRunning: boolean | null;
}) => {
const title = isBrowserWalletInstalled()
? t('Connect Vega wallet')
: t('Get a Vega wallet');
const extendedText = (
<>
<div className="w-full h-full flex justify-center items-center gap-1 text-base">
{t('Connect')}
</div>
<BrowserIcon />
</>
);
return (
<>
<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">
<ConnectDialogTitle>{title}</ConnectDialogTitle>
<p>
{t(
'Connect securely, deposit funds and approve or reject transactions with the Vega wallet'
)}
</p>
<div data-testid="connectors-list" className="flex flex-col mt-6 gap-2">
<div className="last:mb-0">
{isBrowserWalletInstalled() ? (
<ConnectionOption
type="injected"
text={t('Connect Web wallet')}
text={extendedText}
onClick={() => onSelect('injected')}
/>
</li>
)}
{!isMainnet && (
<li className="mb-4 last:mb-0">
<ConnectionOption
type="rest"
text={t('Hosted Fairground wallet')}
onClick={() => onSelect('rest')}
/>
</li>
)}
<li className="mb-4 last:mb-0">
<div className="my-4 text-center">{t('OR')}</div>
) : (
<GetWallet />
)}
</div>
<div>
<ConnectionOption
type="view"
text={t('View as vega user')}
text={t('View as party')}
onClick={() => onSelect('view')}
/>
</li>
</ul>
</div>
<div className="last:mb-0">
<CustomUrlInput
walletUrl={walletUrl}
setWalletUrl={setWalletUrl}
isDesktopWalletRunning={isDesktopWalletRunning}
onSelect={() => onSelect('jsonRpc')}
/>
</div>
</div>
</>
);
};
@ -259,7 +278,7 @@ const SelectedForm = ({
};
reset: () => void;
onConnect: () => void;
riskMessage?: React.ReactNode;
riskMessage?: ReactNode;
}) => {
if (connector instanceof InjectedConnector) {
return (
@ -274,24 +293,6 @@ const SelectedForm = ({
);
}
if (connector instanceof RestConnector) {
return (
<>
<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 (
<JsonRpcConnectorForm
@ -305,7 +306,6 @@ const SelectedForm = ({
/>
);
}
if (connector instanceof ViewConnector) {
return (
<ViewConnectorForm
@ -315,50 +315,116 @@ const SelectedForm = ({
/>
);
}
throw new Error('No connector selected');
};
const GetWallet = () => {
const { MOZILLA_EXTENSION_URL, CHROME_EXTENSION_URL } = useEnvironment();
const isItChrome = window.navigator.userAgent.includes('Chrome');
const isItMozilla =
window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const onClick = () => {
if (isItMozilla) {
window.open(MOZILLA_EXTENSION_URL, '_blank');
return;
}
if (isItChrome) {
window.open(CHROME_EXTENSION_URL, '_blank');
}
};
const buttonContent = (
<>
<div className="flex items-center justify-center gap-1 text-base">
{t('Get the Vega Wallet')}
<Pill size="xxs" intent={Intent.Info}>
ALPHA
</Pill>
</div>
<BrowserIcon />
</>
);
return !isItChrome && !isItMozilla ? (
<div
className={classNames([
'bg-vega-blue-350 hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:hover:bg-vega-blue-600',
'flex gap-2 items-center justify-center rounded h-8 px-3 relative',
])}
data-testid="get-wallet-button"
>
{buttonContent}
</div>
) : (
<TradingButton
onClick={onClick}
intent={Intent.Info}
data-testid="get-wallet-button"
className="relative"
size="small"
fill
>
{buttonContent}
</TradingButton>
);
};
const ConnectionOption = ({
disabled,
type,
text,
onClick,
icon,
}: {
type: WalletType;
text: string;
text: string | ReactNode;
onClick: () => void;
disabled?: boolean;
icon?: ReactNode;
}) => {
return (
<Button
<TradingButton
size="small"
intent={Intent.Info}
onClick={onClick}
size="lg"
fill={true}
variant={['rest', 'view'].includes(type) ? 'default' : 'primary'}
className="relative"
data-testid={`connector-${type}`}
disabled={disabled}
icon={icon}
fill
>
<span className="-mx-10 flex text-left justify-between items-center">
{text}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</span>
</Button>
<span className="flex justify-center items-center text-base">{text}</span>
</TradingButton>
);
};
const CustomUrlInput = ({
walletUrl,
setWalletUrl,
isDesktopWalletRunning,
onSelect,
}: {
walletUrl: string;
setWalletUrl: (url: string) => void;
isDesktopWalletRunning: boolean | null;
onSelect: (type: WalletType) => void;
}) => {
const [urlInputExpanded, setUrlInputExpanded] = useState(false);
return urlInputExpanded ? (
<>
<p className="mb-2">{t('Custom wallet location')}</p>
<div className="flex justify-between mb-1.5">
<p className="text-sm text-secondary">{t('Custom wallet location')}</p>
<button
className="text-sm underline"
onClick={() => setUrlInputExpanded(false)}
>
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
</button>
</div>
<FormGroup
labelFor="wallet-url"
label={t('Custom wallet location')}
hideLabel={true}
hideLabel
>
<Input
value={walletUrl}
@ -366,17 +432,48 @@ const CustomUrlInput = ({
name="wallet-url"
/>
</FormGroup>
<p className="mb-2">{t('Choose wallet app to connect')}</p>
<ConnectionOption
disabled={!isDesktopWalletRunning}
type="jsonRpc"
text={t('Connect the App/CLI')}
onClick={() => onSelect('jsonRpc')}
/>
</>
) : (
<p className="mb-6">
{t(
'Choose wallet app to connect, or to change port or server URL enter a '
<>
<ConnectionOption
disabled={!isDesktopWalletRunning}
type="jsonRpc"
text={t('Use the Desktop App/CLI')}
onClick={() => onSelect('jsonRpc')}
/>
{isDesktopWalletRunning !== null && (
<p className="mb-6 text-sm pt-2">
{isDesktopWalletRunning ? (
<button
className="underline text-default"
onClick={() => setUrlInputExpanded(true)}
>
{t('Enter a custom wallet location')}{' '}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</button>
) : (
<>
<span className="text-default">
{t(
'No running Desktop App/CLI detected. Open your app now to connect or enter a'
)}
</span>{' '}
<button
className="underline"
onClick={() => setUrlInputExpanded(true)}
>
{t('custom wallet location')}
</button>
</>
)}
</p>
)}
<button className="underline" onClick={() => setUrlInputExpanded(true)}>
{t('custom wallet location')}
</button>{' '}
{t(' first')}
</p>
</>
);
};

View File

@ -1,90 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { Button, FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import type { RestConnector } from '../connectors';
import { useVegaWallet } from '../use-vega-wallet';
interface FormFields {
wallet: string;
passphrase: string;
}
interface RestConnectorFormProps {
connector: RestConnector;
onConnect: (connector: RestConnector) => void;
}
export function RestConnectorForm({
connector,
onConnect,
}: RestConnectorFormProps) {
const { connect } = useVegaWallet();
const [error, setError] = useState('');
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormFields>();
async function onSubmit(fields: FormFields) {
const authFailedMessage = t('Authentication failed');
try {
setError('');
const res = await connector.authenticate({
wallet: fields.wallet,
passphrase: fields.passphrase,
});
if (res.success) {
await connect(connector);
onConnect(connector);
} else {
setError(res.error || authFailedMessage);
}
} catch (err) {
if (err instanceof TypeError) {
setError(t(`Wallet not running at ${connector.url}`));
} else if (err instanceof Error) {
setError(authFailedMessage);
} else {
setError(t('Something went wrong'));
}
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} data-testid="rest-connector-form">
<FormGroup label={t('Wallet name')} labelFor="wallet">
<Input
{...register('wallet', { required: t('Required') })}
id="wallet"
data-testid="rest-wallet"
type="text"
/>
{errors.wallet?.message && (
<InputError intent="danger">{errors.wallet.message}</InputError>
)}
</FormGroup>
<FormGroup label={t('Passphrase')} labelFor="passphrase">
<Input
{...register('passphrase', { required: t('Required') })}
id="passphrase"
data-testid="rest-passphrase"
type="password"
/>
{errors.passphrase?.message && (
<InputError intent="danger">{errors.passphrase.message}</InputError>
)}
{error && (
<InputError intent="danger" data-testid="form-error">
{error}
</InputError>
)}
</FormGroup>
<Button variant="primary" type="submit" fill={true}>
{t('Connect')}
</Button>
</form>
);
}

View File

@ -1,21 +1,23 @@
import { t } from '@vegaprotocol/i18n';
import {
Button,
FormGroup,
Input,
InputError,
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import type { ViewConnector } from '../connectors';
import { useVegaWallet } from '../use-vega-wallet';
import { ConnectDialogTitle } from './connect-dialog-elements';
interface FormFields {
address: string;
}
interface RestConnectorFormProps {
interface ViewConnectorFormProps {
connector: ViewConnector;
onConnect: (connector: ViewConnector) => void;
reset?: () => void;
@ -25,7 +27,7 @@ export function ViewConnectorForm({
connector,
onConnect,
reset,
}: RestConnectorFormProps) {
}: ViewConnectorFormProps) {
const { connect } = useVegaWallet();
const {
register,
@ -51,23 +53,8 @@ export function ViewConnectorForm({
return (
<>
{reset && (
<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}
aria-label="back"
size={16}
/>
</button>
)}
<ConnectDialogTitle>{t('VIEW AS VEGA USER')}</ConnectDialogTitle>
<form onSubmit={handleSubmit(onSubmit)} data-testid="view-connector-form">
<h1 className="text-2xl uppercase mb-6 text-center font-alpha calt">
{t('VIEW AS VEGA USER')}
</h1>
<p className="mb-4">
{t(
'Browse from the perspective of another Vega user in read-only mode.'
@ -87,14 +74,25 @@ export function ViewConnectorForm({
<InputError intent="danger">{errors.address.message}</InputError>
)}
</FormGroup>
<Button
<TradingButton
data-testid="connect"
variant="primary"
intent={Intent.Info}
type="submit"
fill={true}
fill
>
{t('Browse network')}
</Button>
</TradingButton>
{reset && (
<div className="flex justify-end">
<button
onClick={reset}
className="p-2 text-sm underline"
data-testid="back-button"
>
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
</button>
</div>
)}
</form>
</>
);

View File

@ -1,5 +1,4 @@
export * from './vega-connector';
export * from './rest-connector';
export * from './injected-connector';
export * from './json-rpc-connector';
export * from './view-connector';

View File

@ -40,6 +40,7 @@ export class JsonRpcConnector implements VegaConnector {
address: cfg.url,
token: cfg.token ?? undefined,
onTokenChange: (token) => {
this.token = token;
setConfig({
token,
connector: 'jsonRpc',
@ -55,12 +56,14 @@ export class JsonRpcConnector implements VegaConnector {
this.client = new WalletClient({
address: url,
token: this.token ?? undefined,
onTokenChange: (token) =>
onTokenChange: (token) => {
this.token = token;
setConfig({
token,
url,
connector: 'jsonRpc',
}),
});
},
});
}
get url() {

View File

@ -1,284 +0,0 @@
import * as Sentry from '@sentry/react';
import { clearConfig, getConfig, setConfig } from '../storage';
import type { Transaction, VegaConnector } from './vega-connector';
import { WalletError } from './vega-connector';
import { z } from 'zod';
import { t } from '@vegaprotocol/i18n';
type TransactionError =
| {
errors: {
[key: string]: string[];
};
details?: string[];
}
| {
error: string;
details?: string[];
};
const VERSION = 'v1';
// Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors
// need to use local storage, I don't think so...
enum Endpoints {
Auth = 'auth/token',
Command = 'command/sync',
Keys = 'keys',
}
export const AuthTokenSchema = z.object({
token: z.string(),
});
export const TransactionResponseSchema = z.object({
txHash: z.string(),
tx: z.object({
signature: z.object({
value: z.string(),
}),
}),
sentAt: z.string(),
receivedAt: z.string(),
});
export type V1TransactionResponse = z.infer<typeof TransactionResponseSchema>;
export const GetKeysSchema = z.object({
keys: z.array(
z.object({
algorithm: z.object({
name: z.string(),
version: z.number(),
}),
index: z.number(),
meta: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
pub: z.string(),
tainted: z.boolean(),
})
),
});
/**
* Connector for using the Vega Wallet Service rest api, requires authentication to get a session token
*/
export class RestConnector implements VegaConnector {
url: string | null = null;
token: string | null = null;
constructor() {
const cfg = getConfig();
if (cfg) {
this.token = cfg.token;
this.url = cfg.url;
}
}
async sessionActive() {
return Boolean(this.token);
}
async authenticate(params: { wallet: string; passphrase: string }) {
try {
const res = await this.request(Endpoints.Auth, {
method: 'post',
body: JSON.stringify(params),
});
if (res.status === 403) {
return { success: false, error: t('Invalid credentials') };
}
if (res.error) {
return { success: false, error: res.error };
}
const data = AuthTokenSchema.parse(res.data);
// Store the token, and other things for later
setConfig({
connector: 'rest',
token: data.token,
url: this.url,
});
this.token = data.token;
return { success: true, error: null };
} catch (err) {
return { success: false, error: 'Authentication failed' };
}
}
async connect() {
try {
const res = await this.request(Endpoints.Keys, {
method: 'get',
headers: {
authorization: `Bearer ${this.token}`,
},
});
if (res.error) {
return null;
}
const data = GetKeysSchema.parse(res.data);
return data.keys.map((k) => {
const nameMeta = k.meta.find((m) => m.key === 'name');
return {
publicKey: k.pub,
name: nameMeta ? nameMeta.value : t('No name'),
};
});
} catch (err) {
// keysGet failed, its likely that the session has expired so remove the token from storage
clearConfig();
return null;
}
}
async disconnect() {
try {
await this.request(Endpoints.Auth, {
method: 'delete',
headers: {
authorization: `Bearer ${this.token}`,
},
});
} catch (err) {
Sentry.captureException(err);
} finally {
// Always clear config, if authTokenDelete fails the user still tried to
// connect so clear the config (and containing token) from storage
clearConfig();
}
}
async sendTx(pubKey: string, transaction: Transaction) {
const body = {
pubKey,
propagate: true,
...transaction,
};
const res = await this.request(Endpoints.Command, {
method: 'post',
body: JSON.stringify(body),
headers: {
authorization: `Bearer ${this.token}`,
},
});
// User rejected
if (res.status === 401) {
return null;
}
if (res.error) {
throw new WalletError(res.error, 1, res.details);
}
const data = TransactionResponseSchema.parse(res.data);
// Make return value match that of v2 service
return {
transactionHash: data.txHash,
signature: data.tx.signature.value,
receivedAt: data.receivedAt,
sentAt: data.sentAt,
};
}
/** Parse more complex error object into a single string */
private parseError(err: TransactionError): string {
if ('error' in err) {
return err.error;
}
if ('errors' in err) {
const result = Object.entries(err.errors)
.map((entry) => {
return `${entry[0]}: ${entry[1].join(' | ')}`;
})
.join(', ');
return result;
}
return t('Something went wrong');
}
/** Parse error details array into a single string */
private parseErrorDetails(err: TransactionError): string | null {
if (err.details && err.details.length > 0) {
return err.details.join(', ');
}
return null;
}
private async request(
endpoint: Endpoints,
options: RequestInit
): Promise<{
status?: number;
data?: unknown;
error?: string;
details?: string;
}> {
try {
const fetchResult = await fetch(
`${this.url}/api/${VERSION}/${endpoint}`,
{
...options,
headers: {
...options.headers,
'Content-Type': 'application/json',
},
}
);
if (!fetchResult.ok) {
const errorData = await fetchResult.json();
const error = this.parseError(errorData);
const errorDetails = this.parseErrorDetails(errorData);
if (errorDetails) {
return {
status: fetchResult.status,
error,
details: errorDetails,
};
}
return {
status: fetchResult.status,
error,
};
}
// auth/token delete doesnt return json
if (endpoint === 'auth/token' && options.method === 'delete') {
const textResult = await fetchResult.text();
return {
status: fetchResult.status,
data: textResult,
};
} else {
const jsonResult = await fetchResult.json();
return {
status: fetchResult.status,
data: jsonResult,
};
}
} catch (err) {
return {
error: 'No wallet detected',
};
}
}
}

View File

@ -13,6 +13,5 @@ export * from './provider';
export * from './connect-dialog';
export * from './utils';
export * from './storage';
export * from './is-browser-wallet-installed';
export * from './__generated__/TransactionResult';
export * from './__generated__/WithdrawalApproval';

View File

@ -1 +0,0 @@
export const isBrowserWalletInstalled = () => Boolean(window.vega);

View File

@ -1,7 +1,6 @@
import { act, renderHook } from '@testing-library/react';
import type { Transaction } from './connectors';
import { ViewConnector } from './connectors';
import { RestConnector } from './connectors';
import { ViewConnector, JsonRpcConnector } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
import { VegaWalletProvider } from './provider';
import { LocalStorage } from '@vegaprotocol/utils';
@ -10,7 +9,7 @@ import { WALLET_KEY } from './storage';
import * as Environment from '@vegaprotocol/environment';
import * as ReactHelpers from '@vegaprotocol/react-helpers';
const restConnector = new RestConnector();
const jsonRpcConnector = new JsonRpcConnector();
const viewConnector = new ViewConnector();
const setup = () => {
@ -30,14 +29,21 @@ describe('VegaWalletProvider', () => {
{ publicKey: '222', name: 'public key 2' },
];
const spyOnConnect = jest
.spyOn(restConnector, 'connect')
.spyOn(jsonRpcConnector, 'connect')
.mockImplementation(() => Promise.resolve(mockPubKeys));
const spyOnSend = jest
.spyOn(restConnector, 'sendTx')
.mockImplementation(() => Promise.resolve(null));
.spyOn(jsonRpcConnector, 'sendTx')
.mockImplementation(() =>
Promise.resolve({
transactionHash: 'tsx',
sentAt: '',
receivedAt: '',
signature: '',
})
);
const storageSpy = jest.spyOn(LocalStorage, 'setItem');
const spyOnDisconnect = jest
.spyOn(restConnector, 'disconnect')
.spyOn(jsonRpcConnector, 'disconnect')
.mockImplementation(() => Promise.resolve());
it('connects, disconnects and retrieve keypairs', async () => {
@ -58,7 +64,7 @@ describe('VegaWalletProvider', () => {
// Connect
await act(async () => {
result.current.connect(restConnector);
result.current.connect(jsonRpcConnector);
});
expect(spyOnConnect).toHaveBeenCalled();
expect(result.current.pubKeys).toHaveLength(mockPubKeys.length);
@ -99,7 +105,7 @@ describe('VegaWalletProvider', () => {
// Connect
await act(async () => {
result.current.connect(restConnector);
result.current.connect(jsonRpcConnector);
result.current.selectPubKey(mockPubKeys[0].publicKey);
});
expect(spyOnConnect).toHaveBeenCalled();
@ -119,7 +125,7 @@ describe('VegaWalletProvider', () => {
expect(result.current.pubKey).toBe(null);
await act(async () => {
result.current.connect(restConnector);
result.current.connect(jsonRpcConnector);
result.current.selectPubKey(mockPubKeys[0].publicKey);
});
expect(result.current.pubKey).toBe(mockPubKeys[0].publicKey);

View File

@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/utils';
interface ConnectorConfig {
token: string | null;
connector: 'injected' | 'rest' | 'jsonRpc' | 'view';
connector: 'injected' | 'jsonRpc' | 'view';
url: string | null;
}

View File

@ -0,0 +1,42 @@
import type { VegaConnector } from './connectors';
import { useCallback, useEffect, useState } from 'react';
import type { JsonRpcConnector } from './connectors';
import { ClientErrors } from './connectors';
export const useIsWalletServiceRunning = (
url: string,
connectors: { [key: string]: VegaConnector },
appChainId: string
) => {
const [run, setRun] = useState<boolean | null>(null);
const checkState = useCallback(async () => {
const connector = connectors['jsonRpc'] as JsonRpcConnector;
connector.url = url;
try {
await connector.checkCompat();
const chainIdResult = await connector.getChainId();
if (chainIdResult.chainID !== appChainId) {
throw ClientErrors.WRONG_NETWORK;
}
} catch (e) {
return false;
}
return true;
}, [connectors, url, appChainId]);
useEffect(() => {
let interval: NodeJS.Timeout;
checkState().then((value) => {
setRun(value);
interval = setInterval(async () => {
setRun(await checkState());
}, 1000 * 10);
});
return () => {
clearInterval(interval);
};
}, [checkState]);
return run;
};

View File

@ -63,3 +63,5 @@ export const normalizeTransfer = <T extends Exact<Transfer, T>>(
oneOff: {},
};
};
export const isBrowserWalletInstalled = () => Boolean(window.vega);

View File

@ -6,9 +6,24 @@ When looking to use Vega via a user interface e.g. Dapp (Decentralized web App),
- If the app loads and already has a connection it can restore "eagerly" (without the user having to click connect) it **could** do so
- **must** select a connection method / wallet type: (<a name="0002-WCON-002" href="#0002-WCON-002">0002-WCON-002</a>)
- if Rest:
- If I don't have the browser wallet installed, I see "get started" on the connect button, otherwise I see "Connect" there. (<a name="0002-WCON-002" href="#0002-WCON-002">0002-WCON-002</a>)
- If I don't have the browser wallet installed, when I press "Get started" I can see immediately a way to get the Vega wallet browser extension. (<a name="0002-WCON-0010" href="#0002-WCON-0010">0002-WCON-0010</a>)
- If I do have the browser wallet installed, I can easily choose to connect to it. (<a name="0002-WCON-0011" href="#0002-WCON-0011">0002-WCON-0011</a>)
- If the desktop wallet or CLI is detected as running, I can see that and choose to connect with my desktop / CLI wallet. (<a name="0002-WCON-0012" href="#0002-WCON-0012">0002-WCON-0012</a>)
- If there is not running desktop wallet or CLI detected, I can see that I need to open my wallet. (<a name="0002-WCON-0013" href="#0002-WCON-0013">0002-WCON-0013</a>)
- I can find out more about supported browsers i.e. there is a link (Issue "List compatible browsers" vegawallet-browser#360 has to be implemented). (<a name="0002-WCON-0013" href="#0002-WCON-0013">0002-WCON-0013</a>)
- I can find out more about the Vega Wallet and see what "other" versions there are i.e. there is a link to the page on the website (currently - https://vega.xyz/wallet#overview). (<a name="0002-WCON-0014" href="#0002-WCON-0014">0002-WCON-0014</a>)
- Browser wallet:
- The browser extension you need is automatically detected if you are using Chrome or Firefox, presenting the specific call to action to install that browser extension in a visible way e.g. with a Chrome or Firefox icon. (<a name="0002-WCON-041" href="#0002-WCON-041">0002-WCON-041</a>)
- The browser extension store opens in a new tab on the Vega Wallet extension page (Chrome or Firefox). (<a name="0002-WCON-042" href="#0002-WCON-042">0002-WCON-042</a>)
- When the browser I am using is not Firefox or Chrome, there is a way to download the browser extension anyway but at my own risk i.e. I can see options for both the chrome and firefox extensions in the CTA. (<a name="0002-WCON-043" href="#0002-WCON-043">0002-WCON-043</a>)
- There is a way to understand the browser extension is an Alpha release e.g. there is a label / description. (<a name="0002-WCON-044" href="#0002-WCON-044">0002-WCON-044</a>)
- if I choose Desktop/CLI App ("jsonRpc" type):
- **must** have the option to input a non-default Wallet location (<a name="0002-WCON-003" href="#0002-WCON-003">0002-WCON-003</a>)
- If I select to enter a custom wallet location, there is a way to go back to the default view i.e. a back button or similar.
- **must** submit attempt to connect to wallet (<a name="0002-WCON-005" href="#0002-WCON-005">0002-WCON-005</a>)
- if the dapp DOES already have a permission with the wallet: **must** see that wallet is connected (<a name="0002-WCON-007" href="#0002-WCON-007">0002-WCON-007</a>) note: if the user want to connect to a different wallet to the one that they were previously connected with, they will have to hit logout.
@ -25,16 +40,6 @@ When looking to use Vega via a user interface e.g. Dapp (Decentralized web App),
- if the dapp is unable to connect for technical reason (e.g. CORS): **must** see an explanation of the error, and a method of fixing the issue (<a name="0002-WCON-016" href="#0002-WCON-016">0002-WCON-016</a>)
- ~~Browser wallet~~ `not available yet`
- Fairground hosted wallet
- **must** only be be shown this option if the dapp is connected to fairground (<a name="0002-WCON-039" href="#0002-WCON-039">0002-WCON-039</a>)
- **must** input a wallet name (<a name="0002-WCON-017" href="#0002-WCON-017">0002-WCON-017</a>)
- **must** input a password (<a name="0002-WCON-018" href="#0002-WCON-018">0002-WCON-018</a>)
- if success: **must** see that the wallet is connected and details of connected key (<a name="0002-WCON-019" href="#0002-WCON-019">0002-WCON-019</a>)
- if failure: **must** see reason for failure (<a name="0002-WCON-020" href="#0002-WCON-020">0002-WCON-020</a>)
- _note: the fairground hosted wallet is configured to automatically approve connections from dapps so there is no need for key selection._
- **must** have the option to select a different method / wallet type if I change my mind (<a name="0002-WCON-021" href="#0002-WCON-021">0002-WCON-021</a>)
... so I can use the interface to read data about my key/party or request my wallet to broadcast transactions to a Vega network.
## Disconnect wallet