Task/remove vegawallet service (#926)
* feat: improve error handling * chore: lint * fix: cypress test incorrect assertion
This commit is contained in:
parent
459defddd2
commit
85d838b9a6
@ -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', () => {
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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<
|
||||||
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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) {
|
||||||
|
@ -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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user