feat(explorer): add full details to oracle page (#6005)

This commit is contained in:
Edd 2024-03-15 16:43:57 +00:00 committed by GitHub
parent 7b162104b6
commit f4212724d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 219 additions and 121 deletions

View File

@ -81,6 +81,7 @@ describe('TxDetailsTransfer', () => {
hash: 'test',
submitter:
'e1943eea46fed576cf2be42972f3c5515ad3d0ac7ac013f56677c12a53a1b3ed',
block: '100',
command: {
nonce: '5188810881378065222',
blockHeight: '14951513',

View File

@ -89,7 +89,7 @@ fragment ExplorerOracleDataSourceSpec on ExternalDataSourceSpec {
}
query ExplorerOracleFormMarkets {
marketsConnection {
marketsConnection(includeSettled: false, pagination: { first: 20 }) {
edges {
node {
...ExplorerOracleForMarketsMarket
@ -102,7 +102,7 @@ query ExplorerOracleFormMarkets {
dataSourceSpec {
...ExplorerOracleDataSourceSpec
}
dataConnection(pagination: { last: 1 }) {
dataConnection(pagination: { first: 1 }) {
edges {
node {
externalData {

View File

@ -120,7 +120,7 @@ export const ExplorerOracleDataSourceSpecFragmentDoc = gql`
`;
export const ExplorerOracleFormMarketsDocument = gql`
query ExplorerOracleFormMarkets {
marketsConnection {
marketsConnection(includeSettled: false, pagination: {first: 20}) {
edges {
node {
...ExplorerOracleForMarketsMarket
@ -133,7 +133,7 @@ export const ExplorerOracleFormMarketsDocument = gql`
dataSourceSpec {
...ExplorerOracleDataSourceSpec
}
dataConnection(pagination: {last: 1}) {
dataConnection(pagination: {first: 1}) {
edges {
node {
externalData {

View File

@ -6,6 +6,11 @@ import {
} from '../../../components/links/external-explorer-link/external-explorer-link';
import { getExternalChainLabel } from '@vegaprotocol/environment';
import { t } from 'i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import isArray from 'lodash/isArray';
import type { components } from '../../../../types/explorer';
type Normalisers = components['schemas']['vegaNormaliser'][];
interface OracleDetailsEthSourceProps {
sourceType: SourceType;
@ -34,12 +39,18 @@ export function OracleEthSource({
const chainLabel = getExternalChainLabel(chain);
const abi = prepareOracleSpecField(sourceType?.sourceType?.abi);
const args = prepareOracleSpecField(sourceType?.sourceType?.args);
const normalisers = serialiseNormalisers(sourceType.sourceType.normalisers);
return (
<TableRow modifier="bordered">
<TableHeader scope="row">
<TableHeader scope="row" className="pt-1 align-text-top">
{chainLabel} {t('Contract')}
</TableHeader>
<TableCell modifier="bordered">
<details>
<summary className="cursor-pointer">
<ExternalExplorerLink
chain={chain}
id={address}
@ -48,7 +59,97 @@ export function OracleEthSource({
/>
<span className="mx-3">&rArr;</span>
<code>{sourceType.sourceType.method}</code>
</summary>
{args && (
<>
<h2 className={'mt-5 mb-1 text-xl'}>{t('Arguments')}</h2>
<div className="max-w-3">
<SyntaxHighlighter
data={JSON.parse(
sourceType.sourceType.args as unknown as string
)}
/>
</div>
</>
)}
{abi && (
<>
<h2 className={'mt-5 mb-1 text-xl'}>{t('ABI')}</h2>
<div className="max-w-3">
<SyntaxHighlighter data={abi} />
</div>
</>
)}
{normalisers && (
<>
<h2 className={'mt-5 mb-1 text-xl'}>{t('Normalisers')}</h2>
<div className="max-w-3 mb-3">
<SyntaxHighlighter data={normalisers} />
</div>
</>
)}
</details>
</TableCell>
</TableRow>
);
}
// Constant to define the absence of a valid string from the Oracle Spec fields
const NO_DATA = false;
/**
* The ABI and args are stored as either a (JSON escaped, probably) string
* or array of strings. Given that OracleEthSource is simply throwing the
* data in to a SyntaxHighlighter, we don't really care about the format,
* so this function will just try to parse the data and return it as a string.
*
* @param abi
* @returns
*/
export function prepareOracleSpecField(
specField?: string[] | null
): string | false {
if (!specField) {
return NO_DATA;
}
try {
if (isArray(specField)) {
return JSON.parse(specField.join(''));
} else {
return JSON.parse(specField);
}
} catch (e) {
return NO_DATA;
}
}
/**
* Similar to prepareOracleSpecField above, but processes an array of normaliser objects
* removing the __typename and returning a serialised array of normalisers for
* SyntaxHighlighter
*
* @param normalisers
* @returns
*/
export function serialiseNormalisers(
normalisers?: Normalisers | null
): Normalisers | false {
if (!normalisers) {
return NO_DATA;
}
try {
return normalisers.map((normaliser) => {
return {
name: normaliser.name,
expression: normaliser.expression,
};
});
} catch (e) {
return NO_DATA;
}
}

View File

@ -1,38 +1,9 @@
import { render } from '@testing-library/react';
import { OracleFilter } from './oracle-filter';
import type { ExplorerOracleDataSourceFragment } from '../__generated__/Oracles';
import {
ConditionOperator,
DataSourceSpecStatus,
PropertyKeyType,
} from '@vegaprotocol/types';
import { ConditionOperator, DataSourceSpecStatus } from '@vegaprotocol/types';
import type { Condition } from '@vegaprotocol/types';
type Spec =
ExplorerOracleDataSourceFragment['dataSourceSpec']['spec']['data']['sourceType'];
const mockExternalSpec: Spec = {
sourceType: {
__typename: 'DataSourceSpecConfiguration',
filters: [
{
__typename: 'Filter',
key: {
type: PropertyKeyType.TYPE_INTEGER,
name: 'testKey',
},
conditions: [
{
__typename: 'Condition',
value: 'testValue',
operator: ConditionOperator.OPERATOR_EQUALS,
},
],
},
],
},
};
function renderComponent(data: ExplorerOracleDataSourceFragment) {
return <OracleFilter data={data} />;
}
@ -50,31 +21,6 @@ describe('Oracle Filter view', () => {
expect(res.container).toBeEmptyDOMElement();
});
it('Renders filters if type is DataSourceSpecConfiguration', () => {
const res = render(
renderComponent({
dataSourceSpec: {
spec: {
id: 'irrelevant-test-data',
createdAt: 'irrelevant-test-data',
status: DataSourceSpecStatus.STATUS_ACTIVE,
data: {
sourceType: mockExternalSpec,
},
},
},
dataConnection: {
edges: [],
},
})
);
// Renders a comprehensible summary of key = value
expect(res.getByText('testKey')).toBeInTheDocument();
expect(res.getByText('=')).toBeInTheDocument();
expect(res.getByText('testValue')).toBeInTheDocument();
});
it('Renders conditions if type is DataSourceSpecConfigurationTime', () => {
const res = render(
renderComponent({
@ -136,7 +82,7 @@ describe('Oracle Filter view', () => {
})
);
// This should never happen, but for coverage sake we test that it does this
// This should never happen, but for coverage we test that it does this
const ul = res.getByRole('list');
expect(ul).toBeInTheDocument();
expect(ul).toBeEmptyDOMElement();

View File

@ -1,5 +1,8 @@
import type { ExplorerOracleDataSourceFragment } from '../__generated__/Oracles';
import { OracleSpecInternalTimeTrigger } from './oracle-spec/internal-time-trigger';
import {
OracleSpecInternalTimeTrigger,
TimeTrigger,
} from './oracle-spec/internal-time-trigger';
import { OracleSpecCondition } from './oracle-spec/condition';
import { getCharacterForOperator } from './oracle-spec/operator';
@ -11,7 +14,7 @@ interface OracleFilterProps {
* Shows the conditions that this oracle is using to filter
* 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
* JSON view.
*/
@ -21,6 +24,7 @@ export function OracleFilter({ data }: OracleFilterProps) {
}
const s = data.dataSourceSpec.spec.data.sourceType.sourceType;
if (s.__typename === 'DataSourceSpecConfigurationTime' && s.conditions) {
return (
<ul>
@ -41,12 +45,10 @@ export function OracleFilter({ data }: OracleFilterProps) {
s.triggers
) {
return <OracleSpecInternalTimeTrigger data={s} />;
} else if (
s.__typename === 'EthCallSpec' ||
s.__typename === 'DataSourceSpecConfiguration'
) {
} else if (s.__typename === 'EthCallSpec') {
if (s.filters !== null && s.filters && 'filters' in s) {
return (
<div>
<ul>
{s.filters.map((f) => {
const prop = <code title={f.key.type}>{f.key.name}</code>;
@ -65,6 +67,8 @@ export function OracleFilter({ data }: OracleFilterProps) {
}
})}
</ul>
{s.trigger && <TimeTrigger data={s.trigger.trigger} />}
</div>
);
}
}

View File

@ -1,5 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import type { DataSourceSpecConfigurationTimeTrigger } from '@vegaprotocol/types';
import type {
DataSourceSpecConfigurationTimeTrigger,
EthTimeTrigger,
InternalTimeTrigger,
Maybe,
} from '@vegaprotocol/types';
import secondsToMinutes from 'date-fns/secondsToMinutes';
import fromUnixTime from 'date-fns/fromUnixTime';
@ -13,24 +18,35 @@ export function OracleSpecInternalTimeTrigger({
return (
<div>
<span>{t('Time')}</span>,&nbsp;
{data.triggers.map((tr) => {
{data.triggers.map((tr) => (
<TimeTrigger data={tr} />
))}
</div>
);
}
export interface TimeTriggerProps {
data: Maybe<InternalTimeTrigger> | Maybe<EthTimeTrigger>;
}
export function TimeTrigger({ data }: TimeTriggerProps) {
const d = parseDate(data?.initial);
return (
<span>
{tr?.initial ? (
<span title={`${tr.initial}`}>
<span key={JSON.stringify(data)}>
{data?.initial ? (
<span title={`${data.initial}`}>
<strong>{t('starting at')}</strong>{' '}
<em className="not-italic underline decoration-dotted">
{fromUnixTime(tr.initial).toLocaleString()}
</em>
<em className="not-italic underline decoration-dotted">{d}</em>
</span>
) : (
''
)}
{tr?.every ? (
<span title={`${tr.every} ${t('seconds')}`}>
{data?.every ? (
<span title={`${data.every} ${t('seconds')}`}>
, <strong>{t('every')}</strong>{' '}
<em className="not-italic underline decoration-dotted">
{secondsToMinutes(tr.every)} {t('minutes')}
<em className="not-italic underline decor</em>ation-dotted">
{secondsToMinutes(data.every)} {t('minutes')}
</em>{' '}
</span>
) : (
@ -38,7 +54,24 @@ export function OracleSpecInternalTimeTrigger({
)}
</span>
);
})}
</div>
);
}
/**
* Dates in oracle triggers can be (or maybe were previously) Unix Time or timestamps
* depending on type. This function handles both cases and returns a nicely formatted date.
*
* @param date
* @returns string Localestring for date
*/
export function parseDate(date?: string | number): string {
if (!date) {
return 'Invalid date';
}
const d = fromUnixTime(+date).toLocaleString();
if (d === 'Invalid Date') {
return new Date(date).toLocaleString();
}
return d;
}

View File

@ -48,6 +48,11 @@ export const OracleDetails = ({
? dataSource.dataSourceSpec.spec.data.sourceType.sourceType.sourceChainId.toString()
: undefined;
const requiredConfirmations =
(sourceType.sourceType.__typename === 'EthCallSpec' &&
sourceType.sourceType.requiredConfirmations) ||
'';
return (
<div>
<TableWithTbody className="mb-2">
@ -64,15 +69,23 @@ export const OracleDetails = ({
{getStatusString(dataSource.dataSourceSpec.spec.status)}
</TableCell>
</TableRow>
<OracleMarkets id={id} />
<OracleSigners sourceType={sourceType} />
<OracleEthSource sourceType={sourceType} chain={chain} />
<OracleMarkets id={id} />
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Filter')}</TableHeader>
<TableHeader scope="row" className="pt-1 align-text-top">
{t('Filter')}
</TableHeader>
<TableCell modifier="bordered">
<OracleFilter data={dataSource} />
</TableCell>
</TableRow>
{requiredConfirmations && requiredConfirmations > 0 && (
<TableRow modifier="bordered">
<TableHeader scope="row">{t('Required Confirmations')}</TableHeader>
<TableCell modifier="bordered">{requiredConfirmations}</TableCell>
</TableRow>
)}
</TableWithTbody>
{dataConnection ? <OracleData data={dataConnection} /> : null}
</div>