Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de21212e85 |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user