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('#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', () => {

View File

@ -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

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 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 {
},
});
if (res.status === 401) {
// User rejected
if (res.status === 401) {
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,7 +212,24 @@ export class RestConnector implements VegaConnector {
LocalStorage.removeItem(this.configKey);
}
private async request(endpoint: Endpoint, options: RequestInit) {
/** Parse more complex error object into a single string */
private parseError(err: TransactionError): string {
if ('error' in err) {
return err.error;
}
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: {
@ -199,6 +238,14 @@ export class RestConnector implements VegaConnector {
},
});
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();
@ -213,5 +260,10 @@ export class RestConnector implements VegaConnector {
data: jsonResult,
};
}
} catch (err) {
return {
error: 'Failed to fetch',
};
}
}
}

View File

@ -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<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 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<TransactionResponse | TransactionError> | null;
) => Promise<TransactionResponse | { error: string }> | null;
}
export const VegaWalletContext = createContext<

View File

@ -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'));
}

View File

@ -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 () => {

View File

@ -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;

View File

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

View File

@ -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) => <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>;
return (
<div data-testid={transaction.status}>
<p>{transaction.error}</p>
</div>
);
}
if (transaction.status === VegaTxStatus.Pending) {

View File

@ -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) => <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> = {
[VegaTxStatus.Default]: {
title: '',
@ -92,7 +80,11 @@ const getProps = (
title: t('Withdrawal transaction failed'),
icon: <Icon name="warning-sign" size={20} />,
intent: Intent.Danger,
children: <Step>{renderVegaTxError()}</Step>,
children: (
<Step>
<p>{vegaTx.error}</p>
</Step>
),
},
[VegaTxStatus.Requested]: {
title: t('Confirm withdrawal'),