Compare commits

..
Author SHA1 Message Date
Matthew Russell aff2e0efdb chore: add to address, for better clarity 2023-02-06 10:56:11 -08:00
Matthew Russell f55fee3250 test: add unit test for transfer-form 2023-02-06 10:46:02 -08:00
Matthew Russell 0f5712d38d feat: update tests after changes 2023-02-06 10:46:02 -08:00
Matthew Russell 921a684b81 feat: add transfer button to dropdown and mobile nav 2023-02-06 10:46:02 -08:00
Matthew Russell 6a9a5f36ab fix: dont allow scientific notation for fee 2023-02-06 10:46:01 -08:00
Matthew Russell 700f5022e7 feat: add better toast feedback for transfer 2023-02-06 10:46:00 -08:00
Matthew Russell 0ecbccc774 chore: disable autocomplete for amount field 2023-02-06 10:43:36 -08:00
Matthew Russell 5bfa940b67 fix: clicking on chevron not working for normal select 2023-02-06 10:43:36 -08:00
Matthew Russell 6c79ae5c66 feat: prevent submit if key matches current key 2023-02-06 10:43:36 -08:00
Matthew Russell 55c087ccd2 feat: only show dropdown wallet has additional pubkeys 2023-02-06 10:43:36 -08:00
Matthew Russell b3872aeba5 chore: remove dummy account selector 2023-02-06 10:43:35 -08:00
Matthew Russell 6f36aaab29 fix: single useNetworkParam hook 2023-02-06 10:43:35 -08:00
Matthew Russell 2d281d5a8a feat: add unit tests for transfer-form components 2023-02-06 10:43:35 -08:00
Matthew Russell ea4798b9ed feat: use input as default if you only have a single key 2023-02-06 10:43:34 -08:00
Matthew Russell 6369ba613f feat: add missing content 2023-02-06 10:43:34 -08:00
Matthew Russell 69c6cd3f32 feat: move manual entry logic 2023-02-06 10:43:34 -08:00
Matthew Russell 88923a8059 feat: fix display of fees, add condition to allow transfer toast 2023-02-06 10:43:34 -08:00
Matthew Russell 1ca3ab87f5 feat: create transaction 2023-02-06 10:43:33 -08:00
Matthew Russell d552bf768f feat: add transfer dialog and form 2023-02-06 10:43:33 -08:00
Matthew Russell 74a2d15200 feat: add transfer transaction types 2023-02-06 10:43:33 -08:00
84 changed files with 542 additions and 2096 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.sandbox.vega.xyz/websocket
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
@@ -2,7 +2,6 @@ query ExplorerMarket($id: ID!) {
market(id: $id) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
name
@@ -8,7 +8,7 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
}>;
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
export const ExplorerMarketDocument = gql`
@@ -16,7 +16,6 @@ export const ExplorerMarketDocument = gql`
market(id: $id) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
name
@@ -55,7 +55,6 @@ describe('Market link component', () => {
market: {
id: '123',
decimalPlaces: 5,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
@@ -19,7 +19,6 @@ fragment ExplorerDeterministicOrderFields on Order {
market {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
name
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID'];
@@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
}>;
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
fragment ExplorerDeterministicOrderFields on Order {
@@ -35,7 +35,6 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
market {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
name
@@ -61,7 +61,6 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
__typename: 'Market',
id: '789',
state: 'STATUS_ACTIVE',
positionDecimalPlaces: 2,
decimalPlaces: '5',
tradableInstrument: {
instrument: {
@@ -89,7 +88,6 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
market: {
id: '789',
decimalPlaces: 5,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
@@ -4,7 +4,6 @@ import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market';
import { Time } from '../time';
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
export interface DeterministicOrderDetailsProps {
id: string;
@@ -91,7 +90,7 @@ const DeterministicOrderDetails = ({
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.size} marketId={o.market.id} />
{o.size}
</h5>
</div>
@@ -38,7 +38,6 @@ const mock = {
__typename: 'Market',
id: '789',
state: 'STATE_ACTIVE',
positionDecimalPlaces: 2,
decimalPlaces: 2,
tradableInstrument: {
instrument: {
@@ -90,7 +90,6 @@ describe('Order TX Summary component', () => {
market: {
id: '123',
decimalPlaces: 2,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
@@ -37,7 +37,6 @@ const fullMock = {
market: {
id: '123',
decimalPlaces: 2,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
@@ -66,9 +66,7 @@ export const Search = () => {
className="text-white"
hasError={Boolean(error?.message)}
type="text"
placeholder={t(
'Enter block number, public key or transaction hash'
)}
placeholder={t('Enter block number, party id or transaction hash')}
/>
{error?.message && (
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b text-black">
@@ -1,69 +0,0 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import SizeInMarket from './size-in-market';
import type { DecimalSource } from './size-in-market';
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
function renderComponent(
price: string,
marketId: string,
mocks: MockedResponse[],
decimalSource: DecimalSource = 'MARKET'
) {
return (
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<SizeInMarket
marketId={marketId}
size={price}
decimalSource={decimalSource}
/>
</MemoryRouter>
</MockedProvider>
);
}
const fullMock = {
request: {
query: ExplorerMarketDocument,
variables: {
id: '123',
},
},
result: {
data: {
market: {
id: '123',
decimalPlaces: 2,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data',
tradableInstrument: {
instrument: {
name: 'test dai',
product: {
__typename: 'Future',
quoteName: 'dai',
settlementAsset: {
decimals: 18,
},
},
},
},
},
},
},
};
describe('Size in Market component', () => {
it('Renders the raw size when there is no market data', () => {
const res = render(renderComponent('100', '123', []));
expect(res.getByText('100')).toBeInTheDocument();
});
it('Renders the formatted size when market data is fetched', async () => {
const res = render(renderComponent('100', '123', [fullMock]));
expect(await res.findByText('1.00')).toBeInTheDocument();
});
});
@@ -1,41 +0,0 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
export type DecimalSource = 'MARKET';
export type PriceInMarketProps = {
marketId: string;
size: string | number;
decimalSource?: DecimalSource;
};
/**
* Given a market ID and an order size it will fetch the market
* order size, and format the size accordingly
*/
const SizeInMarket = ({
marketId,
size,
decimalSource = 'MARKET',
}: PriceInMarketProps) => {
const { data } = useExplorerMarketQuery({
variables: { id: marketId },
fetchPolicy: 'cache-first',
});
let label = size;
if (data) {
if (decimalSource === 'MARKET' && data.market?.positionDecimalPlaces) {
label = addDecimalsFormatNumber(size, data.market.positionDecimalPlaces);
}
}
return (
<label>
<span>{label}</span>
</label>
);
};
export default SizeInMarket;
@@ -3,7 +3,6 @@ import React from 'react';
import classnames from 'classnames';
interface TableProps {
allowWrap?: boolean;
children: React.ReactNode;
className?: string;
}
@@ -26,15 +25,8 @@ interface TableCellProps extends ThHTMLAttributes<HTMLTableCellElement> {
modifier?: 'bordered' | 'background';
}
export const Table = ({
allowWrap,
children,
className,
...props
}: TableProps) => {
const classes = allowWrap
? className
: classnames(className, 'overflow-x-auto whitespace-nowrap');
export const Table = ({ children, className, ...props }: TableProps) => {
const classes = classnames(className, 'overflow-x-auto whitespace-nowrap');
return (
<div className={classes}>
<table className="w-full" {...props}>
@@ -45,14 +37,11 @@ export const Table = ({
};
export const TableWithTbody = ({
allowWrap,
children,
className,
...props
}: TableProps) => {
const classes = allowWrap
? className
: classnames(className, 'overflow-x-auto whitespace-nowrap');
const classes = classnames(className, 'overflow-x-auto whitespace-nowrap');
return (
<div className={classes}>
<table className="w-full" {...props}>
@@ -1,108 +0,0 @@
import { getValues } from './bound-factors';
import type { components } from '../../../../../types/explorer';
type KeyValueBundle = components['schemas']['vegaKeyValueBundle'][];
describe('getValues', () => {
it('handles an empty array by returning a dashed template', () => {
const res = getValues([]);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '-');
expect(res.up).toHaveProperty('value', '-');
});
it('handles undefined', () => {
const res = getValues(undefined as unknown as KeyValueBundle);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '-');
expect(res.up).toHaveProperty('value', '-');
});
it('handles a kvb that only has one side (should not happen)', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { vectorVal: { value: ['0.123'] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '0.123');
});
it('handles a kvb that has a matrixVal instead of a scalarval by ignoring it', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { matrixVal: { value: [{ value: ['0.123'] }] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '-');
});
it('ignores unexpected extra values in the kvb', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { vectorVal: { value: ['0.123', '0.77'] } },
},
{
key: 'down',
tolerance: '0.001',
value: { vectorVal: { value: ['0.321'] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '0.001');
expect(res.down).toHaveProperty('value', '0.321');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '0.123');
});
it('handles a full kvb', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { vectorVal: { value: ['0.123'] } },
},
{
key: 'down',
tolerance: '0.001',
value: { vectorVal: { value: ['0.321'] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '0.001');
expect(res.down).toHaveProperty('value', '0.321');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '0.123');
});
});
@@ -1,87 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import type { components } from '../../../../../types/explorer';
import { Table, TableRow, TableHeader, TableCell } from '../../../table';
interface StateVariableProposalBoundFactorsProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* A dumb as rocks function completely tied to what the structure of this variable should be
* @param kvb The key/value bundle
* @returns Object
*/
export function getValues(kvb: StateVariableProposalBoundFactorsProps['kvb']) {
const template = {
up: {
tolerance: '-',
value: '-',
},
down: {
tolerance: '-',
value: '-',
},
};
if (kvb && kvb.length > 0) {
kvb.forEach((v) => {
if (v.key === 'up') {
template.up.tolerance = v.tolerance || '-';
template.up.value = v.value?.vectorVal?.value
? v.value?.vectorVal.value[0]
: '-';
} else if (v.key === 'down') {
template.down.tolerance = v.tolerance || '-';
template.down.value = v.value?.vectorVal?.value
? v.value?.vectorVal.value[0]
: '-';
}
});
}
return template;
}
/**
* State Variable proposals updating Bound Factors. This contains two bundles,
* an up vector and a down vector
*
* This is nearly identical to risk factors.
*/
export const StateVariableProposalBoundFactors = ({
kvb,
}: StateVariableProposalBoundFactorsProps) => {
const v = getValues(kvb);
return (
<Table allowWrap={true} className="w-1/3">
<thead>
<TableRow modifier="bordered">
<TableHeader align="left">{t('Parameter')}</TableHeader>
<TableHeader align="center">{t('New value')}</TableHeader>
<TableHeader align="right">{t('Tolerance')}</TableHeader>
</TableRow>
</thead>
<tbody>
<TableRow modifier="bordered">
<TableCell>{t('Up')}</TableCell>
<TableCell align="right" className="font-mono">
{v.up.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.up.tolerance}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Down')}</TableCell>
<TableCell align="right" className="font-mono">
{v.down.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.down.tolerance}
</TableCell>
</TableRow>
</tbody>
</Table>
);
};
@@ -1,31 +0,0 @@
import type { components } from '../../../../../types/explorer';
import { StateVariableProposalUnknown } from './unknown';
import { StateVariableProposalBoundFactors } from './bound-factors';
import { StateVariableProposalRiskFactors } from './risk-factors';
interface StateVariableProposalWrapperProps {
stateVariable: string | undefined;
kvb: readonly components['schemas']['vegaKeyValueBundle'][] | undefined;
}
/**
* State Variable proposals
*/
export const StateVariableProposalWrapper = ({
stateVariable,
kvb,
}: StateVariableProposalWrapperProps) => {
if (!stateVariable || !kvb || kvb.length === 0) {
return null;
}
if (stateVariable.indexOf('bound-factors') !== -1) {
return <StateVariableProposalBoundFactors kvb={kvb} />;
} else if (stateVariable.indexOf('risk-factors') !== -1) {
return <StateVariableProposalRiskFactors kvb={kvb} />;
} else if (stateVariable.indexOf('probability_of_trading') !== -1) {
return <StateVariableProposalRiskFactors kvb={kvb} />;
} else {
return <StateVariableProposalUnknown kvb={kvb} />;
}
};
@@ -1,124 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import zip from 'lodash/zip';
import type { components } from '../../../../../types/explorer';
import { Table, TableRow, TableHeader, TableCell } from '../../../table';
import { StateVariableProposalUnknown } from './unknown';
interface StateVariableProposalRiskFactorsProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* A dumb as rocks function completely tied to what the structure of this variable should be
*
* @param kvb The key/value bundle
* @returns Object
*/
export function getValues(kvb: StateVariableProposalRiskFactorsProps['kvb']) {
try {
const template = {
bid: {
offsetTolerance: '-',
probabilityTolerance: '-',
offset: [] as Readonly<string[]>,
probability: [] as Readonly<string[]>,
rows: [] as [string | undefined, string | undefined][],
},
ask: {
offsetTolerance: '-',
probabilityTolerance: '-',
offset: [] as Readonly<string[]>,
probability: [] as Readonly<string[]>,
rows: [] as [string | undefined, string | undefined][],
},
};
kvb.forEach((v) => {
if (v.key === 'bidOffset') {
template.bid.offsetTolerance = v.tolerance || '-';
template.bid.offset = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
} else if (v.key === 'bidProbability') {
template.bid.probabilityTolerance = v.tolerance || '-';
template.bid.probability = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
} else if (v.key === 'askOffset') {
template.ask.offsetTolerance = v.tolerance || '-';
template.ask.offset = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
} else if (v.key === 'askProbability') {
template.ask.probabilityTolerance = v.tolerance || '-';
template.ask.probability = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
}
});
// Bundles up offset and probability in to a row
if (template.bid.offset.length > 0 && template.bid.probability.length > 0) {
template.bid.rows = zip(template.bid.offset, template.bid.probability);
}
if (template.ask.offset.length > 0 && template.ask.probability.length > 0) {
template.ask.rows = zip(template.ask.offset, template.ask.probability);
}
return template;
} catch (e) {
// This will result in the table not being rendered
return null;
}
}
/**
* State Variable proposals updating Risk Factors. This contains two bundles,
* a long vector and a short vector
*/
export const StateVariableProposalRiskFactors = ({
kvb,
}: StateVariableProposalRiskFactorsProps) => {
const v = getValues(kvb);
const all = v ? zip(v.bid.rows, v.ask.rows) : [];
if (all.length === 0) {
// Give up, do a JSON view
return <StateVariableProposalUnknown kvb={kvb} />;
}
return (
<Table allowWrap={true} className="text-xs lg:text-base max-w-2xl">
<thead>
<TableRow modifier="bordered">
<TableHeader align="left">{t('Mid offset')}</TableHeader>
<TableHeader align="right">{t('Bid probability')}</TableHeader>
<TableHeader align="right" className="pl-2">
{t('Ask probability')}
</TableHeader>
</TableRow>
</thead>
<tbody>
{all.map((r) => {
// Simple remapping of the data to protect against undefineds
const row = {
o: r[0] ? r[0][0] : r[1] ? r[1][0] : '-',
b: r[0] ? r[0][1] : '-',
a: r[1] ? r[1][1] : '-',
};
return (
<TableRow key={`${row.o}${row.b}${row.a}`}>
<TableCell align="left">{row.o}</TableCell>
<TableCell align="right" className="font-mono">
{row.b}
</TableCell>
<TableCell align="right" className="pl-2 font-mono">
{row.a}
</TableCell>
</TableRow>
);
})}
</tbody>
</Table>
);
};
@@ -1,168 +0,0 @@
import { getValues, StateVariableProposalRiskFactors } from './risk-factors';
import type { components } from '../../../../../types/explorer';
import { render } from '@testing-library/react';
type kvb = components['schemas']['vegaKeyValueBundle'][];
describe('Risk Factors: getValues', () => {
it('returns null if null is passed in', () => {
const res = getValues(null as unknown as kvb);
expect(res).toBeNull();
});
it('returns a blank template if kvb is empty', () => {
const res = getValues([]);
expect(res).not.toBeNull();
expect(res?.bid.offsetTolerance).toEqual('-');
expect(res?.bid.probabilityTolerance).toEqual('-');
expect(res?.bid.probability).toEqual([]);
expect(res?.bid.offset).toEqual([]);
expect(res?.bid.rows.length).toEqual(0);
expect(res?.ask.offsetTolerance).toEqual('-');
expect(res?.ask.probabilityTolerance).toEqual('-');
expect(res?.ask.probability).toEqual([]);
expect(res?.ask.offset).toEqual([]);
expect(res?.ask.rows.length).toEqual(0);
});
it('parses out a correct bid offset and probability', () => {
const k: kvb = [
{
key: 'bidOffset',
value: { vectorVal: { value: ['1'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['2'] } },
},
];
const res = getValues(k);
expect(res?.bid.offset).toEqual(['1']);
expect(res?.bid.probability).toEqual(['2']);
expect(res?.bid.rows).toEqual([['1', '2']]);
});
it('parses out a correct ask offset and probability', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2'] } },
},
];
const res = getValues(k);
expect(res?.ask.offset).toEqual(['1']);
expect(res?.ask.probability).toEqual(['2']);
expect(res?.ask.rows).toEqual([['1', '2']]);
});
it('parses out a correct ask/bid offset and probability', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2'] } },
},
{
key: 'bidOffset',
value: { vectorVal: { value: ['3'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['4'] } },
},
];
const res = getValues(k);
expect(res?.ask.rows).toEqual([['1', '2']]);
expect(res?.bid.rows).toEqual([['3', '4']]);
});
});
describe('Risk Factors: component', () => {
it('renders 3 rows correctly', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1.1', '1.2', '1.3'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2.2', '2.3', '2.4'] } },
},
{
key: 'bidOffset',
value: { vectorVal: { value: ['1.1', '1.2', '1.3'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['4.4', '4.5', '4.6'] } },
},
];
const screen = render(<StateVariableProposalRiskFactors kvb={k} />);
expect(screen.getByText('Mid offset')).toBeInTheDocument();
expect(screen.getByText('Bid probability')).toBeInTheDocument();
expect(screen.getByText('Ask probability')).toBeInTheDocument();
// First row
expect(screen.getByText('1.1')).toBeInTheDocument();
expect(screen.getByText('2.2')).toBeInTheDocument();
expect(screen.getByText('4.4')).toBeInTheDocument();
// Second row
expect(screen.getByText('1.2')).toBeInTheDocument();
expect(screen.getByText('2.3')).toBeInTheDocument();
expect(screen.getByText('4.5')).toBeInTheDocument();
// Third row
expect(screen.getByText('1.3')).toBeInTheDocument();
expect(screen.getByText('2.4')).toBeInTheDocument();
expect(screen.getByText('4.6')).toBeInTheDocument();
});
it('renders uneven row counts correctly', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1.1'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2.2'] } },
},
{
key: 'bidOffset',
value: { vectorVal: { value: ['1.1', '1.2', '1.3'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['4.4', '4.5', '4.6'] } },
},
];
const screen = render(<StateVariableProposalRiskFactors kvb={k} />);
expect(screen.getByText('Mid offset')).toBeInTheDocument();
expect(screen.getByText('Bid probability')).toBeInTheDocument();
expect(screen.getByText('Ask probability')).toBeInTheDocument();
// First row, as previous test
expect(screen.getByText('1.1')).toBeInTheDocument();
expect(screen.getByText('2.2')).toBeInTheDocument();
expect(screen.getByText('4.4')).toBeInTheDocument();
// Second row - offset comes from bid, not ask
expect(screen.getByText('1.2')).toBeInTheDocument();
expect(screen.getByText('4.5')).toBeInTheDocument();
// Third row
expect(screen.getByText('1.3')).toBeInTheDocument();
expect(screen.getByText('4.6')).toBeInTheDocument();
// The askOffset levels without a probability render -
expect(screen.getAllByText('-')).toHaveLength(2);
});
});
@@ -1,52 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import type { components } from '../../../../../types/explorer';
import { Table, TableRow, TableHeader, TableCell } from '../../../table';
import { getValues } from './bound-factors';
interface StateVariableProposalBoundFactorsProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* State Variable proposals updating Bound Factors. This contains two bundles,
* an up vector and a down vector
*
* This is nearly identical to risk factors.
*/
export const StateVariableProposalBoundFactors = ({
kvb,
}: StateVariableProposalBoundFactorsProps) => {
const v = getValues(kvb);
return (
<Table allowWrap={true} className="w-1/3">
<thead>
<TableRow modifier="bordered">
<TableHeader align="left">{t('Parameter')}</TableHeader>
<TableHeader align="center">{t('New value')}</TableHeader>
<TableHeader align="right">{t('Tolerance')}</TableHeader>
</TableRow>
</thead>
<tbody>
<TableRow modifier="bordered">
<TableCell>{t('Up')}</TableCell>
<TableCell align="right" className="font-mono">
{v.up.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.up.tolerance}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Down')}</TableCell>
<TableCell align="right" className="font-mono">
{v.down.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.down.tolerance}
</TableCell>
</TableRow>
</tbody>
</Table>
);
};
@@ -1,16 +0,0 @@
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
interface StateVariableProposalUnknownProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* State Variable proposals of an unknown type. Let's just dump
* it out.
*/
export const StateVariableProposalUnknown = ({
kvb,
}: StateVariableProposalUnknownProps) => {
return <SyntaxHighlighter data={kvb} />;
};
@@ -53,7 +53,7 @@ export const TxDetailsBatch = ({
let index = 0;
return (
<div key={`tx-${index}`}>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -32,7 +32,7 @@ export const TxDetailsChainEvent = ({
}
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<ChainEvent txData={txData} />
</TableWithTbody>
@@ -38,7 +38,7 @@ export const TxDetailsDataSubmission = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -35,7 +35,7 @@ export const TxDetailsDelegate = ({
txData.command.delegateSubmission;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{d.nodeId ? (
<TableRow modifier="bordered">
@@ -20,8 +20,6 @@ import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
import { TxDetailsDataSubmission } from './tx-data-submission';
import { TxProposalVote } from './tx-proposal-vote';
import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -69,8 +67,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
// These come from https://github.com/vegaprotocol/vega/blob/develop/core/txn/command.go#L72-L98
switch (txData.type) {
case 'Issue Signatures':
return TxDetailsIssueSignatures;
case 'Submit Order':
return TxDetailsOrder;
case 'Submit Oracle Data':
@@ -103,8 +99,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsDelegate;
case 'Undelegate':
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
default:
return TxDetailsGeneric;
}
@@ -11,7 +11,8 @@ interface TxDetailsGenericProps {
}
/**
* A node is
* If there is not yet a custom component for a transaction, just display
* the basic details. This allows someone to view the decoded transaction.
*/
export const TxDetailsGeneric = ({
txData,
@@ -23,7 +24,7 @@ export const TxDetailsGeneric = ({
}
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
</TableWithTbody>
);
@@ -60,7 +60,7 @@ export const TxDetailsHeartbeat = ({
const blockHeight = txData.command.blockHeight || '';
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Node')}</TableCell>
@@ -1,77 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableRow, TableCell, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import {
EthExplorerLink,
EthExplorerLinkTypes,
} from '../../links/eth-explorer-link/eth-explorer-link';
import { NodeLink } from '../../links';
type Command = components['schemas']['v1IssueSignatures'];
const kind: Record<components['schemas']['v1NodeSignatureKind'], string> = {
NODE_SIGNATURE_KIND_UNSPECIFIED: 'Unspecified',
NODE_SIGNATURE_KIND_ASSET_NEW: 'New asset',
NODE_SIGNATURE_KIND_ASSET_WITHDRAWAL: 'Asset withdrawal',
NODE_SIGNATURE_KIND_ASSET_UPDATE: ' Asset update',
NODE_SIGNATURE_KIND_ERC20_MULTISIG_SIGNER_ADDED: 'Multisig signer added',
NODE_SIGNATURE_KIND_ERC20_MULTISIG_SIGNER_REMOVED: 'Multisig signer removed',
};
interface TxDetailsGenericProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* If there is not yet a custom component for a transaction, just display
* the basic details. This allows someone to view the decoded transaction.
*/
export const TxDetailsIssueSignatures = ({
txData,
pubKey,
blockData,
}: TxDetailsGenericProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const cmd: Command = txData.command;
const k = cmd.kind ? kind[cmd.kind] : null;
return (
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{k ? (
<TableRow modifier="bordered">
<TableCell>{t('Kind')}</TableCell>
<TableCell>{k}</TableCell>
</TableRow>
) : null}
{cmd.submitter ? (
<TableRow modifier="bordered">
<TableCell>{t('ETH key')}</TableCell>
<TableCell>
<EthExplorerLink
id={cmd.submitter}
type={EthExplorerLinkTypes.address}
/>
</TableCell>
</TableRow>
) : null}
{cmd.validatorNodeId ? (
<TableRow modifier="bordered">
<TableCell>{t('Validator')}</TableCell>
<TableCell>
<NodeLink id={cmd.validatorNodeId} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
);
};
@@ -36,7 +36,7 @@ export const TxDetailsLiquidityAmendment = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -37,7 +37,7 @@ export const TxDetailsLiquidityCancellation = ({
const marketId: string = cancel.marketId || '-';
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
@@ -35,7 +35,7 @@ export const TxDetailsLiquiditySubmission = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -42,7 +42,7 @@ export const TxDetailsNodeVote = ({
}
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{data && !!data.deposit
? TxDetailsNodeVoteDeposit({ deposit: data })
@@ -29,7 +29,7 @@ export const TxDetailsOrderAmend = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -29,7 +29,7 @@ export const TxDetailsOrderCancel = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -39,7 +39,7 @@ export const TxDetailsOrder = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -32,7 +32,7 @@ export const TxProposalVote = ({
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
@@ -1,57 +0,0 @@
import {
hackyGetMarketFromStateVariable,
hackyGetVariableFromStateVariable,
} from './tx-state-variable-proposal';
describe('Hacky Get market from state variable', () => {
it('Extracts a market id from a known state variable proposal id', () => {
const knownId =
'84fff099818dc4f5319477f0812b4341565cfb32ccf735beede734e386b8108f_5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba_probability_of_trading';
const res = hackyGetMarketFromStateVariable(knownId);
expect(res).toEqual(
'5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba'
);
});
it('Returns null if the string looks a bit like the known one, but with different segments', () => {
const knownId =
'5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba_probability_of_trading';
const res = hackyGetMarketFromStateVariable(knownId);
expect(res).toEqual(null);
});
it('Handles empty/weird data', () => {
expect(hackyGetMarketFromStateVariable(null as unknown as string)).toEqual(
null
);
expect(hackyGetMarketFromStateVariable('')).toEqual(null);
expect(
hackyGetMarketFromStateVariable(undefined as unknown as string)
).toEqual(null);
expect(hackyGetMarketFromStateVariable(2 as unknown as string)).toEqual(
null
);
});
});
describe('Hacky Get Variable from state variable proposal id', () => {
it('Extracts an variable name from a known state variable proposal id', () => {
const knownId =
'84fff099818dc4f5319477f0812b4341565cfb32ccf735beede734e386b8108f_5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba_probability_of_trading';
const res = hackyGetVariableFromStateVariable(knownId);
expect(res).toEqual('probability of trading');
});
it('Handles empty/weird data', () => {
expect(
hackyGetVariableFromStateVariable(null as unknown as string)
).toEqual(null);
expect(hackyGetVariableFromStateVariable('')).toEqual(null);
expect(
hackyGetVariableFromStateVariable(undefined as unknown as string)
).toEqual(null);
expect(hackyGetVariableFromStateVariable(2 as unknown as string)).toEqual(
null
);
});
});
@@ -1,113 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { MarketLink } from '../../links';
import type { components } from '../../../../types/explorer';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { StateVariableProposalWrapper } from './state-variable/data-wrapper';
interface TxDetailsStateVariableProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* There is no market ID in the event, but it appears to be encoded in to the variable
* ID so let's pull it out. MarketLink component will handle if it isn't a real market.
*
* Given how liable to break this is, it's wrapped in a try catch
*
* @param stateVarId The full state variable proposal variable name
* @returns null or a string market id
*/
export function hackyGetMarketFromStateVariable(
stateVarId?: string
): string | null {
try {
const res = stateVarId ? stateVarId.split('_')[1] : null;
return res && res.length === 64 ? res : null;
} catch (e) {
return null;
}
}
/**
* There is no event name in the event, but it appears to be encoded in to the variable
* ID so let's pull it out. Will display nothing if it doesn't parse as expected
*
* Given how liable to break this is, it's wrapped in a try catch
*
* @param stateVarId The full state variable proposal variable name
* @returns null or a string variable name
*/
export function hackyGetVariableFromStateVariable(
stateVarId?: string
): string | null {
try {
if (!stateVarId) {
return null;
}
return stateVarId.split('_').slice(2).join(' ').replace('-', ' ');
} catch (e) {
return null;
}
}
/**
* State Variable proposals
*/
export const TxDetailsStateVariable = ({
txData,
pubKey,
blockData,
}: TxDetailsStateVariableProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const command: components['schemas']['v1StateVariableProposal'] =
txData.command.stateVariableProposal;
const variable = hackyGetVariableFromStateVariable(
command.proposal?.stateVarId
);
const marketId = hackyGetMarketFromStateVariable(
command.proposal?.stateVarId
);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
{marketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
<TableCell>{t('Variable')}</TableCell>
<TableCell className="capitalize">
<span>{variable}</span>
</TableCell>
</TableRow>
</TableWithTbody>
<section>
<StateVariableProposalWrapper
stateVariable={command.proposal?.stateVarId}
kvb={command.proposal?.kvb}
/>
</section>
</>
);
};
@@ -46,7 +46,7 @@ export const TxDetailsUndelegate = ({
txData.command.undelegateSubmission;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{u.nodeId ? (
<TableRow modifier="bordered">
@@ -41,7 +41,7 @@ export const TxDetailsWithdrawSubmission = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8">
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -10,7 +10,7 @@ import { useDocumentTitle } from '../../../hooks/use-document-title';
export const JumpToParty = () => {
const navigate = useNavigate();
useDocumentTitle(['Public keys']);
useDocumentTitle(['Parties']);
const handleSubmit = (e: React.SyntheticEvent) => {
e.preventDefault();
@@ -27,8 +27,8 @@ export const JumpToParty = () => {
};
return (
<JumpTo
label={t('Go to public key')}
placeholder={t('Public key')}
label={t('Go to party')}
placeholder={t('Party id')}
inputId="party-input"
inputType="text"
inputName="partyId"
@@ -40,7 +40,7 @@ export const JumpToParty = () => {
const Parties = () => {
return (
<section>
<RouteTitle data-testid="parties-header">{t('Public keys')}</RouteTitle>
<RouteTitle data-testid="parties-header">{t('Parties')}</RouteTitle>
<JumpToParty />
</section>
);
@@ -14,7 +14,7 @@ type PartyIdErrorProps = {
const PartyIdError = ({ id, error }: PartyIdErrorProps) => {
const end = isValidPartyId(id)
? t('No accounts or transactions found for: ')
: 'Invalid public key: ';
: 'Invalid party id: ';
return (
<section>
<p>
@@ -16,7 +16,7 @@ import { PartyAccounts } from './components/party-accounts';
const Party = () => {
const { party } = useParams<{ party: string }>();
useDocumentTitle(['Public keys', party || '-']);
useDocumentTitle(['Parties', party || '-']);
const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
@@ -44,7 +44,7 @@ const Party = () => {
/>
) : (
<Panel>
<p>No data found for public key {party}</p>
<p>No party found for key {party}</p>
</Panel>
);
@@ -71,7 +71,7 @@ const Party = () => {
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
data-testid="parties-header"
>
{t('Public key')}
{t('Party')}
</h1>
{partyRes.data ? (
<>
+36 -113
View File
@@ -104,7 +104,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "86666.297",
"total_removed": "0",
"locked_amount": "71172.1900799821150969076",
"locked_amount": "71527.9874377224479678513",
"deposits": [
{
"amount": "86666.297",
@@ -170,7 +170,7 @@
"tranche_end": "2023-06-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "1548.704594017094",
"locked_amount": "1569.28784467846975",
"deposits": [
{
"amount": "2500",
@@ -558,7 +558,7 @@
"tranche_end": "2023-08-01T00:00:00.000Z",
"total_added": "37500",
"total_removed": "183.137181525",
"locked_amount": "35997.0361418047875",
"locked_amount": "36307.49069597912625",
"deposits": [
{
"amount": "7500",
@@ -624,7 +624,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "71107.254411762464072205",
"locked_amount": "71462.72714918200044174",
"deposits": [
{
"amount": "129999.45",
@@ -690,7 +690,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "62600",
"total_removed": "0",
"locked_amount": "35458.30266362252656",
"locked_amount": "35715.29892820902776",
"deposits": [
{
"amount": "10000",
@@ -883,7 +883,7 @@
"tranche_end": "2023-09-17T00:00:00.000Z",
"total_added": "5000",
"total_removed": "0",
"locked_amount": "3023.913622526636",
"locked_amount": "3044.440480720446",
"deposits": [
{
"amount": "5000",
@@ -1094,7 +1094,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "97499.58",
"total_removed": "0",
"locked_amount": "12971.7942604813033430448",
"locked_amount": "13320.4799021065353983616",
"deposits": [
{
"amount": "97499.58",
@@ -1127,7 +1127,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "135173.4239508",
"total_removed": "98230.390980249184455396",
"locked_amount": "17730.203267225049376980811848",
"locked_amount": "18206.796341262034809441102696",
"deposits": [
{
"amount": "135173.4239508",
@@ -1173,7 +1173,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "32499.86",
"total_removed": "0",
"locked_amount": "5457.0098344795841878884",
"locked_amount": "5603.6958624323669808404",
"deposits": [
{
"amount": "32499.86",
@@ -1206,7 +1206,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "10833.29",
"total_removed": "0",
"locked_amount": "1776.2037475531047139428",
"locked_amount": "1823.94862624421256173",
"deposits": [
{
"amount": "10833.29",
@@ -1239,7 +1239,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "22749.93",
"total_removed": "0",
"locked_amount": "6639.846316295812322976",
"locked_amount": "6818.3273364692396522472",
"deposits": [
{
"amount": "6500",
@@ -1378,7 +1378,7 @@
"tranche_end": "2023-05-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "3995.28612255",
"locked_amount": "10161.75759668508375",
"locked_amount": "10348.030329189685875",
"deposits": [
{
"amount": "7500",
@@ -1576,7 +1576,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "928642.9598472029154",
"locked_amount": "604543.837686537201986533",
"locked_amount": "612507.96463901900484135",
"deposits": [
{
"amount": "1852091.69",
@@ -2876,7 +2876,7 @@
"tranche_start": "2021-09-05T00:00:00.000Z",
"tranche_end": "2022-09-30T00:00:00.000Z",
"total_added": "60916.66666633337",
"total_removed": "39088.634764730198755268",
"total_removed": "34173.43587066019652061",
"locked_amount": "0",
"deposits": [
{
@@ -2976,16 +2976,6 @@
}
],
"withdrawals": [
{
"amount": "3395.631843574",
"user": "0xa0fF757077B5D796259582b2b9Db99c906277007",
"tx": "0x335be76fc5ff37d4272a624f47822e6163d395e0d8a3a9cfcbcc5f7207239361"
},
{
"amount": "1519.567050496002234658",
"user": "0x6E14D4e15E245d6ECfe3181B08c2FF30d2c1Da19",
"tx": "0x3d0b5ec97be95410571cbc03dd4edd1406243cafbafc40d7c4740f88287efcd3"
},
{
"amount": "1366.363766241335342692",
"user": "0xef633C319801eB899354030DFF2BBD0024C0b39c",
@@ -3616,12 +3606,6 @@
}
],
"withdrawals": [
{
"amount": "1519.567050496002234658",
"user": "0x6E14D4e15E245d6ECfe3181B08c2FF30d2c1Da19",
"tranche_id": 13,
"tx": "0x3d0b5ec97be95410571cbc03dd4edd1406243cafbafc40d7c4740f88287efcd3"
},
{
"amount": "747.099616170667765342",
"user": "0x6E14D4e15E245d6ECfe3181B08c2FF30d2c1Da19",
@@ -3630,8 +3614,8 @@
}
],
"total_tokens": "2266.66666666667",
"withdrawn_tokens": "2266.66666666667",
"remaining_tokens": "0"
"withdrawn_tokens": "747.099616170667765342",
"remaining_tokens": "1519.567050496002234658"
},
{
"address": "0xb14025f7eB7717cFF43e7a33b66d86eaBd6bC7d7",
@@ -3674,12 +3658,6 @@
}
],
"withdrawals": [
{
"amount": "3395.631843574",
"user": "0xa0fF757077B5D796259582b2b9Db99c906277007",
"tranche_id": 13,
"tx": "0x335be76fc5ff37d4272a624f47822e6163d395e0d8a3a9cfcbcc5f7207239361"
},
{
"amount": "4.368156426",
"user": "0xa0fF757077B5D796259582b2b9Db99c906277007",
@@ -3688,8 +3666,8 @@
}
],
"total_tokens": "3400",
"withdrawn_tokens": "3400",
"remaining_tokens": "0"
"withdrawn_tokens": "4.368156426",
"remaining_tokens": "3395.631843574"
},
{
"address": "0x2D69BAB9738b05048be16DE3E5E0A945b8EeEf3a",
@@ -33559,7 +33537,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "589730.8667090699299",
"locked_amount": "953474.62891778338628861474",
"locked_amount": "965712.81375596155222172624",
"deposits": [
{
"amount": "1998.95815",
@@ -34873,8 +34851,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "560317.67585805941693952",
"locked_amount": "8680647.4008609011096496602104470331211138",
"total_removed": "557133.30560592463281452",
"locked_amount": "8724043.2185305122313058695420656514828332",
"deposits": [
{
"amount": "16249.93",
@@ -35438,16 +35416,6 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xa344428c2b1bf9b4685959441983da46bcd434ac0ee5ada325876c8733eba603"
},
{
"amount": "2667.021494",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tx": "0x94fe358ee7c973dd1d28ca4c56250dfaccbcb718673872c8e9cf8651a22a1108"
},
{
"amount": "517.348758134784125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xb6a33b8b8855789fbc2d0f9545ba5c2258e52f143d90bc75bbe2411bd2f9b4a5"
},
{
"amount": "858.360074993579125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -37067,12 +37035,6 @@
"tranche_id": 2,
"tx": "0xa344428c2b1bf9b4685959441983da46bcd434ac0ee5ada325876c8733eba603"
},
{
"amount": "517.348758134784125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xb6a33b8b8855789fbc2d0f9545ba5c2258e52f143d90bc75bbe2411bd2f9b4a5"
},
{
"amount": "858.360074993579125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -38161,8 +38123,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "117484.092376092228875",
"remaining_tokens": "142514.795123907771125"
"withdrawn_tokens": "116966.74361795744475",
"remaining_tokens": "143032.14388204255525"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -38816,12 +38778,6 @@
"tranche_id": 2,
"tx": "0x606ddaa3882cccb0062bc2827cfcfceb64cef8a6c4df41d06747ae651e5dd52e"
},
{
"amount": "2667.021494",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tranche_id": 2,
"tx": "0x94fe358ee7c973dd1d28ca4c56250dfaccbcb718673872c8e9cf8651a22a1108"
},
{
"amount": "1099.300488",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
@@ -39004,8 +38960,8 @@
}
],
"total_tokens": "200000",
"withdrawn_tokens": "90095.748006",
"remaining_tokens": "109904.251994"
"withdrawn_tokens": "87428.726512",
"remaining_tokens": "112571.273488"
},
{
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
@@ -40351,8 +40307,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "3921707.576409189114087282",
"locked_amount": "2292473.027727501476908220604423147",
"total_removed": "3920990.096830703963164282",
"locked_amount": "2332535.769515174096512550505057687",
"deposits": [
{
"amount": "129284.449",
@@ -40611,11 +40567,6 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xc73c47dcfac4093bb6325fe92986dd010d5a773af4bb97e188ad57359a309a52"
},
{
"amount": "717.479578485150923",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x0c292f2355b5cdc3b389ed35e6e138e593b1db9d91fcd58f132835343724bf90"
},
{
"amount": "8950.14985089483210984",
"user": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
@@ -43453,12 +43404,6 @@
"tranche_id": 3,
"tx": "0xc73c47dcfac4093bb6325fe92986dd010d5a773af4bb97e188ad57359a309a52"
},
{
"amount": "717.479578485150923",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x0c292f2355b5cdc3b389ed35e6e138e593b1db9d91fcd58f132835343724bf90"
},
{
"amount": "1192.05386354121365675",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -45849,8 +45794,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "302309.38363864041348675",
"remaining_tokens": "56814.08593635958651325"
"withdrawn_tokens": "301591.90406015526256375",
"remaining_tokens": "57531.56551484473743625"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -47188,7 +47133,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "2730068.739915456784546642",
"locked_amount": "588866.554476394378569370609352176",
"locked_amount": "604695.586054148570133080743399363",
"deposits": [
{
"amount": "552496.6455",
@@ -49170,8 +49115,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "32476.7515062512685",
"locked_amount": "151083.50524536200128704962151192",
"total_removed": "32361.4666889012685",
"locked_amount": "153022.71558941830041836828209032",
"deposits": [
{
"amount": "3000",
@@ -55830,16 +55775,6 @@
"user": "0x0e199b123f71f964d6567869B6B849C1f255A855",
"tx": "0x6df8666e49f7d491c740d6a36df031aac232a6cf37fa197aa1f6b2bb481be99c"
},
{
"amount": "99.47735921",
"user": "0x2E32F49389CF3039ab6365ca59329002922Cc01D",
"tx": "0x7de3c1703d23970f04ce1bebe2cece8a2fe3fbe6b876502da1812c312151b954"
},
{
"amount": "15.80745814",
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
"tx": "0xc0f953d7ecc45bfc1d7b69ecd491a63e8b00b0d732ff006bee008e917a04179f"
},
{
"amount": "78.261187214",
"user": "0x479F059c46c6873C6B970A92911084b3eA036d56",
@@ -68095,12 +68030,6 @@
}
],
"withdrawals": [
{
"amount": "99.47735921",
"user": "0x2E32F49389CF3039ab6365ca59329002922Cc01D",
"tranche_id": 5,
"tx": "0x7de3c1703d23970f04ce1bebe2cece8a2fe3fbe6b876502da1812c312151b954"
},
{
"amount": "35.842421358",
"user": "0x2E32F49389CF3039ab6365ca59329002922Cc01D",
@@ -68109,8 +68038,8 @@
}
],
"total_tokens": "200",
"withdrawn_tokens": "135.319780568",
"remaining_tokens": "64.680219432"
"withdrawn_tokens": "35.842421358",
"remaining_tokens": "164.157578642"
},
{
"address": "0x2F2588aCd44253312b4A94bF6753bE67514A5Cc6",
@@ -69185,12 +69114,6 @@
}
],
"withdrawals": [
{
"amount": "15.80745814",
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
"tranche_id": 5,
"tx": "0xc0f953d7ecc45bfc1d7b69ecd491a63e8b00b0d732ff006bee008e917a04179f"
},
{
"amount": "32.761796044",
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
@@ -69265,8 +69188,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "270.64275748",
"remaining_tokens": "129.35724252"
"withdrawn_tokens": "254.83529934",
"remaining_tokens": "145.16470066"
},
{
"address": "0x5c90765F50629570738fEe7b7FA82ae118f81Ed1",
@@ -15,7 +15,7 @@ export const ProposalFormTransactionDialog = ({
finalizedProposal,
TransactionDialog,
}: ProposalFormTransactionDialogProps) => {
// Render a custom complete UI if the proposal was rejected otherwise
// Render a custom complete UI if the proposal was rejected other wise
// pass undefined so that the default vega transaction dialog UI gets used
const completeContent = finalizedProposal?.rejectionReason ? (
<p>{finalizedProposal.rejectionReason}</p>
@@ -90,15 +90,22 @@ describe('Raw proposal form', () => {
},
result: {
data: {
proposals: {
__typename: 'Proposal',
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
reference: 'proposal-reference',
state: Schema.ProposalState.STATE_OPEN,
rejectionReason:
Schema.ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
errorDetails: 'error-details',
},
busEvents: [
{
__typename: 'BusEvent',
type: Schema.BusEventType.Proposal,
event: {
__typename: 'Proposal',
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
reference: 'proposal-reference',
state: Schema.ProposalState.STATE_OPEN,
rejectionReason:
Schema.ProposalRejectionReason
.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
errorDetails: 'error-details',
},
},
],
},
},
delay: 300,
@@ -3,12 +3,8 @@ import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { ETHERSCAN_TX, useEtherscanLink } from '@vegaprotocol/environment';
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { Panel } from '@vegaprotocol/ui-toolkit';
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import compact from 'lodash/compact';
import type { EthStoredTxState } from '@vegaprotocol/web3';
import {
@@ -48,35 +44,34 @@ const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
if (isWithdraw) label = t('Withdraw');
if (isDeposit) label = t('Deposit');
assetInfo = (
<strong>
{label}{' '}
{formatNumber(toBigNum(tx.args[1], asset.decimals), asset.decimals)}{' '}
{asset.symbol}
</strong>
<div className="mt-[5px]">
<span className="font-mono text-xs p-1 bg-gray-100 rounded">
{label}{' '}
{formatNumber(toBigNum(tx.args[1], asset.decimals), asset.decimals)}{' '}
{asset.symbol}
</span>
</div>
);
}
}
if (assetInfo || tx.requiresConfirmation) {
return (
<Panel>
{assetInfo}
{tx.status === EthTxStatus.Pending && (
<>
<p className="mt-[2px]">
{t('Awaiting confirmations')}{' '}
{`(${tx.confirmations}/${tx.requiredConfirmations})`}
</p>
<ProgressBar
value={(tx.confirmations / tx.requiredConfirmations) * 100}
/>
</>
)}
</Panel>
);
}
return null;
return (
<>
{assetInfo}
{tx.status === EthTxStatus.Pending && (
<div className="mt-[10px]">
<span className="font-mono text-xs">
{t('Awaiting confirmations')}{' '}
{`(${tx.confirmations}/${tx.requiredConfirmations})`}
</span>
<ProgressBar
value={(tx.confirmations / tx.requiredConfirmations) * 100}
intent={Intent.Warning}
/>
</div>
)}
</>
);
};
type EthTxToastContentProps = {
@@ -85,26 +80,26 @@ type EthTxToastContentProps = {
const EthTxRequestedToastContent = ({ tx }: EthTxToastContentProps) => {
return (
<>
<ToastHeading>{t('Action required')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Action required')}</h3>
<p>
{t(
'Please go to your wallet application and approve or reject the transaction.'
)}
</p>
<EthTransactionDetails tx={tx} />
</>
</div>
);
};
const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
return (
<>
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
<p>{t('Please wait for your transaction to be confirmed.')}</p>
<EtherscanLink tx={tx} />
<EthTransactionDetails tx={tx} />
</>
</div>
);
};
@@ -117,11 +112,11 @@ const EthTxErrorToastContent = ({ tx }: EthTxToastContentProps) => {
errorMessage = tx.error.message;
}
return (
<>
<ToastHeading>{t('Error occurred')}</ToastHeading>
<p className="first-letter:uppercase">{errorMessage}</p>
<div>
<h3 className="font-bold">{t('Error occurred')}</h3>
<p>{errorMessage}</p>
<EthTransactionDetails tx={tx} />
</>
</div>
);
};
@@ -141,63 +136,42 @@ const EtherscanLink = ({ tx }: EthTxToastContentProps) => {
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
return (
<>
<ToastHeading>{t('Transaction confirmed')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Transaction confirmed')}</h3>
<p>{t('Your transaction has been confirmed.')}</p>
<EtherscanLink tx={tx} />
<EthTransactionDetails tx={tx} />
</>
</div>
);
};
const EthTxCompletedToastContent = ({ tx }: EthTxToastContentProps) => {
const isDeposit = isDepositTransaction(tx);
return (
<>
<ToastHeading>
<div>
<h3 className="font-bold">
{t('Processing')} {isDeposit && t('deposit')}
</ToastHeading>
</h3>
<p>
{t('Your transaction has been completed.')}{' '}
{isDeposit && t('Waiting for deposit confirmation.')}
</p>
<EtherscanLink tx={tx} />
<EthTransactionDetails tx={tx} />
</>
</div>
);
};
const isFinal = (tx: EthStoredTxState) =>
[EthTxStatus.Confirmed, EthTxStatus.Error].includes(tx.status);
export const useEthereumTransactionToasts = () => {
const [setToast, removeToast] = useToasts((store) => [
store.setToast,
store.remove,
]);
const [dismissTx, deleteTx] = useEthTransactionStore((state) => [
state.dismiss,
state.delete,
]);
const onClose = useCallback(
(tx: EthStoredTxState) => () => {
const safeToDelete = isFinal(tx);
if (safeToDelete) {
deleteTx(tx.id);
} else {
dismissTx(tx.id);
}
removeToast(`eth-${tx.id}`);
},
[deleteTx, dismissTx, removeToast]
const ethTransactions = useEthTransactionStore((state) =>
state.transactions.filter((transaction) => transaction?.dialogOpen)
);
const dismissEthTransaction = useEthTransactionStore(
(state) => state.dismiss
);
const fromEthTransaction = useCallback(
(tx: EthStoredTxState): Toast => {
let content: ToastContent = <TransactionContent {...tx} />;
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
if (tx.status === EthTxStatus.Requested) {
content = <EthTxRequestedToastContent tx={tx} />;
}
@@ -217,21 +191,15 @@ export const useEthereumTransactionToasts = () => {
return {
id: `eth-${tx.id}`,
intent: intentMap[tx.status],
onClose: onClose(tx),
onClose: () => dismissEthTransaction(tx.id),
loader: [EthTxStatus.Pending, EthTxStatus.Complete].includes(tx.status),
content,
closeAfter,
};
},
[onClose]
[dismissEthTransaction]
);
useEthTransactionStore.subscribe(
(state) => compact(state.transactions.filter((tx) => tx?.dialogOpen)),
(txs) => {
txs.forEach((tx) => {
setToast(fromEthTransaction(tx));
});
}
);
return useMemo(() => {
return [...compact(ethTransactions).map(fromEthTransaction)];
}, [ethTransactions, fromEthTransaction]);
};
@@ -1,12 +1,8 @@
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { Panel } from '@vegaprotocol/ui-toolkit';
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { Intent } from '@vegaprotocol/ui-toolkit';
import { ApprovalStatus, VerificationStatus } from '@vegaprotocol/withdraws';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import compact from 'lodash/compact';
import type { EthWithdrawalApprovalState } from '@vegaprotocol/web3';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
@@ -34,72 +30,49 @@ const EthWithdrawalApprovalToastContent = ({
if (tx.status === ApprovalStatus.Delayed) {
title = t('Delayed');
}
if (tx.status === ApprovalStatus.Ready) {
title = t('Approved');
}
const num = formatNumber(
toBigNum(tx.withdrawal.amount, tx.withdrawal.asset.decimals),
tx.withdrawal.asset.decimals
);
const details = (
<Panel>
<strong>
<div className="mt-[5px]">
<span className="font-mono text-xs p-1 bg-gray-100 rounded">
{t('Withdraw')} {num} {tx.withdrawal.asset.symbol}
</strong>
</Panel>
</span>
</div>
);
return (
<>
{title.length > 0 && (
<ToastHeading className="font-bold">{title}</ToastHeading>
)}
<div>
{title.length > 0 && <h3 className="font-bold">{title}</h3>}
<VerificationStatus state={tx} />
{details}
</>
</div>
);
};
const isFinal = (tx: EthWithdrawalApprovalState) =>
[ApprovalStatus.Ready, ApprovalStatus.Error].includes(tx.status);
export const useEthereumWithdrawApprovalsToasts = () => {
const [setToast, remove] = useToasts((state) => [
state.setToast,
state.remove,
]);
const [dismissTx, deleteTx] = useEthWithdrawApprovalsStore((state) => [
state.dismiss,
state.delete,
]);
const { withdrawApprovals, dismissWithdrawApproval } =
useEthWithdrawApprovalsStore((state) => ({
withdrawApprovals: state.transactions.filter(
(transaction) => transaction?.dialogOpen
),
dismissWithdrawApproval: state.dismiss,
}));
const fromWithdrawalApproval = useCallback(
(tx: EthWithdrawalApprovalState): Toast => ({
id: `withdrawal-${tx.id}`,
intent: intentMap[tx.status],
onClose: () => {
if ([ApprovalStatus.Error, ApprovalStatus.Ready].includes(tx.status)) {
deleteTx(tx.id);
} else {
dismissTx(tx.id);
}
remove(`withdrawal-${tx.id}`);
},
onClose: () => dismissWithdrawApproval(tx.id),
loader: tx.status === ApprovalStatus.Pending,
content: <EthWithdrawalApprovalToastContent tx={tx} />,
closeAfter: isFinal(tx) ? CLOSE_AFTER : undefined,
}),
[deleteTx, dismissTx, remove]
[dismissWithdrawApproval]
);
useEthWithdrawApprovalsStore.subscribe(
(state) =>
compact(
state.transactions.filter((transaction) => transaction?.dialogOpen)
),
(txs) => {
txs.forEach((tx) => {
setToast(fromWithdrawalApproval(tx));
});
}
);
const toasts = useMemo(() => {
return [...compact(withdrawApprovals).map(fromWithdrawalApproval)];
}, [fromWithdrawalApproval, withdrawApprovals]);
return toasts;
};
@@ -260,7 +260,7 @@ describe('VegaTransactionDetails', () => {
const { queryByTestId } = render(
<VegaTransactionDetails tx={unsupportedTransaction} />
);
expect(queryByTestId('toast-panel')).toBeNull();
expect(queryByTestId('vega-tx-details')).toBeNull();
});
it.each([
{ tx: withdraw, details: 'Withdraw 12.34 $A' },
@@ -275,6 +275,6 @@ describe('VegaTransactionDetails', () => {
{ tx: batch, details: 'Batch market instruction' },
])('display details for transaction', ({ tx, details }) => {
const { queryByTestId } = render(<VegaTransactionDetails tx={tx} />);
expect(queryByTestId('toast-panel')?.textContent).toEqual(details);
expect(queryByTestId('vega-tx-details')?.textContent).toEqual(details);
});
});
@@ -1,4 +1,5 @@
import { useCallback } from 'react';
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import first from 'lodash/first';
import compact from 'lodash/compact';
import type {
@@ -24,10 +25,6 @@ import {
VegaTxStatus,
} from '@vegaprotocol/wallet';
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { Panel } from '@vegaprotocol/ui-toolkit';
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { Button, ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
@@ -43,7 +40,6 @@ import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Side } from '@vegaprotocol/types';
import { OrderStatus } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
const intentMap: { [s in VegaTxStatus]: Intent } = {
@@ -54,6 +50,15 @@ const intentMap: { [s in VegaTxStatus]: Intent } = {
Complete: Intent.Success,
};
const getIntent = (tx: VegaStoredTxState) => {
// Transaction can be successful
// But the order can be rejected by the network
if (tx.order?.rejectionReason) {
return Intent.Danger;
}
return intentMap[tx.status];
};
const isClosePositionTransaction = (tx: VegaStoredTxState) => {
if (isBatchMarketInstructionsTransaction(tx.body)) {
const amendments =
@@ -93,6 +98,20 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
);
};
const Details = ({
children,
title = '',
}: {
children: ReactNode;
title?: string;
}) => (
<div className="pt-[5px]" data-testid="vega-tx-details" title={title}>
<div className="font-mono text-xs p-2 bg-neutral-100 rounded dark:bg-neutral-700 dark:text-white">
{children}
</div>
</div>
);
type SizeAtPriceProps = {
side: Side;
size: string;
@@ -133,8 +152,8 @@ const SubmitOrderDetails = ({
const side = order ? order.side : data.side;
return (
<Panel>
<h4>
<Details>
<h4 className="font-bold">
{order
? t(
`Submit order - ${OrderStatusMapping[order.status].toLowerCase()}`
@@ -156,7 +175,10 @@ const SubmitOrderDetails = ({
price={price}
/>
</p>
</Panel>
{order && order.rejectionReason && (
<p className="italic">{getRejectionReason(order)}</p>
)}
</Details>
);
};
@@ -172,10 +194,9 @@ const EditOrderDetails = ({
});
const { data: markets } = useMarketList();
const originalOrder = order || orderById?.orderByID;
const marketId = order?.marketId || orderById?.orderByID.market.id;
const originalOrder = orderById?.orderByID;
if (!originalOrder) return null;
const market = markets?.find((m) => m.id === marketId);
const market = markets?.find((m) => m.id === originalOrder.market.id);
if (!market) return null;
const original = (
@@ -207,8 +228,8 @@ const EditOrderDetails = ({
);
return (
<Panel title={data.orderId}>
<h4>
<Details title={data.orderId}>
<h4 className="font-bold">
{order
? t(`Edit order - ${OrderStatusMapping[order.status].toLowerCase()}`)
: t('Edit order')}
@@ -218,7 +239,10 @@ const EditOrderDetails = ({
<s>{original}</s>
</p>
<p>{edited}</p>
</Panel>
{order && order.rejectionReason && (
<p className="italic">{getRejectionReason(order)}</p>
)}
</Details>
);
};
@@ -253,8 +277,8 @@ const CancelOrderDetails = ({
/>
);
return (
<Panel title={orderId}>
<h4>
<Details title={orderId}>
<h4 className="font-bold">
{order
? t(
`Cancel order - ${OrderStatusMapping[order.status].toLowerCase()}`
@@ -265,7 +289,10 @@ const CancelOrderDetails = ({
<p>
<s>{original}</s>
</p>
</Panel>
{order && order.rejectionReason && (
<p className="italic">{getRejectionReason(order)}</p>
)}
</Details>
);
};
@@ -284,11 +311,9 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
asset.decimals
);
return (
<Panel>
<strong>
{t('Withdraw')} {num} {asset.symbol}
</strong>
</Panel>
<Details>
{t('Withdraw')} {num} {asset.symbol}
</Details>
);
}
}
@@ -305,7 +330,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
tx.body.orderCancellation.marketId === undefined &&
tx.body.orderCancellation.orderId === undefined
) {
return <Panel>{t('Cancel all orders')}</Panel>;
return <Details>{t('Cancel all orders')}</Details>;
}
// CANCEL
@@ -328,15 +353,11 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
m.id === (tx.body as OrderCancellationBody).orderCancellation.marketId
)?.tradableInstrument.instrument.code;
return (
<Panel>
{marketName ? (
<>
{t('Cancel all orders for')} <strong>{marketName}</strong>
</>
) : (
t('Cancel all orders')
)}
</Panel>
<Details>
{marketName
? `${t('Cancel all orders for')} ${marketName}`
: t('Cancel all orders')}
</Details>
);
}
}
@@ -358,16 +379,15 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
const market = marketId && markets?.find((m) => m.id === marketId);
if (market) {
return (
<Panel>
{t('Close position for')}{' '}
<strong>{market.tradableInstrument.instrument.code}</strong>
</Panel>
<Details>
{t('Close position for')} {market.tradableInstrument.instrument.code}
</Details>
);
}
}
if (isBatchMarketInstructionsTransaction(tx.body)) {
return <Panel>{t('Batch market instruction')}</Panel>;
return <Details>{t('Batch market instruction')}</Details>;
}
if (isTransferTransaction(tx.body)) {
@@ -377,15 +397,15 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
if (transferAsset) {
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
return (
<Panel>
<h4>{t('Transfer')}</h4>
<Details>
<h4 className="font-bold">{t('Transfer')}</h4>
<p>
{t('To')} {truncateByChars(to)}
</p>
<p>
{value} {transferAsset.symbol}
</p>
</Panel>
</Details>
);
}
}
@@ -396,22 +416,22 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
type VegaTxToastContentProps = { tx: VegaStoredTxState };
const VegaTxRequestedToastContent = ({ tx }: VegaTxToastContentProps) => (
<>
<ToastHeading>{t('Action required')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Action required')}</h3>
<p>
{t(
'Please go to your Vega wallet application and approve or reject the transaction.'
)}
</p>
<VegaTransactionDetails tx={tx} />
</>
</div>
);
const VegaTxPendingToastContentProps = ({ tx }: VegaTxToastContentProps) => {
const explorerLink = useLinks(DApp.Explorer);
return (
<>
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
<p>{t('Please wait for your transaction to be confirmed')}</p>
{tx.txHash && (
<p className="break-all">
@@ -424,7 +444,7 @@ const VegaTxPendingToastContentProps = ({ tx }: VegaTxToastContentProps) => {
</p>
)}
<VegaTransactionDetails tx={tx} />
</>
</div>
);
};
@@ -438,7 +458,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
if (isWithdrawTransaction(tx.body)) {
const completeWithdrawalButton = tx.withdrawal && (
<p className="mt-1">
<div className="mt-[10px]">
<Button
data-testid="toast-complete-withdrawal"
size="xs"
@@ -451,11 +471,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
>
{t('Complete withdrawal')}
</Button>
</p>
</div>
);
return (
<>
<ToastHeading>{t('Funds unlocked')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Funds unlocked')}</h3>
<p>{t('Your funds have been unlocked for withdrawal')}</p>
{tx.txHash && (
<p className="break-all">
@@ -469,32 +489,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
)}
<VegaTransactionDetails tx={tx} />
{completeWithdrawalButton}
</>
);
}
if (tx.order && tx.order.rejectionReason) {
return (
<>
<ToastHeading>{t('Order rejected')}</ToastHeading>
<p>
{t(
'Your order has been rejected because: %s',
getRejectionReason(tx.order) || ''
)}
</p>
{tx.txHash && (
<p className="break-all">
<ExternalLink
href={explorerLink(EXPLORER_TX.replace(':hash', tx.txHash))}
rel="noreferrer"
>
{t('View in block explorer')}
</ExternalLink>
</p>
)}
<VegaTransactionDetails tx={tx} />
</>
</div>
);
}
@@ -529,8 +524,8 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
}
return (
<>
<ToastHeading>{t('Confirmed')}</ToastHeading>
<div>
<h3 className="font-bold">{t('Confirmed')}</h3>
<p>{t('Your transaction has been confirmed ')}</p>
{tx.txHash && (
<p className="break-all">
@@ -543,7 +538,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
</p>
)}
<VegaTransactionDetails tx={tx} />
</>
</div>
);
};
@@ -566,10 +561,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
walletNoConnectionCodes.includes(tx.error.code);
if (orderRejection) {
label = t('Order rejected');
errorMessage = t(
'Your order has been rejected because: %s',
orderRejection
);
errorMessage = orderRejection;
}
if (walletError) {
label = t('Wallet disconnected');
@@ -577,87 +569,60 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
}
return (
<>
<ToastHeading>{label}</ToastHeading>
<p className="first-letter:uppercase">{errorMessage}</p>
<div>
<h3 className="font-bold">{label}</h3>
<p>{errorMessage}</p>
{walletError && (
<Button size="xs" onClick={reconnectVegaWallet}>
{t('Connect vega wallet')}
</Button>
)}
<VegaTransactionDetails tx={tx} />
</>
</div>
);
};
const isFinal = (tx: VegaStoredTxState) =>
[VegaTxStatus.Error, VegaTxStatus.Complete].includes(tx.status);
export const useVegaTransactionToasts = () => {
const [setToast, removeToast] = useToasts((store) => [
store.setToast,
store.remove,
]);
const vegaTransactions = useVegaTransactionStore((state) =>
state.transactions.filter((transaction) => transaction?.dialogOpen)
);
const dismissVegaTransaction = useVegaTransactionStore(
(state) => state.dismiss
);
const [dismissTx, deleteTx] = useVegaTransactionStore((state) => [
state.dismiss,
state.delete,
]);
const onClose = useCallback(
(tx: VegaStoredTxState) => () => {
const safeToDelete = isFinal(tx);
if (safeToDelete) {
deleteTx(tx.id);
} else {
dismissTx(tx.id);
const fromVegaTransaction = useCallback(
(tx: VegaStoredTxState): Toast => {
let content: ToastContent;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
removeToast(`vega-${tx.id}`);
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
return {
id: `vega-${tx.id}`,
intent: getIntent(tx),
onClose: () => dismissVegaTransaction(tx.id),
loader: tx.status === VegaTxStatus.Pending,
content,
};
},
[deleteTx, dismissTx, removeToast]
[dismissVegaTransaction]
);
const fromVegaTransaction = (tx: VegaStoredTxState): Toast => {
let content: ToastContent;
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
const toasts = useMemo(() => {
return [
...compact(vegaTransactions)
.filter((tx) => isTransactionTypeSupported(tx))
.map(fromVegaTransaction),
];
}, [fromVegaTransaction, vegaTransactions]);
// Transaction can be successful but the order can be rejected by the network
const intent =
tx.order && [OrderStatus.STATUS_REJECTED].includes(tx.order.status)
? Intent.Danger
: intentMap[tx.status];
return {
id: `vega-${tx.id}`,
intent,
onClose: onClose(tx),
loader: tx.status === VegaTxStatus.Pending,
content,
closeAfter,
};
};
useVegaTransactionStore.subscribe(
(state) =>
compact(
state.transactions.filter(
(tx) => tx?.dialogOpen && isTransactionTypeSupported(tx)
)
),
(txs) => {
txs.forEach((tx) => setToast(fromVegaTransaction(tx)));
}
);
return toasts;
};
+25 -6
View File
@@ -1,16 +1,35 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { ToastsContainer } from '@vegaprotocol/ui-toolkit';
import { useMemo } from 'react';
import sortBy from 'lodash/sortBy';
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/governance';
import { useVegaTransactionToasts } from '../lib/hooks/use-vega-transaction-toasts';
import { useEthereumTransactionToasts } from '../lib/hooks/use-ethereum-transaction-toasts';
import { useEthereumWithdrawApprovalsToasts } from '../lib/hooks/use-ethereum-withdraw-approval-toasts';
export const ToastsManager = () => {
useUpdateNetworkParametersToasts();
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
const updateNetworkParametersToasts = useUpdateNetworkParametersToasts();
const vegaTransactionToasts = useVegaTransactionToasts();
const ethTransactionToasts = useEthereumTransactionToasts();
const withdrawApprovalToasts = useEthereumWithdrawApprovalsToasts();
const toasts = useMemo(() => {
return sortBy(
[
...vegaTransactionToasts,
...ethTransactionToasts,
...withdrawApprovalToasts,
...updateNetworkParametersToasts,
],
['createdBy']
);
}, [
vegaTransactionToasts,
ethTransactionToasts,
withdrawApprovalToasts,
updateNetworkParametersToasts,
]);
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
};
@@ -10,9 +10,9 @@ interface Props {
export const DealTicketButton = ({ disabled, variant }: Props) => {
const { pubKey } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
return pubKey ? (
<div className="mb-4">
<Button
@@ -31,6 +31,8 @@ import {
usePersistedOrderStoreSubscription,
} from '@vegaprotocol/orders';
export type TransactionStatus = 'default' | 'pending';
export interface DealTicketProps {
market: MarketDealTicket;
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
@@ -7,8 +7,13 @@ fragment ProposalEventFields on Proposal {
}
subscription ProposalEvent($partyId: ID!) {
proposals(partyId: $partyId) {
...ProposalEventFields
busEvents(partyId: $partyId, batchSize: 0, types: [Proposal]) {
type
event {
... on Proposal {
...ProposalEventFields
}
}
}
}
@@ -10,7 +10,7 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
}>;
export type ProposalEventSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } };
export type ProposalEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
export type UpdateNetworkParameterFieldsFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } } };
@@ -55,8 +55,13 @@ export const UpdateNetworkParameterFieldsFragmentDoc = gql`
`;
export const ProposalEventDocument = gql`
subscription ProposalEvent($partyId: ID!) {
proposals(partyId: $partyId) {
...ProposalEventFields
busEvents(partyId: $partyId, batchSize: 0, types: [Proposal]) {
type
event {
... on Proposal {
...ProposalEventFields
}
}
}
}
${ProposalEventFieldsFragmentDoc}`;
@@ -28,15 +28,24 @@ export const useProposalEvent = (transaction: VegaTxState) => {
variables: { partyId },
})
.subscribe(({ data }) => {
if (!data?.proposals) {
if (!data?.busEvents?.length) {
return;
}
// typo in 'proposals' - this should be singular
const proposal = data.proposals;
// No types available for the subscription result
const matchingProposalEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Proposal') {
return false;
}
if (proposal.id === id) {
callback(proposal);
return e.event.id === id;
});
if (
matchingProposalEvent &&
matchingProposalEvent.event.__typename === 'Proposal'
) {
callback(matchingProposalEvent.event);
subRef.current?.unsubscribe();
}
});
@@ -4,7 +4,6 @@ import type { UpdateNetworkParameter } from '@vegaprotocol/types';
import { ProposalStateMapping } from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import compact from 'lodash/compact';
@@ -29,7 +28,7 @@ const UpdateNetworkParameterToastContent = ({
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading>{title}</ToastHeading>
<h3 className="font-bold">{title}</h3>
<p className="italic">
'
{t(
@@ -53,8 +52,9 @@ const UpdateNetworkParameterToastContent = ({
);
};
export const useUpdateNetworkParametersToasts = () => {
const { setToast, remove } = useToasts((store) => ({
export const useUpdateNetworkParametersToasts = (): Toast[] => {
const { proposalToasts, setToast, remove } = useToasts((store) => ({
proposalToasts: store.toasts,
setToast: store.setToast,
remove: store.remove,
}));
@@ -66,9 +66,7 @@ export const useUpdateNetworkParametersToasts = () => {
id: `update-network-param-proposal-${proposal.id}`,
intent: Intent.Warning,
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
onClose: () => remove(id),
closeAfter: CLOSE_AFTER,
};
},
@@ -98,4 +96,6 @@ export const useUpdateNetworkParametersToasts = () => {
}
},
});
return proposalToasts;
};
@@ -1,6 +1,6 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import { useUpdateNetworkParametersToasts } from './use-update-network-paramaters-toasts';
@@ -9,8 +9,8 @@ import type {
OnUpdateNetworkParametersSubscription,
} from './__generated__/Proposal';
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
import waitForNextTick from 'flush-promises';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { waitFor } from '@testing-library/react';
const render = (mocks?: MockedResponse[]) => {
const wrapper = ({ children }: { children: ReactNode }) => (
@@ -92,10 +92,11 @@ const mockedEvent: MockedResponse<OnUpdateNetworkParametersSubscription> = {
},
};
const INITIAL = useToasts.getState();
const clear = () => {
useToasts.setState(INITIAL);
const { result: clearer } = renderHook(() =>
useToasts((store) => store.removeAll)
);
act(() => clearer.current());
};
describe('useUpdateNetworkParametersToasts', () => {
@@ -103,23 +104,29 @@ describe('useUpdateNetworkParametersToasts', () => {
afterAll(clear);
it('returns toast for update network parameters bus event', async () => {
render([mockedEvent]);
await waitFor(() => {
expect(useToasts.getState().count).toBe(1);
const { waitForNextUpdate, result } = render([mockedEvent]);
await act(async () => {
waitForNextUpdate();
await waitForNextTick();
});
expect(result.current.length).toBe(1);
});
it('does not return toast for empty event', async () => {
render([mockedEmptyEvent]);
await waitFor(() => {
expect(useToasts.getState().count).toBe(0);
const { waitForNextUpdate, result } = render([mockedEmptyEvent]);
await act(async () => {
waitForNextUpdate();
await waitForNextTick();
});
expect(result.current.length).toBe(0);
});
it('does not return toast for wrong event', async () => {
render([mockedWrongEvent]);
await waitFor(() => {
expect(useToasts.getState().count).toBe(0);
const { waitForNextUpdate, result } = render([mockedWrongEvent]);
await act(async () => {
waitForNextUpdate();
await waitForNextTick();
});
expect(result.current.length).toBe(0);
});
});
@@ -1,13 +1,18 @@
fragment VoteEventFields on ProposalVote {
fragment VoteEventFields on Vote {
proposalId
vote {
value
datetime
}
value
datetime
}
subscription VoteEvent($partyId: ID!) {
votes(partyId: $partyId) {
...VoteEventFields
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
type
event {
... on Vote {
proposalId
value
datetime
}
}
}
}
@@ -3,31 +3,36 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type VoteEventFieldsFragment = { __typename?: 'ProposalVote', proposalId: string, vote: { __typename?: 'Vote', value: Types.VoteValue, datetime: any } };
export type VoteEventFieldsFragment = { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: any };
export type VoteEventSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type VoteEventSubscription = { __typename?: 'Subscription', votes: { __typename?: 'ProposalVote', proposalId: string, vote: { __typename?: 'Vote', value: Types.VoteValue, datetime: any } } };
export type VoteEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: any } | { __typename?: 'Withdrawal' } }> | null };
export const VoteEventFieldsFragmentDoc = gql`
fragment VoteEventFields on ProposalVote {
fragment VoteEventFields on Vote {
proposalId
vote {
value
datetime
}
value
datetime
}
`;
export const VoteEventDocument = gql`
subscription VoteEvent($partyId: ID!) {
votes(partyId: $partyId) {
...VoteEventFields
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
type
event {
... on Vote {
proposalId
value
datetime
}
}
}
}
${VoteEventFieldsFragmentDoc}`;
`;
/**
* __useVoteEventSubscription__
@@ -25,14 +25,23 @@ export const useVoteEvent = (transaction: VegaTxState) => {
variables: { partyId },
})
.subscribe(({ data }) => {
if (!data?.votes) {
if (!data?.busEvents?.length) {
return;
}
const vote = data.votes;
const matchingVoteEvent = data.busEvents.find((e) => {
if (e.event.__typename !== 'Vote') {
return false;
}
if (vote.proposalId === proposalId) {
callback(vote);
return e.event.proposalId === proposalId;
});
if (
matchingVoteEvent &&
matchingVoteEvent.event.__typename === 'Vote'
) {
callback(matchingVoteEvent.event);
subRef.current?.unsubscribe();
}
});
-5
View File
@@ -174,16 +174,11 @@ module.exports = {
'60%': { transform: 'rotate( 0.0deg)' },
'100%': { transform: 'rotate( 0.0deg)' },
},
progress: {
from: { width: '0' },
to: { width: '100%' },
},
},
animation: {
rotate: 'rotate 2s linear alternate infinite',
'rotate-back': 'rotate 2s linear reverse infinite',
wave: 'wave 2s linear infinite',
progress: 'progress 5s cubic-bezier(.39,.58,.57,1) 1',
},
data: {
selected: 'state~="checked"',
@@ -1,6 +1,7 @@
import { Splash } from '../splash';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import * as Sentry from '@sentry/react';
interface AsyncRendererProps<T> {
loading: boolean;
@@ -26,6 +27,7 @@ export function AsyncRenderer<T = object>({
render,
}: AsyncRendererProps<T>) {
if (error) {
Sentry.captureException(`Error rendering data: ${error.message}`);
if (!data) {
return (
<Splash>
@@ -11,7 +11,6 @@ interface ProgressBarProps {
export const ProgressBar = ({ className, intent, value }: ProgressBarProps) => {
return (
<div
data-progress-bar
style={{ height: '6px' }}
className={classNames(
'bg-neutral-300 dark:bg-neutral-700 relative',
@@ -19,7 +18,6 @@ export const ProgressBar = ({ className, intent, value }: ProgressBarProps) => {
)}
>
<div
data-progress-bar-value
className={classNames(
'absolute left-0 top-0 bottom-0',
intent === undefined || intent === Intent.None
@@ -1,17 +1,21 @@
.initial {
top: 20px;
opacity: 0;
transition: all 0.3s;
max-height: 0;
border: 0;
margin-bottom: 0;
}
.showing {
right: 0;
opacity: 1;
transition: all 0.3s;
max-height: 100vw;
}
.expired {
right: -375px;
opacity: 0;
transition: all 0.5s;
max-height: 0;
transition: all 0.75s;
}
@@ -1,9 +1,7 @@
/* eslint-disable jsx-a11y/accessible-emoji */
import { Panel, Toast, ToastHeading } from './toast';
import { Toast } from './toast';
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Intent } from '../../utils/intent';
import { ExternalLink } from '../link';
import { ProgressBar } from '../progress-bar';
export default {
title: 'Toast',
@@ -11,7 +9,14 @@ export default {
} as ComponentMeta<typeof Toast>;
const Template: ComponentStory<typeof Toast> = (args) => {
return <Toast {...args} />;
const toastContent = (
<>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
<p>Eaque exercitationem saepe cupiditate sunt impedit.</p>
<p>I really like 🥪🥪🥪!</p>
</>
);
return <Toast {...args} content={toastContent} />;
};
export const Default = Template.bind({});
@@ -19,16 +24,6 @@ Default.args = {
id: 'def',
intent: Intent.None,
state: 'showing',
content: (
<>
<ToastHeading>Optional heading</ToastHeading>
<p>This is a message that can return over multiple lines.</p>
<p>
<ExternalLink>Optional link</ExternalLink>
</p>
</>
),
onClose: () => undefined,
};
export const Primary = Template.bind({});
@@ -36,17 +31,6 @@ Primary.args = {
id: 'pri',
intent: Intent.Primary,
state: 'showing',
content: (
<>
<ToastHeading>Optional heading</ToastHeading>
<p>This is a message that can return over multiple lines.</p>
<p>
<ExternalLink>Optional link</ExternalLink>
</p>
<Panel>Lorem ipsum dolor sit amet consectetur adipisicing elit</Panel>
</>
),
onClose: () => undefined,
};
export const Danger = Template.bind({});
@@ -54,17 +38,6 @@ Danger.args = {
id: 'dan',
intent: Intent.Danger,
state: 'showing',
content: (
<>
<ToastHeading>Optional heading</ToastHeading>
<p>This is a message that can return over multiple lines.</p>
<p>
<ExternalLink>Optional link</ExternalLink>
</p>
<Panel>Lorem ipsum dolor sit amet consectetur adipisicing elit</Panel>
</>
),
onClose: () => undefined,
};
export const Warning = Template.bind({});
@@ -72,21 +45,6 @@ Warning.args = {
id: 'war',
intent: Intent.Warning,
state: 'showing',
content: (
<>
<ToastHeading>Optional heading</ToastHeading>
<p>This is a message that can return over multiple lines.</p>
<p>
<ExternalLink>Optional link</ExternalLink>
</p>
<Panel>
<strong>Deposit 10.00 tUSDX</strong>
<p className="mt-[2px]">Awaiting confirmations (1/3)</p>
<ProgressBar value={33.33} />
</Panel>
</>
),
onClose: () => undefined,
};
export const Success = Template.bind({});
@@ -94,15 +52,4 @@ Success.args = {
id: 'suc',
intent: Intent.Success,
state: 'showing',
content: (
<>
<ToastHeading>Optional heading</ToastHeading>
<p>This is a message that can return over multiple lines.</p>
<p>
<ExternalLink>Optional link</ExternalLink>
</p>
<Panel>Lorem ipsum dolor sit amet consectetur adipisicing elit</Panel>
</>
),
onClose: () => undefined,
};
+26 -175
View File
@@ -3,8 +3,7 @@ import styles from './toast.module.css';
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import type { HTMLAttributes, HtmlHTMLAttributes } from 'react';
import { forwardRef, useEffect } from 'react';
import { useEffect } from 'react';
import { useCallback } from 'react';
import { useLayoutEffect } from 'react';
import { useRef } from 'react';
@@ -34,43 +33,20 @@ const toastIconMapping: { [i in Intent]: IconName } = {
[Intent.None]: IconNames.HELP,
[Intent.Primary]: IconNames.INFO_SIGN,
[Intent.Success]: IconNames.TICK_CIRCLE,
[Intent.Warning]: IconNames.WARNING_SIGN,
[Intent.Warning]: IconNames.ERROR,
[Intent.Danger]: IconNames.ERROR,
};
export const CLOSE_DELAY = 500;
export const TICKER = 100;
export const CLOSE_AFTER = 5000;
const getToastAccent = (intent: Intent) => ({
// strip
'bg-gray-200 text-black text-opacity-70': intent === Intent.None,
'bg-vega-blue text-white text-opacity-70': intent === Intent.Primary,
'bg-success text-white text-opacity-70': intent === Intent.Success,
'bg-warning text-white text-opacity-70': intent === Intent.Warning,
'bg-vega-pink text-white text-opacity-70': intent === Intent.Danger,
});
export const Panel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ children, className, ...props }, ref) => {
return (
<div
data-panel
ref={ref}
data-testid="toast-panel"
className={classNames(
'p-2 rounded mt-[10px]',
'font-mono text-[12px] leading-[16px] font-normal',
'[&>h4]:font-bold',
className
)}
{...props}
>
{children}
</div>
);
}
);
export const ToastHeading = forwardRef<
HTMLHeadingElement,
HtmlHTMLAttributes<HTMLHeadingElement>
>(({ children, ...props }, ref) => (
<h3 ref={ref} className="text-sm uppercase mb-1" {...props}>
{children}
</h3>
));
export const CLOSE_DELAY = 750;
export const Toast = ({
id,
@@ -83,9 +59,6 @@ export const Toast = ({
loader = false,
}: ToastProps) => {
const toastRef = useRef<HTMLDivElement>(null);
const progressRef = useRef<HTMLDivElement>(null);
const ticker = useRef<number>(0);
const lock = useRef<boolean>(false);
const closeToast = useCallback(() => {
requestAnimationFrame(() => {
@@ -107,21 +80,16 @@ export const Toast = ({
}
});
return () => cancelAnimationFrame(req);
}, [id, intent, content]); // DO NOT REMOVE DEPS: intent, content
}, [id]);
useEffect(() => {
const i = setInterval(() => {
if (!closeAfter || closeAfter === 0) return;
if (!lock.current) {
ticker.current += 100;
}
if (ticker.current >= closeAfter) {
let t: NodeJS.Timeout;
if (closeAfter && closeAfter > 0) {
t = setTimeout(() => {
closeToast();
}
}, 100);
return () => {
clearInterval(i);
};
}, closeAfter);
}
return () => clearTimeout(t);
}, [closeAfter, closeToast]);
useEffect(() => {
@@ -130,76 +98,14 @@ export const Toast = ({
}
}, [closeToast, signal]);
const withProgress = Boolean(closeAfter && closeAfter > 0);
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<div
data-testid="toast"
data-toast-id={id}
ref={toastRef}
role="dialog"
onMouseLeave={() => {
lock.current = false;
if (progressRef.current) {
progressRef.current.style.animationPlayState = 'running';
}
}}
onMouseEnter={() => {
lock.current = true;
if (progressRef.current) {
progressRef.current.style.animationPlayState = 'paused';
}
}}
className={classNames(
'w-[320px] rounded-md overflow-hidden',
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]',
'text-black dark:text-white',
'font-alpha liga-0-calt-0 text-[14px] leading-[19px]',
// background
{
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
'bg-vega-green-300 dark:bg-vega-green-700': intent === Intent.Success,
'bg-vega-orange-300 dark:bg-vega-orange-700':
intent === Intent.Warning,
'bg-vega-pink-300 dark:bg-vega-pink-700': intent === Intent.Danger,
},
// panel's colours
{
'[&_[data-panel]]:bg-vega-light-150 [&_[data-panel]]:dark:bg-vega-dark-150 ':
intent === Intent.None,
'[&_[data-panel]]:bg-vega-blue-350 [&_[data-panel]]:dark:bg-vega-blue-650':
intent === Intent.Primary,
'[&_[data-panel]]:bg-vega-green-350 [&_[data-panel]]:dark:bg-vega-green-650':
intent === Intent.Success,
'[&_[data-panel]]:bg-vega-orange-350 [&_[data-panel]]:dark:bg-vega-orange-650':
intent === Intent.Warning,
'[&_[data-panel]]:bg-vega-pink-350 [&_[data-panel]]:dark:bg-vega-pink-650':
intent === Intent.Danger,
},
// panels's progress bar colours
'[&_[data-progress-bar]]:mt-[10px] [&_[data-progress-bar]]:mb-[4px]',
{
'[&_[data-progress-bar]]:bg-vega-light-200 [&_[data-progress-bar]]:dark:bg-vega-dark-200 ':
intent === Intent.None,
'[&_[data-progress-bar]]:bg-vega-blue-400 [&_[data-progress-bar]]:dark:bg-vega-blue-600':
intent === Intent.Primary,
'[&_[data-progress-bar-value]]:bg-vega-blue-500 [&_[data-progress-bar-value]]:dark:bg-vega-blue-500':
intent === Intent.Primary,
'[&_[data-progress-bar]]:bg-vega-green-400 [&_[data-progress-bar]]:dark:bg-vega-green-600':
intent === Intent.Success,
'[&_[data-progress-bar-value]]:bg-vega-green-600 [&_[data-progress-bar-value]]:dark:bg-vega-green-500':
intent === Intent.Success,
'[&_[data-progress-bar]]:bg-vega-orange-400 [&_[data-progress-bar]]:dark:bg-vega-orange-600':
intent === Intent.Warning,
'[&_[data-progress-bar-value]]:bg-vega-orange-500 [&_[data-progress-bar-value]]:dark:bg-vega-orange-500':
intent === Intent.Warning,
'[&_[data-progress-bar]]:bg-vega-pink-400 [&_[data-progress-bar]]:dark:bg-vega-pink-600':
intent === Intent.Danger,
'[&_[data-progress-bar-value]]:bg-vega-pink-500 [&_[data-progress-bar-value]]:dark:bg-vega-pink-500':
intent === Intent.Danger,
},
'relative w-[300px] top-0 rounded-md border overflow-hidden mb-2',
'text-black bg-white dark:border-zinc-700',
{
[styles['initial']]: state === 'initial',
[styles['showing']]: state === 'showing',
@@ -212,81 +118,26 @@ export const Toast = ({
type="button"
data-testid="toast-close"
onClick={closeToast}
className="absolute p-[8px] top-[3px] right-[3px] z-20"
className="absolute p-2 top-0 right-0"
>
<Icon
name="cross"
size={3}
className="!block dark:text-white !w-[11px] !h-[11px]"
/>
<Icon name="cross" size={3} className="!block dark:text-white" />
</button>
<div
data-testid="toast-accent"
className={classNames(
{
// gray
'bg-vega-light-200 dark:bg-vega-dark-200 text-vega-light-400 dark:text-vega-dark-100':
intent === Intent.None,
// blue
'bg-vega-blue-500 text-vega-blue-600': intent === Intent.Primary,
// green
'bg-vega-green-500 text-vega-green-600':
intent === Intent.Success,
// orange
'bg-vega-orange-500 text-vega-orange-600':
intent === Intent.Warning,
// pink
'bg-vega-pink-500 text-vega-pink-600': intent === Intent.Danger,
},
'w-8 p-[9px]',
'flex justify-center'
)}
className={classNames(getToastAccent(intent), 'p-2 pt-3 text-center')}
>
{loader ? (
<div className="w-[15px] h-[15px]">
<div className="w-4 h-4">
<Loader size="small" forceTheme="dark" />
</div>
) : (
<Icon
name={toastIconMapping[intent]}
size={4}
className="!block !w-[14px] !h-[14px]"
/>
<Icon name={toastIconMapping[intent]} size={4} className="!block" />
)}
</div>
<div
className={classNames(
'relative',
'overflow-auto flex-1 p-4 pr-[40px] [&>p]:mb-[2.5px]'
)}
className="flex-1 p-2 pr-6 text-sm overflow-auto dark:bg-black dark:text-white"
data-testid="toast-content"
>
{content}
{withProgress && (
<div
ref={progressRef}
data-testid="toast-progress-bar"
className={classNames(
{
'bg-vega-light-200 dark:bg-vega-dark-200 ':
intent === Intent.None,
'bg-vega-blue-400 dark:bg-vega-blue-600':
intent === Intent.Primary,
'bg-vega-green-400 dark:bg-vega-green-600':
intent === Intent.Success,
'bg-vega-orange-400 dark:bg-vega-orange-600':
intent === Intent.Warning,
'bg-vega-pink-400 dark:bg-vega-pink-600':
intent === Intent.Danger,
},
'absolute bottom-0 left-0 w-full h-[4px]',
'animate-progress'
)}
style={{
animationDuration: `${closeAfter}ms`,
}}
></div>
)}
</div>
</div>
</div>
@@ -1,5 +1,5 @@
import { act, render, renderHook, screen } from '@testing-library/react';
import { CLOSE_DELAY, ToastsContainer, useToasts } from '..';
import { ToastsContainer, useToasts } from '..';
import { Intent } from '../../utils/intent';
describe('ToastsContainer', () => {
@@ -108,7 +108,7 @@ describe('ToastsContainer', () => {
) as HTMLButtonElement;
act(() => {
closeBtn.click();
jest.advanceTimersByTime(CLOSE_DELAY);
jest.runAllTimers();
});
rerender(<ToastsContainer order="asc" toasts={result.current.toasts} />);
const toasts = [...screen.queryAllByTestId('toast-content')].map((t) =>
@@ -2,7 +2,6 @@
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { Intent } from '../../utils/intent';
import type { Toast } from './toast';
import { ToastHeading } from './toast';
import { ToastsContainer } from './toasts-container';
import random from 'lodash/random';
import sample from 'lodash/sample';
@@ -60,8 +59,7 @@ const randomWords = [
];
const randomToast = (): Toast => {
const now = new Date().toISOString();
const content = now + ' ' + sample(contents);
const content = sample(contents);
return {
id: String(uniqueId('toast_')),
intent: sample<Intent>([
@@ -85,7 +83,7 @@ const usePrice = create<PriceStore>((set) => ({
const Template: ComponentStory<typeof ToastsContainer> = (args) => {
const setPrice = usePrice((state) => state.setPrice);
const { add, close, closeAll, update, remove, toasts, setToast } = useToasts(
const { add, close, closeAll, update, remove, toasts } = useToasts(
(state) => ({
add: state.add,
close: state.close,
@@ -93,7 +91,6 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
update: state.update,
remove: state.remove,
toasts: state.toasts,
setToast: state.setToast,
})
);
@@ -198,26 +195,6 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
>
🧽
</button>
<button
onClick={() => {
const toasts = Object.values(useToasts.getState().toasts);
if (toasts.length > 0) {
const t = toasts[toasts.length - 1];
setToast({
...t,
intent: Intent.Danger,
content: (
<>
<ToastHeading>Error occurred</ToastHeading>
<p>Something went terribly wrong</p>
</>
),
});
}
}}
>
Set first as Error
</button>
<ToastsContainer {...args} toasts={toasts} />
</div>
);
@@ -225,5 +202,5 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
export const Default = Template.bind({});
Default.args = {
order: 'desc',
order: 'asc',
};
@@ -1,16 +1,8 @@
import { t, usePrevious } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import type { Ref } from 'react';
import { useLayoutEffect, useRef } from 'react';
import { Button } from '../button';
import { Toast } from './toast';
import type { Toasts } from './use-toasts';
import { useToasts } from './use-toasts';
import { Portal } from '@radix-ui/react-portal';
type ToastsContainerProps = {
toasts: Toasts;
toasts: Toast[];
order: 'asc' | 'desc';
};
@@ -18,75 +10,23 @@ export const ToastsContainer = ({
toasts,
order = 'asc',
}: ToastsContainerProps) => {
const ref = useRef<HTMLDivElement>();
const closeAll = useToasts((store) => store.closeAll);
// Scroll to top for desc, bottom for asc when a toast is added.
const count = usePrevious(Object.keys(toasts).length) || 0;
useLayoutEffect(() => {
const t = setTimeout(
() => {
if (Object.keys(toasts).length > count) {
ref.current?.scrollTo({
top: order === 'desc' ? 0 : ref.current.scrollHeight,
behavior: 'smooth',
});
}
},
300 // need to delay scroll down in order for the toast to appear
);
return () => {
clearTimeout(t);
};
}, [count, order, toasts]);
return (
<Portal
ref={ref as Ref<HTMLDivElement>}
<ul
className={classNames(
'group',
'absolute bottom-0 right-0 z-20 ',
'p-[8px_16px_16px_16px]',
'max-w-full max-h-full overflow-x-hidden overflow-y-auto',
'absolute top-0 right-0 pt-2 pr-2 max-w-full z-20 max-h-full overflow-x-hidden overflow-y-auto',
{
hidden: Object.keys(toasts).length === 0,
'flex flex-col-reverse': order === 'desc',
}
)}
>
<ul
className={classNames('relative mt-[38px]', 'flex flex-col gap-[8px]', {
'flex-col-reverse': order === 'desc',
{toasts &&
toasts.map((toast) => {
return (
<li key={toast.id}>
<Toast {...toast} />
</li>
);
})}
>
{toasts &&
Object.values(toasts).map((toast) => {
return (
<li key={toast.id}>
<Toast {...toast} />
</li>
);
})}
<Button
title={t('Dismiss all toasts')}
size="sm"
fill={true}
className={classNames(
'absolute top-[-38px] right-0 z-20',
'transition-opacity',
'opacity-0 group-hover:opacity-50 hover:!opacity-100',
'text-sm text-black dark:text-white bg-white dark:bg-black hover:!bg-white hover:dark:!bg-black',
{
hidden: Object.keys(toasts).length === 0,
}
)}
onClick={() => {
closeAll();
}}
variant={'default'}
>
{t('Dismiss all')}
</Button>
</ul>
</Portal>
</ul>
);
};
@@ -1,127 +0,0 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { Intent } from '../../utils/intent';
import type { Toast } from './toast';
import { useToasts } from './use-toasts';
const T1: Toast = {
id: 'TEST-1',
intent: Intent.None,
content: undefined,
};
const T2: Toast = {
id: 'TEST-2',
intent: Intent.None,
content: undefined,
};
const T3: Toast = {
id: 'TEST-3',
intent: Intent.None,
content: undefined,
};
const INITIAL = useToasts.getState();
describe('useToasts', () => {
beforeEach(() => {
useToasts.setState(INITIAL, true);
});
afterAll(() => {
useToasts.setState(INITIAL, true);
});
it('adds toast', () => {
const { result } = renderHook(() => useToasts());
act(() => {
result.current.add(T1);
});
expect(result.current.toasts[T1.id]).toEqual(T1);
expect(result.current.count).toEqual(1);
});
it('removes toast', () => {
const { result } = renderHook(() => useToasts());
act(() => {
result.current.add(T1);
result.current.add(T2);
result.current.add(T3);
result.current.remove(T1.id);
result.current.remove(T1.id);
result.current.remove(T1.id);
});
expect(result.current.toasts[T1.id]).toBeUndefined();
expect(result.current.count).toEqual(2);
});
it('updates toast', () => {
const { result } = renderHook(() => useToasts());
const data = { content: <p>Burning hot toast</p> };
act(() => {
result.current.add(T1);
result.current.add(T2);
result.current.add(T3);
result.current.update(T2.id, data);
});
expect(result.current.toasts[T2.id]).toHaveProperty(
'content',
data.content
);
expect(result.current.count).toEqual(3);
});
it('removes all toasts', () => {
const { result } = renderHook(() => useToasts());
act(() => {
result.current.add(T1);
result.current.add(T2);
result.current.add(T3);
result.current.removeAll();
});
expect(result.current.toasts).toEqual({});
expect(result.current.count).toEqual(0);
});
it('sends close signal to toast', () => {
const { result } = renderHook(() => useToasts());
act(() => {
result.current.add(T1);
result.current.add(T2);
result.current.add(T3);
result.current.close(T2.id);
});
expect(result.current.toasts[T2.id]).toHaveProperty('signal', 'close');
});
it('sends close signal to all toasts', () => {
const { result } = renderHook(() => useToasts());
act(() => {
result.current.add(T1);
result.current.add(T2);
result.current.add(T3);
result.current.closeAll();
});
Object.values(result.current.toasts).forEach((t) => {
expect(t).toHaveProperty('signal', 'close');
});
});
it('sets toast (adds or update if exists)', () => {
const { result } = renderHook(() => useToasts());
const data = { content: <p>Burning hot toast</p> };
act(() => {
result.current.setToast(T1);
result.current.setToast(T1);
result.current.setToast(T2);
result.current.setToast(T3);
result.current.setToast(T3);
result.current.setToast({ ...T3, ...data });
});
expect(result.current.toasts[T3.id]).toHaveProperty(
'content',
data.content
);
expect(result.current.count).toEqual(3);
});
});
@@ -1,20 +1,11 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { Toast } from './toast';
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
export type Toasts = Record<string, Toast>;
const isUpdateable = (a: Toast, b: Toast) =>
isEqual(omit(a, 'onClose'), omit(b, 'onClose'));
type State = {
toasts: Toasts;
count: number;
};
type Actions = {
type ToastsStore = {
/**
* A list of active toasts
*/
toasts: Toast[];
/**
* Adds/displays a new toast
*/
@@ -45,54 +36,44 @@ type Actions = {
removeAll: () => void;
};
type ToastsStore = State & Actions;
const add =
(toast: Toast) =>
(store: ToastsStore): Partial<ToastsStore> => ({
toasts: [...store.toasts, toast],
});
export const useToasts = create(
immer<ToastsStore>((set, get) => ({
toasts: {},
count: 0,
add: (toast) =>
set((state) => {
state.toasts[toast.id] = toast;
++state.count;
}),
update: (id, toastData) =>
set((state) => {
const found = state.toasts[id];
if (found) {
Object.assign(found, toastData);
}
}),
setToast: (toast: Toast) =>
set((state) => {
const found = state.toasts[toast.id];
if (found) {
if (!isUpdateable(found, toast)) {
Object.assign(found, toast);
}
} else {
state.toasts[toast.id] = toast;
++state.count;
}
}),
close: (id) =>
set((state) => {
const found = state.toasts[id];
if (found) {
found.signal = 'close';
}
}),
closeAll: () =>
set((state) => {
Object.values(state.toasts).forEach((t) => (t.signal = 'close'));
}),
remove: (id) =>
set((state) => {
if (state.toasts[id]) {
delete state.toasts[id];
--state.count;
}
}),
removeAll: () => set({ toasts: {}, count: 0 }),
}))
);
const update =
(id: string, toastData: Partial<Toast>) =>
(store: ToastsStore): Partial<ToastsStore> => {
const toasts = [...store.toasts];
const toastIdx = toasts.findIndex((t) => t.id === id);
if (toastIdx > -1) toasts[toastIdx] = { ...toasts[toastIdx], ...toastData };
return { toasts };
};
export const useToasts = create<ToastsStore>((set) => ({
toasts: [],
add: (toast) => set(add(toast)),
update: (id, toastData) => set(update(id, toastData)),
setToast: (toast: Toast) =>
set((store) => {
if (store.toasts.find((t) => t.id === toast.id)) {
return update(toast.id, toast)(store);
} else {
return add(toast)(store);
}
}),
close: (id) => set(update(id, { signal: 'close' })),
closeAll: () =>
set((store) => ({
toasts: [...store.toasts].map((t) => ({ ...t, signal: 'close' })),
})),
remove: (id) =>
set((store) => ({
toasts: [...store.toasts].filter((t) => t.id !== id),
})),
removeAll: () =>
set(() => ({
toasts: [],
})),
}));
@@ -13,12 +13,10 @@ const mockSendTx = jest.fn<Promise<Partial<TransactionResponse> | null>, []>();
const pubKey = 'pubKey';
const mockDisconnect = jest.fn();
jest.mock('./use-vega-wallet', () => ({
useVegaWallet: () => ({
sendTx: mockSendTx,
pubKey,
disconnect: mockDisconnect,
}),
}));
@@ -106,17 +104,6 @@ describe('useVegaTransactionManager', () => {
mockSendTx.mockRejectedValue(null);
renderHook(useVegaTransactionManager);
await waitForNextTick();
expect(mockDisconnect).not.toHaveBeenCalledWith();
expect(update).toBeCalled();
expect(update.mock.calls[0][1]?.status).toEqual(VegaTxStatus.Error);
});
it('call disconnect if detect no service error', async () => {
mockTransactionStoreState.mockReturnValue(defaultState);
mockSendTx.mockRejectedValue(new TypeError('Failed to fetch'));
renderHook(useVegaTransactionManager);
await waitForNextTick();
expect(mockDisconnect).toHaveBeenCalledWith();
expect(update).toBeCalled();
expect(update.mock.calls[0][1]?.status).toEqual(VegaTxStatus.Error);
});
@@ -1,12 +1,12 @@
import { useVegaWallet } from './use-vega-wallet';
import { useEffect, useRef } from 'react';
import type { WalletError } from './connectors';
import { ClientErrors } from './connectors';
import { VegaTxStatus, orderErrorResolve } from './use-vega-transaction';
import { VegaTxStatus } from './use-vega-transaction';
import { useVegaTransactionStore } from './use-vega-transaction-store';
import { WalletClientError } from '@vegaprotocol/wallet-client';
export const useVegaTransactionManager = () => {
const { sendTx, pubKey, disconnect } = useVegaWallet();
const { sendTx, pubKey } = useVegaWallet();
const processed = useRef<Set<number>>(new Set());
const transaction = useVegaTransactionStore((state) =>
state.transactions.find(
@@ -38,14 +38,10 @@ export const useVegaTransactionManager = () => {
}
})
.catch((err) => {
const error = orderErrorResolve(err);
if ((error as WalletError).code === ClientErrors.NO_SERVICE.code) {
disconnect();
}
update(transaction.id, {
error,
error: err instanceof WalletClientError ? err : ClientErrors.UNKNOWN,
status: VegaTxStatus.Error,
});
});
}, [transaction, pubKey, del, sendTx, update, disconnect]);
}, [transaction, pubKey, del, sendTx, update]);
};
@@ -20,7 +20,6 @@ import type {
} from './__generated__/TransactionResult';
import type { WithdrawalApprovalQuery } from './__generated__/WithdrawalApproval';
import { subscribeWithSelector } from 'zustand/middleware';
export interface VegaStoredTxState extends VegaTxState {
id: number;
createdAt: Date;
@@ -52,8 +51,8 @@ export interface VegaTransactionStore {
) => void;
}
export const useVegaTransactionStore = create(
subscribeWithSelector<VegaTransactionStore>((set, get) => ({
export const useVegaTransactionStore = create<VegaTransactionStore>(
(set, get) => ({
transactions: [] as VegaStoredTxState[],
create: (body: Transaction) => {
const transactions = get().transactions;
@@ -205,5 +204,5 @@ export const useVegaTransactionStore = create(
})
);
},
}))
})
);
@@ -6,18 +6,17 @@ import {
useTransactionEventSubscription,
} from './__generated__/TransactionResult';
import { useVegaTransactionStore } from './use-vega-transaction-store';
import { waitForWithdrawalApproval } from './wait-for-withdrawal-approval';
export const useVegaTransactionUpdater = () => {
const client = useApolloClient();
const updateWithdrawal = useVegaTransactionStore(
(state) => state.updateWithdrawal
);
const updateOrder = useVegaTransactionStore((state) => state.updateOrder);
const updateTransaction = useVegaTransactionStore(
(state) => state.updateTransactionResult
);
const { updateWithdrawal, updateOrder, updateTransaction } =
useVegaTransactionStore((state) => ({
updateWithdrawal: state.updateWithdrawal,
updateOrder: state.updateOrder,
updateTransaction: state.updateTransactionResult,
}));
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
@@ -87,7 +87,7 @@ describe('useVegaTransaction', () => {
);
});
it('handles an unknown error', () => {
it('handles an unkwown error', () => {
const unknownThrow = { foo: 'bar' };
const mockSendTx = jest.fn(() => {
throw unknownThrow;
+3 -24
View File
@@ -1,15 +1,10 @@
import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import {
WalletClientError,
WalletHttpError,
} from '@vegaprotocol/wallet-client';
import { useVegaWallet } from './use-vega-wallet';
import type { VegaTransactionContentMap } from './vega-transaction-dialog';
import { VegaTransactionDialog } from './vega-transaction-dialog';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import type { Transaction } from './connectors';
import type { WalletError } from './connectors';
import { ClientErrors } from './connectors';
export interface DialogProps {
@@ -43,21 +38,8 @@ export const initialState = {
dialogOpen: false,
};
export const orderErrorResolve = (err: Error | unknown): Error => {
if (err instanceof WalletClientError) {
return err;
} else if (err instanceof WalletHttpError) {
return ClientErrors.UNKNOWN;
} else if (err instanceof TypeError) {
return ClientErrors.NO_SERVICE;
} else if (err instanceof Error) {
return err;
}
return ClientErrors.UNKNOWN;
};
export const useVegaTransaction = () => {
const { sendTx, disconnect } = useVegaWallet();
const { sendTx } = useVegaWallet();
const [transaction, _setTransaction] = useState<VegaTxState>(initialState);
const setTransaction = useCallback((update: Partial<VegaTxState>) => {
@@ -106,10 +88,7 @@ export const useVegaTransaction = () => {
return null;
} catch (err) {
const error = orderErrorResolve(err);
if ((error as WalletError).code === ClientErrors.NO_SERVICE.code) {
disconnect();
}
const error = err instanceof Error ? err : ClientErrors.UNKNOWN;
setTransaction({
error,
status: VegaTxStatus.Error,
@@ -117,7 +96,7 @@ export const useVegaTransaction = () => {
return null;
}
},
[sendTx, setTransaction, reset, disconnect]
[sendTx, setTransaction, reset]
);
const Dialog = useMemo(() => {
@@ -9,7 +9,6 @@ import type { DepositBusEventFieldsFragment } from '@vegaprotocol/wallet';
import type { EthTxState } from './use-ethereum-transaction';
import { EthTxStatus } from './use-ethereum-transaction';
import { subscribeWithSelector } from 'zustand/middleware';
type Contract = MultisigControl | CollateralBridge | Token | TokenFaucetable;
type ContractMethod =
@@ -55,8 +54,8 @@ export interface EthTransactionStore {
delete: (index: number) => void;
}
export const useEthTransactionStore = create(
subscribeWithSelector<EthTransactionStore>((set, get) => ({
export const useEthTransactionStore = create<EthTransactionStore>(
(set, get) => ({
transactions: [] as EthStoredTxState[],
create: (
contract: Contract | null,
@@ -140,5 +139,5 @@ export const useEthTransactionStore = create(
})
);
},
}))
})
);
@@ -103,7 +103,7 @@ export const useEthWithdrawApprovalsManager = () => {
update(transaction.id, {
status: ApprovalStatus.Ready,
approval,
dialogOpen: true,
dialogOpen: false,
});
const signer = provider.getSigner();
createEthTransaction(
@@ -5,7 +5,6 @@ import type { WithdrawalBusEventFieldsFragment } from '@vegaprotocol/wallet';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import type { WithdrawalApprovalQuery } from '@vegaprotocol/wallet';
import { subscribeWithSelector } from 'zustand/middleware';
export enum ApprovalStatus {
Idle = 'Idle',
@@ -46,11 +45,10 @@ export interface EthWithdrawApprovalStore {
>
) => void;
dismiss: (index: number) => void;
delete: (index: number) => void;
}
export const useEthWithdrawApprovalsStore = create(
subscribeWithSelector<EthWithdrawApprovalStore>((set, get) => ({
export const useEthWithdrawApprovalsStore = create<EthWithdrawApprovalStore>(
(set, get) => ({
transactions: [] as EthWithdrawalApprovalState[],
create: (
withdrawal: EthWithdrawalApprovalState['withdrawal'],
@@ -109,12 +107,5 @@ export const useEthWithdrawApprovalsStore = create(
})
);
},
delete: (index: number) => {
set(
produce((state: EthWithdrawApprovalStore) => {
delete state.transactions[index];
})
);
},
}))
})
);
+3 -5
View File
@@ -229,15 +229,13 @@ export const VerificationStatus = ({ state }: { state: VerifyState }) => {
);
return (
<>
<p>{t("The amount you're withdrawing has triggered a time delay")}</p>
<p className="mb-2">
{t("The amount you're withdrawing has triggered a time delay")}
</p>
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
</>
);
}
if (state.status === ApprovalStatus.Ready) {
return <p>{t('The withdrawal has been approved.')}</p>;
}
return null;
};