Task/remove vegawallet service (#926)

* feat: improve error handling

* chore: lint

* fix: cypress test incorrect assertion
This commit is contained in:
Matthew Russell 2022-08-02 07:37:46 +01:00 committed by GitHub
parent 459defddd2
commit 85d838b9a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 114 additions and 98 deletions

View File

@ -29,7 +29,7 @@ describe('vega wallet', () => {
cy.getByTestId(form).find('#wallet').click().type('invalid name'); cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password'); cy.getByTestId(form).find('#passphrase').click().type('invalid password');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click(); 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', () => { it('doesnt connect with invalid fields', () => {

View File

@ -146,11 +146,12 @@ it('Successful connection using custom url', async () => {
}); });
it('Unsuccessful connection using rest auth form', async () => { it('Unsuccessful connection using rest auth form', async () => {
const errMessage = 'Error message';
// Error from service // Error from service
let spy = jest let spy = jest
.spyOn(defaultProps.connectors['rest'] as RestConnector, 'authenticate') .spyOn(defaultProps.connectors['rest'] as RestConnector, 'authenticate')
.mockImplementation(() => .mockImplementation(() =>
Promise.resolve({ success: false, error: 'Error message' }) Promise.resolve({ success: false, error: errMessage })
); );
render(generateJSX({ dialogOpen: true })); render(generateJSX({ dialogOpen: true }));
@ -167,9 +168,7 @@ it('Unsuccessful connection using rest auth form', async () => {
expect(spy).toHaveBeenCalledWith(DEFAULT_URL, fields); expect(spy).toHaveBeenCalledWith(DEFAULT_URL, fields);
expect(screen.getByTestId('form-error')).toHaveTextContent( expect(screen.getByTestId('form-error')).toHaveTextContent(errMessage);
'Something went wrong'
);
expect(defaultProps.setDialogOpen).not.toHaveBeenCalled(); expect(defaultProps.setDialogOpen).not.toHaveBeenCalled();
// Fetch failed due to wallet not running // Fetch failed due to wallet not running

View File

@ -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 { WALLET_CONFIG } from '../storage-keys';
import type { VegaConnector } from './vega-connector'; import type { VegaConnector } from './vega-connector';
import type { TransactionSubmission } from '../wallet-types'; import type { TransactionError, TransactionSubmission } from '../wallet-types';
import { z } from 'zod'; import { z } from 'zod';
// Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors // Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors
@ -12,7 +13,11 @@ interface RestConnectorConfig {
url: string | null; 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({ export const AuthTokenSchema = z.object({
token: z.string(), token: z.string(),
@ -87,11 +92,19 @@ export class RestConnector implements VegaConnector {
try { try {
this.url = url; this.url = url;
const res = await this.request('auth/token', { const res = await this.request(Endpoints.Auth, {
method: 'post', method: 'post',
body: JSON.stringify(params), 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); const data = AuthTokenSchema.parse(res.data);
// Store the token, and other things for later // Store the token, and other things for later
@ -104,19 +117,23 @@ export class RestConnector implements VegaConnector {
return { success: true, error: null }; return { success: true, error: null };
} catch (err) { } catch (err) {
return { success: false, error: err }; return { success: false, error: 'Authentication failed' };
} }
} }
async connect() { async connect() {
try { try {
const res = await this.request('keys', { const res = await this.request(Endpoints.Keys, {
method: 'get', method: 'get',
headers: { headers: {
authorization: `Bearer ${this.token}`, authorization: `Bearer ${this.token}`,
}, },
}); });
if (res.error) {
return null;
}
const data = GetKeysSchema.parse(res.data); const data = GetKeysSchema.parse(res.data);
return data.keys; return data.keys;
@ -129,14 +146,14 @@ export class RestConnector implements VegaConnector {
async disconnect() { async disconnect() {
try { try {
await this.request('auth/token', { await this.request(Endpoints.Auth, {
method: 'delete', method: 'delete',
headers: { headers: {
authorization: `Bearer ${this.token}`, authorization: `Bearer ${this.token}`,
}, },
}); });
} catch (err) { } catch (err) {
console.error(err); Sentry.captureException(err);
} finally { } finally {
// Always clear config, if authTokenDelete fails the user still tried to // Always clear config, if authTokenDelete fails the user still tried to
// connect so clear the config (and containing token) from storage // connect so clear the config (and containing token) from storage
@ -146,7 +163,7 @@ export class RestConnector implements VegaConnector {
async sendTx(body: TransactionSubmission) { async sendTx(body: TransactionSubmission) {
try { try {
const res = await this.request('command/sync', { const res = await this.request(Endpoints.Command, {
method: 'post', method: 'post',
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { headers: {
@ -154,18 +171,23 @@ export class RestConnector implements VegaConnector {
}, },
}); });
// User rejected
if (res.status === 401) { if (res.status === 401) {
// User rejected
return null; return null;
} }
if (res.error) {
return {
error: res.error,
};
}
const data = TransactionResponseSchema.parse(res.data); const data = TransactionResponseSchema.parse(res.data);
return data; return data;
} catch (err) { } catch (err) {
return { Sentry.captureException(err);
error: 'Failed to fetch', return null;
};
} }
} }
@ -190,27 +212,57 @@ export class RestConnector implements VegaConnector {
LocalStorage.removeItem(this.configKey); LocalStorage.removeItem(this.configKey);
} }
private async request(endpoint: Endpoint, options: RequestInit) { /** Parse more complex error object into a single string */
const fetchResult = await fetch(`${this.url}/${endpoint}`, { private parseError(err: TransactionError): string {
...options, if ('error' in err) {
headers: { return err.error;
...options.headers, }
'Content-Type': 'application/json',
},
});
// auth/token delete doesnt return json if ('errors' in err) {
if (endpoint === 'auth/token' && options.method === 'delete') { return err.errors['*'].join(', ');
const textResult = await fetchResult.text(); }
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 { return {
status: fetchResult.status, error: 'Failed to fetch',
data: textResult,
};
} else {
const jsonResult = await fetchResult.json();
return {
status: fetchResult.status,
data: jsonResult,
}; };
} }
} }

View File

@ -4,14 +4,6 @@ import type {
TransactionResponse, TransactionResponse,
} from '../wallet-types'; } from '../wallet-types';
type ErrorResponse =
| {
error: string;
}
| {
errors: object;
};
export interface VegaConnector { export interface VegaConnector {
/** Description of how to use this connector */ /** Description of how to use this connector */
description: string; description: string;
@ -25,5 +17,5 @@ export interface VegaConnector {
/** Send a TX to the network. Only support order submission for now */ /** Send a TX to the network. Only support order submission for now */
sendTx: ( sendTx: (
body: TransactionSubmission body: TransactionSubmission
) => Promise<TransactionResponse | ErrorResponse | null>; ) => Promise<TransactionResponse | { error: string } | null>;
} }

View File

@ -1,4 +1,4 @@
import type { TransactionError, VegaKey } from './wallet-types'; import type { VegaKey } from './wallet-types';
import { createContext } from 'react'; import { createContext } from 'react';
import type { VegaConnector } from './connectors'; import type { VegaConnector } from './connectors';
import type { import type {
@ -32,7 +32,7 @@ export interface VegaWalletContextShape {
/** Send a transaction to the network, only order submissions for now */ /** Send a transaction to the network, only order submissions for now */
sendTx: ( sendTx: (
tx: TransactionSubmission tx: TransactionSubmission
) => Promise<TransactionResponse | TransactionError> | null; ) => Promise<TransactionResponse | { error: string }> | null;
} }
export const VegaWalletContext = createContext< export const VegaWalletContext = createContext<

View File

@ -33,6 +33,7 @@ export function RestConnectorForm({
}); });
async function onSubmit(fields: FormFields) { async function onSubmit(fields: FormFields) {
const authFailedMessage = t('Authentication failed');
try { try {
setError(''); setError('');
const res = await connector.authenticate(fields.url, { const res = await connector.authenticate(fields.url, {
@ -43,13 +44,13 @@ export function RestConnectorForm({
if (res.success) { if (res.success) {
onAuthenticate(); onAuthenticate();
} else { } else {
throw res.error; setError(res.error || authFailedMessage);
} }
} catch (err) { } catch (err) {
if (err instanceof TypeError) { if (err instanceof TypeError) {
setError(t(`Wallet not running at ${fields.url}`)); setError(t(`Wallet not running at ${fields.url}`));
} else if (err instanceof Error) { } else if (err instanceof Error) {
setError(t('Authentication failed')); setError(authFailedMessage);
} else { } else {
setError(t('Something went wrong')); setError(t('Something went wrong'));
} }

View File

@ -53,14 +53,12 @@ it('Handles a single error', async () => {
result.current.send({} as OrderSubmissionBody); result.current.send({} as OrderSubmissionBody);
}); });
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); 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 () => { it('Handles multiple errors', async () => {
const errorObj = { const errorObj = {
errors: { error: 'Went wrong!',
something: 'Went wrong!',
},
}; };
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(errorObj)); const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(errorObj));
const { result } = setup({ sendTx: mockSendTx }); const { result } = setup({ sendTx: mockSendTx });
@ -68,7 +66,7 @@ it('Handles multiple errors', async () => {
result.current.send({} as OrderSubmissionBody); result.current.send({} as OrderSubmissionBody);
}); });
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); 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 () => { it('Returns the signature if successful', async () => {

View File

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } 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 { useVegaWallet } from './use-vega-wallet';
import { VegaTransactionDialog } from './vega-transaction-dialog'; import { VegaTransactionDialog } from './vega-transaction-dialog';
import type { Intent } from '@vegaprotocol/ui-toolkit'; import type { Intent } from '@vegaprotocol/ui-toolkit';
@ -22,7 +22,7 @@ export enum VegaTxStatus {
export interface VegaTxState { export interface VegaTxState {
status: VegaTxStatus; status: VegaTxStatus;
error: TransactionError | null; error: string | null;
txHash: string | null; txHash: string | null;
signature: string | null; signature: string | null;
dialogOpen: boolean; dialogOpen: boolean;
@ -47,13 +47,6 @@ export const useVegaTransaction = () => {
})); }));
}, []); }, []);
const handleError = useCallback(
(error: TransactionError) => {
setTransaction({ error, status: VegaTxStatus.Error });
},
[setTransaction]
);
const reset = useCallback(() => { const reset = useCallback(() => {
setTransaction(initialState); setTransaction(initialState);
}, [setTransaction]); }, [setTransaction]);
@ -81,7 +74,7 @@ export const useVegaTransaction = () => {
} }
if (isError(res)) { if (isError(res)) {
handleError(res); setTransaction({ error: res.error, status: VegaTxStatus.Error });
return; return;
} }
@ -99,7 +92,7 @@ export const useVegaTransaction = () => {
return null; return null;
}, },
[sendTx, handleError, setTransaction, reset] [sendTx, setTransaction, reset]
); );
const TransactionDialog = useMemo(() => { const TransactionDialog = useMemo(() => {
@ -125,12 +118,8 @@ export const useVegaTransaction = () => {
}; };
}; };
const isError = (error: unknown): error is TransactionError => { const isError = (error: unknown): error is { error: string } => {
if ( if (error !== null && typeof error === 'object' && 'error' in error) {
error !== null &&
typeof error === 'object' &&
('error' in error || 'errors' in error)
) {
return true; return true;
} }
return false; return false;

View File

@ -57,7 +57,7 @@ describe('VegaTransactionDialog', () => {
{...props} {...props}
transaction={{ transaction={{
...props.transaction, ...props.transaction,
error: { error: 'rejected' }, error: 'rejected',
status: VegaTxStatus.Error, status: VegaTxStatus.Error,
}} }}
/> />

View File

@ -70,18 +70,11 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
} }
if (transaction.status === VegaTxStatus.Error) { if (transaction.status === VegaTxStatus.Error) {
let content = null; return (
<div data-testid={transaction.status}>
if (transaction.error) { <p>{transaction.error}</p>
if ('errors' in transaction.error) { </div>
content = transaction.error.errors['*'].map((e) => <p>{e}</p>); );
} else if ('error' in transaction.error) {
content = <p>{transaction.error.error}</p>;
} else {
content = <p>{t('Something went wrong')}</p>;
}
}
return <div data-testid={transaction.status}>{content}</div>;
} }
if (transaction.status === VegaTxStatus.Pending) { if (transaction.status === VegaTxStatus.Pending) {

View File

@ -69,18 +69,6 @@ const getProps = (
ethTx: EthTxState, ethTx: EthTxState,
ethUrl: string ethUrl: string
) => { ) => {
const renderVegaTxError = () => {
if (vegaTx.error) {
if ('errors' in vegaTx.error) {
return vegaTx.error.errors['*'].map((e) => <p>{e}</p>);
} else if ('error' in vegaTx.error) {
return <p>{vegaTx.error.error}</p>;
} else {
return <p>{t('Something went wrong')}</p>;
}
}
return null;
};
const vegaTxPropsMap: Record<VegaTxStatus, DialogProps> = { const vegaTxPropsMap: Record<VegaTxStatus, DialogProps> = {
[VegaTxStatus.Default]: { [VegaTxStatus.Default]: {
title: '', title: '',
@ -92,7 +80,11 @@ const getProps = (
title: t('Withdrawal transaction failed'), title: t('Withdrawal transaction failed'),
icon: <Icon name="warning-sign" size={20} />, icon: <Icon name="warning-sign" size={20} />,
intent: Intent.Danger, intent: Intent.Danger,
children: <Step>{renderVegaTxError()}</Step>, children: (
<Step>
<p>{vegaTx.error}</p>
</Step>
),
}, },
[VegaTxStatus.Requested]: { [VegaTxStatus.Requested]: {
title: t('Confirm withdrawal'), title: t('Confirm withdrawal'),