feat(explorer): explorer oracle section rework (#5135)

This commit is contained in:
Edd 2023-11-03 14:01:33 +00:00 committed by GitHub
parent 8069aa5ee7
commit 6e677084a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1791 additions and 1311 deletions

View File

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

View File

@ -77,7 +77,7 @@
"executor": "nx:run-commands", "executor": "nx:run-commands",
"options": { "options": {
"commands": [ "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.0/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
] ]
} }
}, },

View File

@ -1,5 +1,6 @@
export type HashProps = { export type HashProps = {
text: string; text: string;
truncate?: boolean;
}; };
/** /**
@ -7,10 +8,16 @@ export type HashProps = {
* are broken when they need to wrap. This will remove the need * are broken when they need to wrap. This will remove the need
* for a lot of the overflow scrolling that currently exists. * 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 ( return (
<code className="break-all font-mono" style={{ wordWrap: 'break-word' }}> <code
{text} title={text}
className="break-all font-mono"
style={{ wordWrap: 'break-word' }}
>
{h}
</code> </code>
); );
}; };

View File

@ -3,6 +3,7 @@ query ExplorerMarket($id: ID!) {
id id
decimalPlaces decimalPlaces
positionDecimalPlaces positionDecimalPlaces
state
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@ -22,6 +23,5 @@ query ExplorerMarket($id: ID!) {
} }
} }
} }
state
} }
} }

View File

@ -17,6 +17,7 @@ export const ExplorerMarketDocument = gql`
id id
decimalPlaces decimalPlaces
positionDecimalPlaces positionDecimalPlaces
state
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@ -36,7 +37,6 @@ export const ExplorerMarketDocument = gql`
} }
} }
} }
state
} }
} }
`; `;

View File

@ -63,6 +63,9 @@ describe('Market link component', () => {
product: { product: {
__typename: 'Future', __typename: 'Future',
quoteName: 'dai', quoteName: 'dai',
settlementAsset: {
decimals: 8,
},
}, },
}, },
}, },

View File

@ -23,6 +23,7 @@ const MarketLink = ({
}: MarketLinkProps) => { }: MarketLinkProps) => {
const { data, error, loading } = useExplorerMarketQuery({ const { data, error, loading } = useExplorerMarketQuery({
variables: { id }, variables: { id },
fetchPolicy: 'cache-first',
}); });
let label = <span>{id}</span>; let label = <span>{id}</span>;

View File

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

View File

@ -3,20 +3,93 @@ import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import Hash from '../hash'; 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>> & { export type OracleLinkProps = Partial<ComponentProps<typeof Link>> & {
// The Oracle ID
id: string; 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 ( return (
<Link <Tooltip description={description}>
className="underline font-mono" <Link
{...props} className={`pl-2 pr-2 font-mono dark:text-black ${bgColour} rounded-sm border-solid border-2 relative`}
to={`/${Routes.ORACLES}/${id}`} {...props}
> to={`/${Routes.ORACLES}/${id}`}
<Hash text={id} /> data-status={getStatusString(status)}
</Link> >
<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>
); );
}; };

View File

@ -74,6 +74,9 @@ function renderExistingAmend(
product: { product: {
__typename: 'Future', __typename: 'Future',
quoteName: '123', quoteName: '123',
settlementAsset: {
decimals: 8,
},
}, },
}, },
}, },
@ -124,6 +127,9 @@ function renderExistingAmend(
product: { product: {
__typename: 'Future', __typename: 'Future',
quoteName: '123', quoteName: '123',
settlementAsset: {
decimals: 8,
},
}, },
}, },
}, },
@ -152,6 +158,9 @@ function renderExistingAmend(
product: { product: {
__typename: 'Future', __typename: 'Future',
quoteName: 'dai', quoteName: 'dai',
settlementAsset: {
decimals: 8,
},
}, },
}, },
}, },

View File

@ -22,5 +22,9 @@ export const TimeAgo = ({ date, ...props }: TimeAgoProps) => {
return <>{t('Date unknown')}</>; return <>{t('Date unknown')}</>;
} }
return <span {...props}>{t(`${distanceToNow} ago`)}</span>; return (
<span {...props} title={date} className="underline decoration-dotted">
{t(`${distanceToNow} ago`)}
</span>
);
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { TableCell, TableRow } from '../../../table'; import { TableCell, TableRow } from '../../../table';
import type { VegaPeggedReference } from '../liquidity-provision/liquidity-provision-details';
import { Side, PeggedReferenceMapping } from '@vegaprotocol/types'; import { Side, PeggedReferenceMapping } from '@vegaprotocol/types';
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market'; import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market'; import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import type { components } from '../../../../../types/explorer';
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
export interface TxDetailsOrderProps { export interface TxDetailsOrderProps {
offset: string; offset: string;

View File

@ -33,6 +33,14 @@ const AccountType: Record<AccountTypes, string> = {
ACCOUNT_TYPE_HOLDING: 'Holding', ACCOUNT_TYPE_HOLDING: 'Holding',
ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution', ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution',
ACCOUNT_TYPE_LP_LIQUIDITY_FEES: 'LP Liquidity Fees', 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 { interface TransferParticipantsProps {

View File

@ -1,10 +1,50 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { TxDetailsShared } from './shared/tx-details-shared'; import { TxDetailsShared } from './shared/tx-details-shared';
import { TableWithTbody } from '../../table'; 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 { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-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 { interface TxDetailsChainEventProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;

View File

@ -5,7 +5,6 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
import { TxDetailsShared } from './shared/tx-details-shared'; import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table'; import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer'; import type { components } from '../../../../types/explorer';
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
import PriceInMarket from '../../price-in-market/price-in-market'; import PriceInMarket from '../../price-in-market/price-in-market';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
@ -40,40 +39,32 @@ export const TxDetailsLiquidityAmendment = ({
: '-'; : '-';
return ( return (
<> <TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8" allowWrap={true}> <TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TxDetailsShared <TableRow modifier="bordered">
txData={txData} <TableCell>{t('Market')}</TableCell>
pubKey={pubKey} <TableCell>
blockData={blockData} <MarketLink id={marketId} />
/> </TableCell>
</TableRow>
{amendment.commitmentAmount ? (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell> <TableCell>{t('Commitment amount')}</TableCell>
<TableCell> <TableCell>
<MarketLink id={marketId} /> <PriceInMarket
price={amendment.commitmentAmount}
marketId={marketId}
decimalSource="SETTLEMENT_ASSET"
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
{amendment.commitmentAmount ? ( ) : null}
<TableRow modifier="bordered"> {amendment.fee ? (
<TableCell>{t('Commitment amount')}</TableCell> <TableRow modifier="bordered">
<TableCell> <TableCell>{t('Fee')}</TableCell>
<PriceInMarket <TableCell>{fee}%</TableCell>
price={amendment.commitmentAmount} </TableRow>
marketId={marketId} ) : null}
decimalSource="SETTLEMENT_ASSET" </TableWithTbody>
/>
</TableCell>
</TableRow>
) : null}
{amendment.fee ? (
<TableRow modifier="bordered">
<TableCell>{t('Fee')}</TableCell>
<TableCell>{fee}%</TableCell>
</TableRow>
) : null}
</TableWithTbody>
<LiquidityProvisionDetails provision={amendment} />
</>
); );
}; };

View File

@ -5,7 +5,6 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
import { TxDetailsShared } from './shared/tx-details-shared'; import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table'; import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer'; import type { components } from '../../../../types/explorer';
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
import PriceInMarket from '../../price-in-market/price-in-market'; import PriceInMarket from '../../price-in-market/price-in-market';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
@ -39,40 +38,32 @@ export const TxDetailsLiquiditySubmission = ({
: '-'; : '-';
return ( return (
<> <TableWithTbody className="mb-8" allowWrap={true}>
<TableWithTbody className="mb-8" allowWrap={true}> <TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TxDetailsShared <TableRow modifier="bordered">
txData={txData} <TableCell>{t('Market')}</TableCell>
pubKey={pubKey} <TableCell>
blockData={blockData} <MarketLink id={marketId} />
/> </TableCell>
</TableRow>
{submission.commitmentAmount ? (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell> <TableCell>{t('Commitment amount')}</TableCell>
<TableCell> <TableCell>
<MarketLink id={marketId} /> <PriceInMarket
price={submission.commitmentAmount}
marketId={marketId}
decimalSource="SETTLEMENT_ASSET"
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
{submission.commitmentAmount ? ( ) : null}
<TableRow modifier="bordered"> {submission.fee ? (
<TableCell>{t('Commitment amount')}</TableCell> <TableRow modifier="bordered">
<TableCell> <TableCell>{t('Fee')}</TableCell>
<PriceInMarket <TableCell>{fee}%</TableCell>
price={submission.commitmentAmount} </TableRow>
marketId={marketId} ) : null}
decimalSource="SETTLEMENT_ASSET" </TableWithTbody>
/>
</TableCell>
</TableRow>
) : null}
{submission.fee ? (
<TableRow modifier="bordered">
<TableCell>{t('Fee')}</TableCell>
<TableCell>{fee}%</TableCell>
</TableRow>
) : null}
</TableWithTbody>
<LiquidityProvisionDetails provision={submission} />
</>
); );
}; };

View File

@ -1,5 +1,5 @@
fragment ExplorerOracleDataConnection on OracleSpec { fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection { dataConnection(pagination: { first: 30 }) {
edges { edges {
node { node {
externalData { externalData {
@ -45,6 +45,16 @@ fragment ExplorerOracleDataSource on OracleSpec {
operator operator
} }
} }
... on DataSourceSpecConfigurationTimeTrigger {
conditions {
value
operator
}
triggers {
initial
every
}
}
} }
} }
... on DataSourceDefinitionExternal { ... 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 { query ExplorerOracleSpecs {
oracleSpecsConnection(pagination: { first: 50 }) { oracleSpecsConnection(pagination: { first: 30 }) {
pageInfo { pageInfo {
hasNextPage hasNextPage
} }

View File

@ -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 { fragment ExplorerOracleForMarketsMarket on Market {
id id
state
tradableInstrument { tradableInstrument {
instrument { instrument {
product { product {
... on Future { ... on Future {
dataSourceSpecForSettlementData { ...ExplorerOracleFuture
id
}
dataSourceSpecForTradingTermination {
id
}
} }
... on Perpetual { ... on Perpetual {
dataSourceSpecForSettlementData { ...ExplorerOraclePerpetual
id }
}
}
}
}
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
}
}
}
}
}
}
}
}
}
} }

View File

@ -5,23 +5,23 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; 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 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 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<{ export type ExplorerOracleSpecByIdQueryVariables = Types.Exact<{
id: Types.Scalars['ID']; 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` export const ExplorerOracleDataConnectionFragmentDoc = gql`
fragment ExplorerOracleDataConnection on OracleSpec { fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection { dataConnection(pagination: {first: 30}) {
edges { edges {
node { node {
externalData { externalData {
@ -68,6 +68,16 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
operator operator
} }
} }
... on DataSourceSpecConfigurationTimeTrigger {
conditions {
value
operator
}
triggers {
initial
every
}
}
} }
} }
... on DataSourceDefinitionExternal { ... 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}`; ${ExplorerOracleDataConnectionFragmentDoc}`;
export const ExplorerOracleSpecsDocument = gql` export const ExplorerOracleSpecsDocument = gql`
query ExplorerOracleSpecs { query ExplorerOracleSpecs {
oracleSpecsConnection(pagination: {first: 50}) { oracleSpecsConnection(pagination: {first: 30}) {
pageInfo { pageInfo {
hasNextPage hasNextPage
} }

View File

@ -3,33 +3,106 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; 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 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` export const ExplorerOracleForMarketsMarketFragmentDoc = gql`
fragment ExplorerOracleForMarketsMarket on Market { fragment ExplorerOracleForMarketsMarket on Market {
id id
state
tradableInstrument { tradableInstrument {
instrument { instrument {
product { product {
... on Future { ... on Future {
dataSourceSpecForSettlementData { ...ExplorerOracleFuture
id
}
dataSourceSpecForTradingTermination {
id
}
} }
... on Perpetual { ... on Perpetual {
dataSourceSpecForSettlementData { ...ExplorerOraclePerpetual
id }
}
}
}
}
${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__ * __useExplorerOracleFormMarketsQuery__

View File

@ -51,15 +51,26 @@ describe('Oracle Data view', () => {
{ {
node: { node: {
externalData: { externalData: {
__typename: 'ExternalData',
data: { 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();
}); });
}); });

View File

@ -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 type { ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
import { TimeAgo } from '../../../components/time-ago';
import { t } from '@vegaprotocol/i18n';
const cellSpacing = 'px-3';
interface OracleDataTypeProps { interface OracleDataTypeProps {
data: ExplorerOracleDataConnectionFragment['dataConnection']; 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) { export function OracleData({ data }: OracleDataTypeProps) {
if (!data || !data.edges?.length || data.edges.length > 1) { if (!data || !data.edges?.length) {
return null; return null;
} }
return ( return (
<details data-testid="oracle-data"> <>
<summary>{t('Broadcast data')}</summary> <h2 className="text-3xl font-bold mb-4 display-5 mt-5">
<ul> {t('Recent data')}
{data.edges.map((d) => { </h2>
if (!d) { <table>
return null; <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 ( const broadcastAt = d.node.externalData.data.broadcastAt;
<li key={d.node.externalData.data.broadcastAt}> const node = d.node.externalData.data.data?.at(0);
<SyntaxHighlighter data={filter(d, ['__typename'])} /> if (!node || !node.value || !node.name || !node || !broadcastAt) {
</li> return null;
); }
})}
</ul> return (
</details> <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>
</>
); );
} }

View File

@ -55,13 +55,13 @@ describe('Oracle type view', () => {
const s = mock('prices.external.whatever'); const s = mock('prices.external.whatever');
expect(isInternalSourceType(s)).toEqual(false); expect(isInternalSourceType(s)).toEqual(false);
const res = render(renderWrappedComponent(s)); const res = render(renderWrappedComponent(s));
expect(res.getByText('External data')).toBeInTheDocument(); expect(res.getByText('External Data')).toBeInTheDocument();
}); });
it('Renders External data otherwise', () => { it('Renders External data otherwise', () => {
const s = mock('prices.external.vegaprotocol.builtin.'); const s = mock('prices.external.vegaprotocol.builtin.');
expect(isInternalSourceType(s)).toEqual(false); expect(isInternalSourceType(s)).toEqual(false);
const res = render(renderWrappedComponent(s)); const res = render(renderWrappedComponent(s));
expect(res.getByText('External data')).toBeInTheDocument(); expect(res.getByText('External Data')).toBeInTheDocument();
}); });
}); });

View File

@ -26,6 +26,19 @@ export function isInternalSourceType(s: SourceType) {
return false; 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 { interface OracleDetailsTypeProps {
sourceType: SourceType; sourceType: SourceType;
} }
@ -39,14 +52,10 @@ export function OracleDetailsType({ sourceType }: OracleDetailsTypeProps) {
return null; return null;
} }
const isInternal = isInternalSourceType(sourceType);
return ( return (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableHeader scope="row">Type</TableHeader> <TableHeader scope="row">Type</TableHeader>
<TableCell modifier="bordered"> <TableCell modifier="bordered">{getTypeString(sourceType)}</TableCell>
{isInternal ? 'Internal data' : 'External data'}
</TableCell>
</TableRow> </TableRow>
); );
} }

View File

@ -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">&rArr;</span>
<code>{sourceType.sourceType.method}</code>
</TableCell>
</TableRow>
);
}

View File

@ -1,14 +1,17 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { getConditionsOrFilters, OracleFilter } from './oracle-filter'; import { OracleFilter } from './oracle-filter';
import type { Filter } from './oracle-filter';
import type { ExplorerOracleDataSourceFragment } from '../__generated__/Oracles'; import type { ExplorerOracleDataSourceFragment } from '../__generated__/Oracles';
import { import {
ConditionOperator, ConditionOperator,
DataSourceSpecStatus, DataSourceSpecStatus,
PropertyKeyType, PropertyKeyType,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import type { Condition } from '@vegaprotocol/types';
const mockExternalSpec = { type Spec =
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType'];
const mockExternalSpec: Spec = {
sourceType: { sourceType: {
__typename: 'DataSourceSpecConfiguration', __typename: 'DataSourceSpecConfiguration',
filters: [ filters: [
@ -16,12 +19,12 @@ const mockExternalSpec = {
__typename: 'Filter', __typename: 'Filter',
key: { key: {
type: PropertyKeyType.TYPE_INTEGER, type: PropertyKeyType.TYPE_INTEGER,
name: 'test', name: 'testKey',
}, },
conditions: [ conditions: [
{ {
__typename: 'Condition', __typename: 'Condition',
value: 'test', value: 'testValue',
operator: ConditionOperator.OPERATOR_EQUALS, 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) { function renderComponent(data: ExplorerOracleDataSourceFragment) {
return <OracleFilter data={data} />; return <OracleFilter data={data} />;
} }
@ -70,11 +63,16 @@ describe('Oracle Filter view', () => {
}, },
}, },
}, },
} as ExplorerOracleDataSourceFragment) dataConnection: {
edges: [],
},
})
); );
expect(res.getByText('Filter')).toBeInTheDocument(); // Renders a comprehensible summary of key = value
// Avoids asserting on how the data is presented because it is very rudimentary expect(res.getByText('testKey')).toBeInTheDocument();
expect(res.getByText('=')).toBeInTheDocument();
expect(res.getByText('testValue')).toBeInTheDocument();
}); });
it('Renders conditions if type is DataSourceSpecConfigurationTime', () => { it('Renders conditions if type is DataSourceSpecConfigurationTime', () => {
@ -88,75 +86,59 @@ describe('Oracle Filter view', () => {
data: { data: {
sourceType: { sourceType: {
__typename: 'DataSourceDefinitionInternal', __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 // Avoids asserting on how the data is presented because it is very rudimentary
}); });
});
describe('getConditionsOrFilter', () => { it('DataSourceSpecConfigurationTime handles empty conditions', () => {
it('Returns null if the type is undetermined (not DataSourceSpecConfiguration or DataSourceSpecConfigurationTime', () => { const res = render(
expect(getConditionsOrFilters({})).toBeNull(); renderComponent({
}); dataSourceSpec: {
spec: {
it('Returns the conditions object for time specs', () => { id: 'irrelevant-test-data',
const mock: Filter = { createdAt: 'irrelevant-test-data',
__typename: 'DataSourceSpecConfigurationTime', status: DataSourceSpecStatus.STATUS_ACTIVE,
conditions: [ data: {
{ sourceType: {
__typename: 'Condition', __typename: 'DataSourceDefinitionInternal',
value: '100', sourceType: {
operator: ConditionOperator.OPERATOR_GREATER_THAN, __typename: 'DataSourceSpecConfigurationTime',
}, conditions: [undefined as unknown as Condition],
], },
}; },
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,
}, },
], },
}, },
], dataConnection: {
}; edges: [],
},
})
);
const res = getConditionsOrFilters(mock); // This should never happen, but for coverage sake we test that it does this
// This ugly construction is due to lazy typing on getConditionsOrFilter const ul = res.getByRole('list');
if (!res || res.length !== 1 || !res[0] || 'value' in res[0]) { expect(ul).toBeInTheDocument();
throw new Error( expect(ul).toBeEmptyDOMElement();
'getConditionsOrFilter did not return filters on a external spec'
);
}
expect(res[0].__typename).toEqual('Filter');
}); });
}); });

View File

@ -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 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 { interface OracleFilterProps {
data: ExplorerOracleDataSourceFragment; 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 * 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) * Renders nothing if there is no data (which will frequently)
* be the case) and if there is data, currently renders a simple * 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 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) { if (!f.conditions || f.conditions.length === 0) {
return null; 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 ( return null;
<details>
<summary>{t('Filter')}</summary>
<SyntaxHighlighter data={filter(f, ['__typename'])} />
</details>
);
} }

View File

@ -11,7 +11,7 @@ function renderComponent(id: string, mocks: MockedResponse[]) {
<MemoryRouter> <MemoryRouter>
<MockedProvider mocks={mocks}> <MockedProvider mocks={mocks}>
<Table> <Table>
<tbody> <tbody data-testid="wrapper">
<OracleMarkets id={id} /> <OracleMarkets id={id} />
</tbody> </tbody>
</Table> </Table>
@ -23,8 +23,7 @@ function renderComponent(id: string, mocks: MockedResponse[]) {
describe('Oracle Markets component', () => { describe('Oracle Markets component', () => {
it('Renders a row with the market ID initially', () => { it('Renders a row with the market ID initially', () => {
const res = render(renderComponent('123', [])); const res = render(renderComponent('123', []));
expect(res.getByText('Market')).toBeInTheDocument(); expect(res.getByTestId('wrapper')).toBeEmptyDOMElement();
expect(res.getByText('123')).toBeInTheDocument();
}); });
it('Renders that this is a termination source for the right market', async () => { it('Renders that this is a termination source for the right market', async () => {
@ -34,21 +33,58 @@ describe('Oracle Markets component', () => {
}, },
result: { result: {
data: { 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: { marketsConnection: {
edges: [ edges: [
{ {
node: { node: {
__typename: 'Market', __typename: 'Market',
id: '123', id: '123',
state: 'Active',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
__typename: 'Future', __typename: 'Future',
dataSourceSpecForSettlementData: { dataSourceSpecForSettlementData: {
id: '456', id: '456',
status: 'Active',
}, },
dataSourceSpecForTradingTermination: { dataSourceSpecForTradingTermination: {
id: '789', id: '789',
status: 'Active',
}, },
}, },
}, },
@ -59,15 +95,18 @@ describe('Oracle Markets component', () => {
node: { node: {
__typename: 'Market', __typename: 'Market',
id: 'abc', id: 'abc',
state: 'Active',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
__typename: 'Future', __typename: 'Future',
dataSourceSpecForSettlementData: { dataSourceSpecForSettlementData: {
id: 'def', id: 'def',
status: 'Active',
}, },
dataSourceSpecForTradingTermination: { dataSourceSpecForTradingTermination: {
id: 'ghi', id: 'ghi',
status: 'Active',
}, },
}, },
}, },
@ -91,21 +130,58 @@ describe('Oracle Markets component', () => {
}, },
result: { result: {
data: { 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: { marketsConnection: {
edges: [ edges: [
{ {
node: { node: {
__typename: 'Market', __typename: 'Market',
id: '123', id: '123',
state: 'Active',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
__typename: 'Future', __typename: 'Future',
dataSourceSpecForSettlementData: { dataSourceSpecForSettlementData: {
id: '789', id: '789',
status: 'Active',
}, },
dataSourceSpecForTradingTermination: { dataSourceSpecForTradingTermination: {
id: '123', id: '123',
status: 'Active',
}, },
}, },
}, },
@ -116,15 +192,18 @@ describe('Oracle Markets component', () => {
node: { node: {
__typename: 'Market', __typename: 'Market',
id: 'abc', id: 'abc',
state: 'Active',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
__typename: 'Future', __typename: 'Future',
dataSourceSpecForSettlementData: { dataSourceSpecForSettlementData: {
id: 'def', id: 'def',
status: 'Active',
}, },
dataSourceSpecForTradingTermination: { dataSourceSpecForTradingTermination: {
id: 'ghi', id: 'ghi',
status: 'Active',
}, },
}, },
}, },

View File

@ -1,5 +1,4 @@
import { getNodes } from '@vegaprotocol/utils'; import { getNodes } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { MarketLink } from '../../../components/links'; import { MarketLink } from '../../../components/links';
import { TableRow, TableCell, TableHeader } from '../../../components/table'; import { TableRow, TableCell, TableHeader } from '../../../components/table';
import type { ExplorerOracleForMarketsMarketFragment } from '../__generated__/OraclesForMarkets'; import type { ExplorerOracleForMarketsMarketFragment } from '../__generated__/OraclesForMarkets';
@ -24,38 +23,37 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
); );
if (markets) { if (markets) {
const m = markets.find((m) => { const m = markets.filter((market) => {
const p = m.tradableInstrument.instrument.product; const p = market.tradableInstrument.instrument.product;
if ( if (
((p.__typename === 'Future' || p.__typename === 'Perpetual') && ((p.__typename === 'Future' || p.__typename === 'Perpetual') &&
p.dataSourceSpecForSettlementData.id === id) || p.dataSourceSpecForSettlementData.id === id) ||
('dataSourceSpecForTradingTermination' in p && ('dataSourceSpecForTradingTermination' in p &&
p.dataSourceSpecForTradingTermination.id === id) p.dataSourceSpecForTradingTermination.id === id) ||
(p.__typename === 'Perpetual' &&
p.dataSourceSpecForSettlementSchedule.id === id)
) { ) {
return true; return true;
} }
return false; return false;
}); });
if (m && m.id) { if (m && m.length > 0) {
return ( return (
<TableRow modifier="bordered"> <>
<TableHeader scope="row">{getLabel(id, m)}</TableHeader> {m.map((market) => (
<TableCell modifier="bordered" data-testid={`m-${m.id}`}> <TableRow modifier="bordered" key={`m-${market.id}`}>
<MarketLink id={m.id} /> <TableHeader scope="row">{getLabel(id, market)}</TableHeader>
</TableCell> <TableCell modifier="bordered" data-testid={`m-${market.id}`}>
</TableRow> <MarketLink id={market.id} />
</TableCell>
</TableRow>
))}
</>
); );
} }
} }
return ( return null;
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Market')}</TableHeader>
<TableCell modifier="bordered">
<span>{id}</span>
</TableCell>
</TableRow>
);
} }
export function getLabel( export function getLabel(

View File

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

View File

@ -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>,&nbsp;
{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>
);
}

View File

@ -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')}>&gt;</span>;
case 'OPERATOR_GREATER_THAN_OR_EQUAL':
return <span title={t('greater than or equal')}>&ge;</span>;
case 'OPERATOR_LESS_THAN':
return <span title={t('less than')}>&lt;</span>;
case 'OPERATOR_LESS_THAN_OR_EQUAL':
return <span title={t('less than or equal')}>&le;</span>;
}
return <span>{operator}</span>;
}

View File

@ -14,7 +14,9 @@ import { OracleFilter } from './oracle-filter';
import { OracleDetailsType } from './oracle-details-type'; import { OracleDetailsType } from './oracle-details-type';
import { OracleMarkets } from './oracle-markets'; import { OracleMarkets } from './oracle-markets';
import { OracleSigners } from './oracle-signers'; 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 = export type SourceType =
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType']; ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType'];
@ -38,10 +40,8 @@ export const OracleDetails = ({
id, id,
dataSource, dataSource,
dataConnection, dataConnection,
showBroadcasts = false,
}: OracleDetailsProps) => { }: OracleDetailsProps) => {
const sourceType = dataSource.dataSourceSpec.spec.data.sourceType; const sourceType = dataSource.dataSourceSpec.spec.data.sourceType;
const reportsCount: number = dataConnection.edges?.length || 0;
return ( return (
<div> <div>
@ -49,23 +49,27 @@ export const OracleDetails = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableHeader scope="row">{t('ID')}</TableHeader> <TableHeader scope="row">{t('ID')}</TableHeader>
<TableCell modifier="bordered"> <TableCell modifier="bordered">
<OracleLink id={id} /> <Hash text={id} />
</TableCell> </TableCell>
</TableRow> </TableRow>
<OracleDetailsType sourceType={sourceType} /> <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} /> <OracleSigners sourceType={sourceType} />
<OracleEthSource sourceType={sourceType} />
<OracleMarkets id={id} /> <OracleMarkets id={id} />
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableHeader scope="row">{t('Matched data')}</TableHeader> <TableHeader scope="row">{t('Filter')}</TableHeader>
<TableCell modifier="bordered"> <TableCell modifier="bordered">
{showBroadcasts ? reportsCount : reportsCount > 0 ? '✅' : '❌'} <OracleFilter data={dataSource} />
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableWithTbody> </TableWithTbody>
<OracleFilter data={dataSource} /> {dataConnection ? <OracleData data={dataConnection} /> : null}
{showBroadcasts && dataConnection ? (
<OracleData data={dataConnection} />
) : null}
</div> </div>
); );
}; };

View File

@ -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 { RouteTitle } from '../../../components/route-title';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useExplorerOracleSpecsQuery } from '../__generated__/Oracles';
import { useDocumentTitle } from '../../../hooks/use-document-title'; import { useDocumentTitle } from '../../../hooks/use-document-title';
import { OracleDetails } from '../components/oracle';
import { useScrollToLocation } from '../../../hooks/scroll-to-location'; 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 Oracles = () => {
const { data, loading, error } = useExplorerOracleSpecsQuery({ const { data, loading, error } = useExplorerOracleFormMarketsQuery({
errorPolicy: 'ignore', errorPolicy: 'ignore',
}); });
useDocumentTitle(['Oracles']); useDocumentTitle(['Oracles']);
useScrollToLocation(); useScrollToLocation();
const [hoveredOracle, setHoveredOracle] = useState('');
return ( return (
<section> <section>
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle> <RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
@ -30,36 +38,148 @@ const Oracles = () => {
data.oracleSpecsConnection.edges?.length === 0 data.oracleSpecsConnection.edges?.length === 0
} }
> >
{data?.oracleSpecsConnection?.edges <table className="text-left">
? data.oracleSpecsConnection.edges.map((o) => { <thead>
const id = o?.node.dataSourceSpec.spec.id; <tr>
if (!id) { <th className={cellSpacing}>Market</th>
return null; <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 ( if (
<div o.node.tradableInstrument.instrument.product.__typename ===
id={id} 'Future'
key={id} ) {
className="mb-10" settlementOracle =
data-testid="oracle-details" o.node.tradableInstrument.instrument.product
> .dataSourceSpecForSettlementData.id;
<OracleDetails terminationOracle =
id={id} o.node.tradableInstrument.instrument.product
dataSource={o?.node} .dataSourceSpecForTradingTermination.id;
dataConnection={dataConnection} settlementOracleStatus =
showBroadcasts={false} o.node.tradableInstrument.instrument.product
/> .dataSourceSpecForSettlementData.status;
<details> terminationOracleStatus =
<summary className="pointer">JSON</summary> o.node.tradableInstrument.instrument.product
<SyntaxHighlighter data={filter(o, ['__typename'])} /> .dataSourceSpecForTradingTermination.status;
</details> } else if (
</div> o.node.tradableInstrument.instrument.product.__typename ===
); 'Perpetual'
}) ) {
: null} 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> </AsyncRenderer>
</section> </section>
); );

View File

@ -40,10 +40,9 @@ export const Oracle = () => {
id={id || ''} id={id || ''}
dataSource={data?.oracleSpec} dataSource={data?.oracleSpec}
dataConnection={data?.oracleSpec.dataConnection} dataConnection={data?.oracleSpec.dataConnection}
showBroadcasts={true}
/> />
<details> <details className="mt-5 cursor-pointer">
<summary className="pointer">JSON</summary> <summary>JSON</summary>
<SyntaxHighlighter data={filter(data, ['__typename'])} /> <SyntaxHighlighter data={filter(data, ['__typename'])} />
</details> </details>
</div> </div>

File diff suppressed because it is too large Load Diff