Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3612a18c | ||
|
|
7a91f48bcb | ||
|
|
b43ea0f60b | ||
|
|
6e31fb03ae | ||
|
|
d76fa13c5d | ||
|
|
89aeed6305 | ||
|
|
6e677084a3 | ||
|
|
8069aa5ee7 | ||
|
|
b0169a0d39 | ||
|
|
06cfd79415 | ||
|
|
443220283c | ||
|
|
a642bf8ce4 | ||
|
|
20233db706 | ||
|
|
d4f50eb70c | ||
|
|
e5d3f90d45 | ||
|
|
673c896e2f | ||
|
|
deea63fa5e | ||
|
|
d31333538b | ||
|
|
4af9979a21 | ||
|
|
6421cf87c6 | ||
|
|
0ebfab64ff | ||
|
|
142f08343b | ||
|
|
61aa45a9ed | ||
|
|
4c95db5fb3 | ||
|
|
3072b7824f | ||
|
|
0580e90171 | ||
|
|
c440abc77d | ||
|
|
fbafc726a4 | ||
|
|
dd1890d8c6 | ||
|
|
c8e624eaba | ||
|
|
cc6629ad27 | ||
|
|
9838efa00e | ||
|
|
dac7142a98 |
@@ -39,7 +39,6 @@ context('Market page', { tags: '@regression' }, function () {
|
||||
cy.contains('Test market 1').click();
|
||||
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
|
||||
cy.validate_element_from_table('Name', 'Test market 1');
|
||||
cy.validate_element_from_table('Market ID', this.createdMarketId);
|
||||
cy.validate_element_from_table('Trading Mode', 'Opening auction');
|
||||
cy.validate_element_from_table('Market Decimal Places', '5');
|
||||
cy.validate_element_from_table('Position Decimal Places', '5');
|
||||
|
||||
@@ -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
@@ -537,10 +537,10 @@ describe(
|
||||
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
|
||||
cy.reload();
|
||||
getProposalFromTitle('Governance transfer proposal').within(() => {
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(governanceTransferToggle).click();
|
||||
cy.getByTestId('proposal-transfer-details-table').within(() => {
|
||||
getProposalInformationFromTable('Source Type')
|
||||
@@ -590,7 +590,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
|
||||
getProposalInformationFromTable('Error details')
|
||||
.invoke('text')
|
||||
.and('eq', 'Governance transfer invalid transfer id not found');
|
||||
|
||||
@@ -909,6 +909,14 @@
|
||||
"BenefitTierReferralDiscountFactorDescription": "The proportion of the referee's taker fees to be discounted",
|
||||
"BenefitTierReferralRewardFactor": "Referral reward factor",
|
||||
"BenefitTierReferralRewardFactorDescription": "The proportion of the referee's taker fees to be rewarded to the referrer",
|
||||
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
|
||||
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
|
||||
"BenefitTierMinimumQuantumBalance": "Minimum quantum balance",
|
||||
"BenefitTierMinimumQuantumBalanceDescription": "The minimum amount of the vesting token to qualify",
|
||||
"BenefitTierVestingMultiplier": "Vesting multiplier",
|
||||
"BenefitTierVestingMultiplierDescription": "Vesting multiplier for the tier",
|
||||
"BenefitTierRewardMultiplier": "Reward multiplier",
|
||||
"BenefitTierRewardMultiplierDescription": "The multiplier",
|
||||
"StakingTiers": "Staking tiers",
|
||||
"StakingTierMinimumStakedTokens": "Minimum staked tokens",
|
||||
"StakingTierMinimumStakedTokensDescription": "Required number of governance tokens ($VEGA) a referrer must have staked to receive the multiplier",
|
||||
|
||||
+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', () => {
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './proposal-update-benefit-tiers-details';
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
appState: {
|
||||
decimals: 2,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockVestingBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockActivityStreakBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
describe('ProposalUpdateBenefitTiers', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalUpdateBenefitTiers proposal={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateNetworkParameter', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are relevant fields that are empty', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a valid vesting benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
|
||||
);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 3')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Minimum quantum balance').length).toBe(3);
|
||||
expect(screen.getAllByText('Reward multiplier').length).toBe(3);
|
||||
|
||||
expect(screen.getByText('0.00000000000001')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.05x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0.0000005')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.1x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0.00001')).toBeInTheDocument();
|
||||
expect(screen.getByText('10x')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a valid activity streak benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers
|
||||
proposal={mockActivityStreakBenefitTierProposal}
|
||||
/>
|
||||
);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 2')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Minimum activity streak').length).toBe(2);
|
||||
expect(screen.getAllByText('Vesting multiplier').length).toBe(2);
|
||||
expect(screen.getAllByText('Reward multiplier').length).toBe(2);
|
||||
|
||||
expect(screen.getByText('10000')).toBeInTheDocument();
|
||||
expect(screen.getByText('5x')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.1x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('10000000000000')).toBeInTheDocument();
|
||||
expect(screen.getByText('100x')).toBeInTheDocument();
|
||||
expect(screen.getByText('10x')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
formatMinimumStakedTokens,
|
||||
formatReferralRewardMultiplier,
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
// These types are not generated as it's not known how dynamic these are
|
||||
type VestingBenefitTier = {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
};
|
||||
|
||||
type ActivityStreakBenefitTier = {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
};
|
||||
|
||||
export type BenefitTiers =
|
||||
| Array<ActivityStreakBenefitTier>
|
||||
| Array<VestingBenefitTier>;
|
||||
|
||||
export function getBenefitTiers(json: string): BenefitTiers {
|
||||
try {
|
||||
const parsed = JSON.parse(json);
|
||||
return parsed.tiers;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const formatVolumeDiscountFactor = (value: string) => {
|
||||
return formatNumberPercentage(new BigNumber(value).times(100));
|
||||
};
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Special rendered for network proposals that change any benefit tiers,
|
||||
* which is detected by:
|
||||
* 1) it being a network parameter change
|
||||
* 2) the name of the field ending in `.benefitTiers`
|
||||
*
|
||||
* It only renders known fields so that they can be formatted correctly.
|
||||
*/
|
||||
export const ProposalUpdateBenefitTiers = ({
|
||||
proposal,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (
|
||||
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
|
||||
const benefitTiers = getBenefitTiers(benefitTiersString);
|
||||
|
||||
if (!benefitTiers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="proposal-update-benefit-tiers">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
{benefitTiers && (
|
||||
<div
|
||||
className="mb-6"
|
||||
data-testid="proposal-volume-discount-program-benefit-tiers"
|
||||
>
|
||||
<h3 className="mb-3 uppercase font-semibold text-lg">
|
||||
{t('BenefitTiers')}
|
||||
</h3>
|
||||
<KeyValueTable>
|
||||
{benefitTiers
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(a.reward_multiplier) - Number(b.reward_multiplier)
|
||||
)
|
||||
.map((benefitTier, index) => (
|
||||
<div className="mb-4" key={index}>
|
||||
<h4 className="font-semibold uppercase">
|
||||
Tier {index + 1}
|
||||
</h4>
|
||||
{'minimum_activity_streak' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`mas-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'BenefitTierMinimumActivityStreakDescription'
|
||||
)}
|
||||
>
|
||||
<span>{t('BenefitTierMinimumActivityStreak')}</span>
|
||||
</Tooltip>
|
||||
|
||||
{benefitTier.minimum_activity_streak}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'minimum_quantum_balance' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`mqb-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'BenefitTierMinimumQuantumBalanceDescription'
|
||||
)}
|
||||
>
|
||||
<span>{t('BenefitTierMinimumQuantumBalance')}</span>
|
||||
</Tooltip>
|
||||
|
||||
{formatMinimumStakedTokens(
|
||||
benefitTier.minimum_quantum_balance,
|
||||
18
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'vesting_multiplier' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`vm-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t('BenefitTierVestingMultiplier')}
|
||||
>
|
||||
<span>{t('BenefitTierVestingMultiplier')}</span>
|
||||
</Tooltip>
|
||||
{formatReferralRewardMultiplier(
|
||||
benefitTier.vesting_multiplier
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'reward_multiplier' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`rm-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip description={t('BenefitTierRewardMultiplier')}>
|
||||
<span>{t('BenefitTierRewardMultiplier')}</span>
|
||||
</Tooltip>
|
||||
{formatReferralRewardMultiplier(
|
||||
benefitTier.reward_multiplier
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
</div>
|
||||
)}
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
@@ -243,6 +244,14 @@ export const Proposal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
|
||||
proposal.terms.change.networkParameter.key.slice(-13) ===
|
||||
'.benefitTiers' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateBenefitTiers proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{governanceTransferDetails}
|
||||
|
||||
<div className="mb-10">
|
||||
|
||||
@@ -22,18 +22,19 @@ const colUpdatedAt = '[col-id="updatedAt"] button';
|
||||
|
||||
const headers = [
|
||||
'Party',
|
||||
'Status',
|
||||
'Commitment (tDAI)',
|
||||
'Obligation',
|
||||
'Fee',
|
||||
'Adjusted stake share',
|
||||
'Share',
|
||||
'Live supplied liquidity',
|
||||
'Live time fraction on book',
|
||||
'Fees accrued this epoch',
|
||||
'Live time on book',
|
||||
'Live liquidity quality score (%)',
|
||||
'Last time fraction on the book',
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Status',
|
||||
'Created',
|
||||
'Updated',
|
||||
];
|
||||
|
||||
+4
-8
@@ -3,17 +3,16 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
|
||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
||||
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
|
||||
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
@@ -26,6 +25,3 @@ NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Routes } from '../../lib/links';
|
||||
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Statistics } from './referral-statistics';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
|
||||
const RELOAD_DELAY = 3000;
|
||||
|
||||
@@ -32,6 +33,7 @@ const validateCode = (value: string) => {
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = () => {
|
||||
const program = useReferralProgram();
|
||||
const navigate = useNavigate();
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
@@ -237,7 +239,7 @@ export const ApplyCodeForm = () => {
|
||||
{previewData ? (
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
|
||||
<Statistics data={previewData} as="referee" />
|
||||
<Statistics data={previewData} program={program} as="referee" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
query Referees($code: ID!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
|
||||
@@ -10,6 +10,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type RefereesQueryVariables = Types.Exact<{
|
||||
code: Types.Scalars['ID'];
|
||||
aggregationDays?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ export type RefereesQuery = { __typename?: 'Query', referralSetReferees: { __typ
|
||||
|
||||
|
||||
export const RefereesDocument = gql`
|
||||
query Referees($code: ID!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
@@ -42,7 +42,7 @@ export const RefereesDocument = gql`
|
||||
* const { data, loading, error } = useRefereesQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* aggregationDays: // value for 'aggregationDays'
|
||||
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string } } | null> } };
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetStatsDocument = gql`
|
||||
@@ -25,6 +25,7 @@ export const ReferralSetStatsDocument = gql`
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
|
||||
const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type UseReferralArgs = (
|
||||
| { code: string }
|
||||
| { pubKey: string | null; role: Role }
|
||||
) & {
|
||||
aggregationDays?: number;
|
||||
aggregationEpochs?: number;
|
||||
};
|
||||
|
||||
const prepareVariables = (
|
||||
@@ -70,9 +70,9 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: referralSet?.id as string,
|
||||
aggregationDays:
|
||||
args.aggregationDays != null
|
||||
? args.aggregationDays
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs !== null
|
||||
? args.aggregationEpochs
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
},
|
||||
skip: !referralSet?.id,
|
||||
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import { CreateCodeContainer } from './create-code-form';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
@@ -27,25 +29,31 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
||||
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();
|
||||
|
||||
const program = useReferralProgram();
|
||||
|
||||
const { data: referee } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
if (referee?.code) {
|
||||
return <Statistics data={referee} as="referee" />;
|
||||
return <Statistics data={referee} program={program} as="referee" />;
|
||||
}
|
||||
|
||||
if (referrer?.code) {
|
||||
return <Statistics data={referrer} as="referrer" />;
|
||||
return <Statistics data={referrer} program={program} as="referrer" />;
|
||||
}
|
||||
|
||||
return <CreateCodeContainer />;
|
||||
@@ -53,14 +61,16 @@ export const ReferralStatistics = () => {
|
||||
|
||||
export const Statistics = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers, details } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data.code,
|
||||
@@ -71,6 +81,13 @@ export const Statistics = ({
|
||||
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const stats =
|
||||
statsData?.referralSetStats.edges &&
|
||||
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
||||
@@ -86,10 +103,13 @@ export const Statistics = ({
|
||||
const runningVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||
: 0;
|
||||
const referrerVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referrerTakerVolume)
|
||||
: 0;
|
||||
const multiplier = statsAvailable
|
||||
? Number(statsAvailable.rewardsMultiplier)
|
||||
: 1;
|
||||
const finalCommissionValue = !isNaN(multiplier)
|
||||
const finalCommissionValue = isNaN(multiplier)
|
||||
? baseCommissionValue
|
||||
: multiplier * baseCommissionValue;
|
||||
|
||||
@@ -102,9 +122,9 @@ export const Statistics = ({
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue =
|
||||
currentBenefitTierValue &&
|
||||
benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
@@ -117,7 +137,13 @@ export const Statistics = ({
|
||||
: 0;
|
||||
|
||||
const baseCommissionTile = (
|
||||
<StatTile title={t('Base commission rate')}>
|
||||
<StatTile
|
||||
title={t('Base commission rate')}
|
||||
description={t('(Combined set volume %s over last %s epochs)', [
|
||||
compactNumFormat.format(runningVolumeValue),
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString(),
|
||||
])}
|
||||
>
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
@@ -133,7 +159,16 @@ export const Statistics = ({
|
||||
</StatTile>
|
||||
);
|
||||
const finalCommissionTile = (
|
||||
<StatTile title={t('Final commission rate')}>
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
||||
finalCommissionValue * 100
|
||||
}%)`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{finalCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
@@ -142,12 +177,21 @@ export const Statistics = ({
|
||||
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = <CodeTile code={data?.code} />;
|
||||
const createdAtTile = (
|
||||
<StatTile title={t('Created at')}>
|
||||
<span className="text-3xl">
|
||||
{getDateFormat().format(new Date(data.createdAt))}
|
||||
</span>
|
||||
const codeTile = (
|
||||
<CodeTile
|
||||
code={data?.code}
|
||||
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
||||
/>
|
||||
);
|
||||
|
||||
const referrerVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'My volume (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
@@ -156,8 +200,11 @@ export const Statistics = ({
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Total commission (last 30 days)')}
|
||||
description={t('(qUSD)')}
|
||||
title={t(
|
||||
'Total commission (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
</StatTile>
|
||||
@@ -173,30 +220,28 @@ export const Statistics = ({
|
||||
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{codeTile}
|
||||
{createdAtTile}
|
||||
{referrerVolumeTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
{currentBenefitTierValue?.tier || '-'}
|
||||
{currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<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>
|
||||
);
|
||||
@@ -255,7 +300,7 @@ export const Statistics = ({
|
||||
{/* Referees (only for referrer view) */}
|
||||
{as === 'referrer' && data.referees.length > 0 && (
|
||||
<div className="mt-20 mb-20">
|
||||
<h2 className="text-2xl mb-5">{t('Referees')}</h2>
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
@@ -281,10 +326,28 @@ export const Statistics = ({
|
||||
columns={[
|
||||
{ name: 'party', displayName: t('Trader') },
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{ name: 'volume', displayName: t('Volume (last 30 days)') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Volume (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: t('Commission earned (last 30 days)'),
|
||||
displayName: (
|
||||
<>
|
||||
{t('Commission earned in')} <QUSDTooltip />{' '}
|
||||
{t(
|
||||
'(last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
@@ -313,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>
|
||||
))}
|
||||
|
||||
@@ -109,6 +109,7 @@ export const TiersContainer = () => {
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
@@ -162,6 +163,7 @@ const StakingTiers = ({
|
||||
|
||||
const TiersTable = ({
|
||||
data,
|
||||
windowLength,
|
||||
}: {
|
||||
data: Array<{
|
||||
tier: number;
|
||||
@@ -170,6 +172,7 @@ const TiersTable = ({
|
||||
discount: string;
|
||||
volume: string;
|
||||
}>;
|
||||
windowLength?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
@@ -181,7 +184,15 @@ const TiersTable = ({
|
||||
tooltip: t('A percentage of commission earned by the referrer'),
|
||||
},
|
||||
{ name: 'discount', displayName: t('Referrer trading discount') },
|
||||
{ name: 'volume', displayName: t('Min. trading volume') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Min. trading volume %s',
|
||||
windowLength
|
||||
? t('(last %s epochs)', windowLength.toString())
|
||||
: undefined
|
||||
),
|
||||
},
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
data={data.map((d) => ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const Tile = ({
|
||||
className,
|
||||
@@ -28,7 +29,7 @@ export const Tile = ({
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
@@ -54,18 +55,23 @@ const FADE_OUT_STYLE = classNames(
|
||||
|
||||
export const CodeTile = ({
|
||||
code,
|
||||
createdAt,
|
||||
className,
|
||||
}: {
|
||||
code: string;
|
||||
createdAt?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<StatTile title="Your referral code">
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<StatTile
|
||||
title={t('Your referral code')}
|
||||
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
|
||||
>
|
||||
<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>
|
||||
@@ -82,7 +88,7 @@ export const CodeTile = ({
|
||||
</Tooltip>
|
||||
<CopyWithTooltip text={code}>
|
||||
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
|
||||
<span className="sr-only">Copy</span>
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
NetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||
import { FeeCard } from './fees-card';
|
||||
import { MarketFees } from './market-fees';
|
||||
@@ -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
|
||||
@@ -284,8 +285,8 @@ export const CurrentVolume = ({
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatNumber(windowLengthVolume)}
|
||||
text={t('Past %s epochs', epochs.toString())}
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getAdjustedFee } from './utils';
|
||||
|
||||
describe('getAdjustedFee', () => {
|
||||
it('simple', () => {
|
||||
const volumeDiscount = 0.5;
|
||||
const referralDiscount = 0.5;
|
||||
|
||||
const infraFee = 0.1;
|
||||
const makerFee = 0.1;
|
||||
const liqFee = 0.1;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
|
||||
// 0.5 * 0.5 = 0.25
|
||||
// 1 - 0.25 = 0.75
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
const infraFee = 0.0005;
|
||||
const makerFee = 0.0002;
|
||||
const liqFee = 0.01;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// formula for calculating adjusted fees
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -88,14 +88,18 @@ export const getReferralBenefitTier = (
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
*
|
||||
* Formula for calculating the adjusted fees
|
||||
* total_discount_factor = 1 - (1 - volumeDiscount) * (1 - referralDiscount)
|
||||
*/
|
||||
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
|
||||
const totalDiscount = discounts.reduce(
|
||||
(sum, d) => sum.plus(d),
|
||||
new BigNumber(0)
|
||||
);
|
||||
return totalFee
|
||||
.times(BigNumber.max(0, new BigNumber(1).minus(totalDiscount)))
|
||||
.toNumber();
|
||||
|
||||
const combinedFactors = discounts.reduce((acc, d) => {
|
||||
return acc.times(new BigNumber(1).minus(d));
|
||||
}, new BigNumber(1));
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
|
||||
};
|
||||
|
||||
@@ -10,15 +10,28 @@ import {
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Indicator } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
ExternalLink,
|
||||
Indicator,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import {
|
||||
useCheckLiquidityStatus,
|
||||
usePaidFeesQuery,
|
||||
} from '@vegaprotocol/liquidity';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export const LiquidityHeader = () => {
|
||||
const { marketId } = useParams();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const { data: marketData } = useStaticMarketData(marketId);
|
||||
const { data: feesPaidRes } = usePaidFeesQuery({
|
||||
variables: { marketId: marketId || '' },
|
||||
});
|
||||
const targetStake = marketData?.targetStake;
|
||||
const suppliedStake = marketData?.suppliedStake;
|
||||
|
||||
@@ -36,6 +49,10 @@ export const LiquidityHeader = () => {
|
||||
triggeringRatio,
|
||||
});
|
||||
|
||||
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
|
||||
(e) => e?.node.marketId === marketId
|
||||
);
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
@@ -82,9 +99,40 @@ export const LiquidityHeader = () => {
|
||||
<HeaderStat heading={t('Liquidity supplied')} testId="liquidity-supplied">
|
||||
<Indicator variant={status} /> {formatNumberPercentage(percentage, 2)}
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<div className="break-word">{marketId}</div>
|
||||
<HeaderStat
|
||||
heading={t('Fees paid')}
|
||||
description={t(
|
||||
'The amount of fees paid to liquidity providers across the whole market during the last epoch %s.',
|
||||
feesObject?.node.epoch.toString() || '-'
|
||||
)}
|
||||
testId="fees-paid"
|
||||
>
|
||||
<div>
|
||||
{feesObject?.node.totalFeesPaid
|
||||
? `${addDecimalsFormatNumber(
|
||||
feesObject?.node.totalFeesPaid,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
</HeaderStat>
|
||||
{marketId && (
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<div className="break-word">
|
||||
<CopyWithTooltip text={marketId}>
|
||||
<button
|
||||
data-testid="copy-eth-oracle-address"
|
||||
className="uppercase text-right"
|
||||
>
|
||||
<span className="flex gap-1">
|
||||
{truncateMiddle(marketId)}
|
||||
<VegaIcon name={VegaIconNames.COPY} size={16} />
|
||||
</span>
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
)}
|
||||
<HeaderStat heading={t('Learn more')} testId="liquidity-learn-more">
|
||||
{DocsLinks ? (
|
||||
<ExternalLink href={DocsLinks.LIQUIDITY}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
|
||||
import { useProposalToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useUpdateNetworkParametersToasts();
|
||||
useProposalToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { addDecimal, truncateByChars } from '@vegaprotocol/utils';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
@@ -9,12 +9,16 @@ 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);
|
||||
@@ -33,20 +37,9 @@ 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))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,10 +58,10 @@ 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}
|
||||
submitTransfer={transfer}
|
||||
accounts={accounts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
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 { removeDecimal } from '@vegaprotocol/utils';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
|
||||
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';
|
||||
@@ -21,7 +37,6 @@ describe('TransferForm', () => {
|
||||
symbol: '€',
|
||||
name: 'EUR',
|
||||
decimals: 2,
|
||||
balance: addDecimal(100000, 2), // 1000
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
@@ -29,9 +44,20 @@ describe('TransferForm', () => {
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
assets: [asset],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
asset,
|
||||
balance: '100000',
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset,
|
||||
balance: '100000',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('form tooltips correctly displayed', async () => {
|
||||
@@ -42,49 +68,45 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-019
|
||||
render(<TransferForm {...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 () => {
|
||||
@@ -92,30 +114,23 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-013
|
||||
// 1003-TRAN-004
|
||||
render(<TransferForm {...props} />);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // 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')[0]).toHaveTextContent(
|
||||
'Invalid Vega key'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
@@ -127,68 +142,58 @@ describe('TransferForm', () => {
|
||||
render(<TransferForm {...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(3); // 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_VESTED_REWARDS
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test amount validation
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
await userEvent.type(amountInput, '0.00000001');
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
).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);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
@@ -200,59 +205,50 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
const mockSubmit = jest.fn();
|
||||
render(<TransferForm {...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(3); // 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_VESTED_REWARDS
|
||||
);
|
||||
|
||||
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,18 +258,17 @@ describe('TransferForm', () => {
|
||||
amount
|
||||
);
|
||||
|
||||
submit();
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
amount: removeDecimal(expectedAmount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
@@ -283,47 +278,30 @@ describe('TransferForm', () => {
|
||||
render(<TransferForm {...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(3); // 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 } }
|
||||
);
|
||||
|
||||
// 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 selectAsset(asset);
|
||||
|
||||
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,7 +316,6 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
pubKeys: ['pubkey-1', 'pubkey-2'],
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
@@ -348,24 +325,18 @@ describe('TransferForm', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
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(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText('Select from wallet'));
|
||||
await userEvent.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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransferFee', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
minSafe,
|
||||
maxSafe,
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
vegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@@ -24,22 +26,22 @@ 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;
|
||||
toVegaKey: string;
|
||||
asset: string;
|
||||
amount: string;
|
||||
fromAccount: AccountType;
|
||||
}
|
||||
|
||||
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: { id: string; symbol: string; name: string; decimals: number };
|
||||
}>;
|
||||
assetId?: string;
|
||||
feeFactor: string | null;
|
||||
@@ -49,10 +51,10 @@ interface TransferFormProps {
|
||||
export const TransferForm = ({
|
||||
pubKey,
|
||||
pubKeys,
|
||||
assets,
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
accounts,
|
||||
}: TransferFormProps) => {
|
||||
const {
|
||||
control,
|
||||
@@ -64,14 +66,50 @@ export const TransferForm = ({
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
asset: initialAssetId,
|
||||
toVegaKey: pubKey || '',
|
||||
},
|
||||
});
|
||||
|
||||
const assets = sortBy(
|
||||
accounts
|
||||
.filter((a) => a.type === AccountType.ACCOUNT_TYPE_GENERAL)
|
||||
.map((account) => ({
|
||||
...account.asset,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
})),
|
||||
'name'
|
||||
);
|
||||
|
||||
const selectedPubKey = watch('toVegaKey');
|
||||
const amount = watch('amount');
|
||||
const fromAccount = watch('fromAccount');
|
||||
const assetId = watch('asset');
|
||||
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
const account = accounts.find(
|
||||
(a) => a.asset.id === assetId && a.type === fromAccount
|
||||
);
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
// General account for the selected asset
|
||||
const generalAccount = accounts.find((a) => {
|
||||
return (
|
||||
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
});
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const min = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
// 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,10 +128,6 @@ 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) {
|
||||
@@ -102,28 +136,21 @@ export const TransferForm = ({
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
});
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
transferAmount,
|
||||
fields.fromAccount,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
}
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer, transferAmount]
|
||||
);
|
||||
|
||||
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,57 +164,47 @@ export const TransferForm = ({
|
||||
className="text-sm"
|
||||
data-testid="transfer-form"
|
||||
>
|
||||
<TradingFormGroup label="Vega key" labelFor="to-address">
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('toAddress', '')}
|
||||
onChange={() => setValue('toVegaKey', '')}
|
||||
select={
|
||||
<TradingSelect
|
||||
{...register('toAddress')}
|
||||
id="to-address"
|
||||
defaultValue=""
|
||||
>
|
||||
<TradingSelect {...register('toVegaKey')} id="toVegaKey">
|
||||
<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>
|
||||
))}
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to-address"
|
||||
id="toVegaKey"
|
||||
type="text"
|
||||
{...register('toAddress', {
|
||||
{...register('toVegaKey', {
|
||||
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}
|
||||
{errors.toVegaKey?.message && (
|
||||
<TradingInputError forInput="toVegaKey">
|
||||
{errors.toVegaKey.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Asset" labelFor="asset">
|
||||
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
@@ -228,6 +245,68 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...register('fromAccount', {
|
||||
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;
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!assetId) return true;
|
||||
return assetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
return (
|
||||
<option value={a.type} key={`${a.type}-${a.asset.id}`}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
{errors.fromAccount.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('To account')} labelFor="toAccount">
|
||||
<TradingSelect
|
||||
id="toAccount"
|
||||
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
|
||||
>
|
||||
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
|
||||
{generalAccount
|
||||
? `${
|
||||
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
|
||||
} (${addDecimalsFormatNumber(
|
||||
generalAccount.balance,
|
||||
generalAccount.asset.decimals
|
||||
)} ${generalAccount.asset.symbol})`
|
||||
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
|
||||
</option>
|
||||
</TradingSelect>
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Amount" labelFor="amount">
|
||||
<TradingInput
|
||||
id="amount"
|
||||
@@ -242,15 +321,24 @@ export const TransferForm = ({
|
||||
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', parseFloat(accountBalance).toString())
|
||||
}
|
||||
>
|
||||
{t('Use max')}
|
||||
</button>
|
||||
)}
|
||||
{errors.amount?.message && (
|
||||
<TradingInputError forInput="amount">
|
||||
{errors.amount.message}
|
||||
@@ -362,40 +450,31 @@ export const TransferFee = ({
|
||||
};
|
||||
|
||||
interface AddressInputProps {
|
||||
pubKeys: string[] | null;
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
pubKeys,
|
||||
select,
|
||||
input,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(() => {
|
||||
if (pubKeys && pubKeys.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [isInput, setIsInput] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
{pubKeys && pubKeys.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="absolute top-0 right-0 ml-auto text-sm underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,31 +1,83 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CandlesMenu } from './candles-menu';
|
||||
import {
|
||||
useCandlesChartSettingsStore,
|
||||
DEFAULT_CHART_SETTINGS,
|
||||
} from './use-candles-chart-settings';
|
||||
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
||||
|
||||
describe('CandlesMenu', () => {
|
||||
it('should render with the correct default studies', async () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
const openDropdown = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Studies',
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
|
||||
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// clear store each time to avoid conditional testing of defaults
|
||||
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
|
||||
});
|
||||
|
||||
it('should render with the correct default overlays', async () => {
|
||||
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it.each(Object.values(Study))('can set %s study', async (study) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with the correct default studies and overlays', async () => {
|
||||
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
|
||||
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Overlays',
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Moving average')).toHaveAttribute(
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ export const CandlesMenu = () => {
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Overlays')}
|
||||
{t('Indicators')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
@@ -132,18 +132,6 @@ export const CandlesMenu = () => {
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownCheckboxItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Studies')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
{Object.values(Study).map((study) => (
|
||||
<TradingDropdownCheckboxItem
|
||||
key={study}
|
||||
|
||||
@@ -24,7 +24,7 @@ const STUDY_ORDER: Study[] = [
|
||||
Study.VOLUME,
|
||||
];
|
||||
|
||||
const DEFAULT_CHART_SETTINGS = {
|
||||
export const DEFAULT_CHART_SETTINGS = {
|
||||
interval: Interval.I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [Overlay.MOVING_AVERAGE],
|
||||
|
||||
@@ -3,7 +3,12 @@ import throttle from 'lodash/throttle';
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import type { OperationVariables } from '@apollo/client';
|
||||
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
|
||||
import type {
|
||||
Subscribe,
|
||||
Load,
|
||||
UpdateCallback,
|
||||
PageInfo,
|
||||
} from './generic-data-provider';
|
||||
import { variablesIsEqualCustomizer } from './generic-data-provider';
|
||||
|
||||
export interface useDataProviderParams<
|
||||
@@ -12,13 +17,23 @@ export interface useDataProviderParams<
|
||||
Variables extends OperationVariables | undefined = undefined
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
|
||||
update?: ({
|
||||
delta,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
delta?: Delta;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
insertionData?: Data | null;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
variables: Variables;
|
||||
skipUpdates?: boolean;
|
||||
@@ -30,7 +45,7 @@ export interface useDataProviderParams<
|
||||
* @param dataProvider subscribe function created by makeDataProvider
|
||||
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
|
||||
* @param variables optional
|
||||
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
*/
|
||||
export const useDataProvider = <
|
||||
Data,
|
||||
@@ -48,6 +63,7 @@ export const useDataProvider = <
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(!skip);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const loadRef = useRef<Load<Data> | undefined>(undefined);
|
||||
@@ -93,6 +109,7 @@ export const useDataProvider = <
|
||||
isInsert,
|
||||
isUpdate,
|
||||
loaded,
|
||||
pageInfo,
|
||||
} = args;
|
||||
setError(error);
|
||||
setLoading(!loaded && loading);
|
||||
@@ -104,21 +121,22 @@ export const useDataProvider = <
|
||||
(skipUpdatesRef.current ||
|
||||
(!skipUpdatesRef.current &&
|
||||
updateRef.current &&
|
||||
updateRef.current({ delta, data })))
|
||||
updateRef.current({ delta, data, pageInfo })))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isInsert &&
|
||||
insertRef.current &&
|
||||
insertRef.current({ insertionData, data })
|
||||
insertRef.current({ insertionData, data, pageInfo })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setData(data);
|
||||
setPageInfo(pageInfo);
|
||||
if (!loading && !isUpdate && updateRef.current) {
|
||||
updateRef.current({ data });
|
||||
updateRef.current({ data, pageInfo });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -136,15 +154,13 @@ export const useDataProvider = <
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setPageInfo(null);
|
||||
setError(undefined);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
updateRef.current({ data: null, pageInfo: null });
|
||||
}
|
||||
if (skip) {
|
||||
setLoading(false);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -165,6 +181,7 @@ export const useDataProvider = <
|
||||
}, [client, dataProvider, callback, variables, skip]);
|
||||
return {
|
||||
data,
|
||||
pageInfo,
|
||||
loading,
|
||||
error,
|
||||
flush,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -26,11 +26,7 @@ import {
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import {
|
||||
sumFees,
|
||||
sumFeesDiscounts,
|
||||
useEstimateFees,
|
||||
} from '../../hooks/use-estimate-fees';
|
||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
@@ -44,6 +40,7 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { FeesBreakdown } from '../fees-breakdown';
|
||||
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
|
||||
|
||||
const emptyValue = '-';
|
||||
|
||||
@@ -63,54 +60,55 @@ export const DealTicketFeeDetails = ({
|
||||
const feeEstimate = useEstimateFees(order, isMarketInAuction);
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
const totalFees = feeEstimate?.fees && sumFees(feeEstimate?.fees);
|
||||
const feesDiscounts =
|
||||
feeEstimate?.fees && sumFeesDiscounts(feeEstimate?.fees);
|
||||
|
||||
const totalPercentageDiscount =
|
||||
feesDiscounts &&
|
||||
totalFees &&
|
||||
feesDiscounts.total !== '0' &&
|
||||
totalFees !== '0' &&
|
||||
new BigNumber(feesDiscounts.total)
|
||||
.dividedBy(BigNumber.sum(totalFees, feesDiscounts.total))
|
||||
.times(100);
|
||||
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
|
||||
const totalDiscountedFeeAmount =
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
getDiscountedFee(
|
||||
feeEstimate.totalFeeAmount,
|
||||
feeEstimate.referralDiscountFactor,
|
||||
feeEstimate.volumeDiscountFactor
|
||||
).discountedFee;
|
||||
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
value={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
<>
|
||||
{totalPercentageDiscount && (
|
||||
{totalDiscountFactor ? (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
-{formatNumberPercentage(totalPercentageDiscount, 2)}
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
2
|
||||
)}
|
||||
</Pill>
|
||||
)}
|
||||
{feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(
|
||||
feeEstimate?.totalFeeAmount,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}`}
|
||||
) : null}
|
||||
{totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
|
||||
</>
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>
|
||||
<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.`
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<FeesBreakdown
|
||||
totalFeeAmount={feeEstimate?.totalFeeAmount}
|
||||
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
|
||||
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user