diff --git a/apps/trading-e2e/src/integration/global.cy.ts b/apps/trading-e2e/src/integration/global.cy.ts index c4c468351..5e44b7a9a 100644 --- a/apps/trading-e2e/src/integration/global.cy.ts +++ b/apps/trading-e2e/src/integration/global.cy.ts @@ -29,7 +29,7 @@ describe('vega wallet', () => { cy.getByTestId(form).find('#wallet').click().type('invalid name'); cy.getByTestId(form).find('#passphrase').click().type('invalid password'); cy.getByTestId('rest-connector-form').find('button[type=submit]').click(); - cy.getByTestId('form-error').should('have.text', 'Authentication failed'); + cy.getByTestId('form-error').should('have.text', 'Invalid credentials'); }); it('doesnt connect with invalid fields', () => { diff --git a/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx b/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx index 77c85e000..6db0d0c25 100644 --- a/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx +++ b/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx @@ -146,11 +146,12 @@ it('Successful connection using custom url', async () => { }); it('Unsuccessful connection using rest auth form', async () => { + const errMessage = 'Error message'; // Error from service let spy = jest .spyOn(defaultProps.connectors['rest'] as RestConnector, 'authenticate') .mockImplementation(() => - Promise.resolve({ success: false, error: 'Error message' }) + Promise.resolve({ success: false, error: errMessage }) ); render(generateJSX({ dialogOpen: true })); @@ -167,9 +168,7 @@ it('Unsuccessful connection using rest auth form', async () => { expect(spy).toHaveBeenCalledWith(DEFAULT_URL, fields); - expect(screen.getByTestId('form-error')).toHaveTextContent( - 'Something went wrong' - ); + expect(screen.getByTestId('form-error')).toHaveTextContent(errMessage); expect(defaultProps.setDialogOpen).not.toHaveBeenCalled(); // Fetch failed due to wallet not running diff --git a/libs/wallet/src/connectors/rest-connector.ts b/libs/wallet/src/connectors/rest-connector.ts index cc66796ae..065ba7180 100644 --- a/libs/wallet/src/connectors/rest-connector.ts +++ b/libs/wallet/src/connectors/rest-connector.ts @@ -1,7 +1,8 @@ -import { LocalStorage } from '@vegaprotocol/react-helpers'; +import * as Sentry from '@sentry/react'; +import { LocalStorage, t } from '@vegaprotocol/react-helpers'; import { WALLET_CONFIG } from '../storage-keys'; import type { VegaConnector } from './vega-connector'; -import type { TransactionSubmission } from '../wallet-types'; +import type { TransactionError, TransactionSubmission } from '../wallet-types'; import { z } from 'zod'; // Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors @@ -12,7 +13,11 @@ interface RestConnectorConfig { url: string | null; } -type Endpoint = 'auth/token' | 'command/sync' | 'keys'; +enum Endpoints { + Auth = 'auth/token', + Command = 'command/sync', + Keys = 'keys', +} export const AuthTokenSchema = z.object({ token: z.string(), @@ -87,11 +92,19 @@ export class RestConnector implements VegaConnector { try { this.url = url; - const res = await this.request('auth/token', { + const res = await this.request(Endpoints.Auth, { method: 'post', body: JSON.stringify(params), }); + if (res.status === 403) { + return { success: false, error: t('Invalid credentials') }; + } + + if (res.error) { + return { success: false, error: res.error }; + } + const data = AuthTokenSchema.parse(res.data); // Store the token, and other things for later @@ -104,19 +117,23 @@ export class RestConnector implements VegaConnector { return { success: true, error: null }; } catch (err) { - return { success: false, error: err }; + return { success: false, error: 'Authentication failed' }; } } async connect() { try { - const res = await this.request('keys', { + const res = await this.request(Endpoints.Keys, { method: 'get', headers: { authorization: `Bearer ${this.token}`, }, }); + if (res.error) { + return null; + } + const data = GetKeysSchema.parse(res.data); return data.keys; @@ -129,14 +146,14 @@ export class RestConnector implements VegaConnector { async disconnect() { try { - await this.request('auth/token', { + await this.request(Endpoints.Auth, { method: 'delete', headers: { authorization: `Bearer ${this.token}`, }, }); } catch (err) { - console.error(err); + Sentry.captureException(err); } finally { // Always clear config, if authTokenDelete fails the user still tried to // connect so clear the config (and containing token) from storage @@ -146,7 +163,7 @@ export class RestConnector implements VegaConnector { async sendTx(body: TransactionSubmission) { try { - const res = await this.request('command/sync', { + const res = await this.request(Endpoints.Command, { method: 'post', body: JSON.stringify(body), headers: { @@ -154,18 +171,23 @@ export class RestConnector implements VegaConnector { }, }); + // User rejected if (res.status === 401) { - // User rejected return null; } + if (res.error) { + return { + error: res.error, + }; + } + const data = TransactionResponseSchema.parse(res.data); return data; } catch (err) { - return { - error: 'Failed to fetch', - }; + Sentry.captureException(err); + return null; } } @@ -190,27 +212,57 @@ export class RestConnector implements VegaConnector { LocalStorage.removeItem(this.configKey); } - private async request(endpoint: Endpoint, options: RequestInit) { - const fetchResult = await fetch(`${this.url}/${endpoint}`, { - ...options, - headers: { - ...options.headers, - 'Content-Type': 'application/json', - }, - }); + /** Parse more complex error object into a single string */ + private parseError(err: TransactionError): string { + if ('error' in err) { + return err.error; + } - // auth/token delete doesnt return json - if (endpoint === 'auth/token' && options.method === 'delete') { - const textResult = await fetchResult.text(); + if ('errors' in err) { + return err.errors['*'].join(', '); + } + + return t("Something wen't wrong"); + } + + private async request( + endpoint: Endpoints, + options: RequestInit + ): Promise<{ status?: number; data?: unknown; error?: string }> { + try { + const fetchResult = await fetch(`${this.url}/${endpoint}`, { + ...options, + headers: { + ...options.headers, + 'Content-Type': 'application/json', + }, + }); + + if (!fetchResult.ok) { + const errorData = await fetchResult.json(); + return { + status: fetchResult.status, + error: this.parseError(errorData), + }; + } + + // auth/token delete doesnt return json + if (endpoint === 'auth/token' && options.method === 'delete') { + const textResult = await fetchResult.text(); + return { + status: fetchResult.status, + data: textResult, + }; + } else { + const jsonResult = await fetchResult.json(); + return { + status: fetchResult.status, + data: jsonResult, + }; + } + } catch (err) { return { - status: fetchResult.status, - data: textResult, - }; - } else { - const jsonResult = await fetchResult.json(); - return { - status: fetchResult.status, - data: jsonResult, + error: 'Failed to fetch', }; } } diff --git a/libs/wallet/src/connectors/vega-connector.ts b/libs/wallet/src/connectors/vega-connector.ts index a59f44f4a..a8fcd4ad4 100644 --- a/libs/wallet/src/connectors/vega-connector.ts +++ b/libs/wallet/src/connectors/vega-connector.ts @@ -4,14 +4,6 @@ import type { TransactionResponse, } from '../wallet-types'; -type ErrorResponse = - | { - error: string; - } - | { - errors: object; - }; - export interface VegaConnector { /** Description of how to use this connector */ description: string; @@ -25,5 +17,5 @@ export interface VegaConnector { /** Send a TX to the network. Only support order submission for now */ sendTx: ( body: TransactionSubmission - ) => Promise; + ) => Promise; } diff --git a/libs/wallet/src/context.ts b/libs/wallet/src/context.ts index bcc8bf26a..00fa7568d 100644 --- a/libs/wallet/src/context.ts +++ b/libs/wallet/src/context.ts @@ -1,4 +1,4 @@ -import type { TransactionError, VegaKey } from './wallet-types'; +import type { VegaKey } from './wallet-types'; import { createContext } from 'react'; import type { VegaConnector } from './connectors'; import type { @@ -32,7 +32,7 @@ export interface VegaWalletContextShape { /** Send a transaction to the network, only order submissions for now */ sendTx: ( tx: TransactionSubmission - ) => Promise | null; + ) => Promise | null; } export const VegaWalletContext = createContext< diff --git a/libs/wallet/src/rest-connector-form.tsx b/libs/wallet/src/rest-connector-form.tsx index 8cbbb6be7..c8dcc075f 100644 --- a/libs/wallet/src/rest-connector-form.tsx +++ b/libs/wallet/src/rest-connector-form.tsx @@ -33,6 +33,7 @@ export function RestConnectorForm({ }); async function onSubmit(fields: FormFields) { + const authFailedMessage = t('Authentication failed'); try { setError(''); const res = await connector.authenticate(fields.url, { @@ -43,13 +44,13 @@ export function RestConnectorForm({ if (res.success) { onAuthenticate(); } else { - throw res.error; + setError(res.error || authFailedMessage); } } catch (err) { if (err instanceof TypeError) { setError(t(`Wallet not running at ${fields.url}`)); } else if (err instanceof Error) { - setError(t('Authentication failed')); + setError(authFailedMessage); } else { setError(t('Something went wrong')); } diff --git a/libs/wallet/src/use-vega-transaction.spec.tsx b/libs/wallet/src/use-vega-transaction.spec.tsx index 54d2f2dba..ca145bf39 100644 --- a/libs/wallet/src/use-vega-transaction.spec.tsx +++ b/libs/wallet/src/use-vega-transaction.spec.tsx @@ -53,14 +53,12 @@ it('Handles a single error', async () => { result.current.send({} as OrderSubmissionBody); }); expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); - expect(result.current.transaction.error).toEqual({ error: errorMessage }); + expect(result.current.transaction.error).toEqual(errorMessage); }); it('Handles multiple errors', async () => { const errorObj = { - errors: { - something: 'Went wrong!', - }, + error: 'Went wrong!', }; const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(errorObj)); const { result } = setup({ sendTx: mockSendTx }); @@ -68,7 +66,7 @@ it('Handles multiple errors', async () => { result.current.send({} as OrderSubmissionBody); }); expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); - expect(result.current.transaction.error).toEqual(errorObj); + expect(result.current.transaction.error).toEqual(errorObj.error); }); it('Returns the signature if successful', async () => { diff --git a/libs/wallet/src/use-vega-transaction.tsx b/libs/wallet/src/use-vega-transaction.tsx index d65b36bed..8df695734 100644 --- a/libs/wallet/src/use-vega-transaction.tsx +++ b/libs/wallet/src/use-vega-transaction.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; import { useCallback, useMemo, useState } from 'react'; -import type { TransactionError, TransactionSubmission } from './wallet-types'; +import type { TransactionSubmission } from './wallet-types'; import { useVegaWallet } from './use-vega-wallet'; import { VegaTransactionDialog } from './vega-transaction-dialog'; import type { Intent } from '@vegaprotocol/ui-toolkit'; @@ -22,7 +22,7 @@ export enum VegaTxStatus { export interface VegaTxState { status: VegaTxStatus; - error: TransactionError | null; + error: string | null; txHash: string | null; signature: string | null; dialogOpen: boolean; @@ -47,13 +47,6 @@ export const useVegaTransaction = () => { })); }, []); - const handleError = useCallback( - (error: TransactionError) => { - setTransaction({ error, status: VegaTxStatus.Error }); - }, - [setTransaction] - ); - const reset = useCallback(() => { setTransaction(initialState); }, [setTransaction]); @@ -81,7 +74,7 @@ export const useVegaTransaction = () => { } if (isError(res)) { - handleError(res); + setTransaction({ error: res.error, status: VegaTxStatus.Error }); return; } @@ -99,7 +92,7 @@ export const useVegaTransaction = () => { return null; }, - [sendTx, handleError, setTransaction, reset] + [sendTx, setTransaction, reset] ); const TransactionDialog = useMemo(() => { @@ -125,12 +118,8 @@ export const useVegaTransaction = () => { }; }; -const isError = (error: unknown): error is TransactionError => { - if ( - error !== null && - typeof error === 'object' && - ('error' in error || 'errors' in error) - ) { +const isError = (error: unknown): error is { error: string } => { + if (error !== null && typeof error === 'object' && 'error' in error) { return true; } return false; diff --git a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx index 145d70b8e..6e92b8b02 100644 --- a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx +++ b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.spec.tsx @@ -57,7 +57,7 @@ describe('VegaTransactionDialog', () => { {...props} transaction={{ ...props.transaction, - error: { error: 'rejected' }, + error: 'rejected', status: VegaTxStatus.Error, }} /> diff --git a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx index 522e4cd03..463df6290 100644 --- a/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx +++ b/libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx @@ -70,18 +70,11 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => { } if (transaction.status === VegaTxStatus.Error) { - let content = null; - - if (transaction.error) { - if ('errors' in transaction.error) { - content = transaction.error.errors['*'].map((e) =>

{e}

); - } else if ('error' in transaction.error) { - content =

{transaction.error.error}

; - } else { - content =

{t('Something went wrong')}

; - } - } - return
{content}
; + return ( +
+

{transaction.error}

+
+ ); } if (transaction.status === VegaTxStatus.Pending) { diff --git a/libs/withdraws/src/lib/withdraw-dialog.tsx b/libs/withdraws/src/lib/withdraw-dialog.tsx index 680d359ef..85bdd9de8 100644 --- a/libs/withdraws/src/lib/withdraw-dialog.tsx +++ b/libs/withdraws/src/lib/withdraw-dialog.tsx @@ -69,18 +69,6 @@ const getProps = ( ethTx: EthTxState, ethUrl: string ) => { - const renderVegaTxError = () => { - if (vegaTx.error) { - if ('errors' in vegaTx.error) { - return vegaTx.error.errors['*'].map((e) =>

{e}

); - } else if ('error' in vegaTx.error) { - return

{vegaTx.error.error}

; - } else { - return

{t('Something went wrong')}

; - } - } - return null; - }; const vegaTxPropsMap: Record = { [VegaTxStatus.Default]: { title: '', @@ -92,7 +80,11 @@ const getProps = ( title: t('Withdrawal transaction failed'), icon: , intent: Intent.Danger, - children: {renderVegaTxError()}, + children: ( + +

{vegaTx.error}

+
+ ), }, [VegaTxStatus.Requested]: { title: t('Confirm withdrawal'),