Compare commits

...
Author SHA1 Message Date
Botond de21212e85 feat: add browser wallet connector 2023-02-24 15:00:20 +00:00
8 changed files with 485 additions and 8 deletions
+3
View File
@@ -2,10 +2,12 @@ import {
RestConnector,
JsonRpcConnector,
ViewConnector,
BrowserConnector,
} from '@vegaprotocol/wallet';
export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector();
export const browser = new BrowserConnector();
let view: ViewConnector;
if (typeof window !== 'undefined') {
@@ -19,4 +21,5 @@ export const Connectors = {
rest,
jsonRpc,
view,
browser,
};
@@ -0,0 +1,213 @@
import capitalize from 'lodash/capitalize';
import { createDocsLinks, t } from '@vegaprotocol/react-helpers';
import {
ButtonLink,
Diamond,
Link,
Loader,
Tick,
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
import type { BrowserConnector } from '../connectors';
import { BrowserClientErrors } from '../connectors';
import { ConnectDialogTitle } from './connect-dialog-elements';
import { Status } from '../use-json-rpc-connect';
import { useEnvironment } from '@vegaprotocol/environment';
export const ServiceErrors = {
NO_HEALTHY_NODE: 1000,
REQUEST_PROCESSING: -32000,
};
export const BrowserConnectorForm = ({
connector,
appChainId,
status,
error,
reset,
}: {
connector: BrowserConnector;
appChainId: string;
status: Status;
error: WalletClientError | null;
onConnect: () => void;
reset: () => void;
}) => {
if (status === Status.Idle) {
return null;
}
return (
<Connecting
status={status}
error={error}
connector={connector}
appChainId={appChainId}
reset={reset}
/>
);
};
const Connecting = ({
status,
error,
connector,
appChainId,
reset,
}: {
status: Status;
error: WalletClientError | null;
connector: BrowserConnector;
appChainId: string;
reset: () => void;
}) => {
if (status === Status.Error) {
return <Error error={error} appChainId={appChainId} onTryAgain={reset} />;
}
if (status === Status.CheckingVersion) {
return (
<>
<ConnectDialogTitle>{t('Checking wallet version')}</ConnectDialogTitle>
<Center>
<Loader />
</Center>
<p className="text-center">
{t('Checking your wallet is compatible with this app')}
</p>
</>
);
}
if (status === Status.GettingChainId) {
return (
<>
<ConnectDialogTitle>{t('Verifying chain')}</ConnectDialogTitle>
<Center>
<Loader />
</Center>
</>
);
}
if (status === Status.Connected) {
return (
<>
<ConnectDialogTitle>{t('Successfully connected')}</ConnectDialogTitle>
<Center>
<Tick />
</Center>
</>
);
}
if (status === Status.Connecting || status === Status.GettingPerms) {
return (
<>
<ConnectDialogTitle>{t('Connecting...')}</ConnectDialogTitle>
<Center>
<Diamond />
</Center>
<p className="text-center">
{t(
"Approve the connection from your Browser wallet app. If you have multiple wallets you'll need to choose which to connect with."
)}
</p>
</>
);
}
return null;
};
const Center = ({ children }: { children: ReactNode }) => {
return (
<div className="flex justify-center items-center my-6">{children}</div>
);
};
const Error = ({
error,
appChainId,
onTryAgain,
}: {
error: WalletClientError | null;
appChainId: string;
onTryAgain: () => void;
}) => {
let title = t('Something went wrong');
let text: ReactNode | undefined = t('An unknown error occurred');
let tryAgain: ReactNode | null = (
<p className="text-center">
<ButtonLink onClick={onTryAgain}>{t('Try again')}</ButtonLink>
</p>
);
const { VEGA_DOCS_URL } = useEnvironment();
if (error) {
if (error.code === BrowserClientErrors.NO_SERVICE.code) {
title = t('No wallet detected');
text = t(
'No Vega Wallet application running. Please install the vega wallet browser extension.'
);
} else if (error.code === BrowserClientErrors.WRONG_NETWORK.code) {
title = t('Wrong network');
text = t(
'To complete your wallet connection, set your wallet network in your app to "%s".',
appChainId
);
} else if (error.code === ServiceErrors.NO_HEALTHY_NODE) {
title = error.title;
text = (
<>
{capitalize(error.message)}
{'. '}
{VEGA_DOCS_URL && (
<Link
href={createDocsLinks(VEGA_DOCS_URL).VEGA_WALLET_CONCEPTS_URL}
>
{t('Read the docs to troubleshoot')}
</Link>
)}
</>
);
} else if (error.code === ServiceErrors.REQUEST_PROCESSING) {
title = t('Connection in progress');
text = t('Approve the connection from your Vega wallet app.');
tryAgain = null;
} else if (error.code === 0) {
title = t('Wrong network');
text = (
<>
{t(
`To complete your wallet connection, set your wallet network in your
app to %s.`,
appChainId
)}
</>
);
} else if (error.code === BrowserClientErrors.INVALID_WALLET.code) {
title = error.title;
const errorData = error.message?.split('\n ') || [];
text = (
<span className="flex flex-col">
{errorData.map((str, i) => (
<span key={i}>{str}</span>
))}
</span>
);
} else {
title = t(error.title);
text = t(error.message);
}
}
return (
<>
<ConnectDialogTitle>{title}</ConnectDialogTitle>
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
{tryAgain}
</>
);
};
@@ -13,9 +13,14 @@ import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { ExternalLinks, t, useChainIdQuery } from '@vegaprotocol/react-helpers';
import type { VegaConnector } from '../connectors';
import { ViewConnector } from '../connectors';
import { JsonRpcConnector, RestConnector } from '../connectors';
import {
JsonRpcConnector,
RestConnector,
BrowserConnector,
} from '../connectors';
import { RestConnectorForm } from './rest-connector-form';
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
import { BrowserConnectorForm } from './browser-connector-form';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import {
ConnectDialogContent,
@@ -24,11 +29,12 @@ import {
} from './connect-dialog-elements';
import type { Status } from '../use-json-rpc-connect';
import { useJsonRpcConnect } from '../use-json-rpc-connect';
import { useBrowserConnect } from '../use-browser-connect';
import { ViewConnectorForm } from './view-connector-form';
export const CLOSE_DELAY = 1700;
type Connectors = { [key: string]: VegaConnector };
type WalletType = 'jsonRpc' | 'hosted' | 'view';
type WalletType = 'jsonRpc' | 'hosted' | 'view' | 'browser';
export interface VegaConnectDialogProps {
connectors: Connectors;
@@ -117,7 +123,7 @@ export const VegaConnectDialog = ({
size="small"
onChange={updateVegaWalletDialog}
>
{renderContent()}
<div id="wallet-dialog-000">{renderContent()}</div>
</Dialog>
);
};
@@ -148,6 +154,8 @@ const ConnectDialogContainer = ({
}, [closeDialog]);
const { connect, ...jsonRpcState } = useJsonRpcConnect(delayedOnConnect);
const { connect: connectBW, ...browserState } =
useBrowserConnect(delayedOnConnect);
const handleSelect = (type: WalletType, isHosted = false) => {
let connector;
@@ -174,6 +182,10 @@ const ConnectDialogContainer = ({
if (connector instanceof JsonRpcConnector) {
connect(connector, appChainId);
}
if (connector instanceof BrowserConnector) {
connectBW(connector, appChainId);
}
};
return selectedConnector !== undefined && walletType !== undefined ? (
@@ -181,6 +193,7 @@ const ConnectDialogContainer = ({
type={walletType}
connector={selectedConnector}
jsonRpcState={jsonRpcState}
browserState={browserState}
onConnect={closeDialog}
appChainId={appChainId}
reset={reset}
@@ -212,6 +225,13 @@ const ConnectorList = ({
<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="browser"
text={t('Connect Browser wallet')}
onClick={() => onSelect('browser')}
/>
</li>
<li className="mb-4 last:mb-0">
<ConnectionOption
type="jsonRpc"
@@ -247,6 +267,7 @@ const SelectedForm = ({
connector,
appChainId,
jsonRpcState,
browserState,
reset,
onConnect,
}: {
@@ -257,6 +278,10 @@ const SelectedForm = ({
status: Status;
error: WalletClientError | null;
};
browserState: {
status: Status;
error: WalletClientError | null;
};
reset: () => void;
onConnect: () => void;
}) => {
@@ -322,6 +347,24 @@ const SelectedForm = ({
);
}
if (connector instanceof BrowserConnector) {
return (
<>
<ConnectDialogContent>
<BrowserConnectorForm
connector={connector}
status={browserState.status}
error={browserState.error}
onConnect={onConnect}
appChainId={appChainId}
reset={reset}
/>
</ConnectDialogContent>
<ConnectDialogFooter />
</>
);
}
throw new Error('No connector selected');
};
@@ -334,6 +377,7 @@ const ConnectionOption = ({
text: string;
onClick: () => void;
}) => {
const id = type === 'browser' ? 'vega-wallet-connect' : undefined;
return (
<Button
onClick={onClick}
@@ -341,6 +385,7 @@ const ConnectionOption = ({
fill={true}
variant={['hosted', 'view'].includes(type) ? 'default' : 'primary'}
data-testid={`connector-${type}`}
id={id}
>
<span className="-mx-6 flex text-left justify-between items-center">
{text}
@@ -0,0 +1,138 @@
import { t } from '@vegaprotocol/react-helpers';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { WalletClient } from '@vegaprotocol/wallet-client';
import { clearConfig, getConfig, setConfig } from '../storage';
import type { Transaction, VegaConnector } from './vega-connector';
import { WalletError } from './vega-connector';
export const BrowserClientErrors = {
NO_SERVICE: new WalletError(t('No service'), 100),
INVALID_WALLET: new WalletError(t('Wallet version invalid'), 103),
WRONG_NETWORK: new WalletError(
t('Wrong network'),
104,
t('App is configured to work with a different chain')
),
UNKNOWN: new WalletError(
t('Something went wrong'),
105,
t('Unknown error occurred')
),
NO_CLIENT: new WalletError(t('No client found.'), 106),
} as const;
export class BrowserConnector implements VegaConnector {
url: null;
reqId = 0;
client?: WalletClient;
constructor() {
this.url = null;
const cfg = getConfig();
if (cfg && cfg.connector === 'browser') {
this.initialize();
this.connectWallet();
}
}
initialize() {
if (!this.client) {
try {
this.client = new WalletClient({
type: 'browser',
firefoxId: '62739efdd16a15a5ff4527bcd08fe6302b18b752@temporary-addon',
chromeId: 'pebgkinfegfpnkamklihgpeolleghngl',
});
} catch (err) {
throw BrowserClientErrors.NO_SERVICE;
}
}
}
async getChainId() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
try {
return await this.client.GetChainId();
} catch (err) {
const {
code = BrowserClientErrors.UNKNOWN.code,
message = BrowserClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
async connectWallet() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
try {
await this.client.ConnectWallet();
return null;
} catch (err) {
const {
code = BrowserClientErrors.UNKNOWN.code,
message = BrowserClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
// connect actually calling list_keys here, not to be confused with connect_wallet
// which retrieves the session token
async connect() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
try {
const result = await this.client.ListKeys();
return result.keys;
} catch (err) {
const {
code = BrowserClientErrors.UNKNOWN.code,
message = BrowserClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
async disconnect() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
await this.client.DisconnectWallet();
clearConfig();
}
async sendTx(pubKey: string, transaction: Transaction) {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
const result = await this.client.SendTransaction({
publicKey: pubKey,
sendingMode: 'TYPE_SYNC',
transaction,
});
return {
transactionHash: result.transactionHash,
sentAt: result.sentAt,
receivedAt: result.receivedAt,
signature: result.transaction.signature.value,
};
}
async checkCompat() {
return true;
}
}
+1
View File
@@ -3,3 +3,4 @@ export * from './rest-connector';
export * from './injected-connector';
export * from './json-rpc-connector';
export * from './view-connector';
export * from './browser-connector';
@@ -37,6 +37,7 @@ export class JsonRpcConnector implements VegaConnector {
if (cfg && cfg.url) {
this.url = cfg.url;
this.client = new WalletClient({
type: 'http',
address: cfg.url,
token: cfg.token ?? undefined,
onTokenChange: (token) => {
@@ -53,6 +54,7 @@ export class JsonRpcConnector implements VegaConnector {
set url(url: string) {
this._url = url;
this.client = new WalletClient({
type: 'http',
address: url,
token: this.token ?? undefined,
onTokenChange: (token) =>
@@ -71,8 +73,7 @@ export class JsonRpcConnector implements VegaConnector {
throw ClientErrors.NO_CLIENT;
}
try {
const { result } = await this.client.GetChainId();
return result;
return await this.client.GetChainId();
} catch (err) {
const {
code = ClientErrors.UNKNOWN.code,
@@ -109,7 +110,7 @@ export class JsonRpcConnector implements VegaConnector {
}
try {
const { result } = await this.client.ListKeys();
const result = await this.client.ListKeys();
return result.keys;
} catch (err) {
const {
@@ -135,7 +136,7 @@ export class JsonRpcConnector implements VegaConnector {
throw ClientErrors.NO_CLIENT;
}
const { result } = await this.client.SendTransaction({
const result = await this.client.SendTransaction({
publicKey: pubKey,
sendingMode: 'TYPE_SYNC',
transaction,
+1 -1
View File
@@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/react-helpers';
interface ConnectorConfig {
token: string | null;
connector: 'rest' | 'jsonRpc' | 'view';
connector: 'rest' | 'jsonRpc' | 'view' | 'browser';
url: string | null;
}
+76
View File
@@ -0,0 +1,76 @@
import { useCallback, useState } from 'react';
import { WalletClientError } from '@vegaprotocol/wallet-client';
import type { BrowserConnector } from './connectors';
import { ClientErrors } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
export enum Status {
Idle = 'Idle',
CheckingVersion = 'CheckingVersion',
GettingChainId = 'GettingChainId',
Connecting = 'Connecting',
GettingPerms = 'GettingPerms',
ListingKeys = 'ListingKeys',
Connected = 'Connected',
Error = 'Error',
}
export const useBrowserConnect = (onConnect: () => void) => {
const { connect } = useVegaWallet();
const [status, setStatus] = useState(Status.Idle);
const [error, setError] = useState<WalletClientError | null>(null);
const attemptConnect = useCallback(
async (connector: BrowserConnector, appChainId: string) => {
try {
connector.initialize();
// Check that the running wallet is compatible with this connector
setStatus(Status.CheckingVersion);
await connector.checkCompat();
// Check if wallet is configured for the same chain as the app
setStatus(Status.GettingChainId);
// Dont throw in when cypress is running as trading app relies on
// mocks which result in a mismatch between chainId for app and
// chainId for wallet
if (!('Cypress' in window)) {
const chainIdResult = await connector.getChainId();
console.log('CHAIN RES: ', chainIdResult);
if (chainIdResult.chainID !== appChainId) {
// Throw wallet error for consitent error handling
throw ClientErrors.WRONG_NETWORK;
}
}
// Start connection flow. User will be prompted to select a wallet and enter
// its password in the wallet application, promise will resolve once successful
// or it will throw
setStatus(Status.Connecting);
await connector.connectWallet();
setStatus(Status.GettingPerms);
// Call connect in the wallet provider. The connector will be stored for
// future actions such as sending transactions
await connect(connector);
setStatus(Status.Connected);
onConnect();
} catch (err) {
console.log('ERROR!!!!!', err);
if (err instanceof WalletClientError) {
setError(err);
}
setStatus(Status.Error);
}
},
[onConnect, connect]
);
return {
status,
error,
connect: attemptConnect,
};
};