From 30bc99a4551f67db47e33798c403e0fcfed855df Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Mon, 12 Feb 2024 19:01:38 -0800 Subject: [PATCH] feat: eager connection --- apps/trading/lib/vega-wallet.ts | 18 +++-- apps/trading/pages/maybe-connect-eagerly.tsx | 22 +----- apps/trading/stores/global.ts | 2 - libs/wallet/src/connectors-v2/index.ts | 4 + .../src/connectors-v2/injected-connector.ts | 5 +- .../src/connectors-v2/json-rpc-connector.ts | 57 ++++++++------ .../src/connectors-v2/read-only-connector.ts | 75 +++++++++++++++++++ .../src/connectors-v2/snap-connector.ts | 47 +++++++----- libs/wallet/src/index.ts | 11 ++- libs/wallet/src/storage.ts | 8 +- libs/wallet/src/types.ts | 4 +- libs/wallet/src/use-eager-connect.ts | 39 +++------- libs/wallet/src/use-vega-wallet.ts | 45 ++++++----- libs/wallet/src/wallet.ts | 2 + 14 files changed, 213 insertions(+), 126 deletions(-) create mode 100644 libs/wallet/src/connectors-v2/index.ts create mode 100644 libs/wallet/src/connectors-v2/read-only-connector.ts diff --git a/apps/trading/lib/vega-wallet.ts b/apps/trading/lib/vega-wallet.ts index 556ad4071..b5b5b2c17 100644 --- a/apps/trading/lib/vega-wallet.ts +++ b/apps/trading/lib/vega-wallet.ts @@ -2,23 +2,31 @@ import { InjectedConnector, JsonRpcConnector, SnapConnector, + ReadOnlyConnector, createConfig, fairground, stagnet, + getConfig, } from '@vegaprotocol/wallet'; -export const injected = new InjectedConnector(); -export const jsonRpc = new JsonRpcConnector({ +const walletCfg = getConfig(); + +const injected = new InjectedConnector(); + +const jsonRpc = new JsonRpcConnector({ url: 'http://localhost:1789/api/v2/requests', + token: walletCfg?.token, }); -export const snap = new SnapConnector({ + +const snap = new SnapConnector({ node: 'https://api.n08.testnet.vega.rocks', - id: 'npm:@vegaprotocol/snap', + snapId: 'npm:@vegaprotocol/snap', version: '0.3.1', }); +const readOnly = new ReadOnlyConnector(walletCfg?.pubKey); export const config = createConfig({ chains: [fairground, stagnet], defaultChainId: stagnet.id, - connectors: [injected, jsonRpc, snap], + connectors: [injected, jsonRpc, snap, readOnly], }); diff --git a/apps/trading/pages/maybe-connect-eagerly.tsx b/apps/trading/pages/maybe-connect-eagerly.tsx index b80bde839..2e39f5533 100644 --- a/apps/trading/pages/maybe-connect-eagerly.tsx +++ b/apps/trading/pages/maybe-connect-eagerly.tsx @@ -1,33 +1,15 @@ -import { useSearchParams } from 'react-router-dom'; -import { useEffect, useState } from 'react'; import { useEnvironment } from '@vegaprotocol/environment'; +import { useEagerConnect as useVegaEagerConnect } from '@vegaprotocol/wallet'; import { useEagerConnect as useEthereumEagerConnect } from '@vegaprotocol/web3'; -// import { -// useEagerConnect as useVegaEagerConnect, -// useVegaWallet, -// } from '@vegaprotocol/wallet'; -import { useGlobalStore } from '../stores'; -// import { useConnectors } from '../lib/vega-connectors'; import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval'; export const MaybeConnectEagerly = () => { const { VEGA_ENV, SENTRY_DSN } = useEnvironment(); - const update = useGlobalStore((store) => store.update); - // const connectors = useConnectors(); - // const eagerConnecting = useVegaEagerConnect(connectors); const [isTelemetryApproved] = useTelemetryApproval(); useEthereumEagerConnect( isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {} ); + useVegaEagerConnect(); - // const { pubKey, connect } = useVegaWallet(); - const [searchParams] = useSearchParams(); - const [query] = useState(searchParams.get('address')); - // if (query && !pubKey) { - // connect(connectors.view); - // } - // useEffect(() => { - // update({ eagerConnecting }); - // }, [update, eagerConnecting]); return null; }; diff --git a/apps/trading/stores/global.ts b/apps/trading/stores/global.ts index e83687eae..e5b69fd04 100644 --- a/apps/trading/stores/global.ts +++ b/apps/trading/stores/global.ts @@ -4,7 +4,6 @@ import produce from 'immer'; interface GlobalStore { marketId: string | null; - eagerConnecting: boolean; update: (store: Partial>) => void; } @@ -15,7 +14,6 @@ interface PageTitleStore { export const useGlobalStore = create()((set) => ({ marketId: LocalStorage.getItem('marketId') || null, - eagerConnecting: false, update: (newState) => { set( produce((state: GlobalStore) => { diff --git a/libs/wallet/src/connectors-v2/index.ts b/libs/wallet/src/connectors-v2/index.ts new file mode 100644 index 000000000..af3f8555a --- /dev/null +++ b/libs/wallet/src/connectors-v2/index.ts @@ -0,0 +1,4 @@ +export { InjectedConnector } from './injected-connector'; +export { SnapConnector } from './snap-connector'; +export { JsonRpcConnector } from './json-rpc-connector'; +export { ReadOnlyConnector } from './read-only-connector'; diff --git a/libs/wallet/src/connectors-v2/injected-connector.ts b/libs/wallet/src/connectors-v2/injected-connector.ts index e610d99e6..a69be15cc 100644 --- a/libs/wallet/src/connectors-v2/injected-connector.ts +++ b/libs/wallet/src/connectors-v2/injected-connector.ts @@ -1,11 +1,13 @@ +import { clearConfig, setConfig } from '../storage'; import { type TransactionParams, type Connector } from '../types'; export class InjectedConnector implements Connector { - id = 'injected'; + readonly id = 'injected'; async connectWallet(chainId: string) { try { await window.vega.connectWallet({ chainId }); + setConfig({ type: this.id, chainId }); return { success: true }; } catch (err) { return { @@ -17,6 +19,7 @@ export class InjectedConnector implements Connector { async disconnectWallet() { try { await window.vega.disconnectWallet(); + clearConfig(); return { success: true }; } catch (err) { return { error: 'failed to disconnect' }; diff --git a/libs/wallet/src/connectors-v2/json-rpc-connector.ts b/libs/wallet/src/connectors-v2/json-rpc-connector.ts index aaac2f38c..2345c4f0c 100644 --- a/libs/wallet/src/connectors-v2/json-rpc-connector.ts +++ b/libs/wallet/src/connectors-v2/json-rpc-connector.ts @@ -1,19 +1,22 @@ +import { clearConfig, setConfig } from '../storage'; import { JsonRpcMethod, type Connector, type TransactionParams, } from '../types'; -type JsonRpcConnectorConfig = { url: string }; +type JsonRpcConnectorConfig = { url: string; token?: string }; export class JsonRpcConnector implements Connector { - id = 'json-rpc'; - config: JsonRpcConnectorConfig; + readonly id = 'jsonRpc'; + + url: string; + token: string | undefined; requestId: number = 0; - token: string | null = null; constructor(config: JsonRpcConnectorConfig) { - this.config = config; + this.url = config.url; + this.token = config.token; } async connectWallet(desiredChainId: string) { @@ -24,28 +27,33 @@ export class JsonRpcConnector implements Connector { throw new Error('incorrect chain id'); } - const { response, data } = await this.request( - JsonRpcMethod.ConnectWallet, - { - hostname: window.location.hostname, - } - ); + if (!this.token) { + const { response, data } = await this.request( + JsonRpcMethod.ConnectWallet, + { + hostname: window.location.hostname, + } + ); - const token = response.headers.get('Authorization'); + const token = response.headers.get('Authorization'); - if (!response.ok) { - if ('error' in data) { - return { error: data.error.data }; + if (!response.ok) { + if ('error' in data) { + return { error: data.error.data }; + } + + return { error: 'failed to connect' }; } - return { error: 'failed to connect' }; + if (!token) { + return { error: 'failed to connect' }; + } + + this.token = token; } - if (!token) { - return { error: 'failed to connect' }; - } + setConfig({ type: this.id, token: this.token, chainId, url: this.url }); - this.token = token; return { success: true }; } catch (err) { return { @@ -57,6 +65,7 @@ export class JsonRpcConnector implements Connector { async disconnectWallet() { try { await this.request(JsonRpcMethod.DisconnectWallet); + clearConfig(); return { success: true }; } catch (err) { return { error: 'failed to disconnect' }; @@ -116,7 +125,7 @@ export class JsonRpcConnector implements Connector { headers.set('Authorization', this.token); } - const response = await fetch(this.config.url, { + const response = await fetch(this.url, { method: 'POST', headers, body: JSON.stringify({ @@ -126,7 +135,13 @@ export class JsonRpcConnector implements Connector { params, }), }); + const data = await response.json(); + + if (!response.ok) { + this.token = undefined; + } + return { data, response, diff --git a/libs/wallet/src/connectors-v2/read-only-connector.ts b/libs/wallet/src/connectors-v2/read-only-connector.ts new file mode 100644 index 000000000..2e3fc1d89 --- /dev/null +++ b/libs/wallet/src/connectors-v2/read-only-connector.ts @@ -0,0 +1,75 @@ +import { clearConfig, setConfig } from '../storage'; +import { type Connector } from '../types'; + +export class ReadOnlyConnector implements Connector { + readonly id = 'readOnly'; + + pubKey: string | undefined; + + constructor(pubKey?: string) { + if (pubKey) { + this.pubKey = pubKey; + } + } + + async connectWallet() { + if (!this.pubKey) { + const pubKey = window.prompt('Enter pubkey'); + + if (!pubKey) { + return { error: 'failed to connect' }; + } + + this.pubKey = pubKey; + setConfig({ type: this.id, pubKey }); + } + return { success: true }; + } + + async disconnectWallet() { + this.pubKey = undefined; + clearConfig(); + return { success: true }; + } + + async getChainId() { + return { + error: `You are connected in a view only state for public key: ${this.pubKey}`, + }; + } + + async listKeys() { + if (!this.pubKey) { + return { error: 'failed to list keys' }; + } + return [ + { + name: 'View only', + publicKey: this.pubKey, + }, + ]; + } + + async isConnected() { + if (this.pubKey) { + return { connected: true }; + } + + return { connected: false }; + } + + // @ts-ignore deliberate fail + async sendTransaction() { + return { + error: `You are connected in a view only state for public key: ${this.pubKey}. In order to send transactions you must connect to a real wallet.`, + }; + } + + on() { + console.warn('events are not supported using a read only connection'); + } + + off() { + console.warn('events are not supported using a read only connection'); + } +} diff --git a/libs/wallet/src/connectors-v2/snap-connector.ts b/libs/wallet/src/connectors-v2/snap-connector.ts index bb6231622..1d7c93f19 100644 --- a/libs/wallet/src/connectors-v2/snap-connector.ts +++ b/libs/wallet/src/connectors-v2/snap-connector.ts @@ -1,3 +1,4 @@ +import { clearConfig, setConfig } from '../storage'; import { JsonRpcMethod, type Connector, @@ -11,18 +12,22 @@ enum EthereumMethod { } type SnapConfig = { - id: string; node: string; version: string; + snapId: string; }; export class SnapConnector implements Connector { - id = 'snap'; + readonly id = 'snap'; - config: SnapConfig; + node: string; + version: string; + snapId: string; constructor(config: SnapConfig) { - this.config = config; + this.node = config.node; + this.version = config.version; + this.snapId = config.snapId; } async connectWallet(desiredChainId: string) { @@ -35,6 +40,7 @@ export class SnapConnector implements Connector { throw new Error('incorrect chain id'); } + setConfig({ type: this.id, chainId }); return { success: true }; } catch (err) { return { @@ -44,15 +50,16 @@ export class SnapConnector implements Connector { } async disconnectWallet() { - console.warn('disconnectWallet not implemented'); - return { error: 'failed to disconnect' }; + clearConfig(); + return { success: true }; + // return { error: 'failed to disconnect' }; } // deprecated, pass chain on connect async getChainId() { try { const res = await this.invokeSnap(JsonRpcMethod.GetChainId, { - networkEndpoints: [this.config.node], + networkEndpoints: [this.node], }); return { chainId: res.chainID }; } catch (err) { @@ -80,7 +87,7 @@ export class SnapConnector implements Connector { publicKey: params.publicKey, sendingMode: params.sendingMode, transaction: params.transaction, - networkEndpoints: [this.config.node], + networkEndpoints: [this.node], }); return { @@ -95,14 +102,22 @@ export class SnapConnector implements Connector { } } + on() { + console.warn('events are not supported in json rpc wallet'); + } + + off() { + console.warn('events are not supported in json rpc wallet'); + } + //////////////////////////////////// // Snap methods //////////////////////////////////// async requestSnap() { const res = await this.request(EthereumMethod.RequestSnaps, { - [this.config.id]: { - version: this.config.version, + [this.snapId]: { + version: this.version, }, }); console.log(res); @@ -111,13 +126,13 @@ export class SnapConnector implements Connector { async getSnap() { const snaps = await this.request(EthereumMethod.GetSnaps); return Object.values(snaps).find( - (s) => s.id === this.config.id && s.version === this.config.version + (s) => s.id === this.snapId && s.version === this.version ); } async invokeSnap(method: JsonRpcMethod, params?: any) { return await this.request(EthereumMethod.InvokeSnap, { - snapId: this.config.id, + snapId: this.snapId, request: { method: method, params, @@ -136,12 +151,4 @@ export class SnapConnector implements Connector { params, }); } - - on() { - console.warn('events are not supported in json rpc wallet'); - } - - off() { - console.warn('events are not supported in json rpc wallet'); - } } diff --git a/libs/wallet/src/index.ts b/libs/wallet/src/index.ts index 7b44df8d0..1d1d8657b 100644 --- a/libs/wallet/src/index.ts +++ b/libs/wallet/src/index.ts @@ -1,6 +1,9 @@ -export { InjectedConnector } from './connectors-v2/injected-connector'; -export { SnapConnector } from './connectors-v2/snap-connector'; -export { JsonRpcConnector } from './connectors-v2/json-rpc-connector'; +export { + InjectedConnector, + SnapConnector, + JsonRpcConnector, + ReadOnlyConnector, +} from './connectors-v2'; export * from './types'; export { createConfig, VegaWalletProvider } from './wallet'; @@ -19,5 +22,7 @@ export { ConnectDialog } from './connect-dialog-v2'; export { useVegaWalletDialogStore } from './connect-dialog/vega-wallet-dialog-store'; export { useVegaWallet } from './use-vega-wallet'; export { isBrowserWalletInstalled } from './utils'; +export { useEagerConnect } from './use-eager-connect'; +export { getConfig } from './storage'; export { fairground, stagnet, Chain } from './chains'; diff --git a/libs/wallet/src/storage.ts b/libs/wallet/src/storage.ts index 238775a34..f975f3eff 100644 --- a/libs/wallet/src/storage.ts +++ b/libs/wallet/src/storage.ts @@ -1,9 +1,11 @@ import { LocalStorage } from '@vegaprotocol/utils'; interface ConnectorConfig { - token: string | null; - connector: 'injected' | 'jsonRpc' | 'view' | 'snap'; - url: string | null; + type: 'injected' | 'jsonRpc' | 'readOnly' | 'snap'; + url?: string; + chainId?: string; + token?: string; + pubKey?: string; } export const WALLET_CONFIG = 'vega_wallet_config'; diff --git a/libs/wallet/src/types.ts b/libs/wallet/src/types.ts index edb6432d2..116ccafa1 100644 --- a/libs/wallet/src/types.ts +++ b/libs/wallet/src/types.ts @@ -22,9 +22,9 @@ export interface TransactionParams { } export interface Connector { - id: string; + readonly id: string; - connectWallet(chainId: string): Promise<{ success: boolean } | IWalletError>; + connectWallet(chainId?: string): Promise<{ success: boolean } | IWalletError>; disconnectWallet(): Promise<{ success: boolean } | IWalletError>; diff --git a/libs/wallet/src/use-eager-connect.ts b/libs/wallet/src/use-eager-connect.ts index c430a7a9a..fb3c453fc 100644 --- a/libs/wallet/src/use-eager-connect.ts +++ b/libs/wallet/src/use-eager-connect.ts @@ -1,48 +1,27 @@ import { useEffect, useState } from 'react'; -import { InjectedConnector } from './connectors/injected-connector'; -import { SnapConnector } from './connectors/snap-connector'; import { getConfig } from './storage'; -import type { Connectors } from './connectors'; +import { useConnect } from './wallet'; import { useVegaWallet } from './use-vega-wallet'; -export function useEagerConnect(connectors: Connectors) { +export function useEagerConnect() { + const { connect, connectors } = useConnect(); + const { onConnect } = useVegaWallet(); const [connecting, setConnecting] = useState(true); - const { vegaUrl, chainId, connect, acknowledgeNeeded } = useVegaWallet(); useEffect(() => { const attemptConnect = async () => { const cfg = getConfig(); // No stored config, or config was malformed or no risk accepted - if (!cfg || !cfg.connector || acknowledgeNeeded) { + if (!cfg || !cfg.type) { setConnecting(false); return; } - // Use the connector string in local storage to find the right connector to auto - // connect to - const connector = connectors[cfg.connector]; - - // Developer hasn't provided this connector - if (!connector) { - setConnecting(false); - console.error( - `Can't eager connect, connector: ${cfg.connector} not found` - ); - return; - } - try { - if (connector instanceof InjectedConnector) { - await connector.connectWallet(chainId); - await connect(connector); - } else if (connector instanceof SnapConnector) { - connector.nodeAddress = new URL(vegaUrl).origin; - await connect(connector); - } else { - await connect(connector); - } + await connect(cfg.type); + onConnect(); } catch { - console.warn(`Failed to connect with connector: ${cfg.connector}`); + console.warn(`Failed to connect with connector: ${cfg.type}`); } finally { setConnecting(false); } @@ -51,7 +30,7 @@ export function useEagerConnect(connectors: Connectors) { if (typeof window !== 'undefined') { attemptConnect(); } - }, [connect, connectors, acknowledgeNeeded, vegaUrl, chainId]); + }, [connect, connectors, onConnect]); return connecting; } diff --git a/libs/wallet/src/use-vega-wallet.ts b/libs/wallet/src/use-vega-wallet.ts index 11274d44a..90aeeb4a2 100644 --- a/libs/wallet/src/use-vega-wallet.ts +++ b/libs/wallet/src/use-vega-wallet.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; -import { useConfig } from './wallet'; +import { useConfig, useWallet } from './wallet'; import { useVegaWalletDialogStore } from './connect-dialog'; +import { useCallback } from 'react'; interface PubKeyStore { pubKey: string | null; @@ -14,26 +15,32 @@ export const useVegaWalletStore = create()((set) => ({ // Only for vega apps that expect a single selected key export const useVegaWallet = () => { - const store = useVegaWalletStore(); - const dialog = useVegaWalletDialogStore(); - const config = useConfig(); + const pubKey = useVegaWalletStore((store) => store.pubKey); + const setPubKey = useVegaWalletStore((store) => store.setPubKey); + const updateDialog = useVegaWalletDialogStore( + (store) => store.updateVegaWalletDialog + ); + const pubKeys = useWallet((store) => store.keys); + + const onConnect = useCallback(() => { + const state = config.store.getState(); + + if (state.keys.length) { + setPubKey(state.keys[0].publicKey); + } + + if (state.status === 'connected') { + setTimeout(() => { + updateDialog(false); + }, 1000); + } + }, [config.store, setPubKey, updateDialog]); return { - pubKey: store.pubKey, - selectPubKey: store.setPubKey, - onConnect: () => { - const state = config.store.getState(); - - if (state.keys.length) { - store.setPubKey(state.keys[0].publicKey); - } - - if (state.status === 'connected') { - setTimeout(() => { - dialog.updateVegaWalletDialog(false); - }, 1000); - } - }, + pubKeys, + pubKey, + selectPubKey: setPubKey, + onConnect, }; }; diff --git a/libs/wallet/src/wallet.ts b/libs/wallet/src/wallet.ts index c67bcea27..0ad773f39 100644 --- a/libs/wallet/src/wallet.ts +++ b/libs/wallet/src/wallet.ts @@ -80,6 +80,8 @@ export function createConfig(cfg: Config): Wallet { if (!connector) return; + await connector.disconnectWallet(); + store.setState({ status: 'disconnected', current: undefined, keys: [] }); }