feat: make consistent errors for connectors

This commit is contained in:
Matthew Russell
2024-02-29 16:21:26 +00:00
parent d1ec5f5136
commit 7b82882fdc
10 changed files with 131 additions and 90 deletions
@@ -13,7 +13,7 @@ import { Routes } from '../../lib/links';
import { WelcomeDialogContent } from './welcome-dialog-content';
import { useOnboardingStore } from './use-get-onboarding-step';
import { ensureSuffix } from '@vegaprotocol/utils';
import { type ConnectorType } from '@vegaprotocol/wallet';
import { ConnectorErrors, type ConnectorType } from '@vegaprotocol/wallet';
/**
* A list of paths on which the welcome dialog should be omitted.
@@ -106,8 +106,10 @@ const ConnectionOptions = ({ onConnect }: { onConnect: () => void }) => {
);
})}
</ul>
{error && !error.includes('the user rejected') && (
<p className="text-danger text-sm first-letter:uppercase">{error}</p>
{error && error.code !== ConnectorErrors.userRejected.code && (
<p className="text-danger text-sm first-letter:uppercase">
{error.message}
</p>
)}
</div>
);
@@ -5,7 +5,11 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { type ConnectorType, type Status } from '@vegaprotocol/wallet';
import {
ConnectorErrors,
type ConnectorType,
type Status,
} from '@vegaprotocol/wallet';
import { useWallet } from '../../hooks/use-wallet';
import { useConnect } from '../../hooks/use-connect';
import classNames from 'classnames';
@@ -59,8 +63,10 @@ export const ConnectionOptions = ({ onConnect }: { onConnect: () => void }) => {
);
})}
</ul>
{error && !error.includes('the user rejected') && (
<p className="text-danger text-sm first-letter:uppercase">{error}</p>
{error && error.code !== ConnectorErrors.userRejected.code && (
<p className="text-danger text-sm first-letter:uppercase">
{error.message}
</p>
)}
</div>
);
+21
View File
@@ -2,3 +2,24 @@ export { InjectedConnector } from './injected-connector';
export { SnapConnector } from './snap-connector';
export { JsonRpcConnector } from './json-rpc-connector';
export { ReadOnlyConnector } from './read-only-connector';
export class ConnectorError extends Error {
code: number;
constructor(message: string, code: number) {
super(message);
this.code = code;
}
}
export const ConnectorErrors = {
userRejected: new ConnectorError('user rejected', 0),
noConnector: new ConnectorError('no connector', 1),
connect: new ConnectorError('failed to connect', 2),
disconnect: new ConnectorError('failed to disconnect', 3),
chainId: new ConnectorError('incorrect chain', 4),
listKeys: new ConnectorError('failed to list keys', 5),
isConnected: new ConnectorError('failed to check connection', 6),
sendTransaction: new ConnectorError('failed to send transaction', 7),
unknown: new ConnectorError('unknown error', 8),
};
@@ -1,4 +1,9 @@
import { type TransactionParams, type Connector } from '../types';
import { ConnectorErrors } from '.';
import {
type TransactionParams,
type Connector,
type VegaWalletEvent,
} from '../types';
export class InjectedConnector implements Connector {
readonly id = 'injected';
@@ -13,9 +18,7 @@ export class InjectedConnector implements Connector {
await window.vega.connectWallet({ chainId });
return { success: true };
} catch (err) {
return {
error: err instanceof Error ? err.message : 'failed to connect',
};
throw ConnectorErrors.connect;
}
}
@@ -24,7 +27,7 @@ export class InjectedConnector implements Connector {
await window.vega.disconnectWallet();
return { success: true };
} catch (err) {
return { error: 'failed to disconnect' };
throw ConnectorErrors.disconnect;
}
}
@@ -34,7 +37,7 @@ export class InjectedConnector implements Connector {
const res = await window.vega.getChainId();
return { chainId: res.chainID };
} catch (err) {
return { error: 'failed to get chain id' };
throw ConnectorErrors.chainId;
}
}
@@ -43,7 +46,7 @@ export class InjectedConnector implements Connector {
const res = await window.vega.listKeys();
return res.keys;
} catch (err) {
return { error: 'failed to list keys' };
throw ConnectorErrors.listKeys;
}
}
@@ -52,7 +55,7 @@ export class InjectedConnector implements Connector {
const res = await window.vega.isConnected();
return { connected: res };
} catch (err) {
return { error: 'failed to check isConnected' };
throw ConnectorErrors.isConnected;
}
}
@@ -67,16 +70,15 @@ export class InjectedConnector implements Connector {
sentAt: res.sentAt,
};
} catch (err) {
console.error(err);
return { error: 'failed to send transaction' };
throw ConnectorErrors.isConnected;
}
}
on(event: 'client.disconnected', callback: () => void) {
on(event: VegaWalletEvent, callback: () => void) {
window.vega.on(event, callback);
}
off(event: 'client.disconnected') {
off(event: VegaWalletEvent) {
window.vega.off(event);
}
}
@@ -5,6 +5,7 @@ import {
type TransactionParams,
type Store,
} from '../types';
import { ConnectorError, ConnectorErrors } from '.';
type JsonRpcConnectorConfig = { url: string; token?: string };
@@ -31,11 +32,11 @@ export class JsonRpcConnector implements Connector {
const chainRes = await this.getChainId();
if ('error' in chainRes) {
return { error: chainRes.error };
throw ConnectorErrors.chainId;
}
if (chainRes.chainId !== desiredChainId) {
return { error: 'incorrect chain' };
throw ConnectorErrors.chainId;
}
if (!this.token) {
@@ -49,15 +50,16 @@ export class JsonRpcConnector implements Connector {
const token = response.headers.get('Authorization');
if (!response.ok) {
if ('error' in data) {
return { error: data.error.data };
// TODO: extend ConnectorError with data on jsonrpc error
if ('error' in data && data.error.code === 3001) {
// user rejected
throw ConnectorErrors.userRejected;
}
return { error: 'failed to connect' };
throw ConnectorErrors.connect;
}
if (!token) {
return { error: 'failed to connect' };
throw ConnectorErrors.connect;
}
this.token = token;
@@ -65,7 +67,11 @@ export class JsonRpcConnector implements Connector {
return { success: true };
} catch (err) {
return { error: 'wallet not running' };
if (err instanceof ConnectorError) {
throw err;
}
throw ConnectorErrors.noConnector;
}
}
@@ -74,7 +80,7 @@ export class JsonRpcConnector implements Connector {
await this.request(JsonRpcMethod.DisconnectWallet);
return { success: true };
} catch (err) {
return { error: 'wallet not running' };
throw ConnectorErrors.disconnect;
}
}
@@ -85,7 +91,7 @@ export class JsonRpcConnector implements Connector {
return { chainId: data.result.chainID };
} catch (err) {
return { error: 'wallet not running' };
throw ConnectorErrors.chainId;
}
}
@@ -94,7 +100,7 @@ export class JsonRpcConnector implements Connector {
const { data } = await this.request(JsonRpcMethod.ListKeys);
return data.result.keys as Array<{ publicKey: string; name: string }>;
} catch (err) {
return { error: 'wallet not running' };
throw ConnectorErrors.noConnector;
}
}
@@ -103,7 +109,7 @@ export class JsonRpcConnector implements Connector {
await this.listKeys();
return { connected: true };
} catch (err) {
return { error: 'wallet not running' };
throw ConnectorErrors.noConnector;
}
}
@@ -114,6 +120,8 @@ export class JsonRpcConnector implements Connector {
params
);
// TODO handle not okay responses but wallet is running
return {
transactionHash: data.result.transactionHash,
signature: data.result.transaction.signature.value,
@@ -121,7 +129,11 @@ export class JsonRpcConnector implements Connector {
sentAt: data.result.sentAt,
};
} catch (err) {
return { error: 'wallet not running' };
if (err instanceof ConnectorError) {
throw err;
}
throw ConnectorErrors.noConnector;
}
}
@@ -1,6 +1,7 @@
import { type StoreApi } from 'zustand';
import { type Store, type Connector } from '../types';
import { isValidVegaPublicKey } from '@vegaprotocol/utils';
import { ConnectorError, ConnectorErrors } from '.';
export class ReadOnlyConnector implements Connector {
readonly id = 'readOnly';
@@ -20,20 +21,33 @@ export class ReadOnlyConnector implements Connector {
}
async connectWallet() {
if (!this.pubKey) {
try {
if (this.pubKey) {
return { success: true };
}
const value = window.prompt('Enter public key');
if (value === null) {
return { error: 'the user rejected' };
throw ConnectorErrors.userRejected;
}
// TODO: extend connect error with messaging for invalid public key
if (!isValidVegaPublicKey(value)) {
return { error: 'invalid public key' };
// throw new Error('invalid public key');
throw ConnectorErrors.connect;
}
this.pubKey = value;
return { success: true };
} catch (err) {
if (err instanceof ConnectorError) {
throw err;
}
throw ConnectorErrors.connect;
}
return { success: true };
}
async disconnectWallet() {
@@ -42,14 +56,12 @@ export class ReadOnlyConnector implements Connector {
}
async getChainId() {
return {
error: `You are connected in a view only state for public key: ${this.pubKey}`,
};
throw ConnectorErrors.chainId;
}
async listKeys() {
if (!this.pubKey) {
return { error: 'failed to list keys' };
throw ConnectorErrors.listKeys;
}
return [
{
@@ -69,9 +81,12 @@ export class ReadOnlyConnector implements Connector {
// @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.`,
};
// TODO: extend send tx with more information
//
// 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.`,
// };
throw ConnectorErrors.sendTransaction;
}
on() {
+12 -10
View File
@@ -1,3 +1,4 @@
import { ConnectorError, ConnectorErrors } from '.';
import {
JsonRpcMethod,
type Connector,
@@ -41,20 +42,22 @@ export class SnapConnector implements Connector {
const { chainId } = await this.getChainId();
if (chainId !== desiredChainId) {
throw new Error('incorrect chain id');
throw ConnectorErrors.chainId;
}
return { success: true };
} catch (err) {
return {
error: err instanceof Error ? err.message : 'failed to connect',
};
if (err instanceof ConnectorError) {
throw err;
}
throw ConnectorErrors.noConnector;
}
}
// TODO: check how snaps should actually disconnect
async disconnectWallet() {
return { success: true };
// return { error: 'failed to disconnect' };
}
// deprecated, pass chain on connect
@@ -65,7 +68,7 @@ export class SnapConnector implements Connector {
});
return { chainId: res.chainID };
} catch (err) {
return { error: 'failed to get chain id' };
throw ConnectorErrors.chainId;
}
}
@@ -74,13 +77,13 @@ export class SnapConnector implements Connector {
const res = await this.invokeSnap(JsonRpcMethod.ListKeys);
return res.keys as Array<{ publicKey: string; name: string }>;
} catch (err) {
return { error: 'failed to list keys' };
throw ConnectorErrors.listKeys;
}
}
async isConnected() {
console.warn('isConnected not implemented');
return { error: 'failed to check if connected' };
throw ConnectorErrors.isConnected;
}
async sendTransaction(params: TransactionParams) {
@@ -99,8 +102,7 @@ export class SnapConnector implements Connector {
sentAt: res.sentAt,
};
} catch (err) {
console.error(err);
return { error: 'failed to send transaction' };
throw ConnectorErrors.sendTransaction;
}
}
+2
View File
@@ -16,6 +16,8 @@ export {
SnapConnector,
JsonRpcConnector,
ReadOnlyConnector,
ConnectorError,
ConnectorErrors,
} from './connectors';
// Utils
+10 -21
View File
@@ -4,6 +4,7 @@ import {
type TransactionResponse,
} from './transaction-types';
import { type Chain } from './chains';
import { type ConnectorError } from './connectors';
export enum JsonRpcMethod {
ConnectWallet = 'client.connect_wallet',
@@ -14,17 +15,13 @@ export enum JsonRpcMethod {
GetChainId = 'client.get_chain_id',
}
export interface IWalletError {
error: string;
}
export interface TransactionParams {
publicKey: string;
transaction: Transaction;
sendingMode: 'TYPE_SYNC';
}
type VegaWalletEvent = 'client.disconnected';
export type VegaWalletEvent = 'client.disconnected';
export type ConnectorType =
| 'injected'
@@ -39,16 +36,12 @@ export interface Connector {
readonly description: string;
bindStore(state: StoreApi<Store>): void;
connectWallet(chainId?: string): Promise<{ success: boolean } | IWalletError>;
disconnectWallet(): Promise<{ success: boolean } | IWalletError>;
getChainId(): Promise<{ chainId: string } | IWalletError>;
listKeys(): Promise<
Array<{ publicKey: string; name: string }> | IWalletError
>;
isConnected(): Promise<{ connected: boolean } | IWalletError>;
sendTransaction(
params: TransactionParams
): Promise<TransactionResponse | IWalletError>;
connectWallet(chainId?: string): Promise<{ success: boolean }>;
disconnectWallet(): Promise<{ success: boolean }>;
getChainId(): Promise<{ chainId: string }>;
listKeys(): Promise<Array<{ publicKey: string; name: string }>>;
isConnected(): Promise<{ connected: boolean }>;
sendTransaction(params: TransactionParams): Promise<TransactionResponse>;
on(event: VegaWalletEvent, callback: () => void): void;
off(event: VegaWalletEvent): void;
}
@@ -65,14 +58,12 @@ export type CoreStore = {
status: Status;
current: ConnectorType | undefined;
keys: Key[];
// setKeys: (keys: Key[]) => void;
error: string | undefined;
error: ConnectorError | undefined;
jsonRpcToken: string | undefined;
};
export type SingleKeyStore = {
pubKey: string | undefined;
// setPubKey: (key: string) => void;
};
export type Store = CoreStore & SingleKeyStore;
@@ -89,9 +80,7 @@ export type Wallet = {
connect: (id: ConnectorType) => Promise<{ success: boolean } | undefined>;
disconnect: () => Promise<{ success: boolean } | undefined>;
refreshKeys: () => Promise<void>;
sendTransaction: (
params: TransactionParams
) => Promise<TransactionResponse | IWalletError>;
sendTransaction: (params: TransactionParams) => Promise<TransactionResponse>;
reset: () => void;
};
+7 -17
View File
@@ -10,6 +10,7 @@ import {
type Connector,
type ConnectorType,
} from './types';
import { ConnectorError, ConnectorErrors } from './connectors';
// get/set functions are not used in the slices so these
// can be plain objects
@@ -78,31 +79,20 @@ export function createConfig(cfg: Config): Wallet {
try {
store.setState({ status: 'connecting', current: id, error: undefined });
const connectWalletRes = await connector.connectWallet(
store.getState().chainId
);
if ('error' in connectWalletRes) {
throw new Error(connectWalletRes.error);
}
const listKeysRes = await connector.listKeys();
if ('error' in listKeysRes) {
throw new Error('failed to get keys');
}
await connector.connectWallet(store.getState().chainId);
const keys = await connector.listKeys();
// TODO: this shouldnt be in core as we dont want to enforce single key usage
const storedPubKey = store.getState().pubKey;
let defaultKey;
if (listKeysRes.find((k) => k.publicKey === storedPubKey)) {
if (keys.find((k) => k.publicKey === storedPubKey)) {
defaultKey = storedPubKey;
} else {
defaultKey = listKeysRes[0].publicKey;
defaultKey = keys[0].publicKey;
}
store.setState({
keys: listKeysRes,
keys,
status: 'connected',
pubKey: defaultKey,
});
@@ -117,7 +107,7 @@ export function createConfig(cfg: Config): Wallet {
status: 'disconnected',
current: undefined,
keys: [],
error: err instanceof Error ? err.message : 'failed to connect',
error: err instanceof ConnectorError ? err : ConnectorErrors.unknown,
});
return { success: false };
}