Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f8ba8be93 | ||
|
|
026d48f47c | ||
|
|
bb67bee4bc | ||
|
|
f49398d76f | ||
|
|
40a997b1bf | ||
|
|
2304ce9763 | ||
|
|
79a16b8562 | ||
|
|
5661b74082 | ||
|
|
22703d937e | ||
|
|
f780013846 | ||
|
|
73f37c2477 | ||
|
|
607ad06971 | ||
|
|
7aee2a3a7b | ||
|
|
1fdf519780 | ||
|
|
9233778e85 | ||
|
|
7a91f48bcb | ||
|
|
b43ea0f60b | ||
|
|
6e31fb03ae | ||
|
|
d76fa13c5d | ||
|
|
89aeed6305 | ||
|
|
6e677084a3 | ||
|
|
8069aa5ee7 | ||
|
|
b0169a0d39 | ||
|
|
06cfd79415 | ||
|
|
443220283c | ||
|
|
a642bf8ce4 | ||
|
|
20233db706 | ||
|
|
d4f50eb70c | ||
|
|
e5d3f90d45 | ||
|
|
673c896e2f | ||
|
|
deea63fa5e | ||
|
|
d31333538b | ||
|
|
4af9979a21 |
@@ -1,68 +0,0 @@
|
||||
context('Oracle page', { tags: '@smoke' }, () => {
|
||||
describe('Verify elements on page', () => {
|
||||
before('create market and navigate to oracle page', () => {
|
||||
cy.createMarket();
|
||||
cy.visit('/oracles');
|
||||
});
|
||||
it('should see oracle data', () => {
|
||||
cy.getByTestId('oracle-details').should('have.length.at.least', 2);
|
||||
cy.getByTestId('oracle-details')
|
||||
.should('exist')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.text', 'ID');
|
||||
cy.get('a').invoke('text').should('have.length', 64);
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/oracles/');
|
||||
});
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.text', 'Type');
|
||||
cy.get('td').should('have.text', 'External data');
|
||||
});
|
||||
cy.get('tr')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.text', 'Signer');
|
||||
cy.getByTestId('keytype').should('have.text', 'Vega');
|
||||
cy.get('a').invoke('text').should('have.length', 64);
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/parties/');
|
||||
});
|
||||
cy.get('tr')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.text', 'Settlement for');
|
||||
cy.get('a').invoke('text').should('have.length', 64);
|
||||
cy.get('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/markets/');
|
||||
});
|
||||
cy.get('tr')
|
||||
.eq(4)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.text', 'Matched data');
|
||||
cy.get('td').should('have.text', '❌');
|
||||
});
|
||||
cy.get('details')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.contains('Filter').click();
|
||||
cy.get('.language-json').should('exist');
|
||||
});
|
||||
cy.get('details')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.contains('JSON').click();
|
||||
cy.get('.language-json').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -77,7 +77,7 @@
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.72.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
||||
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.73.1/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type HashProps = {
|
||||
text: string;
|
||||
truncate?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -7,10 +8,16 @@ export type HashProps = {
|
||||
* are broken when they need to wrap. This will remove the need
|
||||
* for a lot of the overflow scrolling that currently exists.
|
||||
*/
|
||||
const Hash = ({ text }: HashProps) => {
|
||||
const Hash = ({ text, truncate = false }: HashProps) => {
|
||||
const h = truncate ? text.slice(0, 6) : text;
|
||||
|
||||
return (
|
||||
<code className="break-all font-mono" style={{ wordWrap: 'break-word' }}>
|
||||
{text}
|
||||
<code
|
||||
title={text}
|
||||
className="break-all font-mono"
|
||||
style={{ wordWrap: 'break-word' }}
|
||||
>
|
||||
{h}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ query ExplorerMarket($id: ID!) {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
state
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
@@ -22,6 +23,5 @@ query ExplorerMarket($id: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,6 +17,7 @@ export const ExplorerMarketDocument = gql`
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
state
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
@@ -36,7 +37,6 @@ export const ExplorerMarketDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -63,6 +63,9 @@ describe('Market link component', () => {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'dai',
|
||||
settlementAsset: {
|
||||
decimals: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ const MarketLink = ({
|
||||
}: MarketLinkProps) => {
|
||||
const { data, error, loading } = useExplorerMarketQuery({
|
||||
variables: { id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
let label = <span>{id}</span>;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import OracleLink, { getStatusString } from './oracle-link';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
describe('getStatusString', () => {
|
||||
it("returns 'Unknown' for undefined status", () => {
|
||||
expect(getStatusString(undefined)).toBe('Unknown');
|
||||
});
|
||||
|
||||
it('returns the correct string for a known status', () => {
|
||||
expect(getStatusString('STATUS_ACTIVE')).toBe('Active');
|
||||
expect(getStatusString('STATUS_DEACTIVATED')).toBe('Deactivated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OracleLink', () => {
|
||||
it('renders the truncated Oracle ID', () => {
|
||||
const id = '123456789';
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<OracleLink id={id} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
const idElement = screen.getByText(id.slice(0, 6));
|
||||
expect(idElement).toBeInTheDocument();
|
||||
expect(idElement).toHaveAttribute('title', id);
|
||||
});
|
||||
|
||||
it('renders the Oracle status', () => {
|
||||
const id = '123';
|
||||
const status = 'STATUS_ACTIVE';
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<OracleLink id={id} status={status} data-testid="link" />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('link')).toHaveAttribute('data-status', 'Active');
|
||||
});
|
||||
|
||||
it('renders the Oracle data indicator', () => {
|
||||
const id = '123';
|
||||
const hasSeenOracleReports = true;
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<OracleLink id={id} hasSeenOracleReports={hasSeenOracleReports} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('oracle-data-indicator')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -3,20 +3,93 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DataSourceSpecStatus,
|
||||
DataSourceSpecStatusMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
/**
|
||||
* Returns a human-readable string for the given status, or a meaningful
|
||||
* default if the status is unrecognised
|
||||
* @param status string status
|
||||
*/
|
||||
export function getStatusString(status: string | undefined): string {
|
||||
if (status && status in DataSourceSpecStatus) {
|
||||
return DataSourceSpecStatusMapping[status as DataSourceSpecStatus];
|
||||
}
|
||||
|
||||
return t('Unknown');
|
||||
}
|
||||
|
||||
export type OracleLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
// The Oracle ID
|
||||
id: string;
|
||||
// If available, the oracle status
|
||||
status?: string;
|
||||
// If the oracle has corresponding data in the OracleDataConnection
|
||||
hasSeenOracleReports?: boolean;
|
||||
};
|
||||
|
||||
const OracleLink = ({ id, ...props }: OracleLinkProps) => {
|
||||
/**
|
||||
* Given an Oracle ID, renders a data-dense link to the Oracle page. Data density is achieved by:
|
||||
* - Colour coding the link based on the Oracle's status
|
||||
* - Showing a small indicator if the Oracle has matched data
|
||||
* - Showing a tooltip with the Oracle's status and whether it has matched data
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const OracleLink = ({
|
||||
id,
|
||||
status,
|
||||
hasSeenOracleReports = false,
|
||||
...props
|
||||
}: OracleLinkProps) => {
|
||||
const bgColour =
|
||||
status === 'STATUS_ACTIVE'
|
||||
? 'bg-yellow-100 hover:bg-yellow-200 border-yellow-200 dark:bg-yellow-200 dark:border-yellow-200 dark:text-gray-900 dark:border-yellow-300'
|
||||
: 'bg-gray-100 hover:bg-gray-200 border-gray-200';
|
||||
const indicatorColour =
|
||||
status === 'STATUS_ACTIVE'
|
||||
? 'bg-yellow-300 hover:bg-yellow-500 dark:bg-yellow-500'
|
||||
: 'bg-gray-300 hover:bg-gray-500';
|
||||
|
||||
const description = (
|
||||
<div>
|
||||
<p>
|
||||
<strong>{`Status: `}</strong>
|
||||
{getStatusString(status)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{`Matched data: `}</strong>
|
||||
{hasSeenOracleReports ? (
|
||||
<VegaIcon name={VegaIconNames.TICK} />
|
||||
) : (
|
||||
<VegaIcon name={VegaIconNames.CROSS} />
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="underline font-mono"
|
||||
{...props}
|
||||
to={`/${Routes.ORACLES}/${id}`}
|
||||
>
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
<Tooltip description={description}>
|
||||
<Link
|
||||
className={`pl-2 pr-2 font-mono dark:text-black ${bgColour} rounded-sm border-solid border-2 relative`}
|
||||
{...props}
|
||||
to={`/${Routes.ORACLES}/${id}`}
|
||||
data-status={getStatusString(status)}
|
||||
>
|
||||
<Hash text={id} truncate={true} />
|
||||
{hasSeenOracleReports ? (
|
||||
<strong
|
||||
data-testid="oracle-data-indicator"
|
||||
className={`absolute top-0 right-0 w-1 h-full font-thin ${indicatorColour}`}
|
||||
title="Oracle has matched data"
|
||||
></strong>
|
||||
) : null}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ function renderExistingAmend(
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: '123',
|
||||
settlementAsset: {
|
||||
decimals: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -124,6 +127,9 @@ function renderExistingAmend(
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: '123',
|
||||
settlementAsset: {
|
||||
decimals: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -152,6 +158,9 @@ function renderExistingAmend(
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'dai',
|
||||
settlementAsset: {
|
||||
decimals: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,5 +22,9 @@ export const TimeAgo = ({ date, ...props }: TimeAgoProps) => {
|
||||
return <>{t('Date unknown')}</>;
|
||||
}
|
||||
|
||||
return <span {...props}>{t(`${distanceToNow} ago`)}</span>;
|
||||
return (
|
||||
<span {...props} title={date} className="underline decoration-dotted">
|
||||
{t(`${distanceToNow} ago`)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
query ExplorerSettlementAssetForMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerSettlementAssetForMarketQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerSettlementAssetForMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number } | null };
|
||||
|
||||
|
||||
export const ExplorerSettlementAssetForMarketDocument = gql`
|
||||
query ExplorerSettlementAssetForMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerSettlementAssetForMarketQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerSettlementAssetForMarketQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerSettlementAssetForMarketQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerSettlementAssetForMarketQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerSettlementAssetForMarketQuery(baseOptions: Apollo.QueryHookOptions<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>(ExplorerSettlementAssetForMarketDocument, options);
|
||||
}
|
||||
export function useExplorerSettlementAssetForMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>(ExplorerSettlementAssetForMarketDocument, options);
|
||||
}
|
||||
export type ExplorerSettlementAssetForMarketQueryHookResult = ReturnType<typeof useExplorerSettlementAssetForMarketQuery>;
|
||||
export type ExplorerSettlementAssetForMarketLazyQueryHookResult = ReturnType<typeof useExplorerSettlementAssetForMarketLazyQuery>;
|
||||
export type ExplorerSettlementAssetForMarketQueryResult = Apollo.QueryResult<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>;
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import type { LiquidityOrder } from '@vegaprotocol/types';
|
||||
import { PeggedReference } from '@vegaprotocol/types';
|
||||
import { LiquidityProvisionDetailsRow } from './liquidity-provision-details-row';
|
||||
import type { VegaSide } from './liquidity-provision-details-row';
|
||||
|
||||
describe('LiquidityProvisionDetails component', () => {
|
||||
function renderComponent(
|
||||
order: LiquidityOrder,
|
||||
side: VegaSide,
|
||||
normaliseProportionsTo: number,
|
||||
marketId: string
|
||||
) {
|
||||
return render(
|
||||
<MockedProvider>
|
||||
<table>
|
||||
<tbody data-testid="container">
|
||||
<LiquidityProvisionDetailsRow
|
||||
order={order}
|
||||
marketId={marketId}
|
||||
normaliseProportionsTo={normaliseProportionsTo}
|
||||
side={side}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
it('renders null for an order with no proportion', () => {
|
||||
const mockOrder = {
|
||||
offset: '1',
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
};
|
||||
|
||||
const res = renderComponent(
|
||||
mockOrder as LiquidityOrder,
|
||||
Side.SIDE_BUY,
|
||||
100,
|
||||
'123'
|
||||
);
|
||||
expect(res.getByTestId('container')).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders null for a null order', () => {
|
||||
const res = renderComponent(
|
||||
null as unknown as LiquidityOrder,
|
||||
Side.SIDE_BUY,
|
||||
100,
|
||||
'123'
|
||||
);
|
||||
expect(res.getByTestId('container')).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders a row when the order is as expected', () => {
|
||||
const mockOrder = {
|
||||
offset: '1',
|
||||
proportion: 20,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
};
|
||||
|
||||
const res = renderComponent(
|
||||
mockOrder as LiquidityOrder,
|
||||
Side.SIDE_BUY,
|
||||
100,
|
||||
'123'
|
||||
);
|
||||
// Row test ids and keys are based on the side, reference and proportion
|
||||
expect(res.getByTestId('SIDE_BUY-20-1')).toBeInTheDocument();
|
||||
expect(res.getByText('+1')).toBeInTheDocument();
|
||||
expect(res.getByText('Mid')).toBeInTheDocument();
|
||||
expect(res.getByText('20%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('normalises offsets when normaliseToProportion is not 100', () => {
|
||||
const mockOrder = {
|
||||
offset: '1',
|
||||
proportion: 20,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_BEST_BID,
|
||||
};
|
||||
|
||||
const res = renderComponent(
|
||||
mockOrder as LiquidityOrder,
|
||||
Side.SIDE_SELL,
|
||||
50,
|
||||
'123'
|
||||
);
|
||||
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
|
||||
expect(res.getByTestId('SIDE_SELL-40-1')).toBeInTheDocument();
|
||||
expect(res.getByText('-1')).toBeInTheDocument();
|
||||
expect(res.getByText('Best Bid')).toBeInTheDocument();
|
||||
expect(res.getByText('40%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles a missing offset gracefully (should not happen)', () => {
|
||||
const mockOrder = {
|
||||
proportion: 20,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_BEST_BID,
|
||||
};
|
||||
|
||||
const res = renderComponent(
|
||||
mockOrder as LiquidityOrder,
|
||||
Side.SIDE_SELL,
|
||||
50,
|
||||
'123'
|
||||
);
|
||||
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
|
||||
expect(res.getByTestId('SIDE_SELL-40-')).toBeInTheDocument();
|
||||
expect(res.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles a missing reference gracefully (should not happen)', () => {
|
||||
const mockOrder = {
|
||||
offset: '1',
|
||||
proportion: 20,
|
||||
};
|
||||
|
||||
const res = renderComponent(
|
||||
mockOrder as LiquidityOrder,
|
||||
Side.SIDE_SELL,
|
||||
50,
|
||||
'123'
|
||||
);
|
||||
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
|
||||
expect(res.getByTestId('SIDE_SELL-40-1')).toBeInTheDocument();
|
||||
expect(res.getByText('40%')).toBeInTheDocument();
|
||||
expect(res.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
import type { components } from '../../../../../../types/explorer';
|
||||
import { TableRow } from '../../../../table';
|
||||
import { LiquidityProvisionOffset } from './liquidity-provision-offset';
|
||||
|
||||
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
|
||||
export type VegaSide = components['schemas']['vegaSide'];
|
||||
|
||||
export type LiquidityProvisionOrder =
|
||||
components['schemas']['vegaLiquidityOrder'];
|
||||
|
||||
export const LiquidityReferenceLabel: Record<VegaPeggedReference, string> = {
|
||||
PEGGED_REFERENCE_BEST_ASK: t('Best Ask'),
|
||||
PEGGED_REFERENCE_BEST_BID: t('Best Bid'),
|
||||
PEGGED_REFERENCE_MID: t('Mid'),
|
||||
PEGGED_REFERENCE_UNSPECIFIED: '-',
|
||||
};
|
||||
|
||||
export type LiquidityProvisionDetailsRowProps = {
|
||||
order?: LiquidityProvisionOrder;
|
||||
marketId?: string;
|
||||
side: VegaSide;
|
||||
// If this is
|
||||
normaliseProportionsTo: number;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Note: offset is formatted by settlement asset on the market, assuming that is available
|
||||
* Note: Due to the mix of references (MID vs BEST_X), it's not possible to correctly order
|
||||
* the orders by their actual distance from a midpoint. This would require us knowing
|
||||
* the best bid (now or at placement) and the mid. Getting the data for *now* would be
|
||||
* misleading for LP submissions in the past. There is no API for getting <mid />
|
||||
* at the time of a transaction.
|
||||
*/
|
||||
export function LiquidityProvisionDetailsRow({
|
||||
normaliseProportionsTo,
|
||||
order,
|
||||
side,
|
||||
marketId,
|
||||
}: LiquidityProvisionDetailsRowProps) {
|
||||
if (!order || !order.proportion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const proportion =
|
||||
normaliseProportionsTo === 100
|
||||
? order.proportion
|
||||
: Math.round((order.proportion / normaliseProportionsTo) * 100);
|
||||
|
||||
const key = `${side}-${proportion}-${order.offset ? order.offset : ''}`;
|
||||
|
||||
return (
|
||||
<TableRow modifier="bordered" key={key} data-testid={key}>
|
||||
<td className="text-right px-2">
|
||||
{order.offset && marketId ? (
|
||||
<LiquidityProvisionOffset
|
||||
offset={order.offset}
|
||||
side={side}
|
||||
marketId={marketId}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{order.reference ? LiquidityReferenceLabel[order.reference] : '-'}
|
||||
</td>
|
||||
<td className="text-center">{proportion}%</td>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { LiquidityProvisionMid } from './liquidity-provision-mid';
|
||||
|
||||
describe('LiquidityProvisionMid component', () => {
|
||||
function renderComponent() {
|
||||
return render(
|
||||
<table>
|
||||
<tbody data-testid="container">
|
||||
<LiquidityProvisionMid />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
it('renders a basic row that spans the whole table', () => {
|
||||
const res = renderComponent();
|
||||
const display = res.getByTestId('mid-display');
|
||||
expect(res.getByTestId('mid')).toBeInTheDocument();
|
||||
expect(display).toBeInTheDocument();
|
||||
expect(display).toHaveAttribute('colspan', '3');
|
||||
});
|
||||
});
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { TableRow } from '../../../../table';
|
||||
|
||||
/**
|
||||
* In a LiquidityProvision table, this row is the midpoint. Above our LP orders on the
|
||||
* buy side, below are LP orders on the sell side. This component simply divides them.
|
||||
*
|
||||
* There is no API that can give us the mid price when the order was created, and even
|
||||
* if there was it isn't clear that would be appropriate for this centre row. So instead
|
||||
* it's a simple divider.
|
||||
*/
|
||||
export function LiquidityProvisionMid() {
|
||||
return (
|
||||
<TableRow modifier="bordered" data-testid="mid">
|
||||
<td data-testid="mid-display" colSpan={3} className="bg-white"></td>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ExplorerSettlementAssetForMarketDocument } from '../__generated__/Explorer-settlement-asset';
|
||||
import type { ExplorerSettlementAssetForMarketQuery } from '../__generated__/Explorer-settlement-asset';
|
||||
import type { VegaSide } from './liquidity-provision-details-row';
|
||||
import {
|
||||
getFormattedOffset,
|
||||
LiquidityProvisionOffset,
|
||||
} from './liquidity-provision-offset';
|
||||
const decimalsMock: ExplorerSettlementAssetForMarketQuery = {
|
||||
market: {
|
||||
id: '123',
|
||||
__typename: 'Market',
|
||||
decimalPlaces: 5,
|
||||
},
|
||||
};
|
||||
|
||||
describe('LiquidityProvisionOffset component', () => {
|
||||
function renderComponent(
|
||||
offset: string,
|
||||
side: VegaSide,
|
||||
marketId: string,
|
||||
mocks: MockedResponse[]
|
||||
) {
|
||||
return render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<LiquidityProvisionOffset
|
||||
offset={offset}
|
||||
side={side}
|
||||
marketId={marketId}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
it('renders a simple row before market data comes in', () => {
|
||||
const res = renderComponent('1', 'SIDE_BUY', '123', []);
|
||||
expect(res.getByText('+1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('replaces unformatted with formatted if the market data comes in', () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerSettlementAssetForMarketDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
result: {
|
||||
data: decimalsMock,
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = renderComponent('1', 'SIDE_BUY', '123', [mock]);
|
||||
expect(res.getByText('+1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('getFormattedOffset returns the unformatted offset if there is not enough data', () => {
|
||||
const res = getFormattedOffset('1', {});
|
||||
expect(res).toEqual('1');
|
||||
});
|
||||
|
||||
it('getFormattedOffset decimal formats a number if it comes in with market data', () => {
|
||||
const res = getFormattedOffset('1', decimalsMock);
|
||||
expect(res).toEqual('0.00001');
|
||||
});
|
||||
});
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
import { useExplorerSettlementAssetForMarketQuery } from '../__generated__/Explorer-settlement-asset';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import type { ExplorerSettlementAssetForMarketQuery } from '../__generated__/Explorer-settlement-asset';
|
||||
import type { VegaSide } from './liquidity-provision-details-row';
|
||||
|
||||
export type LiquidityProvisionOffsetProps = {
|
||||
side: VegaSide;
|
||||
offset: string;
|
||||
marketId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Correctly formats an LP's offset according to the market settlement decimal places.
|
||||
* Initially this will appear unformatted, then when the query loads in the proper formatted
|
||||
* value will be displayed
|
||||
*
|
||||
* @see getFormattedOffset
|
||||
*/
|
||||
export function LiquidityProvisionOffset({
|
||||
side,
|
||||
offset,
|
||||
marketId,
|
||||
}: LiquidityProvisionOffsetProps) {
|
||||
const { data } = useExplorerSettlementAssetForMarketQuery({
|
||||
variables: {
|
||||
id: marketId,
|
||||
},
|
||||
});
|
||||
|
||||
// getFormattedOffset handles missing results/loading states
|
||||
const formattedOffset = getFormattedOffset(offset, data);
|
||||
|
||||
const label = side === 'SIDE_BUY' ? '+' : '-';
|
||||
const className = side === 'SIDE_BUY' ? 'text-vega-green' : 'text-vega-pink';
|
||||
return <span className={className}>{`${label}${formattedOffset}`}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the work of formatting the number now we have the market decimal places.
|
||||
* If no market data is assigned (i.e. during loading, or if the market doesn't exist)
|
||||
* this function will return the unformatted number
|
||||
*
|
||||
* @see LiquidityProvisionOffset
|
||||
* @param data the result of a ExplorerSettlementAssetForMarketQuery
|
||||
* @param offset the unformatted offset
|
||||
* @returns string the offset of this lp order formatted with the settlement decimal places
|
||||
*/
|
||||
export function getFormattedOffset(
|
||||
offset: string,
|
||||
data?: ExplorerSettlementAssetForMarketQuery
|
||||
) {
|
||||
const decimals = data?.market?.decimalPlaces;
|
||||
|
||||
if (!decimals) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
return addDecimalsFormatNumber(offset, decimals);
|
||||
}
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { LiquidityOrder } from '@vegaprotocol/types';
|
||||
import { PeggedReference } from '@vegaprotocol/types';
|
||||
import type { LiquiditySubmission } from '../tx-liquidity-submission';
|
||||
import {
|
||||
LiquidityProvisionDetails,
|
||||
sumProportions,
|
||||
} from './liquidity-provision-details';
|
||||
|
||||
function mockProportion(proportion: number): LiquidityOrder {
|
||||
return {
|
||||
proportion,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
offset: '1',
|
||||
};
|
||||
}
|
||||
describe('sumProportions function', () => {
|
||||
it('returns 0 if the side is undefined', () => {
|
||||
const side: LiquidityOrder[] = undefined as unknown as LiquidityOrder[];
|
||||
const res = sumProportions(side);
|
||||
|
||||
expect(res).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns 0 if the side is empty', () => {
|
||||
const side: LiquidityOrder[] = [];
|
||||
const res = sumProportions(side);
|
||||
|
||||
expect(res).toEqual(0);
|
||||
});
|
||||
|
||||
it('sums 1 item correctly (under 100%)', () => {
|
||||
const side: LiquidityOrder[] = [mockProportion(10)];
|
||||
const res = sumProportions(side);
|
||||
|
||||
expect(res).toEqual(10);
|
||||
});
|
||||
|
||||
it('sums 2 item correctly (exactly 100%)', () => {
|
||||
const side: LiquidityOrder[] = [mockProportion(50), mockProportion(50)];
|
||||
const res = sumProportions(side);
|
||||
|
||||
expect(res).toEqual(100);
|
||||
});
|
||||
|
||||
it('sums 3 item correctly to over 100%', () => {
|
||||
const side: LiquidityOrder[] = [
|
||||
mockProportion(20),
|
||||
mockProportion(40),
|
||||
mockProportion(50),
|
||||
];
|
||||
const res = sumProportions(side);
|
||||
|
||||
expect(res).toEqual(110);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LiquidityProvisionDetails component', () => {
|
||||
function renderComponent(provision: LiquiditySubmission) {
|
||||
return render(
|
||||
<MockedProvider>
|
||||
<LiquidityProvisionDetails provision={provision} />
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
it('handles an LP with no buys or sells by returning empty (should never happen)', () => {
|
||||
const mock: LiquiditySubmission = {};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('handles an LP with no sells by just rendering buys', () => {
|
||||
const mock: LiquiditySubmission = {
|
||||
marketId: '123',
|
||||
buys: [
|
||||
{
|
||||
offset: '1',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
{
|
||||
offset: '2',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.getByText('Price offset')).toBeInTheDocument();
|
||||
expect(res.getByText('Price reference')).toBeInTheDocument();
|
||||
expect(res.getByText('Proportion')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_BUY-50-1')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_BUY-50-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles an LP with no buys by just rendering sells', () => {
|
||||
const mock: LiquiditySubmission = {
|
||||
marketId: '123',
|
||||
sells: [
|
||||
{
|
||||
offset: '1',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
{
|
||||
offset: '2',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.getByText('Price offset')).toBeInTheDocument();
|
||||
expect(res.getByText('Price reference')).toBeInTheDocument();
|
||||
expect(res.getByText('Proportion')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_SELL-50-1')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_SELL-50-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles an LP with sells by just rendering buys', () => {
|
||||
const mock: LiquiditySubmission = {
|
||||
marketId: '123',
|
||||
buys: [
|
||||
{
|
||||
offset: '1',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
{
|
||||
offset: '2',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.getByText('Price offset')).toBeInTheDocument();
|
||||
expect(res.getByText('Price reference')).toBeInTheDocument();
|
||||
expect(res.getByText('Proportion')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_BUY-50-1')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_BUY-50-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles an LP with both sides', () => {
|
||||
const mock: LiquiditySubmission = {
|
||||
marketId: '123',
|
||||
buys: [
|
||||
{
|
||||
offset: '1',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
{
|
||||
offset: '2',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
],
|
||||
sells: [
|
||||
{
|
||||
offset: '4',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
{
|
||||
offset: '2',
|
||||
proportion: 50,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.getByText('Price offset')).toBeInTheDocument();
|
||||
expect(res.getByText('Price reference')).toBeInTheDocument();
|
||||
expect(res.getByText('Proportion')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_BUY-50-1')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_BUY-50-2')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_SELL-50-4')).toBeInTheDocument();
|
||||
expect(res.getByTestId('SIDE_SELL-50-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('normalises proportions when they do not total 100%', () => {
|
||||
const mock: LiquiditySubmission = {
|
||||
marketId: '123',
|
||||
buys: [
|
||||
{
|
||||
offset: '1',
|
||||
proportion: 25,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
{
|
||||
offset: '2',
|
||||
proportion: 30,
|
||||
reference: PeggedReference.PEGGED_REFERENCE_MID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.getByText('45%')).toBeInTheDocument();
|
||||
expect(res.getByText('55%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import type { LiquiditySubmission } from '../tx-liquidity-submission';
|
||||
import { TableRow } from '../../../table';
|
||||
import { LiquidityProvisionMid } from './components/liquidity-provision-mid';
|
||||
import { LiquidityProvisionDetailsRow } from './components/liquidity-provision-details-row';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
|
||||
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
|
||||
|
||||
export type LiquidityProvisionOrder =
|
||||
components['schemas']['vegaLiquidityOrder'];
|
||||
|
||||
export const LiquidityReferenceLabel: Record<VegaPeggedReference, string> = {
|
||||
PEGGED_REFERENCE_BEST_ASK: t('Best Ask'),
|
||||
PEGGED_REFERENCE_BEST_BID: t('Best Bid'),
|
||||
PEGGED_REFERENCE_MID: t('Mid'),
|
||||
PEGGED_REFERENCE_UNSPECIFIED: '-',
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a side of a liquidity provision order, returns the total
|
||||
* It should be 100%, but it isn't always and if it isn't the proportion
|
||||
* reported for each order should be scaled
|
||||
*
|
||||
* @returns number
|
||||
*/
|
||||
export function sumProportions(
|
||||
side: LiquiditySubmission['buys'] | LiquiditySubmission['sells']
|
||||
): number {
|
||||
if (!side || side.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return side.reduce((total, o) => total + (o.proportion || 0), 0);
|
||||
}
|
||||
|
||||
export type LiquidityProvisionDetailsProps = {
|
||||
provision: LiquiditySubmission;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a table displaying all buys and sells in this LP. It is valid for there
|
||||
* to be no buys or sells.
|
||||
*
|
||||
* It might seem logical to turn proportions in to values based on the total commitment
|
||||
* but based on the current API structure it is awkward, and given that non-LP orders
|
||||
* will change the amount that is actually deployed vs assigned to a level, we decided
|
||||
* not to bother going down that route.
|
||||
*/
|
||||
export function LiquidityProvisionDetails({
|
||||
provision,
|
||||
}: LiquidityProvisionDetailsProps) {
|
||||
if (!provision.buys?.length && !provision.sells?.length) {
|
||||
return null;
|
||||
}
|
||||
// We need to do some additional calcs if these aren't both 100
|
||||
const buyTotal = sumProportions(provision.buys);
|
||||
const sellTotal = sumProportions(provision.sells);
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<TableRow modifier="bordered">
|
||||
<th className="px-2 pb-1">{t('Price offset')}</th>
|
||||
<th className="px-2 pb-1">{t('Price reference')}</th>
|
||||
<th className="px-2 pb-1">{t('Proportion')}</th>
|
||||
</TableRow>
|
||||
</thead>
|
||||
<tbody>
|
||||
{provision.buys?.map((b, i) => (
|
||||
<LiquidityProvisionDetailsRow
|
||||
order={b}
|
||||
marketId={provision.marketId}
|
||||
side={Side.SIDE_BUY}
|
||||
key={`SIDE_BUY-${i}`}
|
||||
normaliseProportionsTo={buyTotal}
|
||||
/>
|
||||
))}
|
||||
<LiquidityProvisionMid />
|
||||
{provision.sells?.map((s, i) => (
|
||||
<LiquidityProvisionDetailsRow
|
||||
order={s}
|
||||
marketId={provision.marketId}
|
||||
side={Side.SIDE_SELL}
|
||||
key={`SIDE_SELL-${i}`}
|
||||
normaliseProportionsTo={sellTotal}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { TableCell, TableRow } from '../../../table';
|
||||
import type { VegaPeggedReference } from '../liquidity-provision/liquidity-provision-details';
|
||||
import { Side, PeggedReferenceMapping } from '@vegaprotocol/types';
|
||||
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
|
||||
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
|
||||
|
||||
export const LiquidityReferenceLabel: Record<VegaPeggedReference, string> = {
|
||||
PEGGED_REFERENCE_BEST_ASK: t('Best Ask'),
|
||||
PEGGED_REFERENCE_BEST_BID: t('Best Bid'),
|
||||
PEGGED_REFERENCE_MID: t('Mid'),
|
||||
PEGGED_REFERENCE_UNSPECIFIED: '-',
|
||||
};
|
||||
|
||||
export interface TxDetailsOrderProps {
|
||||
offset: string;
|
||||
reference: VegaPeggedReference;
|
||||
|
||||
@@ -33,6 +33,14 @@ const AccountType: Record<AccountTypes, string> = {
|
||||
ACCOUNT_TYPE_HOLDING: 'Holding',
|
||||
ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution',
|
||||
ACCOUNT_TYPE_LP_LIQUIDITY_FEES: 'LP Liquidity Fees',
|
||||
ACCOUNT_TYPE_NETWORK_TREASURY: 'Network Treasury',
|
||||
ACCOUNT_TYPE_VESTING_REWARDS: 'Vesting Rewards',
|
||||
ACCOUNT_TYPE_VESTED_REWARDS: 'Vested Rewards',
|
||||
ACCOUNT_TYPE_REWARD_AVERAGE_POSITION: 'Reward Average Position',
|
||||
ACCOUNT_TYPE_REWARD_RELATIVE_RETURN: 'Reward Relative Return',
|
||||
ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY: 'Reward Return Volatility',
|
||||
ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING: 'Reward Validator Ranking',
|
||||
ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD: 'Pending Fee Referral Reward',
|
||||
};
|
||||
|
||||
interface TransferParticipantsProps {
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableWithTbody } from '../../table';
|
||||
import { defaultAbiCoder, base64 } from 'ethers/lib/utils';
|
||||
import { ChainEvent } from './chain-events';
|
||||
import { BigNumber } from 'ethers';
|
||||
|
||||
import type { AbiType } from '../../../lib/encoders/abis/abi-types';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { ChainEvent } from './chain-events';
|
||||
|
||||
interface AbiOutput {
|
||||
type: AbiType;
|
||||
internalType: AbiType;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the b64/ABIcoded result from an eth cal
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export function decodeEthCallResult(
|
||||
data: BlockExplorerTransactionResult
|
||||
): string {
|
||||
const ethResult = data.command.chainEvent?.contractCall.result;
|
||||
|
||||
try {
|
||||
// Decode the result string: base64 => uint8array
|
||||
const data = base64.decode(ethResult);
|
||||
|
||||
// Parse the escaped ABI in to an object
|
||||
const abi = JSON.parse(
|
||||
'[{"inputs":[],"name":"latestAnswer","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"}]'
|
||||
);
|
||||
// Pull the expected types out of the Oracles ABI
|
||||
const types: AbiType[] = abi[0].outputs.map((o: AbiOutput) => o.type);
|
||||
|
||||
const rawResult = defaultAbiCoder.decode(types, data);
|
||||
|
||||
// Finally, convert the resulting BigNumber in to a string
|
||||
const res = BigNumber.from(rawResult[0]).toString();
|
||||
return res;
|
||||
} catch (e) {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
interface TxDetailsChainEventProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { TxDetailsOrderAmend } from './tx-order-amend';
|
||||
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
|
||||
import { TxDetailsDelegate } from './tx-delegation';
|
||||
import { TxDetailsUndelegate } from './tx-undelegation';
|
||||
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
||||
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
|
||||
import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
|
||||
import { TxDetailsDataSubmission } from './tx-data-submission';
|
||||
@@ -28,6 +27,7 @@ import { TxDetailsStateVariable } from './tx-state-variable-proposal';
|
||||
import { TxProposal } from './tx-proposal';
|
||||
import { TxDetailsTransfer } from './tx-transfer';
|
||||
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
|
||||
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -107,7 +107,7 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsWithdrawSubmission;
|
||||
case 'Liquidity Provision Order':
|
||||
return TxDetailsLiquiditySubmission;
|
||||
case 'Amend LiquidityProvision Order':
|
||||
case 'Amend Liquidity Provision Order':
|
||||
return TxDetailsLiquidityAmendment;
|
||||
case 'Cancel LiquidityProvision Order':
|
||||
return TxDetailsLiquidityCancellation;
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
|
||||
import PriceInMarket from '../../price-in-market/price-in-market';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
@@ -40,40 +39,32 @@ export const TxDetailsLiquidityAmendment = ({
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
blockData={blockData}
|
||||
/>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Market')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink id={marketId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{amendment.commitmentAmount ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Market')}</TableCell>
|
||||
<TableCell>{t('Commitment amount')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink id={marketId} />
|
||||
<PriceInMarket
|
||||
price={amendment.commitmentAmount}
|
||||
marketId={marketId}
|
||||
decimalSource="SETTLEMENT_ASSET"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{amendment.commitmentAmount ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Commitment amount')}</TableCell>
|
||||
<TableCell>
|
||||
<PriceInMarket
|
||||
price={amendment.commitmentAmount}
|
||||
marketId={marketId}
|
||||
decimalSource="SETTLEMENT_ASSET"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{amendment.fee ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Fee')}</TableCell>
|
||||
<TableCell>{fee}%</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
|
||||
<LiquidityProvisionDetails provision={amendment} />
|
||||
</>
|
||||
) : null}
|
||||
{amendment.fee ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Fee')}</TableCell>
|
||||
<TableCell>{fee}%</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
describe('TxDetailsLiquiditySubmission', () => {
|
||||
const mockTxData = {
|
||||
hash: 'test',
|
||||
command: {
|
||||
liquidityProvisionSubmission: {
|
||||
marketId: 'BTC-USD',
|
||||
commitmentAmount: 100,
|
||||
fee: '0.01',
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockPubKey = '123';
|
||||
const mockBlockData = {
|
||||
result: {
|
||||
block: {
|
||||
header: {
|
||||
height: '123',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should render the component with correct data', () => {
|
||||
const { getByText } = render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<TxDetailsLiquiditySubmission
|
||||
txData={mockTxData as BlockExplorerTransactionResult}
|
||||
pubKey={mockPubKey}
|
||||
blockData={mockBlockData as TendermintBlocksResponse}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(getByText('Market')).toBeInTheDocument();
|
||||
expect(getByText('BTC-USD')).toBeInTheDocument();
|
||||
expect(getByText('Commitment amount')).toBeInTheDocument();
|
||||
expect(getByText('100')).toBeInTheDocument();
|
||||
expect(getByText('Fee')).toBeInTheDocument();
|
||||
expect(getByText('1%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display awaiting message when tx data is undefined', () => {
|
||||
const { getByText } = render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<TxDetailsLiquiditySubmission
|
||||
txData={undefined}
|
||||
pubKey={mockPubKey}
|
||||
blockData={mockBlockData as TendermintBlocksResponse}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText('Awaiting Block Explorer transaction details')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display awaiting message when liquidityProvisionSubmission is undefined', () => {
|
||||
const { getByText } = render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<TxDetailsLiquiditySubmission
|
||||
txData={{ command: {} } as BlockExplorerTransactionResult}
|
||||
pubKey={mockPubKey}
|
||||
blockData={mockBlockData as TendermintBlocksResponse}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText('Awaiting Block Explorer transaction details')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,31 +1,30 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import { MarketLink } from '../../links/';
|
||||
import { MarketLink } from '../../links';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
|
||||
import PriceInMarket from '../../price-in-market/price-in-market';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export type LiquiditySubmission =
|
||||
components['schemas']['v1LiquidityProvisionSubmission'];
|
||||
|
||||
interface TxDetailsLiquiditySubmissionProps {
|
||||
interface TxDetailsLiquidityAmendmentProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Someone cancelled an order
|
||||
* An existing liquidity order is being created.
|
||||
*/
|
||||
export const TxDetailsLiquiditySubmission = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsLiquiditySubmissionProps) => {
|
||||
}: TxDetailsLiquidityAmendmentProps) => {
|
||||
if (!txData || !txData.command.liquidityProvisionSubmission) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
@@ -39,40 +38,38 @@ export const TxDetailsLiquiditySubmission = ({
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
blockData={blockData}
|
||||
/>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Market')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink id={marketId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{submission.commitmentAmount ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Market')}</TableCell>
|
||||
<TableCell>{t('Commitment amount')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink id={marketId} />
|
||||
<PriceInMarket
|
||||
price={submission.commitmentAmount}
|
||||
marketId={marketId}
|
||||
decimalSource="SETTLEMENT_ASSET"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{submission.commitmentAmount ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Commitment amount')}</TableCell>
|
||||
<TableCell>
|
||||
<PriceInMarket
|
||||
price={submission.commitmentAmount}
|
||||
marketId={marketId}
|
||||
decimalSource="SETTLEMENT_ASSET"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{submission.fee ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Fee')}</TableCell>
|
||||
<TableCell>{fee}%</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
|
||||
<LiquidityProvisionDetails provision={submission} />
|
||||
</>
|
||||
) : null}
|
||||
{submission.fee ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Fee')}</TableCell>
|
||||
<TableCell>{fee}%</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{submission.reference ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Reference')}</TableCell>
|
||||
<TableCell>{submission.reference}</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,11 +18,13 @@ import { FilterLabel } from './tx-filter-label';
|
||||
export type FilterOption =
|
||||
| 'Amend LiquidityProvision Order'
|
||||
| 'Amend Order'
|
||||
| 'Apply Referral Code'
|
||||
| 'Batch Market Instructions'
|
||||
| 'Cancel LiquidityProvision Order'
|
||||
| 'Cancel Order'
|
||||
| 'Cancel Transfer Funds'
|
||||
| 'Chain Event'
|
||||
| 'Create Referral Set'
|
||||
| 'Delegate'
|
||||
| 'Ethereum Key Rotate Submission'
|
||||
| 'Issue Signatures'
|
||||
@@ -40,6 +42,7 @@ export type FilterOption =
|
||||
| 'Submit Order'
|
||||
| 'Transfer Funds'
|
||||
| 'Undelegate'
|
||||
| 'Update Referral Set'
|
||||
| 'Validator Heartbeat'
|
||||
| 'Vote on Proposal'
|
||||
| 'Withdraw';
|
||||
|
||||
@@ -104,10 +104,30 @@ export function getLabelForProposal(
|
||||
}
|
||||
} else if (proposal.terms?.updateMarket) {
|
||||
return t('Proposal: Update market');
|
||||
} else if (proposal.terms?.updateSpotMarket) {
|
||||
return t('Proposal: Update spot');
|
||||
} else if (proposal.terms?.updateMarketState) {
|
||||
const type = proposal.terms.updateMarketState.changes?.updateType;
|
||||
if (type === 'MARKET_STATE_UPDATE_TYPE_TERMINATE') {
|
||||
return t('Proposal: Market terminate');
|
||||
} else if (type === 'MARKET_STATE_UPDATE_TYPE_SUSPEND') {
|
||||
return t('Proposal: Market suspend');
|
||||
} else if (type === 'MARKET_STATE_UPDATE_TYPE_RESUME') {
|
||||
return t('Proposal: Market resume');
|
||||
}
|
||||
return t('Proposal: Market state');
|
||||
} else if (proposal.terms?.updateNetworkParameter) {
|
||||
return t('Proposal: Network parameter');
|
||||
} else if (proposal.terms?.updateReferralProgram) {
|
||||
return t('Proposal: Referral program');
|
||||
} else if (proposal.terms?.updateVolumeDiscountProgram) {
|
||||
return t('Proposal: Discount program');
|
||||
} else if (proposal.terms?.newFreeform) {
|
||||
return t('Proposal: Freeform');
|
||||
} else if (proposal.terms?.newTransfer) {
|
||||
return t('Proposal: Transfer');
|
||||
} else if (proposal.terms?.cancelTransfer) {
|
||||
return t('Proposal: Transfer cancel');
|
||||
} else {
|
||||
return t('Proposal');
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('Txs infinite list item', () => {
|
||||
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders data correctly', () => {
|
||||
it('renders data even with missing time', () => {
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
@@ -105,5 +105,33 @@ describe('Txs infinite list item', () => {
|
||||
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
||||
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
||||
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('tx-time')).toHaveTextContent('-');
|
||||
});
|
||||
|
||||
it('renders data correctly', () => {
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<table>
|
||||
<tbody>
|
||||
<TxsInfiniteListItem
|
||||
type="testType"
|
||||
submitter="testPubKey"
|
||||
hash="testTxHash"
|
||||
block="1"
|
||||
code={0}
|
||||
command={{}}
|
||||
createdAt="1970-11-01T18:07:15Z"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
|
||||
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
||||
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
||||
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('tx-time').textContent).toMatch(/years ago/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PartyLink } from '../links';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import type { Screen } from '@vegaprotocol/react-helpers';
|
||||
import { useMemo } from 'react';
|
||||
import { TimeAgo } from '../time-ago';
|
||||
|
||||
const DEFAULT_TRUNCATE_LENGTH = 7;
|
||||
|
||||
@@ -32,6 +33,7 @@ export const TxsInfiniteListItem = ({
|
||||
type,
|
||||
block,
|
||||
command,
|
||||
createdAt,
|
||||
}: Partial<BlockExplorerTransactionResult>) => {
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const idTruncateLength = useMemo(
|
||||
@@ -85,6 +87,11 @@ export const TxsInfiniteListItem = ({
|
||||
endChars={5}
|
||||
/>
|
||||
</td>
|
||||
{['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize) && (
|
||||
<td className="text-sm items-center font-mono" data-testid="tx-time">
|
||||
{createdAt ? <TimeAgo date={createdAt} /> : '-'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||
import EmptyList from '../empty-list/empty-list';
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface TxsInfiniteListProps {
|
||||
hasMoreTxs: boolean;
|
||||
@@ -19,7 +20,16 @@ interface ItemProps {
|
||||
}
|
||||
|
||||
const Item = ({ tx }: ItemProps) => {
|
||||
const { hash, submitter, type, command, block, code, index: blockIndex } = tx;
|
||||
const {
|
||||
hash,
|
||||
submitter,
|
||||
type,
|
||||
command,
|
||||
block,
|
||||
code,
|
||||
createdAt,
|
||||
index: blockIndex,
|
||||
} = tx;
|
||||
return (
|
||||
<TxsInfiniteListItem
|
||||
type={type}
|
||||
@@ -29,6 +39,7 @@ const Item = ({ tx }: ItemProps) => {
|
||||
hash={hash}
|
||||
block={block}
|
||||
index={blockIndex}
|
||||
createdAt={createdAt}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -39,6 +50,7 @@ export const TxsInfiniteList = ({
|
||||
className,
|
||||
hasFilters = false,
|
||||
}: TxsInfiniteListProps) => {
|
||||
const { screenSize } = useScreenDimensions();
|
||||
if (!txs || txs.length === 0) {
|
||||
if (!areTxsLoading) {
|
||||
return (
|
||||
@@ -66,6 +78,9 @@ export const TxsInfiniteList = ({
|
||||
<th>{t('Type')}</th>
|
||||
<th className="text-left">{t('From')}</th>
|
||||
<th>{t('Block')}</th>
|
||||
{['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize) && (
|
||||
<th>{t('Time')}</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const getTxsDataUrl = (params: IGetTxsDataUrl) => {
|
||||
url.searchParams.append('first', count);
|
||||
url.searchParams.append('after', params.after);
|
||||
} else {
|
||||
url.searchParams.append('first', count);
|
||||
url.searchParams.append('last', count);
|
||||
}
|
||||
|
||||
// Hacky fix for param as array
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('getTxsDataUrl', () => {
|
||||
count: 10,
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?first=10';
|
||||
const expectedUrl = 'https://example.com/transactions?last=10';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe('getTxsDataUrl', () => {
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl =
|
||||
'https://example.com/transactions?first=10&filters[cmd.type]=Made%20Up%20Transaction&filters[tx.submitter]=1234';
|
||||
'https://example.com/transactions?last=10&filters[cmd.type]=Made%20Up%20Transaction&filters[tx.submitter]=1234';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
@@ -36,30 +36,50 @@ const PERCENTAGE_PARAMS = [
|
||||
'governance.proposal.updateNetParam.requiredMajority',
|
||||
'governance.proposal.updateNetParam.requiredParticipation',
|
||||
'governance.proposal.updateMarket.minProposerEquityLikeShare',
|
||||
'governance.proposal.VolumeDiscountProgram.requiredMajority',
|
||||
'governance.proposal.VolumeDiscountProgram.requiredParticipation',
|
||||
'governance.proposal.referralProgram.requiredMajority',
|
||||
'governance.proposal.transfer.requiredMajority',
|
||||
'governance.proposal.updateAsset.requiredMajority',
|
||||
'governance.proposal.updateAsset.requiredParticipation',
|
||||
'governance.proposal.transfer.requiredParticipation',
|
||||
'governance.proposal.referralProgram.requiredParticipation',
|
||||
'network.validators.ersatz.rewardFactor',
|
||||
'network.validators.ersatz.multipleOfTendermintValidators',
|
||||
'validators.vote.required',
|
||||
];
|
||||
'referralProgram.maxReferralRewardFactor',
|
||||
'referralProgram.maxReferralDiscountFactor',
|
||||
'referralProgram.maxReferralRewardProportion',
|
||||
].map((p) => p.toLowerCase());
|
||||
|
||||
const BIG_NUMBER_PARAMS = [
|
||||
'spam.protection.delegation.min.tokens',
|
||||
'validators.delegation.minAmount',
|
||||
'governance.proposal.transfer.maxAmount',
|
||||
'reward.staking.delegation.minimumValidatorStake',
|
||||
'reward.staking.delegation.maxPayoutPerParticipant',
|
||||
'reward.staking.delegation.maxPayoutPerEpoch',
|
||||
'spam.protection.voting.min.tokens',
|
||||
'spam.protection.proposal.min.tokens',
|
||||
'governance.proposal.transfer.minVoterBalance',
|
||||
'governance.proposal.freeform.minProposerBalance',
|
||||
'governance.proposal.updateNetParam.minVoterBalance',
|
||||
'governance.proposal.updateMarket.minVoterBalance',
|
||||
'governance.proposal.asset.minVoterBalance',
|
||||
'governance.proposal.updateNetParam.minProposerBalance',
|
||||
'governance.proposal.freeform.minVoterBalance',
|
||||
'spam.protection.proposal.min.tokens',
|
||||
'governance.proposal.updateMarket.minProposerBalance',
|
||||
'governance.proposal.asset.minProposerBalance',
|
||||
'governance.proposal.transfer.minProposerBalance',
|
||||
'governance.proposal.market.minProposerBalance',
|
||||
'governance.proposal.market.minVoterBalance',
|
||||
'governance.proposal.updateAsset.minProposerBalance',
|
||||
'governance.proposal.updateAsset.minVoterBalance',
|
||||
];
|
||||
'governance.proposal.referralProgram.minProposerBalance',
|
||||
'governance.proposal.referralProgram.minVoterBalance',
|
||||
'governance.proposal.VolumeDiscountProgram.minProposerBalance',
|
||||
'governance.proposal.VolumeDiscountProgram.minVoterBalance',
|
||||
].map((p) => p.toLowerCase());
|
||||
|
||||
export const renderGroupedParams = (
|
||||
group: GroupedParams,
|
||||
@@ -131,12 +151,12 @@ export const NetworkParameterRow = ({
|
||||
<div className="pb-2">
|
||||
<SyntaxHighlighter data={JSON.parse(value)} />
|
||||
</div>
|
||||
) : BIG_NUMBER_PARAMS.includes(key.toLowerCase()) ? (
|
||||
addDecimalsFormatNumber(Number(value), 18)
|
||||
) : PERCENTAGE_PARAMS.includes(key.toLowerCase()) ? (
|
||||
`${formatNumber(Number(value) * 100, 0)}%`
|
||||
) : isNaN(Number(value)) ? (
|
||||
value
|
||||
) : BIG_NUMBER_PARAMS.includes(key) ? (
|
||||
addDecimalsFormatNumber(Number(value), 18)
|
||||
) : PERCENTAGE_PARAMS.includes(key) ? (
|
||||
`${formatNumber(Number(value) * 100, 0)}%`
|
||||
) : (
|
||||
formatNumber(Number(value), 4)
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fragment ExplorerOracleDataConnection on OracleSpec {
|
||||
dataConnection {
|
||||
dataConnection(pagination: { first: 30 }) {
|
||||
edges {
|
||||
node {
|
||||
externalData {
|
||||
@@ -45,6 +45,16 @@ fragment ExplorerOracleDataSource on OracleSpec {
|
||||
operator
|
||||
}
|
||||
}
|
||||
... on DataSourceSpecConfigurationTimeTrigger {
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
triggers {
|
||||
initial
|
||||
every
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
@@ -103,6 +113,23 @@ fragment ExplorerOracleDataSource on OracleSpec {
|
||||
}
|
||||
}
|
||||
}
|
||||
... on EthCallSpec {
|
||||
abi
|
||||
address
|
||||
requiredConfirmations
|
||||
method
|
||||
filters {
|
||||
key {
|
||||
type
|
||||
name
|
||||
numberDecimalPlaces
|
||||
}
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +139,7 @@ fragment ExplorerOracleDataSource on OracleSpec {
|
||||
}
|
||||
|
||||
query ExplorerOracleSpecs {
|
||||
oracleSpecsConnection(pagination: { first: 50 }) {
|
||||
oracleSpecsConnection(pagination: { first: 30 }) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
|
||||
@@ -1,22 +1,85 @@
|
||||
fragment ExplorerOraclePerpetual on Perpetual {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
status
|
||||
}
|
||||
dataSourceSpecForSettlementSchedule {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExplorerOracleFuture on Future {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
status
|
||||
}
|
||||
dataSourceSpecForTradingTermination {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExplorerOracleForMarketsMarket on Market {
|
||||
id
|
||||
state
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
... on Future {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
}
|
||||
dataSourceSpecForTradingTermination {
|
||||
id
|
||||
}
|
||||
...ExplorerOracleFuture
|
||||
}
|
||||
... on Perpetual {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
...ExplorerOraclePerpetual
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExplorerOracleDataSourceSpec on ExternalDataSourceSpec {
|
||||
spec {
|
||||
id
|
||||
status
|
||||
data {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
}
|
||||
... on DataSourceSpecConfigurationTimeTrigger {
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
triggers {
|
||||
initial
|
||||
every
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSourceSpecForSettlementSchedule {
|
||||
id
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on EthCallSpec {
|
||||
address
|
||||
}
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,4 +95,27 @@ query ExplorerOracleFormMarkets {
|
||||
}
|
||||
}
|
||||
}
|
||||
oracleSpecsConnection {
|
||||
edges {
|
||||
node {
|
||||
dataSourceSpec {
|
||||
...ExplorerOracleDataSourceSpec
|
||||
}
|
||||
dataConnection(pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
externalData {
|
||||
data {
|
||||
data {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-5
@@ -5,23 +5,23 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
|
||||
|
||||
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array<string> | null, args?: Array<string> | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger' } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
|
||||
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array<string> | null, args?: Array<string> | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
|
||||
|
||||
export type ExplorerOracleSpecsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array<string> | null, args?: Array<string> | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger' } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
|
||||
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array<string> | null, args?: Array<string> | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
|
||||
|
||||
export type ExplorerOracleSpecByIdQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array<string> | null, args?: Array<string> | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger' } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
|
||||
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array<string> | null, args?: Array<string> | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
|
||||
|
||||
export const ExplorerOracleDataConnectionFragmentDoc = gql`
|
||||
fragment ExplorerOracleDataConnection on OracleSpec {
|
||||
dataConnection {
|
||||
dataConnection(pagination: {first: 30}) {
|
||||
edges {
|
||||
node {
|
||||
externalData {
|
||||
@@ -68,6 +68,16 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
|
||||
operator
|
||||
}
|
||||
}
|
||||
... on DataSourceSpecConfigurationTimeTrigger {
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
triggers {
|
||||
initial
|
||||
every
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
@@ -126,6 +136,23 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
... on EthCallSpec {
|
||||
abi
|
||||
address
|
||||
requiredConfirmations
|
||||
method
|
||||
filters {
|
||||
key {
|
||||
type
|
||||
name
|
||||
numberDecimalPlaces
|
||||
}
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +163,7 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
|
||||
${ExplorerOracleDataConnectionFragmentDoc}`;
|
||||
export const ExplorerOracleSpecsDocument = gql`
|
||||
query ExplorerOracleSpecs {
|
||||
oracleSpecsConnection(pagination: {first: 50}) {
|
||||
oracleSpecsConnection(pagination: {first: 30}) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
|
||||
+110
-13
@@ -3,33 +3,106 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerOracleForMarketsMarketFragment = { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string } } | { __typename?: 'Spot' } } } };
|
||||
export type ExplorerOraclePerpetualFragment = { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } };
|
||||
|
||||
export type ExplorerOracleFutureFragment = { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } };
|
||||
|
||||
export type ExplorerOracleForMarketsMarketFragment = { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } };
|
||||
|
||||
export type ExplorerOracleDataSourceSpecFragment = { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } };
|
||||
|
||||
export type ExplorerOracleFormMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerOracleFormMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string } } | { __typename?: 'Spot' } } } } }> } | null };
|
||||
export type ExplorerOracleFormMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } } }> } | null, oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
|
||||
|
||||
export const ExplorerOracleFutureFragmentDoc = gql`
|
||||
fragment ExplorerOracleFuture on Future {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
status
|
||||
}
|
||||
dataSourceSpecForTradingTermination {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ExplorerOraclePerpetualFragmentDoc = gql`
|
||||
fragment ExplorerOraclePerpetual on Perpetual {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
status
|
||||
}
|
||||
dataSourceSpecForSettlementSchedule {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ExplorerOracleForMarketsMarketFragmentDoc = gql`
|
||||
fragment ExplorerOracleForMarketsMarket on Market {
|
||||
id
|
||||
state
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
... on Future {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
}
|
||||
dataSourceSpecForTradingTermination {
|
||||
id
|
||||
}
|
||||
...ExplorerOracleFuture
|
||||
}
|
||||
... on Perpetual {
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
...ExplorerOraclePerpetual
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${ExplorerOracleFutureFragmentDoc}
|
||||
${ExplorerOraclePerpetualFragmentDoc}`;
|
||||
export const ExplorerOracleDataSourceSpecFragmentDoc = gql`
|
||||
fragment ExplorerOracleDataSourceSpec on ExternalDataSourceSpec {
|
||||
spec {
|
||||
id
|
||||
status
|
||||
data {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
}
|
||||
... on DataSourceSpecConfigurationTimeTrigger {
|
||||
conditions {
|
||||
value
|
||||
operator
|
||||
}
|
||||
triggers {
|
||||
initial
|
||||
every
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSourceSpecForSettlementSchedule {
|
||||
id
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on EthCallSpec {
|
||||
address
|
||||
}
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +119,32 @@ export const ExplorerOracleFormMarketsDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
oracleSpecsConnection {
|
||||
edges {
|
||||
node {
|
||||
dataSourceSpec {
|
||||
...ExplorerOracleDataSourceSpec
|
||||
}
|
||||
dataConnection(pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
externalData {
|
||||
data {
|
||||
data {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${ExplorerOracleForMarketsMarketFragmentDoc}`;
|
||||
${ExplorerOracleForMarketsMarketFragmentDoc}
|
||||
${ExplorerOracleDataSourceSpecFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useExplorerOracleFormMarketsQuery__
|
||||
|
||||
@@ -51,15 +51,26 @@ describe('Oracle Data view', () => {
|
||||
{
|
||||
node: {
|
||||
externalData: {
|
||||
__typename: 'ExternalData',
|
||||
data: {
|
||||
broadcastAt: '2022-01-01',
|
||||
__typename: 'Data',
|
||||
matchedSpecIds: ['123'],
|
||||
broadcastAt: '2023-01-01T00:00:00Z',
|
||||
data: [
|
||||
{
|
||||
__typename: 'Property',
|
||||
name: 'Test-name',
|
||||
value: 'Test-data',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as DataConnection)
|
||||
} as ExplorerOracleDataConnectionFragment['dataConnection'])
|
||||
);
|
||||
expect(res.getByText('Broadcast data')).toBeInTheDocument();
|
||||
expect(res.getByText('Test-name')).toBeInTheDocument();
|
||||
expect(res.getByText('Test-data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,55 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import filter from 'recursive-key-filter';
|
||||
import type { ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
|
||||
import { TimeAgo } from '../../../components/time-ago';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
const cellSpacing = 'px-3';
|
||||
|
||||
interface OracleDataTypeProps {
|
||||
data: ExplorerOracleDataConnectionFragment['dataConnection'];
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is data that has matched this oracle, this view will
|
||||
* render the data inside a collapsed element so that it can be viewed.
|
||||
* Currently the data is just rendered as a JSON view, because
|
||||
* that Does The Job, rather than because it's good.
|
||||
*/
|
||||
export function OracleData({ data }: OracleDataTypeProps) {
|
||||
if (!data || !data.edges?.length || data.edges.length > 1) {
|
||||
if (!data || !data.edges?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<details data-testid="oracle-data">
|
||||
<summary>{t('Broadcast data')}</summary>
|
||||
<ul>
|
||||
{data.edges.map((d) => {
|
||||
if (!d) {
|
||||
return null;
|
||||
}
|
||||
<>
|
||||
<h2 className="text-3xl font-bold mb-4 display-5 mt-5">
|
||||
{t('Recent data')}
|
||||
</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className="text-left">
|
||||
<th className={cellSpacing}>Value</th>
|
||||
<th className={cellSpacing}>Key</th>
|
||||
<th className={cellSpacing}>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.edges.map((d) => {
|
||||
if (!d) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={d.node.externalData.data.broadcastAt}>
|
||||
<SyntaxHighlighter data={filter(d, ['__typename'])} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
const broadcastAt = d.node.externalData.data.broadcastAt;
|
||||
const node = d.node.externalData.data.data?.at(0);
|
||||
if (!node || !node.value || !node.name || !node || !broadcastAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={d.node.externalData.data.broadcastAt}>
|
||||
<td className={`${cellSpacing} font-mono`}>{node.value}</td>
|
||||
<td className={`${cellSpacing} font-mono`}>{node.name}</td>
|
||||
<td className={cellSpacing}>
|
||||
<TimeAgo date={broadcastAt} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,13 +55,13 @@ describe('Oracle type view', () => {
|
||||
const s = mock('prices.external.whatever');
|
||||
expect(isInternalSourceType(s)).toEqual(false);
|
||||
const res = render(renderWrappedComponent(s));
|
||||
expect(res.getByText('External data')).toBeInTheDocument();
|
||||
expect(res.getByText('External Data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders External data otherwise', () => {
|
||||
const s = mock('prices.external.vegaprotocol.builtin.');
|
||||
expect(isInternalSourceType(s)).toEqual(false);
|
||||
const res = render(renderWrappedComponent(s));
|
||||
expect(res.getByText('External data')).toBeInTheDocument();
|
||||
expect(res.getByText('External Data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,19 @@ export function isInternalSourceType(s: SourceType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getExternalType(s: SourceType) {
|
||||
if (s.sourceType.__typename === 'EthCallSpec') {
|
||||
return 'Ethereum Contract Call';
|
||||
} else {
|
||||
return 'External Data';
|
||||
}
|
||||
}
|
||||
|
||||
export function getTypeString(s: SourceType) {
|
||||
const isInternal = isInternalSourceType(s);
|
||||
return isInternal ? 'Internal data' : getExternalType(s);
|
||||
}
|
||||
|
||||
interface OracleDetailsTypeProps {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
@@ -39,14 +52,10 @@ export function OracleDetailsType({ sourceType }: OracleDetailsTypeProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isInternal = isInternalSourceType(sourceType);
|
||||
|
||||
return (
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">Type</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
{isInternal ? 'Internal data' : 'External data'}
|
||||
</TableCell>
|
||||
<TableCell modifier="bordered">{getTypeString(sourceType)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TableRow, TableCell, TableHeader } from '../../../components/table';
|
||||
import type { SourceType } from './oracle';
|
||||
import {
|
||||
EthExplorerLink,
|
||||
EthExplorerLinkTypes,
|
||||
} from '../../../components/links/eth-explorer-link/eth-explorer-link';
|
||||
|
||||
interface OracleDetailsEthSourceProps {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
/**
|
||||
* Given an Oracle that sources data from Ethereum, this component will render
|
||||
* a link to the smart contract and some basic details
|
||||
*/
|
||||
export function OracleEthSource({ sourceType }: OracleDetailsEthSourceProps) {
|
||||
if (
|
||||
sourceType.__typename !== 'DataSourceDefinitionExternal' ||
|
||||
sourceType.sourceType.__typename !== 'EthCallSpec'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const address = sourceType.sourceType.address;
|
||||
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">Ethereum Contract</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
<EthExplorerLink id={address} type={EthExplorerLinkTypes.address} />
|
||||
<span className="mx-3">⇒</span>
|
||||
<code>{sourceType.sourceType.method}</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { getConditionsOrFilters, OracleFilter } from './oracle-filter';
|
||||
import type { Filter } from './oracle-filter';
|
||||
import { OracleFilter } from './oracle-filter';
|
||||
import type { ExplorerOracleDataSourceFragment } from '../__generated__/Oracles';
|
||||
import {
|
||||
ConditionOperator,
|
||||
DataSourceSpecStatus,
|
||||
PropertyKeyType,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { Condition } from '@vegaprotocol/types';
|
||||
|
||||
const mockExternalSpec = {
|
||||
type Spec =
|
||||
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType'];
|
||||
|
||||
const mockExternalSpec: Spec = {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
filters: [
|
||||
@@ -16,12 +19,12 @@ const mockExternalSpec = {
|
||||
__typename: 'Filter',
|
||||
key: {
|
||||
type: PropertyKeyType.TYPE_INTEGER,
|
||||
name: 'test',
|
||||
name: 'testKey',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
__typename: 'Condition',
|
||||
value: 'test',
|
||||
value: 'testValue',
|
||||
operator: ConditionOperator.OPERATOR_EQUALS,
|
||||
},
|
||||
],
|
||||
@@ -30,16 +33,6 @@ const mockExternalSpec = {
|
||||
},
|
||||
};
|
||||
|
||||
const mockTimeSpec = {
|
||||
__typename: 'DataSourceSpecConfigurationTime',
|
||||
conditions: [
|
||||
{
|
||||
value: '123',
|
||||
operator: ConditionOperator.OPERATOR_EQUALS,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function renderComponent(data: ExplorerOracleDataSourceFragment) {
|
||||
return <OracleFilter data={data} />;
|
||||
}
|
||||
@@ -70,11 +63,16 @@ describe('Oracle Filter view', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ExplorerOracleDataSourceFragment)
|
||||
dataConnection: {
|
||||
edges: [],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.getByText('Filter')).toBeInTheDocument();
|
||||
// Avoids asserting on how the data is presented because it is very rudimentary
|
||||
// Renders a comprehensible summary of key = value
|
||||
expect(res.getByText('testKey')).toBeInTheDocument();
|
||||
expect(res.getByText('=')).toBeInTheDocument();
|
||||
expect(res.getByText('testValue')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders conditions if type is DataSourceSpecConfigurationTime', () => {
|
||||
@@ -88,75 +86,59 @@ describe('Oracle Filter view', () => {
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal',
|
||||
sourceType: mockTimeSpec,
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfigurationTime',
|
||||
conditions: [
|
||||
{
|
||||
value: '1',
|
||||
operator: ConditionOperator.OPERATOR_EQUALS,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ExplorerOracleDataSourceFragment)
|
||||
dataConnection: {
|
||||
edges: [],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(res.getByText('Filter')).toBeInTheDocument();
|
||||
expect(res.getByText('Time')).toBeInTheDocument();
|
||||
expect(res.getByText('=')).toBeInTheDocument();
|
||||
expect(res.getByTitle('1').textContent).toMatch(/1970/);
|
||||
// Avoids asserting on how the data is presented because it is very rudimentary
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConditionsOrFilter', () => {
|
||||
it('Returns null if the type is undetermined (not DataSourceSpecConfiguration or DataSourceSpecConfigurationTime', () => {
|
||||
expect(getConditionsOrFilters({})).toBeNull();
|
||||
});
|
||||
|
||||
it('Returns the conditions object for time specs', () => {
|
||||
const mock: Filter = {
|
||||
__typename: 'DataSourceSpecConfigurationTime',
|
||||
conditions: [
|
||||
{
|
||||
__typename: 'Condition',
|
||||
value: '100',
|
||||
operator: ConditionOperator.OPERATOR_GREATER_THAN,
|
||||
},
|
||||
],
|
||||
};
|
||||
const res = getConditionsOrFilters(mock);
|
||||
// This ugly construction is due to lazy typing on getConditionsOrFilter
|
||||
if (!res || res.length !== 1 || !res[0] || 'key' in res[0]) {
|
||||
throw new Error(
|
||||
'getConditionsOrFilter did not return conditions on a time spec'
|
||||
);
|
||||
}
|
||||
|
||||
expect(res[0].__typename).toEqual('Condition');
|
||||
});
|
||||
|
||||
it('Returns the filters object for external specs', () => {
|
||||
const mock: Filter = {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
filters: [
|
||||
{
|
||||
__typename: 'Filter',
|
||||
key: {
|
||||
type: PropertyKeyType.TYPE_INTEGER,
|
||||
name: 'test',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
__typename: 'Condition',
|
||||
value: 'test',
|
||||
operator: ConditionOperator.OPERATOR_EQUALS,
|
||||
it('DataSourceSpecConfigurationTime handles empty conditions', () => {
|
||||
const res = render(
|
||||
renderComponent({
|
||||
dataSourceSpec: {
|
||||
spec: {
|
||||
id: 'irrelevant-test-data',
|
||||
createdAt: 'irrelevant-test-data',
|
||||
status: DataSourceSpecStatus.STATUS_ACTIVE,
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfigurationTime',
|
||||
conditions: [undefined as unknown as Condition],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
dataConnection: {
|
||||
edges: [],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = getConditionsOrFilters(mock);
|
||||
// This ugly construction is due to lazy typing on getConditionsOrFilter
|
||||
if (!res || res.length !== 1 || !res[0] || 'value' in res[0]) {
|
||||
throw new Error(
|
||||
'getConditionsOrFilter did not return filters on a external spec'
|
||||
);
|
||||
}
|
||||
|
||||
expect(res[0].__typename).toEqual('Filter');
|
||||
// This should never happen, but for coverage sake we test that it does this
|
||||
const ul = res.getByRole('list');
|
||||
expect(ul).toBeInTheDocument();
|
||||
expect(ul).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import filter from 'recursive-key-filter';
|
||||
import type { ExplorerOracleDataSourceFragment } from '../__generated__/Oracles';
|
||||
import { OracleSpecInternalTimeTrigger } from './oracle-spec/internal-time-trigger';
|
||||
import { OracleSpecCondition } from './oracle-spec/condition';
|
||||
import { getCharacterForOperator } from './oracle-spec/operator';
|
||||
|
||||
interface OracleFilterProps {
|
||||
data: ExplorerOracleDataSourceFragment;
|
||||
}
|
||||
|
||||
export type Filter =
|
||||
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType']['sourceType'];
|
||||
|
||||
/**
|
||||
* Given the main Filter view just uses a JSON dump view, this function
|
||||
* selects the correct filter to dump in to that view. Internal oracles
|
||||
* (i.e. the Time oracle) have conditions while external data sources
|
||||
* have filters
|
||||
*
|
||||
* @param s A data source
|
||||
* @returns Object an object containing conditions or filters
|
||||
*/
|
||||
export function getConditionsOrFilters(s: Filter) {
|
||||
if (s.__typename === 'DataSourceSpecConfiguration') {
|
||||
return s.filters;
|
||||
} else if (s.__typename === 'DataSourceSpecConfigurationTime') {
|
||||
return s.conditions;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the conditions that this oracle is using to filter
|
||||
* data sources.
|
||||
* data sources, as a list.
|
||||
*
|
||||
* Renders nothing if there is no data (which will frequently)
|
||||
* be the case) and if there is data, currently renders a simple
|
||||
@@ -42,16 +21,53 @@ export function OracleFilter({ data }: OracleFilterProps) {
|
||||
}
|
||||
|
||||
const s = data.dataSourceSpec.spec.data.sourceType.sourceType;
|
||||
const f = getConditionsOrFilters(s);
|
||||
if (s.__typename === 'DataSourceSpecConfigurationTime' && s.conditions) {
|
||||
return (
|
||||
<ul>
|
||||
{s.conditions
|
||||
.filter((c) => !!c)
|
||||
.map((c) => {
|
||||
if (!c) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<OracleSpecCondition key={c.value} data={c} type={s.__typename} />
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
} else if (
|
||||
s.__typename === 'DataSourceSpecConfigurationTimeTrigger' &&
|
||||
s.triggers
|
||||
) {
|
||||
return <OracleSpecInternalTimeTrigger data={s} />;
|
||||
} else if (
|
||||
s.__typename === 'EthCallSpec' ||
|
||||
s.__typename === 'DataSourceSpecConfiguration'
|
||||
) {
|
||||
if (s.filters !== null && s.filters && 'filters' in s) {
|
||||
return (
|
||||
<ul>
|
||||
{s.filters.map((f) => {
|
||||
const prop = <code title={f.key.type}>{f.key.name}</code>;
|
||||
|
||||
if (!f) {
|
||||
return null;
|
||||
if (!f.conditions || f.conditions.length === 0) {
|
||||
return prop;
|
||||
} else {
|
||||
return f.conditions.map((c) => {
|
||||
return (
|
||||
<li key={`${prop}${c.value}`}>
|
||||
{prop} {getCharacterForOperator(c.operator)}{' '}
|
||||
<code>{c.value ? c.value : '-'}</code>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<details>
|
||||
<summary>{t('Filter')}</summary>
|
||||
<SyntaxHighlighter data={filter(f, ['__typename'])} />
|
||||
</details>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ function renderComponent(id: string, mocks: MockedResponse[]) {
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<Table>
|
||||
<tbody>
|
||||
<tbody data-testid="wrapper">
|
||||
<OracleMarkets id={id} />
|
||||
</tbody>
|
||||
</Table>
|
||||
@@ -23,8 +23,7 @@ function renderComponent(id: string, mocks: MockedResponse[]) {
|
||||
describe('Oracle Markets component', () => {
|
||||
it('Renders a row with the market ID initially', () => {
|
||||
const res = render(renderComponent('123', []));
|
||||
expect(res.getByText('Market')).toBeInTheDocument();
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(res.getByTestId('wrapper')).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('Renders that this is a termination source for the right market', async () => {
|
||||
@@ -34,21 +33,58 @@ describe('Oracle Markets component', () => {
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
oracleSpecsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
dataConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
externalData: {
|
||||
data: {
|
||||
data: {
|
||||
name: '123',
|
||||
value: '456',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
dataSourceSpec: {
|
||||
spec: {
|
||||
id: '789',
|
||||
state: 'Active',
|
||||
status: 'Active',
|
||||
data: {
|
||||
sourceType: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
marketsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: '123',
|
||||
state: 'Active',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: '456',
|
||||
status: 'Active',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
id: '789',
|
||||
status: 'Active',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -59,15 +95,18 @@ describe('Oracle Markets component', () => {
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'abc',
|
||||
state: 'Active',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: 'def',
|
||||
status: 'Active',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
id: 'ghi',
|
||||
status: 'Active',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,21 +130,58 @@ describe('Oracle Markets component', () => {
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
oracleSpecsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
dataConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
externalData: {
|
||||
data: {
|
||||
data: {
|
||||
name: '123',
|
||||
value: '456',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
dataSourceSpec: {
|
||||
spec: {
|
||||
id: '789',
|
||||
state: 'Active',
|
||||
status: 'Active',
|
||||
data: {
|
||||
sourceType: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
marketsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: '123',
|
||||
state: 'Active',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: '789',
|
||||
status: 'Active',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
id: '123',
|
||||
status: 'Active',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -116,15 +192,18 @@ describe('Oracle Markets component', () => {
|
||||
node: {
|
||||
__typename: 'Market',
|
||||
id: 'abc',
|
||||
state: 'Active',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: 'def',
|
||||
status: 'Active',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
id: 'ghi',
|
||||
status: 'Active',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getNodes } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MarketLink } from '../../../components/links';
|
||||
import { TableRow, TableCell, TableHeader } from '../../../components/table';
|
||||
import type { ExplorerOracleForMarketsMarketFragment } from '../__generated__/OraclesForMarkets';
|
||||
@@ -24,38 +23,37 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
|
||||
);
|
||||
|
||||
if (markets) {
|
||||
const m = markets.find((m) => {
|
||||
const p = m.tradableInstrument.instrument.product;
|
||||
const m = markets.filter((market) => {
|
||||
const p = market.tradableInstrument.instrument.product;
|
||||
if (
|
||||
((p.__typename === 'Future' || p.__typename === 'Perpetual') &&
|
||||
p.dataSourceSpecForSettlementData.id === id) ||
|
||||
('dataSourceSpecForTradingTermination' in p &&
|
||||
p.dataSourceSpecForTradingTermination.id === id)
|
||||
p.dataSourceSpecForTradingTermination.id === id) ||
|
||||
(p.__typename === 'Perpetual' &&
|
||||
p.dataSourceSpecForSettlementSchedule.id === id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (m && m.id) {
|
||||
if (m && m.length > 0) {
|
||||
return (
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">{getLabel(id, m)}</TableHeader>
|
||||
<TableCell modifier="bordered" data-testid={`m-${m.id}`}>
|
||||
<MarketLink id={m.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<>
|
||||
{m.map((market) => (
|
||||
<TableRow modifier="bordered" key={`m-${market.id}`}>
|
||||
<TableHeader scope="row">{getLabel(id, market)}</TableHeader>
|
||||
<TableCell modifier="bordered" data-testid={`m-${market.id}`}>
|
||||
<MarketLink id={market.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">{t('Market')}</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
<span>{id}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getLabel(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import { getCharacterForOperator } from './operator';
|
||||
import type { Condition } from '@vegaprotocol/types';
|
||||
|
||||
export interface OracleSpecCondition {
|
||||
data: Condition;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export function OracleSpecCondition({ data, type }: OracleSpecCondition) {
|
||||
const c = getCharacterForOperator(data.operator);
|
||||
const value =
|
||||
type === 'DataSourceSpecConfigurationTime' && data.value
|
||||
? fromUnixTime(parseInt(data.value)).toLocaleString()
|
||||
: data.value;
|
||||
const typeLabel =
|
||||
type === 'DataSourceSpecConfigurationTime' ? (
|
||||
<span>{t('Time')}</span>
|
||||
) : (
|
||||
type
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={`${typeLabel}${c}${value}`}>
|
||||
{typeLabel} {c}{' '}
|
||||
{value && (
|
||||
<span
|
||||
title={data.value || value}
|
||||
className="underline decoration-dotted"
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { DataSourceSpecConfigurationTimeTrigger } from '@vegaprotocol/types';
|
||||
import secondsToMinutes from 'date-fns/secondsToMinutes';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
|
||||
export interface OracleSpecInternalTimeTriggerProps {
|
||||
data: DataSourceSpecConfigurationTimeTrigger;
|
||||
}
|
||||
|
||||
export function OracleSpecInternalTimeTrigger({
|
||||
data,
|
||||
}: OracleSpecInternalTimeTriggerProps) {
|
||||
return (
|
||||
<div>
|
||||
<span>{t('Time')}</span>,
|
||||
{data.triggers.map((tr) => {
|
||||
return (
|
||||
<span>
|
||||
{tr?.initial ? (
|
||||
<span title={`${tr.initial}`}>
|
||||
<strong>{t('starting at')}</strong>{' '}
|
||||
<em className="not-italic underline decoration-dotted">
|
||||
{fromUnixTime(tr.initial).toLocaleString()}
|
||||
</em>
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{tr?.every ? (
|
||||
<span title={`${tr.every} ${t('seconds')}`}>
|
||||
, <strong>{t('every')}</strong>{' '}
|
||||
<em className="not-italic underline decoration-dotted">
|
||||
{secondsToMinutes(tr.every)} {t('minutes')}
|
||||
</em>{' '}
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { ConditionOperator } from '@vegaprotocol/types';
|
||||
|
||||
export function getCharacterForOperator(
|
||||
operator: ConditionOperator
|
||||
): React.ReactElement {
|
||||
switch (operator) {
|
||||
case 'OPERATOR_EQUALS':
|
||||
return <span title={t('equals')}>=</span>;
|
||||
case 'OPERATOR_GREATER_THAN':
|
||||
return <span title={t('greater than')}>></span>;
|
||||
case 'OPERATOR_GREATER_THAN_OR_EQUAL':
|
||||
return <span title={t('greater than or equal')}>≥</span>;
|
||||
case 'OPERATOR_LESS_THAN':
|
||||
return <span title={t('less than')}><</span>;
|
||||
case 'OPERATOR_LESS_THAN_OR_EQUAL':
|
||||
return <span title={t('less than or equal')}>≤</span>;
|
||||
}
|
||||
|
||||
return <span>{operator}</span>;
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import { OracleFilter } from './oracle-filter';
|
||||
import { OracleDetailsType } from './oracle-details-type';
|
||||
import { OracleMarkets } from './oracle-markets';
|
||||
import { OracleSigners } from './oracle-signers';
|
||||
import OracleLink from '../../../components/links/oracle-link/oracle-link';
|
||||
import { OracleEthSource } from './oracle-eth-source';
|
||||
import Hash from '../../../components/links/hash';
|
||||
import { getStatusString } from '../../../components/links/oracle-link/oracle-link';
|
||||
|
||||
export type SourceType =
|
||||
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType'];
|
||||
@@ -38,10 +40,8 @@ export const OracleDetails = ({
|
||||
id,
|
||||
dataSource,
|
||||
dataConnection,
|
||||
showBroadcasts = false,
|
||||
}: OracleDetailsProps) => {
|
||||
const sourceType = dataSource.dataSourceSpec.spec.data.sourceType;
|
||||
const reportsCount: number = dataConnection.edges?.length || 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -49,23 +49,27 @@ export const OracleDetails = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">{t('ID')}</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
<OracleLink id={id} />
|
||||
<Hash text={id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<OracleDetailsType sourceType={sourceType} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">{t('Status')}</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
{getStatusString(dataSource.dataSourceSpec.spec.status)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<OracleSigners sourceType={sourceType} />
|
||||
<OracleEthSource sourceType={sourceType} />
|
||||
<OracleMarkets id={id} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">{t('Matched data')}</TableHeader>
|
||||
<TableHeader scope="row">{t('Filter')}</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
{showBroadcasts ? reportsCount : reportsCount > 0 ? '✅' : '❌'}
|
||||
<OracleFilter data={dataSource} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
<OracleFilter data={dataSource} />
|
||||
{showBroadcasts && dataConnection ? (
|
||||
<OracleData data={dataConnection} />
|
||||
) : null}
|
||||
{dataConnection ? <OracleData data={dataConnection} /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import compact from 'lodash/compact';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useExplorerOracleSpecsQuery } from '../__generated__/Oracles';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
import { OracleDetails } from '../components/oracle';
|
||||
import { useScrollToLocation } from '../../../hooks/scroll-to-location';
|
||||
import filter from 'recursive-key-filter';
|
||||
import { useExplorerOracleFormMarketsQuery } from '../__generated__/OraclesForMarkets';
|
||||
import { MarketLink } from '../../../components/links';
|
||||
import { OracleLink } from '../../../components/links/oracle-link/oracle-link';
|
||||
import { useState } from 'react';
|
||||
import { MarketStateMapping } from '@vegaprotocol/types';
|
||||
import type { MarketState } from '@vegaprotocol/types';
|
||||
|
||||
const cellSpacing = 'px-3';
|
||||
|
||||
const Oracles = () => {
|
||||
const { data, loading, error } = useExplorerOracleSpecsQuery({
|
||||
const { data, loading, error } = useExplorerOracleFormMarketsQuery({
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
useDocumentTitle(['Oracles']);
|
||||
useScrollToLocation();
|
||||
|
||||
const [hoveredOracle, setHoveredOracle] = useState('');
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
|
||||
@@ -30,36 +38,148 @@ const Oracles = () => {
|
||||
data.oracleSpecsConnection.edges?.length === 0
|
||||
}
|
||||
>
|
||||
{data?.oracleSpecsConnection?.edges
|
||||
? data.oracleSpecsConnection.edges.map((o) => {
|
||||
const id = o?.node.dataSourceSpec.spec.id;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
<table className="text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={cellSpacing}>Market</th>
|
||||
<th className={cellSpacing}>Type</th>
|
||||
<th className={cellSpacing}>State</th>
|
||||
<th className={cellSpacing}>Settlement</th>
|
||||
<th className={cellSpacing}>Termination</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.marketsConnection?.edges
|
||||
? data.marketsConnection.edges.map((o) => {
|
||||
let hasSeenOracleReports = false;
|
||||
let settlementOracle = '-';
|
||||
let settlementOracleStatus = '-';
|
||||
let terminationOracle = '-';
|
||||
let terminationOracleStatus = '-';
|
||||
|
||||
const dataConnection = o?.node.dataConnection;
|
||||
const id = o?.node.id;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
key={id}
|
||||
className="mb-10"
|
||||
data-testid="oracle-details"
|
||||
>
|
||||
<OracleDetails
|
||||
id={id}
|
||||
dataSource={o?.node}
|
||||
dataConnection={dataConnection}
|
||||
showBroadcasts={false}
|
||||
/>
|
||||
<details>
|
||||
<summary className="pointer">JSON</summary>
|
||||
<SyntaxHighlighter data={filter(o, ['__typename'])} />
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
if (
|
||||
o.node.tradableInstrument.instrument.product.__typename ===
|
||||
'Future'
|
||||
) {
|
||||
settlementOracle =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementData.id;
|
||||
terminationOracle =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForTradingTermination.id;
|
||||
settlementOracleStatus =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementData.status;
|
||||
terminationOracleStatus =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForTradingTermination.status;
|
||||
} else if (
|
||||
o.node.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual'
|
||||
) {
|
||||
settlementOracle =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementData.id;
|
||||
terminationOracle =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementSchedule.id;
|
||||
settlementOracleStatus =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementData.status;
|
||||
terminationOracleStatus =
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementSchedule.status;
|
||||
}
|
||||
const oracleInformationUnfiltered =
|
||||
data?.oracleSpecsConnection?.edges?.map((e) =>
|
||||
e && e.node ? e.node : undefined
|
||||
) || [];
|
||||
|
||||
const oracleInformation = compact(oracleInformationUnfiltered)
|
||||
.filter(
|
||||
(o) =>
|
||||
o.dataConnection.edges &&
|
||||
o.dataConnection.edges.length > 0 &&
|
||||
(o.dataSourceSpec.spec.id === settlementOracle ||
|
||||
o.dataSourceSpec.spec.id === terminationOracle)
|
||||
)
|
||||
.at(0);
|
||||
if (oracleInformation) {
|
||||
hasSeenOracleReports = true;
|
||||
}
|
||||
|
||||
const oracleList = `${settlementOracle} ${terminationOracle}`;
|
||||
|
||||
return (
|
||||
<tr
|
||||
id={id}
|
||||
key={id}
|
||||
className={
|
||||
hoveredOracle.length > 0 &&
|
||||
oracleList.indexOf(hoveredOracle) > -1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: ''
|
||||
}
|
||||
data-testid="oracle-details"
|
||||
data-oracles={oracleList}
|
||||
>
|
||||
<td className={cellSpacing}>
|
||||
<MarketLink id={id} />
|
||||
</td>
|
||||
<td className={cellSpacing}>
|
||||
{
|
||||
o.node.tradableInstrument.instrument.product
|
||||
.__typename
|
||||
}
|
||||
</td>
|
||||
<td className={cellSpacing}>
|
||||
{MarketStateMapping[o.node.state as MarketState]}
|
||||
</td>
|
||||
<td
|
||||
className={
|
||||
hoveredOracle.length > 0 &&
|
||||
hoveredOracle === settlementOracle
|
||||
? `indent-1 ${cellSpacing}`
|
||||
: cellSpacing
|
||||
}
|
||||
>
|
||||
<OracleLink
|
||||
id={settlementOracle}
|
||||
status={settlementOracleStatus}
|
||||
hasSeenOracleReports={hasSeenOracleReports}
|
||||
onMouseOver={() => setHoveredOracle(settlementOracle)}
|
||||
onMouseOut={() => setHoveredOracle('')}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className={
|
||||
hoveredOracle.length > 0 &&
|
||||
hoveredOracle === terminationOracle
|
||||
? `indent-1 ${cellSpacing}`
|
||||
: cellSpacing
|
||||
}
|
||||
>
|
||||
<OracleLink
|
||||
id={terminationOracle}
|
||||
status={terminationOracleStatus}
|
||||
hasSeenOracleReports={hasSeenOracleReports}
|
||||
onMouseOver={() =>
|
||||
setHoveredOracle(terminationOracle)
|
||||
}
|
||||
onMouseOut={() => setHoveredOracle('')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -40,10 +40,9 @@ export const Oracle = () => {
|
||||
id={id || ''}
|
||||
dataSource={data?.oracleSpec}
|
||||
dataConnection={data?.oracleSpec.dataConnection}
|
||||
showBroadcasts={true}
|
||||
/>
|
||||
<details>
|
||||
<summary className="pointer">JSON</summary>
|
||||
<details className="mt-5 cursor-pointer">
|
||||
<summary>JSON</summary>
|
||||
<SyntaxHighlighter data={filter(data, ['__typename'])} />
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,10 @@ export interface BlockExplorerTransactionResult {
|
||||
value: string;
|
||||
};
|
||||
error?: string;
|
||||
// These aren't strictly optional but are new in 0.73.0 so we need to make them optional
|
||||
createdAt?: string;
|
||||
version?: string;
|
||||
pow?: string;
|
||||
}
|
||||
|
||||
export interface BlockExplorerTransactions {
|
||||
|
||||
+670
-247
File diff suppressed because it is too large
Load Diff
@@ -133,7 +133,7 @@ export const proposalsData = {
|
||||
instrument: {
|
||||
name: 'UNIDAI Monthly (Dec 2022)',
|
||||
code: 'UNIDAI.MF21',
|
||||
futureProduct: {
|
||||
product: {
|
||||
settlementAsset: { symbol: 'tDAI', __typename: 'Asset' },
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
@@ -240,7 +240,7 @@ export const proposalsData = {
|
||||
instrument: {
|
||||
name: 'ETHBTC Quarterly (Feb 2023)',
|
||||
code: 'ETHBTC.QM21',
|
||||
futureProduct: {
|
||||
product: {
|
||||
settlementAsset: { symbol: 'tBTC', __typename: 'Asset' },
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ENV } from '../../config';
|
||||
|
||||
import noIcon from '../../images/token-no-icon.png';
|
||||
import vegaBlack from '../../images/vega_black.png';
|
||||
import vegaVesting from '../../images/vega_vesting.png';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import type { WalletCardAssetProps } from '../wallet-card';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
@@ -102,7 +103,10 @@ export const usePollForDelegations = () => {
|
||||
setAccounts(
|
||||
accounts
|
||||
.filter(
|
||||
(a) => a.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
(a) =>
|
||||
a.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL ||
|
||||
a.type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS ||
|
||||
a.type === Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS
|
||||
)
|
||||
.map((a) => {
|
||||
const isVega =
|
||||
@@ -115,14 +119,23 @@ export const usePollForDelegations = () => {
|
||||
subheading: isVega ? t('collateral') : a.asset.symbol,
|
||||
symbol: a.asset.symbol,
|
||||
decimals: a.asset.decimals,
|
||||
assetId: a.asset.id,
|
||||
balance: new BigNumber(
|
||||
addDecimal(a.balance, a.asset.decimals)
|
||||
),
|
||||
image: isVega ? vegaBlack : noIcon,
|
||||
image: isVega
|
||||
? vegaBlack
|
||||
: a.type ===
|
||||
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS ||
|
||||
a.type ===
|
||||
Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS
|
||||
? vegaVesting
|
||||
: noIcon,
|
||||
border: isVega,
|
||||
address: isAssetTypeERC20(a.asset)
|
||||
? a.asset.source.contractAddress
|
||||
: undefined,
|
||||
type: a.type,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useAnimateValue } from '../../hooks/use-animate-value';
|
||||
import type { BigNumber } from '../../lib/bignumber';
|
||||
import { useNumberParts } from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AnchorButton, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
CONSOLE_TRANSFER_ASSET,
|
||||
DApp,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useNetworkParam } from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface WalletCardProps {
|
||||
children: React.ReactNode;
|
||||
@@ -100,8 +109,10 @@ export interface WalletCardAssetProps {
|
||||
symbol: string;
|
||||
balance: BigNumber;
|
||||
decimals: number;
|
||||
assetId?: string;
|
||||
border?: boolean;
|
||||
subheading?: string;
|
||||
type?: Schema.AccountType;
|
||||
}
|
||||
|
||||
export const WalletCardAsset = ({
|
||||
@@ -110,16 +121,37 @@ export const WalletCardAsset = ({
|
||||
symbol,
|
||||
balance,
|
||||
decimals,
|
||||
assetId,
|
||||
border,
|
||||
subheading,
|
||||
type,
|
||||
}: WalletCardAssetProps) => {
|
||||
const [integers, decimalsPlaces, separator] = useNumberParts(
|
||||
balance,
|
||||
decimals
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
const transferAssetLink = (assetId: string) =>
|
||||
consoleLink(CONSOLE_TRANSFER_ASSET.replace(':assetId', assetId));
|
||||
const { param: baseRate } = useNetworkParam('rewards_vesting_baseRate');
|
||||
|
||||
const isRedeemable =
|
||||
type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS && assetId;
|
||||
|
||||
const accountTypeTooltip = useMemo(() => {
|
||||
if (type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS) {
|
||||
return t('VestedRewardsTooltip');
|
||||
}
|
||||
if (type === Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS && baseRate) {
|
||||
return t('VestingRewardsTooltip', { baseRate });
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [baseRate, t, type]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap mt-2 mb-4">
|
||||
<div className="flex flex-nowrap gap-2 mt-2 mb-4">
|
||||
<img
|
||||
alt="Vega"
|
||||
src={image}
|
||||
@@ -129,15 +161,37 @@ export const WalletCardAsset = ({
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="flex align-center text-base"
|
||||
className="flex align-center items-baseline text-base gap-2"
|
||||
data-testid="currency-title"
|
||||
>
|
||||
<div className="mb-0 px-2 uppercase">{name}</div>
|
||||
<div className="mb-0 uppercase">{name}</div>
|
||||
<div className="mb-0 uppercase text-neutral-400">
|
||||
{subheading || symbol}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 basis-full font-mono" data-testid="currency-value">
|
||||
{type ? (
|
||||
<div className="mb-[2px] flex gap-2 items-baseline">
|
||||
<Tooltip description={accountTypeTooltip}>
|
||||
<span className="px-2 py-1 leading-none text-xs bg-vega-cdark-700 rounded">
|
||||
{Schema.AccountTypeMapping[type]}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{isRedeemable ? (
|
||||
<Tooltip description={t('RedeemRewardsTooltip')}>
|
||||
<AnchorButton
|
||||
variant="primary"
|
||||
size="xs"
|
||||
href={transferAssetLink(assetId)}
|
||||
target="_blank"
|
||||
className="px-2 py-1 leading-none text-xs bg-vega-yellow text-black rounded"
|
||||
>
|
||||
{t('Redeem')}
|
||||
</AnchorButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="basis-full font-mono" data-testid="currency-value">
|
||||
<span>
|
||||
{integers}
|
||||
{separator}
|
||||
|
||||
@@ -953,5 +953,8 @@
|
||||
"ACCOUNT_TYPE_REWARD_RELATIVE_RETURN": "Relative return reward account",
|
||||
"ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY": "Return volatility reward account",
|
||||
"ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING": "Validator ranking reward account",
|
||||
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account"
|
||||
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account",
|
||||
"VestingRewardsTooltip": "Vesting rewards will be moved to vested account at a rate of {{baseRate}} per epoch.",
|
||||
"VestedRewardsTooltip": "Vested rewards can be redeemed using Console",
|
||||
"RedeemRewardsTooltip": "Click to redeem vested rewards in Console"
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ describe('Proposal header', () => {
|
||||
__typename: 'InstrumentConfiguration',
|
||||
name: 'Some market',
|
||||
code: 'FX:BTCUSD/DEC99',
|
||||
futureProduct: {
|
||||
product: {
|
||||
__typename: 'FutureProduct',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
|
||||
+14
-2
@@ -39,6 +39,18 @@ export const ProposalHeader = ({
|
||||
|
||||
const titleContent = shorten(title ?? '', 100);
|
||||
|
||||
const getAsset = (proposal: ProposalQuery['proposal']) => {
|
||||
const terms = proposal?.terms;
|
||||
if (
|
||||
terms?.change.__typename === 'NewMarket' &&
|
||||
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
|
||||
terms.change.instrument.product?.__typename === 'PerpetualProduct')
|
||||
) {
|
||||
return terms.change.instrument.product.settlementAsset;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
switch (change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
proposalType =
|
||||
@@ -54,10 +66,10 @@ export const ProposalHeader = ({
|
||||
<span>
|
||||
{t('Code')}: {change.instrument.code}.
|
||||
</span>{' '}
|
||||
{change.instrument.futureProduct?.settlementAsset.symbol ? (
|
||||
{proposal?.terms && getAsset(proposal)?.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">
|
||||
{change.instrument.futureProduct.settlementAsset.symbol}
|
||||
{getAsset(proposal)?.symbol}
|
||||
</span>{' '}
|
||||
{t('settled future')}.
|
||||
</>
|
||||
|
||||
+2
-2
@@ -36,13 +36,13 @@ describe('ProposalReferralProgramDetails helper functions', () => {
|
||||
it('should format referral discount factor correctly', () => {
|
||||
const input = '0.05';
|
||||
const formatted = formatReferralDiscountFactor(input);
|
||||
expect(formatted).toBe('5.00%');
|
||||
expect(formatted).toBe('5%');
|
||||
});
|
||||
|
||||
it('should format referral reward factor correctly', () => {
|
||||
const input = '0.1';
|
||||
const formatted = formatReferralRewardFactor(input);
|
||||
expect(formatted).toBe('10.00%');
|
||||
expect(formatted).toBe('10%');
|
||||
});
|
||||
|
||||
it('should format minimum staked tokens correctly', () => {
|
||||
|
||||
@@ -132,46 +132,25 @@ query Proposal(
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
futureProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
@@ -179,52 +158,44 @@ query Proposal(
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# dataSourceSpecForTradingTermination {
|
||||
# sourceType {
|
||||
# ... on DataSourceDefinitionInternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfigurationTime {
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ... on DataSourceDefinitionExternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfiguration {
|
||||
# signers {
|
||||
# signer {
|
||||
# ... on PubKey {
|
||||
# key
|
||||
# }
|
||||
# ... on ETHAddress {
|
||||
# address
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# filters {
|
||||
# key {
|
||||
# name
|
||||
# type
|
||||
# }
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -101,9 +101,16 @@ fragment ProposalFields on Proposal {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
futureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -11,7 +11,7 @@ export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: {
|
||||
|
||||
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
|
||||
|
||||
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
|
||||
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
|
||||
|
||||
export type ProposalsQueryVariables = Types.Exact<{
|
||||
includeNewMarketProductFields: Types.Scalars['Boolean'];
|
||||
@@ -21,7 +21,7 @@ export type ProposalsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
|
||||
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
|
||||
|
||||
export const NewMarketProductFieldsFragmentDoc = gql`
|
||||
fragment NewMarketProductFields on Proposal {
|
||||
@@ -131,9 +131,16 @@ export const ProposalFieldsFragmentDoc = gql`
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
futureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const colUpdatedAt = '[col-id="updatedAt"] button';
|
||||
|
||||
const headers = [
|
||||
'Party',
|
||||
'Status',
|
||||
'Commitment (tDAI)',
|
||||
'Obligation',
|
||||
'Fee',
|
||||
@@ -34,7 +35,6 @@ const headers = [
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Status',
|
||||
'Created',
|
||||
'Updated',
|
||||
];
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"next",
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"ignorePatterns": ["!**/*", "__generated__"],
|
||||
"ignorePatterns": ["!**/*", "__generated__", ".next"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
|
||||
@@ -82,6 +82,13 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
}
|
||||
);
|
||||
if (
|
||||
market?.tradableInstrument.instrument.product.__typename !==
|
||||
'Perpetual' &&
|
||||
(key === 'funding' || key === 'fundingPayments')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
data-testid={key}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Route, Routes, useParams } from 'react-router-dom';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useMarket } from '@vegaprotocol/markets';
|
||||
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
SidebarButton,
|
||||
@@ -11,12 +9,7 @@ import {
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
|
||||
export const MarketsSidebar = () => {
|
||||
const { marketId } = useParams();
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const { data } = useMarket(marketId);
|
||||
const active =
|
||||
data &&
|
||||
[MarketState.STATE_ACTIVE, MarketState.STATE_PENDING].includes(data.state);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,14 +37,12 @@ export const MarketsSidebar = () => {
|
||||
element={
|
||||
<>
|
||||
<SidebarDivider />
|
||||
{active && (
|
||||
<SidebarButton
|
||||
view={ViewType.Order}
|
||||
icon={VegaIconNames.TICKET}
|
||||
tooltip={t('Order')}
|
||||
routeId={currentRouteId}
|
||||
/>
|
||||
)}
|
||||
<SidebarButton
|
||||
view={ViewType.Order}
|
||||
icon={VegaIconNames.TICKET}
|
||||
tooltip={t('Order')}
|
||||
routeId={currentRouteId}
|
||||
/>
|
||||
<SidebarButton
|
||||
view={ViewType.Info}
|
||||
icon={VegaIconNames.BREAKDOWN}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
variables: {
|
||||
code: referralSet?.id as string,
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs != null
|
||||
args.aggregationEpochs !== null
|
||||
? args.aggregationEpochs
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
@@ -28,6 +30,7 @@ import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -201,7 +204,7 @@ export const Statistics = ({
|
||||
'Total commission (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
description={t('(qUSD)')}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
</StatTile>
|
||||
@@ -233,7 +236,12 @@ export const Statistics = ({
|
||||
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile title={t('Combined volume')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Combined volume (last %s epochs)',
|
||||
details?.windowLength.toString()
|
||||
)}
|
||||
>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -241,24 +249,14 @@ export const Statistics = ({
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Volume to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
<StatTile title={t('Volume to next tier')}>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Epochs to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
<StatTile title={t('Epochs to next tier')}>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -339,11 +337,16 @@ export const Statistics = ({
|
||||
},
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: t(
|
||||
'Commission earned (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
displayName: (
|
||||
<>
|
||||
{t('Commission earned in')} <QUSDTooltip />{' '}
|
||||
{t(
|
||||
'(last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
@@ -373,3 +376,25 @@ export const Statistics = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const QUSDTooltip = () => (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
|
||||
)}
|
||||
</p>
|
||||
{DocsLinks && (
|
||||
<ExternalLink href={DocsLinks.QUANTUM}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
underline={true}
|
||||
>
|
||||
<span>{t('qUSD')}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, type HTMLAttributes } from 'react';
|
||||
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
type TableColumnDefinition = {
|
||||
displayName?: string;
|
||||
displayName?: ReactNode;
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
@@ -46,7 +46,7 @@ export const Table = forwardRef<
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<span>{displayName}</span>
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip}>
|
||||
@@ -102,17 +102,14 @@ export const Table = forwardRef<
|
||||
key={`${i}-${name}`}
|
||||
>
|
||||
{/** display column name in mobile view */}
|
||||
{!noCollapse &&
|
||||
!noHeader &&
|
||||
displayName &&
|
||||
displayName.length > 0 && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
{!noCollapse && !noHeader && displayName && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="px-0 font-mono text-xs md:hidden text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
<span>{d[name]}</span>
|
||||
</td>
|
||||
))}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const Tile = ({
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
@@ -67,11 +67,11 @@ export const CodeTile = ({
|
||||
title={t('Your referral code')}
|
||||
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Tooltip
|
||||
description={
|
||||
<div className="break-all">
|
||||
<span className="text-xl bg-rainbow bg-clip-text text-transparent">
|
||||
<span className="text-xl text-transparent bg-rainbow bg-clip-text">
|
||||
{code}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ jest.mock('@vegaprotocol/accounts', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
|
||||
jest.mock('../../components/welcome-dialog/get-started', () => ({
|
||||
GetStarted: () => <div>GetStarted</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ jest.mock('../../components/withdraw-container', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
|
||||
jest.mock('../../components/welcome-dialog/get-started', () => ({
|
||||
GetStarted: () => <div>GetStarted</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -86,14 +86,14 @@ describe('CurerntVolume', () => {
|
||||
],
|
||||
tierIndex: 0,
|
||||
windowLengthVolume,
|
||||
epochs: 5,
|
||||
windowLength: 5,
|
||||
};
|
||||
|
||||
render(<CurrentVolume {...props} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(formatNumber(windowLengthVolume)).nextElementSibling
|
||||
).toHaveTextContent(`Past ${props.epochs} epochs`);
|
||||
).toHaveTextContent(`Past ${props.windowLength} epochs`);
|
||||
|
||||
expect(
|
||||
screen.getByText(formatNumber(nextTierVolume - windowLengthVolume))
|
||||
|
||||
@@ -30,16 +30,16 @@ export const FeesContainer = () => {
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
|
||||
const volumeDiscountEpochs =
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountEpochs =
|
||||
const referralDiscountWindowLength =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs,
|
||||
referralDiscountEpochs,
|
||||
volumeDiscountEpochs: volumeDiscountWindowLength,
|
||||
referralDiscountEpochs: referralDiscountWindowLength,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
});
|
||||
@@ -101,7 +101,7 @@ export const FeesContainer = () => {
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
epochs={volumeDiscountEpochs}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
@@ -112,7 +112,7 @@ export const FeesContainer = () => {
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountEpochs}
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
</FeeCard>
|
||||
</>
|
||||
@@ -126,6 +126,7 @@ export const FeesContainer = () => {
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
lastEpochVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
@@ -269,12 +270,12 @@ export const CurrentVolume = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
windowLengthVolume,
|
||||
epochs,
|
||||
windowLength,
|
||||
}: {
|
||||
tiers: Array<{ minimumRunningNotionalTakerVolume: string }>;
|
||||
tierIndex: number;
|
||||
windowLengthVolume: number;
|
||||
epochs: number;
|
||||
windowLength: number;
|
||||
}) => {
|
||||
const nextTier = tiers[tierIndex + 1];
|
||||
const requiredForNextTier = nextTier
|
||||
@@ -285,7 +286,7 @@ export const CurrentVolume = ({
|
||||
<div>
|
||||
<Stat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past %s epochs', epochs.toString())}
|
||||
text={t('Past %s epochs', windowLength.toString())}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
<Stat
|
||||
@@ -356,6 +357,7 @@ const VolumeTiers = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
lastEpochVolume,
|
||||
windowLength,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
volumeDiscountFactor: string;
|
||||
@@ -363,6 +365,7 @@ const VolumeTiers = ({
|
||||
}>;
|
||||
tierIndex: number;
|
||||
lastEpochVolume: number;
|
||||
windowLength: number;
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
@@ -380,7 +383,7 @@ const VolumeTiers = ({
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('My volume (last epoch)')}</Th>
|
||||
<Th>{t('My volume (last %s epochs)', windowLength.toString())}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash -e
|
||||
yarn --pure-lockfile
|
||||
app=${1:-trading}
|
||||
envCmd="envCmd="yarn env-cmd -f ./apps/${app}/.env.${2:-mainnet}"
|
||||
envCmd="envCmd="yarn -f ./apps/${app}/.env.${2:-mainnet}"
|
||||
yarn install
|
||||
if [ "${app}" = "trading" ]; then
|
||||
$envCmd yarn nx export trading
|
||||
DIST_LOCATION=dist/apps/trading/exported
|
||||
DIST_LOCATION=dist/apps/trading/exported/
|
||||
else
|
||||
$envCmd yarn nx build ${app}
|
||||
DIST_LOCATION=dist/apps/${app}
|
||||
|
||||
@@ -105,6 +105,8 @@ export interface AccountFields extends Account {
|
||||
breakdown?: AccountFields[];
|
||||
}
|
||||
|
||||
// The total balance of these accounts will be used for the 'used' column in the
|
||||
// collateral table
|
||||
const USE_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
AccountType.ACCOUNT_TYPE_BOND,
|
||||
@@ -150,9 +152,6 @@ const getAssetAccountAggregation = (
|
||||
};
|
||||
|
||||
const breakdown = accounts
|
||||
.filter((a) =>
|
||||
[...USE_ACCOUNT_TYPES, AccountType.ACCOUNT_TYPE_GENERAL].includes(a.type)
|
||||
)
|
||||
.map((a) => ({
|
||||
...a,
|
||||
asset: accounts[0].asset,
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
import { AccountTable } from './accounts-table';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const asset1 = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-1',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
name: 'T BTC',
|
||||
};
|
||||
const asset2 = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-2',
|
||||
symbol: 'aBTC',
|
||||
decimals: 5,
|
||||
name: 'A BTC',
|
||||
};
|
||||
const singleRow = {
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
@@ -13,12 +26,7 @@ const singleRow = {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
asset: asset1,
|
||||
available: '125600000',
|
||||
used: '125600000',
|
||||
total: '251200000',
|
||||
@@ -33,12 +41,7 @@ const secondRow = {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'aBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
asset: asset2,
|
||||
available: '125600001',
|
||||
used: '125600001',
|
||||
total: '251200002',
|
||||
@@ -134,7 +137,7 @@ describe('AccountsTable', () => {
|
||||
|
||||
it('should sort assets', async () => {
|
||||
// 7001-COLL-010
|
||||
const { container } = render(
|
||||
render(
|
||||
<AccountTable
|
||||
rowData={multiRowData}
|
||||
onClickAsset={() => null}
|
||||
@@ -142,13 +145,13 @@ describe('AccountsTable', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const headerCell = screen.getByText('Asset');
|
||||
await userEvent.click(headerCell);
|
||||
const rows = container.querySelectorAll(
|
||||
'.ag-center-cols-container .ag-row'
|
||||
);
|
||||
expect(rows[0].textContent).toContain('aBTC');
|
||||
expect(rows[1].textContent).toContain('tBTC');
|
||||
const headerCell = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((h) => h?.getAttribute('col-id') === 'asset.symbol') as HTMLElement;
|
||||
|
||||
await userEvent.click(within(headerCell).getByText(/asset/i));
|
||||
|
||||
expect(headerCell).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
it('should apply correct formatting in view as user mode', async () => {
|
||||
@@ -176,12 +179,7 @@ describe('AccountsTable', () => {
|
||||
rowData={singleRowData}
|
||||
onClickAsset={() => null}
|
||||
isReadOnly={false}
|
||||
pinnedAsset={{
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
name: 'tBTC',
|
||||
}}
|
||||
pinnedAsset={asset1}
|
||||
/>
|
||||
);
|
||||
await screen.findAllByRole('rowgroup');
|
||||
@@ -213,23 +211,13 @@ describe('AccountsTable', () => {
|
||||
const result = getAccountData([singleRow]);
|
||||
const expected = [
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
asset: asset1,
|
||||
available: '0',
|
||||
balance: '0',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
asset: asset1,
|
||||
available: '0',
|
||||
balance: '125600000',
|
||||
total: '125600000',
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { addDecimal, truncateByChars } from '@vegaprotocol/utils';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParam,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { accountsDataProvider } from './accounts-data-provider';
|
||||
import { TransferForm } from './transfer-form';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const ALLOWED_ACCOUNTS = [
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
];
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||
]);
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
@@ -33,20 +42,10 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
[create]
|
||||
);
|
||||
|
||||
const assets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data
|
||||
.filter(
|
||||
(account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
)
|
||||
.map((account) => ({
|
||||
id: account.asset.id,
|
||||
symbol: account.asset.symbol,
|
||||
name: account.asset.name,
|
||||
decimals: account.asset.decimals,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
}));
|
||||
}, [data]);
|
||||
const accounts = data
|
||||
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
||||
: [];
|
||||
const sortedAccounts = sortBy(accounts, (a) => a.asset.symbol.toLowerCase());
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,10 +64,11 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
<TransferForm
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
assets={sortBy(assets, 'name')}
|
||||
assetId={assetId}
|
||||
feeFactor={param}
|
||||
feeFactor={params.transfer_fee_factor}
|
||||
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||
submitTransfer={transfer}
|
||||
accounts={sortedAccounts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,37 +1,76 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { addDecimal, formatNumber, removeDecimal } from '@vegaprotocol/utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
AddressField,
|
||||
TransferFee,
|
||||
TransferForm,
|
||||
type TransferFormProps,
|
||||
} from './transfer-form';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
|
||||
const renderComponent = (props: TransferFormProps) => {
|
||||
return render(<TransferForm {...props} />);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', { name: 'Confirm transfer' })
|
||||
);
|
||||
};
|
||||
|
||||
const selectAsset = async (asset: {
|
||||
id: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
}) => {
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
fireEvent.change(document.querySelector('select[name="asset"]')!, {
|
||||
target: { value: asset.id },
|
||||
});
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
};
|
||||
|
||||
const amount = '100';
|
||||
const pubKey =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
const pubKey = '1'.repeat(64);
|
||||
const asset = {
|
||||
id: 'eur',
|
||||
symbol: '€',
|
||||
name: 'EUR',
|
||||
decimals: 2,
|
||||
balance: addDecimal(100000, 2), // 1000
|
||||
quantum: '1',
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
pubKeys: [
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
assets: [asset],
|
||||
pubKeys: [pubKey, '2'.repeat(64)],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
asset,
|
||||
balance: '100000',
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset,
|
||||
balance: '10000',
|
||||
},
|
||||
],
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
it('form tooltips correctly displayed', async () => {
|
||||
@@ -40,150 +79,134 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-017
|
||||
// 1003-TRAN-018
|
||||
// 1003-TRAN-019
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
await selectAsset(asset);
|
||||
|
||||
// set valid amount
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amount },
|
||||
});
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(amountInput).toHaveValue(amount);
|
||||
|
||||
userEvent.hover(screen.getByText('Include transfer fee'));
|
||||
const includeTransferLabel = screen.getByText('Include transfer fee');
|
||||
await userEvent.hover(includeTransferLabel);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
'The fee will be taken from the amount you are transferring.'
|
||||
);
|
||||
await userEvent.unhover(screen.getByText('Include transfer fee'));
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
const transferFee = screen.getByText('Transfer fee');
|
||||
await userEvent.hover(transferFee);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
/transfer.fee.factor/
|
||||
);
|
||||
await userEvent.unhover(transferFee);
|
||||
|
||||
userEvent.hover(screen.getByText('Transfer fee'));
|
||||
const amountToBeTransferred = screen.getByText('Amount to be transferred');
|
||||
await userEvent.hover(amountToBeTransferred);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
/without the fee/
|
||||
);
|
||||
await userEvent.unhover(amountToBeTransferred);
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
|
||||
userEvent.hover(screen.getByText('Amount to be transferred'));
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
|
||||
userEvent.hover(screen.getByText('Total amount (with fee)'));
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toBeVisible();
|
||||
});
|
||||
const totalAmountWithFee = screen.getByText('Total amount (with fee)');
|
||||
await userEvent.hover(totalAmountWithFee);
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
/total amount taken from your account/
|
||||
);
|
||||
});
|
||||
|
||||
it('validates a manually entered address', async () => {
|
||||
// 1003-TRAN-012
|
||||
// 1003-TRAN-013
|
||||
// 1003-TRAN-004
|
||||
render(<TransferForm {...props} />);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
renderComponent(props);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
fireEvent.click(toggle);
|
||||
await userEvent.click(toggle);
|
||||
// has switched to input
|
||||
expect(toggle).toHaveTextContent('Select from wallet');
|
||||
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: 'invalid-address' },
|
||||
});
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
||||
});
|
||||
|
||||
// same pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: pubKey },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
||||
});
|
||||
expect(screen.getByLabelText('To Vega key')).toHaveAttribute(
|
||||
'type',
|
||||
'text'
|
||||
);
|
||||
await userEvent.type(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
'invalid-address'
|
||||
);
|
||||
expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent(
|
||||
'Invalid Vega key'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
it('sends transfer from general accounts', async () => {
|
||||
// 1003-TRAN-002
|
||||
// 1003-TRAN-003
|
||||
// 1002-WITH-010
|
||||
// 1003-TRAN-011
|
||||
// 1003-TRAN-014
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
|
||||
expect(keySelect.children).toHaveLength(3);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
pubKey,
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
|
||||
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
await selectAsset(asset);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('1000.00');
|
||||
|
||||
// Test amount validation
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
await screen.findByText(/Amount below minimum requirement/)
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '9999999' },
|
||||
});
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '9999999');
|
||||
expect(
|
||||
await screen.findByText(/cannot transfer more/i)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||
new BigNumber(props.feeFactor).times(amount).toFixed()
|
||||
);
|
||||
|
||||
submit();
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
@@ -198,61 +221,211 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends transfer from vested accounts', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({
|
||||
...props,
|
||||
submitTransfer: mockSubmit,
|
||||
minQuantumMultiple: '100000',
|
||||
});
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${asset.id}`
|
||||
);
|
||||
|
||||
// Check switch back to connected key
|
||||
expect(screen.getByLabelText('To Vega key')).toHaveValue(props.pubKey);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '50');
|
||||
|
||||
expect(await screen.findByText(/Use max to bypass/)).toBeInTheDocument();
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100.00');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
const expectedFee = '0';
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKey,
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles lots of decimal places', async () => {
|
||||
const balance = '904195168829277777';
|
||||
const expectedBalance = '0.904195168829277777';
|
||||
|
||||
const longDecimalAsset = {
|
||||
id: 'assetId',
|
||||
symbol: 'VEGA',
|
||||
name: 'VEGA',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
};
|
||||
|
||||
const account = {
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset: longDecimalAsset,
|
||||
balance,
|
||||
};
|
||||
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
...props,
|
||||
accounts: [account],
|
||||
submitTransfer: mockSubmit,
|
||||
minQuantumMultiple: '100000',
|
||||
});
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(longDecimalAsset);
|
||||
|
||||
const accountSelect = screen.getByLabelText('From account');
|
||||
const option = within(accountSelect)
|
||||
.getAllByRole('option')
|
||||
.find(
|
||||
(o) => o.getAttribute('value') === `${account.type}-${account.asset.id}`
|
||||
);
|
||||
// plus one for disabled 'please select' option
|
||||
|
||||
expect(option).toHaveTextContent(
|
||||
`${AccountTypeMapping[account.type]} (${expectedBalance} ${
|
||||
account.asset.symbol
|
||||
})`
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
accountSelect,
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
|
||||
);
|
||||
|
||||
expect(accountSelect).toHaveValue(
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
|
||||
);
|
||||
|
||||
// Check switch back to connected key
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue(expectedBalance);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKey,
|
||||
asset: longDecimalAsset.id,
|
||||
amount: balance,
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({ ...props, submitTransfer: mockSubmit });
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
await selectAsset(asset);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
|
||||
// 1003-TRAN-022
|
||||
expect(checkbox).not.toBeChecked();
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
// check include fees checkbox
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(checkbox).toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
||||
|
||||
// 1003-TRAN-020
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
@@ -262,68 +435,55 @@ describe('TransferForm', () => {
|
||||
amount
|
||||
);
|
||||
|
||||
submit();
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
amount: removeDecimal(expectedAmount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('validates fields when checkbox is not checked', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
renderComponent(props);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
await selectAsset(asset);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
});
|
||||
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(checkbox).not.toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
@@ -338,33 +498,28 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
pubKeys: ['pubkey-1', 'pubkey-2'],
|
||||
mode: 'select' as const,
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('toggles content and calls onChange', async () => {
|
||||
it('renders correct content by mode prop and calls onChange', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||
const { rerender } = render(
|
||||
<AddressField {...props} onChange={mockOnChange} />
|
||||
);
|
||||
|
||||
// select should be shown as multiple pubkeys provided
|
||||
// select should be shown by default
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Enter manually'));
|
||||
await userEvent.click(screen.getByText('Enter manually'));
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
|
||||
rerender(<AddressField {...props} mode="input" />);
|
||||
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Does not provide select option if there is only a single key', () => {
|
||||
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
minSafe,
|
||||
maxSafe,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@@ -24,35 +25,45 @@ import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
|
||||
interface FormFields {
|
||||
toAddress: string;
|
||||
asset: string;
|
||||
toVegaKey: string;
|
||||
asset: string; // This is used to simply filter the from account list, the fromAccount type should be used in the tx
|
||||
amount: string;
|
||||
fromAccount: string; // AccountType-AssetId
|
||||
}
|
||||
|
||||
interface TransferFormProps {
|
||||
interface Asset {
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
quantum: string;
|
||||
}
|
||||
|
||||
export interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
assets: Array<{
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
asset: Asset;
|
||||
}>;
|
||||
assetId?: string;
|
||||
feeFactor: string | null;
|
||||
minQuantumMultiple: string | null;
|
||||
submitTransfer: (transfer: Transfer) => void;
|
||||
}
|
||||
|
||||
export const TransferForm = ({
|
||||
pubKey,
|
||||
pubKeys,
|
||||
assets,
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
accounts,
|
||||
minQuantumMultiple,
|
||||
}: TransferFormProps) => {
|
||||
const {
|
||||
control,
|
||||
@@ -64,14 +75,65 @@ export const TransferForm = ({
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
asset: initialAssetId,
|
||||
toVegaKey: pubKey || '',
|
||||
},
|
||||
});
|
||||
|
||||
const [toVegaKeyMode, setToVegaKeyMode] = useState<ToVegaKeyMode>('select');
|
||||
|
||||
const assets = sortBy(
|
||||
accounts
|
||||
.filter(
|
||||
(a) =>
|
||||
a.type === AccountType.ACCOUNT_TYPE_GENERAL ||
|
||||
a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
)
|
||||
// Sum the general and vested account balances so the value shown in the asset
|
||||
// dropdown is correct for all transferable accounts
|
||||
.reduce((merged, account) => {
|
||||
const existing = merged.findIndex(
|
||||
(m) => m.asset.id === account.asset.id
|
||||
);
|
||||
if (existing > -1) {
|
||||
const balance = new BigNumber(merged[existing].balance)
|
||||
.plus(new BigNumber(account.balance))
|
||||
.toString();
|
||||
merged[existing] = { ...merged[existing], balance };
|
||||
return merged;
|
||||
}
|
||||
return [...merged, account];
|
||||
}, [] as typeof accounts)
|
||||
.map((account) => ({
|
||||
key: account.asset.id,
|
||||
...account.asset,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
})),
|
||||
(a) => a.symbol.toLowerCase()
|
||||
);
|
||||
|
||||
const selectedPubKey = watch('toVegaKey');
|
||||
const amount = watch('amount');
|
||||
const assetId = watch('asset');
|
||||
const fromAccount = watch('fromAccount');
|
||||
const selectedAssetId = watch('asset');
|
||||
|
||||
// Convert the account type (Type-AssetId) into separate values
|
||||
const [accountType, accountAssetId] = fromAccount
|
||||
? parseFromAccount(fromAccount)
|
||||
: [undefined, undefined];
|
||||
const fromVested = accountType === AccountType.ACCOUNT_TYPE_VESTED_REWARDS;
|
||||
const asset = assets.find((a) => a.id === accountAssetId);
|
||||
|
||||
const account = accounts.find(
|
||||
(a) => a.asset.id === accountAssetId && a.type === accountType
|
||||
);
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Max amount given selected asset and from account
|
||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||
|
||||
const transferAmount = useMemo(() => {
|
||||
if (!amount) return undefined;
|
||||
if (includeFee && feeFactor) {
|
||||
@@ -90,40 +152,34 @@ export const TransferForm = ({
|
||||
);
|
||||
}, [amount, includeFee, transferAmount, feeFactor]);
|
||||
|
||||
const asset = useMemo(() => {
|
||||
return assets.find((a) => a.id === assetId);
|
||||
}, [assets, assetId]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
});
|
||||
|
||||
const [type, assetId] = parseFromAccount(fields.fromAccount);
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
transferAmount,
|
||||
type,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
}
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer, transferAmount]
|
||||
[submitTransfer, transferAmount, assets]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const minViableAmount = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
return minViableAmount;
|
||||
}, [asset]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
const maxAmount = asset ? new BigNumber(asset.balance) : new BigNumber(0);
|
||||
return maxAmount;
|
||||
}, [asset]);
|
||||
|
||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||
useEffect(() => {
|
||||
if (!pubKey) {
|
||||
@@ -137,65 +193,10 @@ export const TransferForm = ({
|
||||
className="text-sm"
|
||||
data-testid="transfer-form"
|
||||
>
|
||||
<TradingFormGroup label="Vega key" labelFor="to-address">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('toAddress', '')}
|
||||
select={
|
||||
<TradingSelect
|
||||
{...register('toAddress')}
|
||||
id="to-address"
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys
|
||||
.filter((pk) => pk !== pubKey) // remove currently selected pubkey
|
||||
.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{pk}
|
||||
</option>
|
||||
))}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to-address"
|
||||
type="text"
|
||||
{...register('toAddress', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
sameKey: (value) => {
|
||||
if (value === pubKey) {
|
||||
return t('Vega key is the same as current key');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.toAddress?.message && (
|
||||
<TradingInputError forInput="to-address">
|
||||
{errors.toAddress.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Asset" labelFor="asset">
|
||||
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
@@ -203,20 +204,16 @@ export const TransferForm = ({
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<AssetOption
|
||||
key={a.id}
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={
|
||||
<Balance
|
||||
balance={formatNumber(a.balance, a.decimals)}
|
||||
symbol={a.symbol}
|
||||
/>
|
||||
}
|
||||
balance={<Balance balance={a.balance} symbol={a.symbol} />}
|
||||
/>
|
||||
))}
|
||||
</TradingRichSelect>
|
||||
@@ -228,6 +225,126 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||
<Controller
|
||||
control={control}
|
||||
name="fromAccount"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
sameAccount: (value) => {
|
||||
if (
|
||||
pubKey === selectedPubKey &&
|
||||
value === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
) {
|
||||
return t(
|
||||
'Cannot transfer to the same account type for the connected key'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
|
||||
)
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
{errors.fromAccount.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<AddressField
|
||||
onChange={() => {
|
||||
setValue('toVegaKey', '');
|
||||
setToVegaKeyMode((curr) => (curr === 'input' ? 'select' : 'input'));
|
||||
}}
|
||||
mode={toVegaKeyMode}
|
||||
select={
|
||||
<TradingSelect
|
||||
{...register('toVegaKey')}
|
||||
disabled={fromVested}
|
||||
id="toVegaKey"
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
fromVested ? null : (
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="toVegaKey"
|
||||
type="text"
|
||||
disabled={fromVested}
|
||||
{...register('toVegaKey', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.toVegaKey?.message && (
|
||||
<TradingInputError forInput="toVegaKey">
|
||||
{errors.toVegaKey.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Amount" labelFor="amount">
|
||||
<TradingInput
|
||||
id="amount"
|
||||
@@ -238,19 +355,66 @@ export const TransferForm = ({
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
minSafe: (v) => {
|
||||
if (!asset || !minQuantumMultiple) return true;
|
||||
|
||||
const value = new BigNumber(v);
|
||||
|
||||
if (value.isZero()) {
|
||||
return t('Amount cannot be 0');
|
||||
}
|
||||
|
||||
const minByQuantumMultiple = toBigNum(
|
||||
minQuantumMultiple,
|
||||
asset.decimals
|
||||
);
|
||||
|
||||
if (fromVested) {
|
||||
// special conditions which let you bypass min transfer rules set by quantum multiple
|
||||
if (value.isGreaterThanOrEqualTo(max)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.isLessThan(minByQuantumMultiple)) {
|
||||
return t(
|
||||
'Amount below minimum requirements for partial transfer. Use max to bypass'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (value.isLessThan(minByQuantumMultiple)) {
|
||||
return t(
|
||||
'Amount below minimum requirement set by transfer.minTransferQuantumMultiple'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(max)) {
|
||||
return t(
|
||||
'You cannot transfer more than your available collateral'
|
||||
);
|
||||
return t('You cannot transfer more than available');
|
||||
}
|
||||
return maxSafe(max)(v);
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{accountBalance && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
onClick={() =>
|
||||
setValue('amount', accountBalance, {
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Use max')}
|
||||
</button>
|
||||
)}
|
||||
{errors.amount?.message && (
|
||||
<TradingInputError forInput="amount">
|
||||
{errors.amount.message}
|
||||
@@ -266,10 +430,10 @@ export const TransferForm = ({
|
||||
<div>
|
||||
<TradingCheckbox
|
||||
name="include-transfer-fee"
|
||||
disabled={!transferAmount}
|
||||
disabled={!transferAmount || fromVested}
|
||||
label={t('Include transfer fee')}
|
||||
checked={includeFee}
|
||||
onCheckedChange={() => setIncludeFee(!includeFee)}
|
||||
onCheckedChange={() => setIncludeFee((x) => !x)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -279,7 +443,7 @@ export const TransferForm = ({
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fee}
|
||||
fee={fromVested ? '0' : fee}
|
||||
decimals={asset?.decimals}
|
||||
/>
|
||||
)}
|
||||
@@ -361,37 +525,30 @@ export const TransferFee = ({
|
||||
);
|
||||
};
|
||||
|
||||
type ToVegaKeyMode = 'input' | 'select';
|
||||
|
||||
interface AddressInputProps {
|
||||
pubKeys: string[] | null;
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
mode: ToVegaKeyMode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
pubKeys,
|
||||
select,
|
||||
input,
|
||||
mode,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(() => {
|
||||
if (pubKeys && pubKeys.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const isInput = mode === 'input';
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
{pubKeys && pubKeys.length > 1 && (
|
||||
{select && input && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="absolute top-0 right-0 ml-auto text-sm underline"
|
||||
onClick={onChange}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
@@ -399,3 +556,7 @@ export const AddressField = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const parseFromAccount = (fromAccountStr: string) => {
|
||||
return fromAccountStr.split('-') as [AccountType, string];
|
||||
};
|
||||
|
||||
@@ -20,7 +20,11 @@ query Candles($marketId: ID!, $interval: Interval!, $since: String!) {
|
||||
code
|
||||
}
|
||||
}
|
||||
candlesConnection(interval: $interval, since: $since) {
|
||||
candlesConnection(
|
||||
interval: $interval
|
||||
since: $since
|
||||
pagination: { last: 5000 }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...CandleFields
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export const CandlesDocument = gql`
|
||||
code
|
||||
}
|
||||
}
|
||||
candlesConnection(interval: $interval, since: $since) {
|
||||
candlesConnection(interval: $interval, since: $since, pagination: {last: 5000}) {
|
||||
edges {
|
||||
node {
|
||||
...CandleFields
|
||||
|
||||
@@ -23,3 +23,4 @@ export * from './lib/type-helpers';
|
||||
export * from './lib/cells/grid-progress-bar';
|
||||
|
||||
export * from './lib/use-datagrid-events';
|
||||
export * from './lib/pagination';
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Pagination } from './pagination';
|
||||
|
||||
describe('Pagination', () => {
|
||||
const props = {
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
},
|
||||
count: 0,
|
||||
onLoad: () => undefined,
|
||||
showRetentionMessage: false,
|
||||
hasDisplayedRows: false,
|
||||
};
|
||||
|
||||
it('renders message for 0 rows', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
render(<Pagination {...props} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText('0 rows loaded')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders mesasge for multiple rows', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
const count = 10;
|
||||
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders message for a single row', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
const count = 1;
|
||||
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText(`${count} row loaded`)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the data rentention message', () => {
|
||||
render(<Pagination {...props} showRetentionMessage={true} />);
|
||||
expect(screen.getByText(/data node retention/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the row filter message', () => {
|
||||
render(<Pagination {...props} count={1} hasDisplayedRows={false} />);
|
||||
expect(screen.getByText(/No rows matching/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const Pagination = ({
|
||||
count,
|
||||
pageInfo,
|
||||
onLoad,
|
||||
hasDisplayedRows,
|
||||
showRetentionMessage,
|
||||
}: {
|
||||
count: number;
|
||||
pageInfo: { hasNextPage?: boolean } | null;
|
||||
onLoad: () => void;
|
||||
hasDisplayedRows: boolean;
|
||||
showRetentionMessage: boolean;
|
||||
}) => {
|
||||
let rowMessage = '';
|
||||
|
||||
if (count && !pageInfo?.hasNextPage) {
|
||||
rowMessage = t('all %s rows loaded', count.toString());
|
||||
} else {
|
||||
if (count === 1) {
|
||||
rowMessage = t('%s row loaded', count.toString());
|
||||
} else {
|
||||
rowMessage = t('%s rows loaded', count.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-1 border-t border-default">
|
||||
<div className="text-xs">
|
||||
{false}
|
||||
{showRetentionMessage &&
|
||||
t(
|
||||
'Depending on data node retention you may not be able see the full history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center text-xs">
|
||||
<span>{rowMessage}</span>
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={onLoad}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{count && hasDisplayedRows === false ? (
|
||||
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
{t('No rows matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -72,29 +72,31 @@ export const DealTicketFeeDetails = ({
|
||||
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
value={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
label={
|
||||
<>
|
||||
{totalDiscountFactor && (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
{t('Fees')}
|
||||
{totalDiscountFactor ? (
|
||||
<Pill size="xxs" intent={Intent.Info} className="ml-1">
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
2
|
||||
)}
|
||||
</Pill>
|
||||
)}
|
||||
{totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
value={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
|
||||
)}
|
||||
@@ -108,7 +110,7 @@ export const DealTicketFeeDetails = ({
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { FeesBreakdown } from './fees-breakdown';
|
||||
|
||||
describe('FeesBreakdown', () => {
|
||||
it('formats fee factors correctly', () => {
|
||||
const feeFactors = {
|
||||
makerFee: '0.00005',
|
||||
infrastructureFee: '0.001',
|
||||
liquidityFee: '0.5',
|
||||
};
|
||||
const fees = {
|
||||
makerFee: '100',
|
||||
infrastructureFee: '100',
|
||||
liquidityFee: '100',
|
||||
};
|
||||
const props = {
|
||||
totalFeeAmount: '100',
|
||||
fees,
|
||||
feeFactors,
|
||||
symbol: 'USD',
|
||||
decimals: 2,
|
||||
referralDiscountFactor: '0.01',
|
||||
volumeDiscountFactor: '0.01',
|
||||
};
|
||||
render(<FeesBreakdown {...props} />);
|
||||
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
|
||||
'0.005%'
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Infrastructure fee').nextElementSibling
|
||||
).toHaveTextContent('0.1%');
|
||||
expect(
|
||||
screen.getByText('Liquidity fee').nextElementSibling
|
||||
).toHaveTextContent('50%');
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@ const FeesBreakdownItem = ({
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
{factor && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(new BigNumber(factor).times(100), 2)}
|
||||
{formatNumberPercentage(new BigNumber(factor).times(100))}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
@@ -79,7 +79,11 @@ export const FeesBreakdown = ({
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { volumeDiscount, referralDiscount } = getDiscountedFee(
|
||||
const {
|
||||
discountedFee: discountedTotalFeeAmount,
|
||||
volumeDiscount,
|
||||
referralDiscount,
|
||||
} = getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
@@ -131,7 +135,7 @@ export const FeesBreakdown = ({
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
|
||||
value={totalFeeAmount}
|
||||
value={discountedTotalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const data: EstimateFeesQuery = {
|
||||
epoch: {
|
||||
id: '2',
|
||||
id: '3',
|
||||
},
|
||||
volumeDiscountStats: {
|
||||
edges: [
|
||||
@@ -117,7 +117,7 @@ describe('useEstimateFees', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 0 discounts if discount stats are not at the current epoch', () => {
|
||||
it('returns 0 discounts if discount stats are not at the current epoch -1', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEstimateFees(
|
||||
{
|
||||
@@ -136,7 +136,7 @@ describe('useEstimateFees', () => {
|
||||
});
|
||||
|
||||
it('returns discounts', () => {
|
||||
data.epoch.id = '1';
|
||||
data.epoch.id = '2';
|
||||
const { result } = renderHook(() =>
|
||||
useEstimateFees(
|
||||
{
|
||||
|
||||
@@ -34,14 +34,13 @@ export const useEstimateFees = (
|
||||
skip: !pubKey || !order?.size || !order?.price || order.postOnly,
|
||||
});
|
||||
const data = loading ? currentData || previousData : currentData;
|
||||
const atEpoch = (Number(data?.epoch.id) || 0) - 1;
|
||||
const volumeDiscountFactor =
|
||||
(data?.volumeDiscountStats.edges[0]?.node.atEpoch.toString() ===
|
||||
data?.epoch.id &&
|
||||
(data?.volumeDiscountStats.edges[0]?.node.atEpoch === atEpoch &&
|
||||
data?.volumeDiscountStats.edges[0]?.node.discountFactor) ||
|
||||
'0';
|
||||
const referralDiscountFactor =
|
||||
(data?.referralSetStats.edges[0]?.node.atEpoch.toString() ===
|
||||
data?.epoch.id &&
|
||||
(data?.referralSetStats.edges[0]?.node.atEpoch === atEpoch &&
|
||||
data?.referralSetStats.edges[0]?.node.discountFactor) ||
|
||||
'0';
|
||||
if (order?.postOnly) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { MockedResponse } from '@apollo/react-testing';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
renderHook,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
NodeCheckTimeUpdateSubscription,
|
||||
@@ -11,30 +17,33 @@ import {
|
||||
NodeCheckTimeUpdateDocument,
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import type { RowDataProps } from './row-data';
|
||||
import { POLL_INTERVAL } from './row-data';
|
||||
import {
|
||||
POLL_INTERVAL,
|
||||
Result,
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import type { HeaderEntry } from '@vegaprotocol/apollo-client';
|
||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client', () => ({
|
||||
useHeaderStore: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
const statsQueryMock: MockedResponse<NodeCheckQuery> = {
|
||||
const mockStatsQuery = (
|
||||
blockHeight = '1234'
|
||||
): MockedResponse<NodeCheckQuery> => ({
|
||||
request: {
|
||||
query: NodeCheckDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight: '1234', // the actual value used in the component is the value from the header store
|
||||
blockHeight,
|
||||
vegaTime: new Date().toISOString(),
|
||||
chainId: 'test-chain-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const subMock: MockedResponse<NodeCheckTimeUpdateSubscription> = {
|
||||
request: {
|
||||
@@ -59,18 +68,6 @@ global.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||
},
|
||||
]);
|
||||
|
||||
const mockHeaders = (
|
||||
url: string,
|
||||
headers: Partial<HeaderEntry> = {
|
||||
blockHeight: 100,
|
||||
timestamp: new Date(),
|
||||
}
|
||||
) => {
|
||||
(useHeaderStore as unknown as jest.Mock).mockReturnValue({
|
||||
[url]: headers,
|
||||
});
|
||||
};
|
||||
|
||||
const renderComponent = (
|
||||
props: RowDataProps,
|
||||
queryMock: MockedResponse<NodeCheckQuery>,
|
||||
@@ -86,6 +83,98 @@ const renderComponent = (
|
||||
);
|
||||
};
|
||||
|
||||
describe('useNodeSubscriptionStatus', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
const mockWrapper =
|
||||
(withData = false) =>
|
||||
({ children }: { children: React.ReactNode }) =>
|
||||
(
|
||||
<MockedProvider mocks={withData ? [subMock, subMock, subMock] : []}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
);
|
||||
it('results initially as loading', async () => {
|
||||
const { result } = renderHook(() => useNodeSubscriptionStatus(), {
|
||||
wrapper: mockWrapper(true),
|
||||
});
|
||||
expect(result.current.status).toBe(Result.Loading);
|
||||
});
|
||||
it('results as successful when data received', async () => {
|
||||
const { result } = renderHook(() => useNodeSubscriptionStatus(), {
|
||||
wrapper: mockWrapper(true),
|
||||
});
|
||||
expect(result.current.status).toBe(Result.Loading);
|
||||
await act(() => {
|
||||
jest.advanceTimersByTime(SUBSCRIPTION_TIMEOUT);
|
||||
});
|
||||
expect(result.current.status).toBe(Result.Successful);
|
||||
});
|
||||
it('result as failed when no data received', async () => {
|
||||
const { result } = renderHook(() => useNodeSubscriptionStatus(), {
|
||||
wrapper: mockWrapper(false),
|
||||
});
|
||||
expect(result.current.status).toBe(Result.Loading);
|
||||
await act(() => {
|
||||
jest.advanceTimersByTime(SUBSCRIPTION_TIMEOUT);
|
||||
});
|
||||
expect(result.current.status).toBe(Result.Failed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useNodeBasicStatus', () => {
|
||||
const mockWrapper =
|
||||
(withData = false) =>
|
||||
({ children }: { children: React.ReactNode }) =>
|
||||
(
|
||||
<MockedProvider mocks={withData ? [mockStatsQuery('1234')] : []}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
);
|
||||
it('results initially as loading', async () => {
|
||||
const { result } = renderHook(() => useNodeBasicStatus(), {
|
||||
wrapper: mockWrapper(true),
|
||||
});
|
||||
expect(result.current.status).toBe(Result.Loading);
|
||||
expect(result.current.currentBlockHeight).toBeNaN();
|
||||
});
|
||||
it('results as successful when data received', async () => {
|
||||
const { result } = renderHook(() => useNodeBasicStatus(), {
|
||||
wrapper: mockWrapper(true),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toBe(Result.Successful);
|
||||
expect(result.current.currentBlockHeight).toBe(1234);
|
||||
});
|
||||
});
|
||||
it('result as failed when no data received', async () => {
|
||||
const { result } = renderHook(() => useNodeBasicStatus(), {
|
||||
wrapper: mockWrapper(false),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toBe(Result.Failed);
|
||||
expect(result.current.currentBlockHeight).toBeNaN();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RowData', () => {
|
||||
const props = {
|
||||
id: '0',
|
||||
@@ -94,9 +183,13 @@ describe('RowData', () => {
|
||||
onBlockHeight: jest.fn(),
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('radio button enabled after stats query successful', async () => {
|
||||
mockHeaders(props.url);
|
||||
render(renderComponent(props, statsQueryMock, subMock));
|
||||
render(renderComponent(props, mockStatsQuery('100'), subMock));
|
||||
|
||||
// radio should be enabled until query resolves
|
||||
expect(
|
||||
@@ -127,8 +220,6 @@ describe('RowData', () => {
|
||||
});
|
||||
|
||||
it('radio button still enabled if query fails', async () => {
|
||||
mockHeaders(props.url, {});
|
||||
|
||||
const failedQueryMock: MockedResponse<NodeCheckQuery> = {
|
||||
request: {
|
||||
query: NodeCheckDocument,
|
||||
@@ -178,12 +269,11 @@ describe('RowData', () => {
|
||||
|
||||
it('highlights rows with a slow block height', async () => {
|
||||
const blockHeight = 100;
|
||||
mockHeaders(props.url, { blockHeight });
|
||||
|
||||
const { rerender } = render(
|
||||
renderComponent(
|
||||
{ ...props, highestBlock: blockHeight + BLOCK_THRESHOLD },
|
||||
statsQueryMock,
|
||||
mockStatsQuery(String(blockHeight)),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
@@ -201,7 +291,7 @@ describe('RowData', () => {
|
||||
rerender(
|
||||
renderComponent(
|
||||
{ ...props, highestBlock: blockHeight + BLOCK_THRESHOLD + 1 },
|
||||
statsQueryMock,
|
||||
mockStatsQuery(String(blockHeight)),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
@@ -216,7 +306,7 @@ describe('RowData', () => {
|
||||
...props,
|
||||
id: CUSTOM_NODE_KEY,
|
||||
},
|
||||
statsQueryMock,
|
||||
mockStatsQuery('1234'),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
@@ -230,16 +320,17 @@ describe('RowData', () => {
|
||||
it('updates highest block after new header received', async () => {
|
||||
const mockOnBlockHeight = jest.fn();
|
||||
const blockHeight = 200;
|
||||
mockHeaders(props.url, { blockHeight });
|
||||
render(
|
||||
renderComponent(
|
||||
{ ...props, onBlockHeight: mockOnBlockHeight },
|
||||
statsQueryMock,
|
||||
mockStatsQuery(String(blockHeight)),
|
||||
subMock
|
||||
)
|
||||
);
|
||||
|
||||
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
|
||||
await waitFor(() => {
|
||||
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
|
||||
});
|
||||
});
|
||||
|
||||
it('should poll the query unless an errors is returned', async () => {
|
||||
@@ -275,7 +366,6 @@ describe('RowData', () => {
|
||||
};
|
||||
};
|
||||
|
||||
mockHeaders(props.url);
|
||||
const statsQueryMock1 = createStatsQueryMock('1234');
|
||||
const statsQueryMock2 = createStatsQueryMock('1235');
|
||||
const statsQueryMock3 = createFailedStatsQueryMock();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -12,6 +10,7 @@ import {
|
||||
import { LayoutCell } from './layout-cell';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
export const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export interface RowDataProps {
|
||||
@@ -21,15 +20,39 @@ export interface RowDataProps {
|
||||
onBlockHeight: (blockHeight: number) => void;
|
||||
}
|
||||
|
||||
export const RowData = ({
|
||||
id,
|
||||
url,
|
||||
highestBlock,
|
||||
onBlockHeight,
|
||||
}: RowDataProps) => {
|
||||
const [subFailed, setSubFailed] = useState(false);
|
||||
const [time, setTime] = useState<number>();
|
||||
// no use of data here as we need the data nodes reference to block height
|
||||
export enum Result {
|
||||
Successful,
|
||||
Failed,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export const useNodeSubscriptionStatus = () => {
|
||||
const [status, setStatus] = useState<Result>(Result.Loading);
|
||||
const { data, error } = useNodeCheckTimeUpdateSubscription();
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setStatus(Result.Failed);
|
||||
}
|
||||
if (data?.busEvents && data.busEvents.length > 0) {
|
||||
setStatus(Result.Successful);
|
||||
}
|
||||
// set as failed when no data received after SUBSCRIPTION_TIMEOUT ms
|
||||
const timeout = setTimeout(() => {
|
||||
if (!data || error) {
|
||||
setStatus(Result.Failed);
|
||||
}
|
||||
}, SUBSCRIPTION_TIMEOUT);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [data, error]);
|
||||
|
||||
return { status };
|
||||
};
|
||||
|
||||
export const useNodeBasicStatus = () => {
|
||||
const [status, setStatus] = useState<Result>(Result.Loading);
|
||||
|
||||
const { data, error, loading, startPolling, stopPolling } = useNodeCheckQuery(
|
||||
{
|
||||
pollInterval: POLL_INTERVAL,
|
||||
@@ -38,28 +61,7 @@ export const RowData = ({
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
const headerStore = useHeaderStore();
|
||||
const headers = headerStore[url];
|
||||
|
||||
const {
|
||||
data: subData,
|
||||
error: subError,
|
||||
loading: subLoading,
|
||||
} = useNodeCheckTimeUpdateSubscription();
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (!subData) {
|
||||
setSubFailed(true);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [subData]);
|
||||
|
||||
// handle polling
|
||||
useEffect(() => {
|
||||
const handleStartPoll = () => {
|
||||
if (error) return;
|
||||
@@ -83,56 +85,57 @@ export const RowData = ({
|
||||
};
|
||||
}, [startPolling, stopPolling, error]);
|
||||
|
||||
// measure response time
|
||||
const currentBlockHeight = parseInt(
|
||||
data?.statistics.blockHeight || 'NONE',
|
||||
10
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
setStatus(Result.Loading);
|
||||
return;
|
||||
}
|
||||
if (!error && !isNaN(currentBlockHeight)) {
|
||||
setStatus(Result.Successful);
|
||||
return;
|
||||
}
|
||||
setStatus(Result.Failed);
|
||||
}, [currentBlockHeight, error, loading]);
|
||||
|
||||
return {
|
||||
status,
|
||||
currentBlockHeight,
|
||||
};
|
||||
};
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
||||
// every time we get data measure response speed
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } =
|
||||
(requests.length && requests[requests.length - 1]) || {};
|
||||
setTime(duration);
|
||||
}, [url, data]);
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const RowData = ({
|
||||
id,
|
||||
url,
|
||||
highestBlock,
|
||||
onBlockHeight,
|
||||
}: RowDataProps) => {
|
||||
const { status: subStatus } = useNodeSubscriptionStatus();
|
||||
const { status, currentBlockHeight } = useNodeBasicStatus();
|
||||
const { responseTime } = useResponseTime(url, currentBlockHeight); // measure response time (ms) every time we get data (block height)
|
||||
useEffect(() => {
|
||||
if (headers?.blockHeight) {
|
||||
onBlockHeight(headers.blockHeight);
|
||||
if (!isNaN(currentBlockHeight)) {
|
||||
onBlockHeight(currentBlockHeight);
|
||||
}
|
||||
}, [headers?.blockHeight, onBlockHeight]);
|
||||
|
||||
const getHasError = () => {
|
||||
// the stats query errored
|
||||
if (error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if we are still awaiting a header entry its not an error
|
||||
// we are still waiting for the query to resolve
|
||||
if (!headers) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// highlight this node as 'error' if its more than BLOCK_THRESHOLD blocks behind the most
|
||||
// advanced node
|
||||
if (
|
||||
highestBlock !== null &&
|
||||
headers.blockHeight < highestBlock - BLOCK_THRESHOLD
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getSubFailed = (
|
||||
subError: ApolloError | undefined,
|
||||
subFailed: boolean
|
||||
) => {
|
||||
if (subError) return true;
|
||||
if (subFailed) return true;
|
||||
return false;
|
||||
};
|
||||
}, [currentBlockHeight, onBlockHeight]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -143,72 +146,58 @@ export const RowData = ({
|
||||
)}
|
||||
<LayoutCell
|
||||
label={t('Response time')}
|
||||
isLoading={!error && loading}
|
||||
hasError={Boolean(error)}
|
||||
isLoading={status === Result.Loading}
|
||||
hasError={status === Result.Failed}
|
||||
dataTestId="response-time-cell"
|
||||
>
|
||||
{getResponseTimeDisplayValue(time, error)}
|
||||
{display(status, formatResponseTime(responseTime))}
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
label={t('Block')}
|
||||
isLoading={loading}
|
||||
hasError={getHasError()}
|
||||
isLoading={status === Result.Loading}
|
||||
hasError={
|
||||
status === Result.Failed ||
|
||||
(highestBlock != null &&
|
||||
!isNaN(currentBlockHeight) &&
|
||||
currentBlockHeight < highestBlock - BLOCK_THRESHOLD)
|
||||
}
|
||||
dataTestId="block-height-cell"
|
||||
>
|
||||
<span
|
||||
data-testid="query-block-height"
|
||||
data-query-block-height={
|
||||
error ? 'failed' : data?.statistics.blockHeight
|
||||
status === Result.Failed ? 'failed' : currentBlockHeight
|
||||
}
|
||||
>
|
||||
{getBlockDisplayValue(headers?.blockHeight, error)}
|
||||
{display(status, currentBlockHeight)}
|
||||
</span>
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
label={t('Subscription')}
|
||||
isLoading={subFailed ? false : subLoading}
|
||||
hasError={getSubFailed(subError, subFailed)}
|
||||
isLoading={subStatus === Result.Loading}
|
||||
hasError={subStatus === Result.Failed}
|
||||
dataTestId="subscription-cell"
|
||||
>
|
||||
{getSubscriptionDisplayValue(subFailed, subData?.busEvents, subError)}
|
||||
{display(subStatus, t('Yes'), t('No'))}
|
||||
</LayoutCell>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getResponseTimeDisplayValue = (
|
||||
responseTime?: number,
|
||||
error?: ApolloError
|
||||
) => {
|
||||
if (error) {
|
||||
return t('n/a');
|
||||
}
|
||||
if (typeof responseTime === 'number') {
|
||||
return `${Number(responseTime).toFixed(2)}ms`;
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
const formatResponseTime = (time: number | undefined) =>
|
||||
time != null ? `${Number(time).toFixed(2)}ms` : '-';
|
||||
|
||||
const getBlockDisplayValue = (block?: number, error?: ApolloError) => {
|
||||
if (error) {
|
||||
return t('n/a');
|
||||
}
|
||||
if (block) {
|
||||
return block;
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
const getSubscriptionDisplayValue = (
|
||||
subFailed: boolean,
|
||||
events?: { id: string }[] | null,
|
||||
error?: ApolloError
|
||||
const display = (
|
||||
status: Result,
|
||||
yes: string | number | undefined,
|
||||
no = t('n/a')
|
||||
) => {
|
||||
if (subFailed || error) {
|
||||
return t('No');
|
||||
switch (status) {
|
||||
case Result.Successful:
|
||||
return yes;
|
||||
case Result.Failed:
|
||||
return no;
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
if (events?.length) {
|
||||
return t('Yes');
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
@@ -84,6 +84,7 @@ export const DocsLinks = VEGA_DOCS_URL
|
||||
ETH_DATA_SOURCES: `${VEGA_DOCS_URL}/concepts/trading-on-vega/data-sources#ethereum-data-sources`,
|
||||
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
|
||||
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
|
||||
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -125,6 +126,11 @@ export const useEtherscanLink = () => {
|
||||
return link;
|
||||
};
|
||||
|
||||
// Console pages
|
||||
export const CONSOLE_TRANSFER = '#/portfolio/assets/transfer';
|
||||
export const CONSOLE_TRANSFER_ASSET =
|
||||
'#/portfolio/assets/transfer?assetId=:assetId';
|
||||
|
||||
// Governance pages
|
||||
export const TOKEN_NEW_MARKET_PROPOSAL = '/proposals/propose/new-market';
|
||||
export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user