feat(explorer): add full details to oracle page (#6005)
This commit is contained in:
parent
7b162104b6
commit
f4212724d0
@ -81,6 +81,7 @@ describe('TxDetailsTransfer', () => {
|
||||
hash: 'test',
|
||||
submitter:
|
||||
'e1943eea46fed576cf2be42972f3c5515ad3d0ac7ac013f56677c12a53a1b3ed',
|
||||
block: '100',
|
||||
command: {
|
||||
nonce: '5188810881378065222',
|
||||
blockHeight: '14951513',
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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">⇒</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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
{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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user