fix(wallet): wallet connection and error fixes (#5927)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com> Co-authored-by: bwallacee <ben@vega.xyz>
This commit is contained in:
parent
93643f1737
commit
6504912284
@ -26,9 +26,9 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
const { token, staking, vesting } = useContracts();
|
||||
const setAssociatedBalances = useRefreshAssociatedBalances();
|
||||
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
|
||||
const vegaConnecting = useEagerConnect();
|
||||
const vegaWalletStatus = useEagerConnect();
|
||||
|
||||
const loaded = balancesLoaded && !vegaConnecting;
|
||||
const loaded = balancesLoaded && vegaWalletStatus !== 'connecting';
|
||||
|
||||
React.useEffect(() => {
|
||||
const run = async () => {
|
||||
@ -169,3 +169,5 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
}
|
||||
return <Suspense fallback={loading}>{children}</Suspense>;
|
||||
};
|
||||
|
||||
AppLoader.displayName = 'AppLoader';
|
||||
|
@ -111,3 +111,4 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
||||
</ContractsContext.Provider>
|
||||
);
|
||||
};
|
||||
ContractsProvider.displayName = 'ContractsProvider';
|
||||
|
@ -50,8 +50,6 @@ def truncate_middle(market_id, start=6, end=4):
|
||||
def change_keys(page: Page, vega: VegaServiceNull, key_name):
|
||||
page.get_by_test_id("manage-vega-wallet").click()
|
||||
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
|
||||
page.click(
|
||||
f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
|
||||
page.reload()
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
"Get MetaMask": "Get MetaMask",
|
||||
"Get the Vega Wallet": "Get the Vega Wallet",
|
||||
"I agree": "I agree",
|
||||
"Once you have the added the extension, <0>refresh</0> you browser.": "Once you have the added the extension, <0>refresh</0> you browser.",
|
||||
"Successfully connected": "Successfully connected",
|
||||
"Transaction was not successful": "Transaction was not successful",
|
||||
"Wallet rejected transaction": "Wallet rejected transaction"
|
||||
|
@ -17,8 +17,8 @@ export const InputError = ({
|
||||
...props
|
||||
}: InputErrorProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
'text-sm flex items-center first-letter:uppercase',
|
||||
'mt-2',
|
||||
'text-sm block items-center first-letter:capitalize',
|
||||
'mt-2 min-w-0 break-words',
|
||||
{
|
||||
'border-danger': intent === 'danger',
|
||||
'border-warning': intent === 'warning',
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { type ReactNode, type FunctionComponent, forwardRef } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
type FunctionComponent,
|
||||
forwardRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
ConnectorErrors,
|
||||
isBrowserWalletInstalled,
|
||||
@ -12,6 +17,7 @@ import { useConnect } from '../../hooks/use-connect';
|
||||
import { Links } from '../../constants';
|
||||
import { ConnectorIcon } from './connector-icon';
|
||||
import { useUserAgent } from '@vegaprotocol/react-helpers';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const vegaExtensionsLinks = {
|
||||
chrome: Links.chromeExtension,
|
||||
@ -29,48 +35,67 @@ export const ConnectionOptions = ({
|
||||
onConnect: (id: ConnectorType) => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const error = useWallet((store) => store.error);
|
||||
const { connectors } = useConnect();
|
||||
const error = useWallet((store) => store.error);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<h2 className="text-xl">{t('Connect to Vega')}</h2>
|
||||
<ul
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-1 -mx-2"
|
||||
data-testid="connectors-list"
|
||||
>
|
||||
{connectors.map((c) => {
|
||||
const ConnectionOption = ConnectionOptionRecord[c.id];
|
||||
const props = {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
showDescription: false,
|
||||
onClick: () => onConnect(c.id),
|
||||
};
|
||||
|
||||
if (ConnectionOption) {
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOption {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOptionDefault {...props} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{error && error.code !== ConnectorErrors.userRejected.code && (
|
||||
<p
|
||||
className="text-danger text-sm first-letter:uppercase"
|
||||
data-testid="connection-error"
|
||||
>
|
||||
{error.message}
|
||||
{isInstalling ? (
|
||||
<p className="text-warning">
|
||||
<Trans
|
||||
i18nKey="Once you have the added the extension, <0>refresh</0> you browser."
|
||||
components={[
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="underline underline-offset-4"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<ul
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-1 -mx-2"
|
||||
data-testid="connectors-list"
|
||||
>
|
||||
{connectors.map((c) => {
|
||||
const ConnectionOption = ConnectionOptionRecord[c.id];
|
||||
const props = {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
showDescription: false,
|
||||
onClick: () => onConnect(c.id),
|
||||
onInstall: () => setIsInstalling(true),
|
||||
};
|
||||
|
||||
if (ConnectionOption) {
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOption {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOptionDefault {...props} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{error && error.code !== ConnectorErrors.userRejected.code && (
|
||||
<p
|
||||
className="text-danger text-sm first-letter:uppercase"
|
||||
data-testid="connection-error"
|
||||
>
|
||||
{error.message}
|
||||
{error.data ? `: ${error.data}` : ''}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<a
|
||||
href={Links.walletOverview}
|
||||
@ -90,6 +115,7 @@ interface ConnectionOptionProps {
|
||||
description: string;
|
||||
showDescription?: boolean;
|
||||
onClick: () => void;
|
||||
onInstall?: () => void;
|
||||
}
|
||||
|
||||
const CONNECTION_OPTION_CLASSES =
|
||||
@ -142,6 +168,7 @@ export const ConnectionOptionInjected = ({
|
||||
description,
|
||||
showDescription = false,
|
||||
onClick,
|
||||
onInstall,
|
||||
}: ConnectionOptionProps) => {
|
||||
const t = useT();
|
||||
const userAgent = useUserAgent();
|
||||
@ -158,7 +185,11 @@ export const ConnectionOptionInjected = ({
|
||||
</span>
|
||||
</ConnectionOptionButtonWithDescription>
|
||||
) : (
|
||||
<ConnectionOptionLinkWithDescription id={id} href={link}>
|
||||
<ConnectionOptionLinkWithDescription
|
||||
id={id}
|
||||
href={link}
|
||||
onClick={onInstall}
|
||||
>
|
||||
<span className="flex flex-col justify-start text-left">
|
||||
<span className="capitalize leading-5">
|
||||
{t('Get the Vega Wallet')}
|
||||
@ -183,7 +214,7 @@ export const ConnectionOptionInjected = ({
|
||||
{name}
|
||||
</ConnectionOptionButton>
|
||||
) : (
|
||||
<ConnectionOptionLink id={id} href={link}>
|
||||
<ConnectionOptionLink id={id} href={link} onClick={onInstall}>
|
||||
{t('Get the Vega Wallet')}
|
||||
</ConnectionOptionLink>
|
||||
)}
|
||||
@ -275,8 +306,9 @@ const ConnectionOptionLink = forwardRef<
|
||||
children: ReactNode;
|
||||
id: ConnectorType;
|
||||
href: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
>(({ children, id, href }, ref) => {
|
||||
>(({ children, id, href, onClick }, ref) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
@ -285,6 +317,7 @@ const ConnectionOptionLink = forwardRef<
|
||||
className={CONNECTION_OPTION_CLASSES}
|
||||
data-testid={`connector-${id}`}
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ConnectorIcon id={id} />
|
||||
{children}
|
||||
@ -320,8 +353,10 @@ const ConnectionOptionLinkWithDescription = forwardRef<
|
||||
children: ReactNode;
|
||||
id: ConnectorType;
|
||||
href: string;
|
||||
|
||||
onClick?: () => void;
|
||||
}
|
||||
>(({ children, id, href }, ref) => {
|
||||
>(({ children, id, href, onClick }, ref) => {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
@ -329,6 +364,7 @@ const ConnectionOptionLinkWithDescription = forwardRef<
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span>
|
||||
<ConnectorIcon id={id} />
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useWallet } from './use-wallet';
|
||||
import { useConnect } from './use-connect';
|
||||
|
||||
export function useEagerConnect() {
|
||||
const current = useWallet((store) => store.current);
|
||||
const status = useWallet((store) => store.status);
|
||||
const { connect } = useConnect();
|
||||
const [connecting, setConnecting] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const attemptConnect = async () => {
|
||||
// No stored config, or config was malformed or no risk accepted
|
||||
if (!current) {
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status !== 'disconnected') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -19,15 +22,13 @@ export function useEagerConnect() {
|
||||
await connect(current);
|
||||
} catch {
|
||||
console.warn(`Failed to connect with connector: ${current}`);
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
attemptConnect();
|
||||
}
|
||||
}, [connect, current, connecting]);
|
||||
}, [status, connect, current]);
|
||||
|
||||
return connecting;
|
||||
return status;
|
||||
}
|
||||
|
@ -65,12 +65,12 @@ export const useSimpleTransaction = (opts?: Options) => {
|
||||
if (err.code === ConnectorErrors.userRejected.code) {
|
||||
setStatus('idle');
|
||||
} else {
|
||||
setError(err.message);
|
||||
setError(`${err.message}${err.data ? `: ${err.data}` : ''}`);
|
||||
setStatus('idle');
|
||||
opts?.onError?.(err.message);
|
||||
}
|
||||
} else {
|
||||
const msg = t('Wallet rejected transaction');
|
||||
const msg = t('Something went wrong');
|
||||
setError(msg);
|
||||
setStatus('idle');
|
||||
opts?.onError?.(msg);
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
listKeysError,
|
||||
noWalletError,
|
||||
sendTransactionError,
|
||||
userRejectedError,
|
||||
} from '../errors';
|
||||
import {
|
||||
type TransactionParams,
|
||||
@ -14,6 +15,19 @@ import {
|
||||
type VegaWalletEvent,
|
||||
} from '../types';
|
||||
|
||||
interface InjectedError {
|
||||
message: string;
|
||||
code: number;
|
||||
data:
|
||||
| {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
| string;
|
||||
}
|
||||
|
||||
const USER_REJECTED_CODE = -4;
|
||||
|
||||
export class InjectedConnector implements Connector {
|
||||
readonly id = 'injected';
|
||||
readonly name = 'Vega Wallet';
|
||||
@ -85,15 +99,55 @@ export class InjectedConnector implements Connector {
|
||||
sentAt: res.sentAt,
|
||||
};
|
||||
} catch (err) {
|
||||
if (this.isInjectedError(err)) {
|
||||
if (err.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
|
||||
if (typeof err.data === 'string') {
|
||||
throw sendTransactionError(err.data);
|
||||
} else {
|
||||
throw sendTransactionError(err.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw sendTransactionError();
|
||||
}
|
||||
}
|
||||
|
||||
on(event: VegaWalletEvent, callback: () => void) {
|
||||
window.vega.on(event, callback);
|
||||
// Check for on/off in case user is on older versions which don't support it
|
||||
// We can remove this check once FF is at the latest version
|
||||
if (
|
||||
typeof window.vega !== 'undefined' &&
|
||||
typeof window.vega.on === 'function'
|
||||
) {
|
||||
window.vega.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
off(event: VegaWalletEvent, callback: () => void) {
|
||||
window.vega.off(event, callback);
|
||||
// Check for on/off in case user is on older versions which don't support it
|
||||
// We can remove this check once FF is at the latest version
|
||||
if (
|
||||
typeof window.vega !== 'undefined' &&
|
||||
typeof window.vega.off === 'function'
|
||||
) {
|
||||
window.vega.off(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private isInjectedError(obj: unknown): obj is InjectedError {
|
||||
if (
|
||||
obj !== undefined &&
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'code' in obj &&
|
||||
'message' in obj &&
|
||||
'data' in obj
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
|
||||
type JsonRpcConnectorConfig = { url: string; token?: string };
|
||||
|
||||
const USER_REJECTED_CODE = 3001;
|
||||
|
||||
export class JsonRpcConnector implements Connector {
|
||||
readonly id = 'jsonRpc';
|
||||
readonly name = 'Command Line Wallet';
|
||||
@ -27,7 +29,7 @@ export class JsonRpcConnector implements Connector {
|
||||
requestId: number = 0;
|
||||
store: StoreApi<Store> | undefined;
|
||||
pollRef: NodeJS.Timer | undefined;
|
||||
ee: EventEmitter;
|
||||
ee: InstanceType<typeof EventEmitter>;
|
||||
|
||||
constructor(config: JsonRpcConnectorConfig) {
|
||||
this.url = config.url;
|
||||
@ -63,7 +65,7 @@ export class JsonRpcConnector implements Connector {
|
||||
const token = response.headers.get('Authorization');
|
||||
|
||||
if (!response.ok) {
|
||||
if ('error' in data && data.error.code === 3001) {
|
||||
if ('error' in data && data.error.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
throw connectError('response not ok');
|
||||
@ -137,7 +139,7 @@ export class JsonRpcConnector implements Connector {
|
||||
|
||||
if (!response.ok) {
|
||||
if ('error' in data) {
|
||||
if (data.error.code === 3001) {
|
||||
if (data.error.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
ConnectorError,
|
||||
chainIdError,
|
||||
@ -6,13 +5,13 @@ import {
|
||||
listKeysError,
|
||||
noWalletError,
|
||||
sendTransactionError,
|
||||
userRejectedError,
|
||||
} from '../errors';
|
||||
import { type Transaction } from '../transaction-types';
|
||||
import {
|
||||
JsonRpcMethod,
|
||||
type Connector,
|
||||
type TransactionParams,
|
||||
type VegaWalletEvent,
|
||||
} from '../types';
|
||||
|
||||
enum EthereumMethod {
|
||||
@ -43,7 +42,6 @@ declare global {
|
||||
type WindowEthereumProvider = {
|
||||
isMetaMask: boolean;
|
||||
request<T = unknown>(args: RequestArguments): Promise<T>;
|
||||
selectedAddress: string | null;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
@ -52,6 +50,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface SnapRPCError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: {
|
||||
originalError: { code: number };
|
||||
};
|
||||
}
|
||||
|
||||
const USER_REJECTED_CODE = -4;
|
||||
|
||||
export class SnapConnector implements Connector {
|
||||
readonly id = 'snap';
|
||||
readonly name = 'MetaMask Snap';
|
||||
@ -61,8 +69,6 @@ export class SnapConnector implements Connector {
|
||||
node: string;
|
||||
version: string;
|
||||
snapId: string;
|
||||
pollRef: NodeJS.Timer | undefined;
|
||||
ee: EventEmitter;
|
||||
|
||||
// Note: apps may not know which node is selected on start up so its up
|
||||
// to the app to make sure class intances are renewed if the node changes
|
||||
@ -70,14 +76,21 @@ export class SnapConnector implements Connector {
|
||||
this.node = config.node;
|
||||
this.version = config.version;
|
||||
this.snapId = config.snapId;
|
||||
this.ee = new EventEmitter();
|
||||
}
|
||||
|
||||
bindStore() {}
|
||||
|
||||
async connectWallet(desiredChainId: string) {
|
||||
try {
|
||||
await this.requestSnap();
|
||||
const res = await this.requestSnap();
|
||||
|
||||
if (res[this.snapId].blocked) {
|
||||
throw connectError('snap is blocked');
|
||||
}
|
||||
|
||||
if (!res[this.snapId].enabled) {
|
||||
throw connectError('snap is not enabled');
|
||||
}
|
||||
|
||||
const { chainId } = await this.getChainId();
|
||||
|
||||
@ -87,7 +100,6 @@ export class SnapConnector implements Connector {
|
||||
);
|
||||
}
|
||||
|
||||
this.startPoll();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof ConnectorError) {
|
||||
@ -98,57 +110,66 @@ export class SnapConnector implements Connector {
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectWallet() {
|
||||
this.stopPoll();
|
||||
}
|
||||
async disconnectWallet() {}
|
||||
|
||||
// deprecated, pass chain on connect
|
||||
async getChainId() {
|
||||
try {
|
||||
const res = await this.invokeSnap<{ chainID: string }>(
|
||||
const data = await this.invokeSnap<{ chainID: string }>(
|
||||
JsonRpcMethod.GetChainId,
|
||||
{
|
||||
networkEndpoints: [this.node],
|
||||
}
|
||||
);
|
||||
return { chainId: res.chainID };
|
||||
|
||||
if ('error' in data) {
|
||||
throw chainIdError(data.error.message);
|
||||
}
|
||||
|
||||
return { chainId: data.chainID };
|
||||
} catch (err) {
|
||||
this.stopPoll();
|
||||
if (err instanceof ConnectorError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw chainIdError();
|
||||
}
|
||||
}
|
||||
|
||||
async listKeys() {
|
||||
try {
|
||||
const res = await this.invokeSnap<{
|
||||
const data = await this.invokeSnap<{
|
||||
keys: Array<{ publicKey: string; name: string }>;
|
||||
}>(JsonRpcMethod.ListKeys);
|
||||
return res.keys;
|
||||
|
||||
if ('error' in data) {
|
||||
throw listKeysError(data.error.message);
|
||||
}
|
||||
|
||||
return data.keys;
|
||||
} catch (err) {
|
||||
this.stopPoll();
|
||||
if (err instanceof ConnectorError) {
|
||||
throw err;
|
||||
}
|
||||
throw listKeysError();
|
||||
}
|
||||
}
|
||||
|
||||
async isConnected() {
|
||||
try {
|
||||
// Check if metamask is unlocked
|
||||
if (!window.ethereum.selectedAddress) {
|
||||
throw noWalletError();
|
||||
}
|
||||
|
||||
// If this throws its likely the snap is disabled or has been uninstalled
|
||||
await this.listKeys();
|
||||
return { connected: true };
|
||||
} catch (err) {
|
||||
this.stopPoll();
|
||||
return { connected: false };
|
||||
}
|
||||
}
|
||||
|
||||
async sendTransaction(params: TransactionParams) {
|
||||
try {
|
||||
const res = await this.invokeSnap<{
|
||||
// If the transaction is invalid this will throw with SnapRPCError
|
||||
// but if its rejected it will resolve with 'error' in data
|
||||
const data = await this.invokeSnap<{
|
||||
transactionHash: string;
|
||||
transaction: { signature: { value: string } };
|
||||
receivedAt: string;
|
||||
@ -160,115 +181,99 @@ export class SnapConnector implements Connector {
|
||||
networkEndpoints: [this.node],
|
||||
});
|
||||
|
||||
if ('error' in data) {
|
||||
if (data.error.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
|
||||
throw sendTransactionError(`${data.error.message}: ${data.error.data}`);
|
||||
}
|
||||
|
||||
return {
|
||||
transactionHash: res.transactionHash,
|
||||
signature: res.transaction.signature.value,
|
||||
receivedAt: res.receivedAt,
|
||||
sentAt: res.sentAt,
|
||||
transactionHash: data.transactionHash,
|
||||
signature: data.transaction.signature.value,
|
||||
receivedAt: data.receivedAt,
|
||||
sentAt: data.sentAt,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof ConnectorError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (this.isSnapRPCError(err)) {
|
||||
throw sendTransactionError(err.message);
|
||||
}
|
||||
|
||||
throw sendTransactionError();
|
||||
}
|
||||
}
|
||||
|
||||
on(event: VegaWalletEvent, callback: () => void) {
|
||||
this.ee.on(event, callback);
|
||||
}
|
||||
on() {}
|
||||
|
||||
off() {}
|
||||
|
||||
off(event: VegaWalletEvent, callback?: () => void) {
|
||||
this.ee.off(event, callback);
|
||||
}
|
||||
////////////////////////////////////
|
||||
// Snap methods
|
||||
////////////////////////////////////
|
||||
|
||||
private startPoll() {
|
||||
// This only event we need to poll for right now is client.disconnect,
|
||||
// if more events get added we will need more logic here
|
||||
this.pollRef = setInterval(async () => {
|
||||
const result = await this.isConnected();
|
||||
if (result.connected) return;
|
||||
this.ee.emit('client.disconnected');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private stopPoll() {
|
||||
if (this.pollRef) {
|
||||
clearInterval(this.pollRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permission for a website to communicate with the specified snaps
|
||||
* and attempts to install them if they're not already installed.
|
||||
* If the installation of any snap fails, returns the error that caused the failure.
|
||||
* More informations here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_requestsnaps
|
||||
*/
|
||||
private async requestSnap() {
|
||||
await this.request(EthereumMethod.RequestSnaps, {
|
||||
[this.snapId]: {
|
||||
version: this.version,
|
||||
private async requestSnap(): Promise<{
|
||||
[snapId: string]: {
|
||||
blocked: boolean;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
version: string;
|
||||
};
|
||||
}> {
|
||||
return window.ethereum.request({
|
||||
method: EthereumMethod.RequestSnaps,
|
||||
params: {
|
||||
[this.snapId]: {
|
||||
version: this.version,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: check if this is needed, its used in use-snap-status
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * Gets the list of all installed snaps.
|
||||
// * More information here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_getsnaps
|
||||
// */
|
||||
// async getSnap() {
|
||||
// const snaps = await this.request(EthereumMethod.GetSnaps);
|
||||
// return Object.values(snaps).find(
|
||||
// (s) => s.id === this.snapId && s.version === this.version
|
||||
// );
|
||||
// }
|
||||
|
||||
/**
|
||||
* Calls a method on the specified snap, always vega in this case
|
||||
* should always be npm:@vegaprotocol/snap
|
||||
*/
|
||||
private async invokeSnap<TResult>(
|
||||
method: JsonRpcMethod,
|
||||
params?: SnapInvocationParams
|
||||
): Promise<TResult> {
|
||||
return await this.request(EthereumMethod.InvokeSnap, {
|
||||
snapId: this.snapId,
|
||||
request: {
|
||||
method,
|
||||
params,
|
||||
params: SnapInvocationParams = {}
|
||||
): Promise<TResult | { error: SnapRPCError }> {
|
||||
// MetaMask in Firefox doesn't like undefined properties or some properties
|
||||
// on __proto__ so we need to strip them out with JSON.strinfify
|
||||
params = JSON.parse(JSON.stringify(params));
|
||||
|
||||
return window.ethereum.request({
|
||||
method: EthereumMethod.InvokeSnap,
|
||||
params: {
|
||||
snapId: this.snapId,
|
||||
request: {
|
||||
method,
|
||||
params,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls window.ethereum.request with method and params
|
||||
*/
|
||||
private async request<TResult>(
|
||||
method: EthereumMethod,
|
||||
params?: object
|
||||
): Promise<TResult> {
|
||||
if (window.ethereum?.request && window.ethereum?.isMetaMask) {
|
||||
// MetaMask in Firefox doesn't like undefined properties or some properties
|
||||
// on __proto__ so we need to strip them out with JSON.strinfify
|
||||
try {
|
||||
params = JSON.parse(JSON.stringify(params));
|
||||
} catch (err) {
|
||||
throw sendTransactionError();
|
||||
}
|
||||
|
||||
return window.ethereum.request({
|
||||
method,
|
||||
params,
|
||||
});
|
||||
private isSnapRPCError(obj: unknown): obj is SnapRPCError {
|
||||
if (
|
||||
obj !== undefined &&
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'code' in obj &&
|
||||
'message' in obj
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw noWalletError();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export class ConnectorError extends Error {
|
||||
|
||||
export const ConnectorErrors = {
|
||||
userRejected: { message: 'user rejected', code: 0 },
|
||||
noConnector: { message: 'no connector', code: 1 },
|
||||
noConnector: { message: 'not connected', code: 1 },
|
||||
connect: { message: 'failed to connect', code: 2 },
|
||||
disconnect: { message: 'failed to disconnect', code: 3 },
|
||||
chainId: { message: 'incorrect chain id', code: 4 },
|
||||
|
@ -93,7 +93,6 @@ describe('disconnect', () => {
|
||||
expect(result).toEqual({ status: 'disconnected' });
|
||||
expect(config.store.getState()).toMatchObject({
|
||||
status: 'disconnected',
|
||||
error: noConnectorError(),
|
||||
current: undefined,
|
||||
keys: [],
|
||||
pubKey: undefined,
|
||||
@ -130,7 +129,7 @@ describe('refresh keys', () => {
|
||||
it('handles invalid connector', async () => {
|
||||
await config.refreshKeys();
|
||||
expect(config.store.getState()).toMatchObject({
|
||||
error: noConnectorError(),
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -132,18 +132,20 @@ export function createConfig(cfg: Config): Wallet {
|
||||
store.setState(getInitialState(), true);
|
||||
return { status: 'disconnected' as const };
|
||||
} catch (err) {
|
||||
store.setState({
|
||||
...getInitialState(),
|
||||
error: err instanceof ConnectorError ? err : unknownError(),
|
||||
});
|
||||
store.setState(getInitialState(), true);
|
||||
return { status: 'disconnected' as const };
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshKeys() {
|
||||
const connector = connectors
|
||||
.getState()
|
||||
.find((x) => x.id === store.getState().current);
|
||||
const state = store.getState();
|
||||
const connector = connectors.getState().find((x) => x.id === state.current);
|
||||
|
||||
// Only refresh keys if connnected. If you aren't connect when you connect
|
||||
// you will get the latest keys
|
||||
if (state.status !== 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!connector) {
|
||||
|
Loading…
Reference in New Issue
Block a user