feat(oracles): oracle full profile (#3545)

Co-authored-by: asiaznik <artur@vegaprotocol.io>
This commit is contained in:
m.ray 2023-05-05 12:52:03 +03:00 committed by GitHub
parent 023a83e0f7
commit bf63f9d46d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 815 additions and 173 deletions

View File

@ -10,6 +10,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo

View File

@ -14,10 +14,72 @@ import {
SettlementAssetInfoPanel, SettlementAssetInfoPanel,
} from '@vegaprotocol/market-info'; } from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info'; import { MarketInfoTable } from '@vegaprotocol/market-info';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual';
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => { export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
if (!market) return null; if (!market) return null;
const settlementData =
market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
.data;
const terminationData =
market.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }, i) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
const oraclePanels = isEqual(
getSigners(settlementData),
getSigners(terminationData)
)
? [
{
title: t('Settlement Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
{
title: t('Termination Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="termination"
/>
),
},
]
: [
{
title: t('Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
];
const panels = [ const panels = [
{ {
title: t('Key details'), title: t('Key details'),
@ -95,22 +157,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} /> <LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
), ),
}, },
{ ...oraclePanels,
title: t('Settlement Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
{
title: t('Termination Oracle'),
content: (
<OracleInfoPanel noBorder={false} market={market} type="termination" />
),
},
]; ];
return ( return (

View File

@ -184,17 +184,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
.getByTestId('provider-name') .getByTestId('provider-name')
.and('contain', 'Another oracle'); .and('contain', 'Another oracle');
cy.getByTestId(accordionContent)
.getByTestId('signed-proofs')
.and('contain', '1');
cy.getByTestId(accordionContent) cy.getByTestId(accordionContent)
.getByTestId('verified-proofs') .getByTestId('verified-proofs')
.and('contain', '1'); .and('contain', '1');
cy.getByTestId(accordionContent)
.getByTestId('signed-proofs')
.and('contain', '1');
}); });
it('proposal displayed', () => { it('proposal displayed', () => {

View File

@ -198,4 +198,4 @@ export function useMarketInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
} }
export type MarketInfoQueryHookResult = ReturnType<typeof useMarketInfoQuery>; export type MarketInfoQueryHookResult = ReturnType<typeof useMarketInfoQuery>;
export type MarketInfoLazyQueryHookResult = ReturnType<typeof useMarketInfoLazyQuery>; export type MarketInfoLazyQueryHookResult = ReturnType<typeof useMarketInfoLazyQuery>;
export type MarketInfoQueryResult = Apollo.QueryResult<MarketInfoQuery, MarketInfoQueryVariables>; export type MarketInfoQueryResult = Apollo.QueryResult<MarketInfoQuery, MarketInfoQueryVariables>;

View File

@ -35,6 +35,8 @@ import {
RiskParametersInfoPanel, RiskParametersInfoPanel,
SettlementAssetInfoPanel, SettlementAssetInfoPanel,
} from './market-info-panels'; } from './market-info-panels';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual';
export interface MarketInfoAccordionProps { export interface MarketInfoAccordionProps {
market: MarketInfo; market: MarketInfo;
@ -70,7 +72,7 @@ export const MarketInfoAccordionContainer = ({
); );
}; };
const MarketInfoAccordion = ({ export const MarketInfoAccordion = ({
market, market,
onSelect, onSelect,
}: MarketInfoAccordionProps) => { }: MarketInfoAccordionProps) => {
@ -103,8 +105,64 @@ const MarketInfoAccordion = ({
content: <InsurancePoolInfoPanel market={market} account={a} />, content: <InsurancePoolInfoPanel market={market} account={a} />,
})), })),
]; ];
const product = market.tradableInstrument.instrument.product; const settlementData =
market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
.data;
const terminationData =
market.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }, i) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
const oraclePanels = isEqual(
getSigners(settlementData),
getSigners(terminationData)
)
? [
{
title: t('Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
]
: [
{
title: t('Settlement Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
{
title: t('Termination Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="termination"
/>
),
},
];
const marketSpecPanels = [ const marketSpecPanels = [
{ {
title: t('Key details'), title: t('Key details'),
@ -114,14 +172,7 @@ const MarketInfoAccordion = ({
title: t('Instrument'), title: t('Instrument'),
content: <InstrumentInfoPanel market={market} />, content: <InstrumentInfoPanel market={market} />,
}, },
product.dataSourceSpecForSettlementData && { ...oraclePanels,
title: t('Settlement Oracle'),
content: <OracleInfoPanel market={market} type="settlementData" />,
},
product.dataSourceSpecForTradingTermination && {
title: t('Termination Oracle'),
content: <OracleInfoPanel market={market} type="termination" />,
},
{ {
title: t('Settlement asset'), title: t('Settlement asset'),
content: <SettlementAssetInfoPanel market={market} />, content: <SettlementAssetInfoPanel market={market} />,
@ -182,33 +233,37 @@ const MarketInfoAccordion = ({
title: t('Proposal'), title: t('Proposal'),
content: ( content: (
<div className=""> <div className="">
<ExternalLink {VEGA_TOKEN_URL && (
className="mb-2 w-full" <ExternalLink
href={generatePath(TokenLinks.PROPOSAL_PAGE, { className="mb-2 w-full"
tokenUrl: VEGA_TOKEN_URL, href={generatePath(TokenLinks.PROPOSAL_PAGE, {
proposalId: market.proposal?.id || '', tokenUrl: VEGA_TOKEN_URL,
})} proposalId: market.proposal?.id || '',
title={ })}
market.proposal?.rationale.title || title={
market.proposal?.rationale.description || market.proposal?.rationale.title ||
'' market.proposal?.rationale.description ||
} ''
> }
{t('View governance proposal')} >
</ExternalLink> {t('View governance proposal')}
<ExternalLink </ExternalLink>
className="w-full" )}
href={generatePath(TokenLinks.UPDATE_PROPOSAL_PAGE, { {VEGA_TOKEN_URL && (
tokenUrl: VEGA_TOKEN_URL, <ExternalLink
})} className="w-full"
title={ href={generatePath(TokenLinks.UPDATE_PROPOSAL_PAGE, {
market.proposal?.rationale.title || tokenUrl: VEGA_TOKEN_URL,
market.proposal?.rationale.description || })}
'' title={
} market.proposal?.rationale.title ||
> market.proposal?.rationale.description ||
{t('Propose a change to market')} ''
</ExternalLink> }
>
{t('Propose a change to market')}
</ExternalLink>
)}
</div> </div>
), ),
}, },
@ -225,7 +280,7 @@ const MarketInfoAccordion = ({
<h3 className={headerClassName}>{t('Market specification')}</h3> <h3 className={headerClassName}>{t('Market specification')}</h3>
<Accordion panels={marketSpecPanels} /> <Accordion panels={marketSpecPanels} />
</div> </div>
{VEGA_TOKEN_URL && market.proposal?.id && ( {VEGA_TOKEN_URL && marketGovPanels && market.proposal?.id && (
<div> <div>
<h3 className={headerClassName}>{t('Market governance')}</h3> <h3 className={headerClassName}>{t('Market governance')}</h3>
<Accordion panels={marketGovPanels} /> <Accordion panels={marketGovPanels} />

View File

@ -5,62 +5,13 @@ import {
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { DataSourceProof } from './market-info-panels'; import { DataSourceProof } from './market-info-panels';
jest.mock('@vegaprotocol/oracles', () => ({
useOracleMarkets: () => [],
}));
describe('DataSourceProof', () => { describe('DataSourceProof', () => {
const ORACLE_PUBKEY = const ORACLE_PUBKEY =
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'; '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f';
it('renders correct proof for external data sources', () => {
const props = {
data: {
sourceType: {
__typename: 'DataSourceDefinitionExternal' as const,
sourceType: {
__typename: 'DataSourceSpecConfiguration' as const,
signers: [
{
__typename: 'Signer' as const,
signer: {
__typename: 'PubKey' as const,
key: ORACLE_PUBKEY,
},
},
],
},
},
},
providers: [
{
name: 'Another oracle',
url: 'https://zombo.com',
description_markdown:
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
oracle: {
status: 'GOOD' as const,
status_reason: '',
first_verified: '2022-01-01T00:00:00.000Z',
last_verified: '2022-12-31T00:00:00.000Z',
type: 'public_key' as const,
public_key: ORACLE_PUBKEY,
},
proofs: [
{
format: 'signed_message' as const,
available: true,
type: 'public_key' as const,
public_key: ORACLE_PUBKEY,
message: 'SOMEHEX',
},
],
github_link: `https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/PubKey-${ORACLE_PUBKEY}.toml`,
},
],
type: 'termination' as const,
};
render(<DataSourceProof {...props} />);
expect(screen.getByRole('link')).toHaveAttribute(
'href',
props.providers[0].github_link
);
});
it('renders message if there are no providers', () => { it('renders message if there are no providers', () => {
const props = { const props = {
@ -84,7 +35,7 @@ describe('DataSourceProof', () => {
providers: [], providers: [],
type: 'termination' as const, type: 'termination' as const,
}; };
render(<DataSourceProof {...props} />); render(<DataSourceProof id={''} {...props} />);
expect( expect(
screen.getByText('No oracle proof for termination') screen.getByText('No oracle proof for termination')
).toBeInTheDocument(); ).toBeInTheDocument();
@ -137,7 +88,7 @@ describe('DataSourceProof', () => {
], ],
type: 'settlementData' as const, type: 'settlementData' as const,
}; };
render(<DataSourceProof {...props} />); render(<DataSourceProof id={''} {...props} />);
expect( expect(
screen.getByText('No oracle proof for settlement data') screen.getByText('No oracle proof for settlement data')
).toBeInTheDocument(); ).toBeInTheDocument();
@ -177,7 +128,7 @@ describe('DataSourceProof', () => {
providers: [], providers: [],
type: 'termination' as const, type: 'termination' as const,
}; };
render(<DataSourceProof {...props} />); render(<DataSourceProof id={''} {...props} />);
expect(screen.getByText('Internal conditions')).toBeInTheDocument(); expect(screen.getByText('Internal conditions')).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(

View File

@ -1,4 +1,5 @@
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import { useState } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets'; import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
@ -6,7 +7,7 @@ import {
totalFeesPercentage, totalFeesPercentage,
marketDataProvider, marketDataProvider,
} from '@vegaprotocol/market-list'; } from '@vegaprotocol/market-list';
import { ExternalLink, Splash } from '@vegaprotocol/ui-toolkit'; import { Dialog, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumber, formatNumber,
@ -26,7 +27,12 @@ import { ConditionOperatorMapping } from '@vegaprotocol/types';
import { MarketTradingModeMapping } from '@vegaprotocol/types'; import { MarketTradingModeMapping } from '@vegaprotocol/types';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import type { Provider } from '@vegaprotocol/oracles'; import type { Provider } from '@vegaprotocol/oracles';
import { OracleBasicProfile, useOracleProofs } from '@vegaprotocol/oracles'; import { OracleProfileTitle, OracleFullProfile } from '@vegaprotocol/oracles';
import {
OracleBasicProfile,
useOracleProofs,
useOracleMarkets,
} from '@vegaprotocol/oracles';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
type PanelProps = Pick< type PanelProps = Pick<
@ -456,6 +462,11 @@ export const OracleInfoPanel = ({
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment(); const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
const { data } = useOracleProofs(ORACLE_PROOFS_URL); const { data } = useOracleProofs(ORACLE_PROOFS_URL);
const dataSourceSpecId =
type === 'settlementData'
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id;
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<DataSourceProof <DataSourceProof
@ -467,6 +478,7 @@ export const OracleInfoPanel = ({
} }
providers={data} providers={data}
type={type} type={type}
id={dataSourceSpecId}
/> />
<ExternalLink <ExternalLink
data-testid="oracle-spec-links" data-testid="oracle-spec-links"
@ -488,10 +500,12 @@ export const DataSourceProof = ({
data, data,
providers, providers,
type, type,
id,
}: { }: {
data: DataSourceDefinition; data: DataSourceDefinition;
providers: Provider[] | undefined; providers: Provider[] | undefined;
type: 'settlementData' | 'termination'; type: 'settlementData' | 'termination';
id: string;
}) => { }) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') { if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || []; const signers = data.sourceType.sourceType.signers || [];
@ -509,7 +523,7 @@ export const DataSourceProof = ({
providers={providers} providers={providers}
signer={signer} signer={signer}
type={type} type={type}
index={i} id={id}
/> />
); );
})} })}
@ -540,11 +554,12 @@ const OracleLink = ({
providers, providers,
signer, signer,
type, type,
id,
}: { }: {
providers: Provider[]; providers: Provider[];
signer: SignerKind; signer: SignerKind;
type: 'settlementData' | 'termination'; type: 'settlementData' | 'termination';
index: number; id: string;
}) => { }) => {
const signerProviders = providers.filter((p) => { const signerProviders = providers.filter((p) => {
if (signer.__typename === 'PubKey') { if (signer.__typename === 'PubKey') {
@ -575,10 +590,7 @@ const OracleLink = ({
return ( return (
<div> <div>
{signerProviders.map((provider) => ( {signerProviders.map((provider) => (
<OracleBasicProfile <OracleProfile key={id} provider={provider} id={id} />
key={provider.name}
provider={provider}
></OracleBasicProfile>
))} ))}
</div> </div>
); );
@ -598,3 +610,36 @@ const NoOracleProof = ({
</p> </p>
); );
}; };
const OracleProfile = ({
provider,
id,
}: {
provider: Provider;
id: string;
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
const oracleMarkets = useOracleMarkets(provider);
return (
<div key={provider.name}>
<OracleBasicProfile
provider={provider}
onClick={() => setDialogOpen(!dialogOpen)}
markets={oracleMarkets}
/>
<Dialog
title={<OracleProfileTitle provider={provider} />}
open={dialogOpen}
onChange={() => setDialogOpen(!dialogOpen)}
aria-labelledby="oracle-proof-dialog"
>
<OracleFullProfile
provider={provider}
id={id}
key={id}
markets={oracleMarkets}
/>
</Dialog>
</div>
);
};

View File

@ -1 +1,7 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import 'jest-canvas-mock';
import ResizeObserver from 'resize-observer-polyfill';
import { defaultFallbackInView } from 'react-intersection-observer';
defaultFallbackInView(true);
global.ResizeObserver = ResizeObserver;

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,63 @@
fragment OracleMarketSpecFields on Market {
id
state
tradingMode
tradableInstrument {
instrument {
id
name
code
product {
... on Future {
dataSourceSpecForSettlementData {
id
data {
...DataSourceSpec
}
}
dataSourceSpecForTradingTermination {
id
data {
...DataSourceSpec
}
}
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
}
}
}
}
fragment DataSourceSpec on DataSourceDefinition {
sourceType {
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
}
}
}
}
}
query OracleMarketsSpec {
marketsConnection {
edges {
node {
...OracleMarketSpecFields
}
}
}
}

View File

@ -0,0 +1,109 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type OracleMarketSpecFieldsFragment = { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, 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?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, 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?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } } };
export type DataSourceSpecFragment = { __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?: 'DataSourceDefinitionInternal' } };
export type OracleMarketsSpecQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type OracleMarketsSpecQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, 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?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, 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?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } } } }> } | null };
export const DataSourceSpecFragmentDoc = gql`
fragment DataSourceSpec on DataSourceDefinition {
sourceType {
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
}
}
}
}
}
`;
export const OracleMarketSpecFieldsFragmentDoc = gql`
fragment OracleMarketSpecFields on Market {
id
state
tradingMode
tradableInstrument {
instrument {
id
name
code
product {
... on Future {
dataSourceSpecForSettlementData {
id
data {
...DataSourceSpec
}
}
dataSourceSpecForTradingTermination {
id
data {
...DataSourceSpec
}
}
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
}
}
}
}
${DataSourceSpecFragmentDoc}`;
export const OracleMarketsSpecDocument = gql`
query OracleMarketsSpec {
marketsConnection {
edges {
node {
...OracleMarketSpecFields
}
}
}
}
${OracleMarketSpecFieldsFragmentDoc}`;
/**
* __useOracleMarketsSpecQuery__
*
* To run a query within a React component, call `useOracleMarketsSpecQuery` and pass it any options that fit your needs.
* When your component renders, `useOracleMarketsSpecQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useOracleMarketsSpecQuery({
* variables: {
* },
* });
*/
export function useOracleMarketsSpecQuery(baseOptions?: Apollo.QueryHookOptions<OracleMarketsSpecQuery, OracleMarketsSpecQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<OracleMarketsSpecQuery, OracleMarketsSpecQueryVariables>(OracleMarketsSpecDocument, options);
}
export function useOracleMarketsSpecLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<OracleMarketsSpecQuery, OracleMarketsSpecQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<OracleMarketsSpecQuery, OracleMarketsSpecQueryVariables>(OracleMarketsSpecDocument, options);
}
export type OracleMarketsSpecQueryHookResult = ReturnType<typeof useOracleMarketsSpecQuery>;
export type OracleMarketsSpecLazyQueryHookResult = ReturnType<typeof useOracleMarketsSpecLazyQuery>;
export type OracleMarketsSpecQueryResult = Apollo.QueryResult<OracleMarketsSpecQuery, OracleMarketsSpecQueryVariables>;

View File

@ -1 +1,2 @@
export * from './oracle-basic-profile'; export * from './oracle-basic-profile';
export * from './oracle-full-profile';

View File

@ -35,11 +35,10 @@ describe('OracleBasicProfile', () => {
}); });
it('should render the name', () => { it('should render the name', () => {
render(<OracleBasicProfile provider={testProvider} />); render(<OracleBasicProfile provider={testProvider} markets={undefined} />);
expect(screen.getByTestId('provider-name')).toHaveTextContent( expect(screen.getByTestId('provider-name')).toHaveTextContent(
'Test oracle' 'Test oracle'
); );
expect(screen.getByTestId('verified-proofs')).toHaveTextContent('1'); expect(screen.getByTestId('verified-proofs')).toHaveTextContent('1');
expect(screen.getByTestId('signed-proofs')).toHaveTextContent('1');
}); });
}); });

View File

@ -1,18 +1,19 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { Provider } from '../../oracle-schema'; import type { Provider } from '../../oracle-schema';
import { import {
ButtonLink,
ExternalLink, ExternalLink,
Icon, Icon,
Intent, Intent,
Link,
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons'; import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleMarketsSpec';
const getVerifiedStatusIcon = (provider: Provider) => { export const getVerifiedStatusIcon = (provider: Provider) => {
const getIconIntent = () => { const getIconIntent = () => {
switch (provider.oracle.status) { switch (provider.oracle.status) {
case 'GOOD': case 'GOOD':
@ -54,17 +55,21 @@ const getVerifiedStatusIcon = (provider: Provider) => {
}; };
}; };
export const OracleBasicProfile = ({ provider }: { provider: Provider }) => { export const OracleBasicProfile = ({
provider,
onClick,
markets: oracleMarkets,
}: {
provider: Provider;
markets?: OracleMarketSpecFieldsFragment[] | undefined;
onClick?: (value?: boolean) => void;
}) => {
const { icon, message, intent } = getVerifiedStatusIcon(provider); const { icon, message, intent } = getVerifiedStatusIcon(provider);
const verifiedProofs = provider.proofs.filter( const verifiedProofs = provider.proofs.filter(
(proof) => proof.available === true (proof) => proof.available === true
); );
const signedProofs = provider.proofs.filter(
(proof) => proof.format === 'signed_message' && proof.available === true
);
const links = provider.proofs const links = provider.proofs
.filter((proof) => proof.format === 'url' && proof.available === true) .filter((proof) => proof.format === 'url' && proof.available === true)
.map((proof) => ({ .map((proof) => ({
@ -77,23 +82,20 @@ export const OracleBasicProfile = ({ provider }: { provider: Provider }) => {
<> <>
<span className="flex gap-1"> <span className="flex gap-1">
{provider.url && ( {provider.url && (
<Link <span className="flex align-items-bottom text-md gap-1">
href={provider.github_link} <ButtonLink
className="flex align-items-bottom text-md" onClick={() => onClick && onClick(true)}
target="_blank" data-testid="provider-name"
> >
<span> {provider.name}
<span data-testid="provider-name" className="underline pr-1"> </ButtonLink>
{provider.name} <span
</span> className="dark:text-vega-light-300 text-vega-dark-300"
<span data-testid="verified-proofs"
data-testid="verified-proofs" >
className="dark:text-vega-light-300 text-vega-dark-300" ({verifiedProofs.length})
>
({verifiedProofs.length})
</span>
</span> </span>
</Link> </span>
)} )}
<span <span
className={classNames( className={classNames(
@ -115,10 +117,11 @@ export const OracleBasicProfile = ({ provider }: { provider: Provider }) => {
data-testid="signed-proofs" data-testid="signed-proofs"
className="dark:text-vega-light-300 text-vega-dark-300" className="dark:text-vega-light-300 text-vega-dark-300"
> >
{t('Involved in %s %s', [ {oracleMarkets &&
signedProofs.length.toString(), t('Involved in %s %s', [
signedProofs.length !== 1 ? t('markets') : t('market'), oracleMarkets.length.toString(),
])} oracleMarkets.length !== 1 ? t('markets') : t('market'),
])}
</p> </p>
{links.length > 0 && ( {links.length > 0 && (
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
@ -133,7 +136,7 @@ export const OracleBasicProfile = ({ provider }: { provider: Provider }) => {
<VegaIcon name={getLinkIcon(link.type)} /> <VegaIcon name={getLinkIcon(link.type)} />
</span> </span>
<span className="underline capitalize"> <span className="underline capitalize">
{link.type}{' '} {link.type}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} /> <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</span> </span>
</ExternalLink> </ExternalLink>

View File

@ -0,0 +1,2 @@
export * from './oracle-full-profile.stories';
export * from './oracle-full-profile';

View File

@ -0,0 +1,54 @@
import { OracleFullProfile } from './oracle-full-profile';
import type { Provider } from '../../oracle-schema';
import { render, screen } from '@testing-library/react';
describe('OracleFullProfile', () => {
const testProvider = {
name: 'Test oracle',
url: 'https://zombo.com',
description_markdown:
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
oracle: {
status: 'GOOD',
status_reason: '',
first_verified: '2023-02-28T00:00:00.000Z',
last_verified: '2023-02-28T00:00:00.000Z',
type: 'eth_address',
eth_address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
proofs: [
{
format: 'signed_message',
available: true,
type: 'eth_address',
eth_address: '0x949AF81E51D57831AE52591d17fBcdd1014a5f52',
message: 'SOMEHEX',
},
],
github_link:
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
} as Provider;
it('should render successfully', () => {
const component = (
<OracleFullProfile provider={testProvider} id={''} markets={[]} />
);
expect(component).toBeTruthy();
});
it('should render the name', () => {
render(
<OracleFullProfile
provider={testProvider}
id={'oracle-id'}
markets={[]}
/>
);
expect(screen.getByTestId('github-link')).toHaveTextContent(
'Oracle repository'
);
expect(screen.getByTestId('block-explorer-link')).toHaveTextContent(
'Block explorer'
);
});
});

View File

@ -0,0 +1,41 @@
import type { Story, Meta } from '@storybook/react';
import { OracleFullProfile } from './oracle-full-profile';
export default {
component: OracleFullProfile,
title: 'OracleFullProfile',
} as Meta;
const Template: Story = (args) => (
<OracleFullProfile provider={args['provider']} id="4578" />
);
export const OraclePrimary = Template.bind({});
OraclePrimary.args = {
provider: {
name: 'Test oracle',
url: 'https://zombo.com',
description_markdown:
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
oracle: {
status: 'GOOD',
status_reason: '',
first_verified: '2023-02-28T00:00:00.000Z',
last_verified: '2023-02-28T00:00:00.000Z',
type: 'eth_address',
eth_address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
proofs: [
{
format: 'signed_message',
available: true,
type: 'eth_address',
eth_address: '0x949AF81E51D57831AE52591d17fBcdd1014a5f52',
message: 'SOMEHEX',
},
],
github_link:
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
},
};

View File

@ -0,0 +1,216 @@
import { t } from '@vegaprotocol/i18n';
import type { Provider } from '../../oracle-schema';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
import {
ButtonLink,
ExternalLink,
Icon,
Intent,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import classNames from 'classnames';
import { getLinkIcon, getVerifiedStatusIcon } from '../oracle-basic-profile';
import { useEnvironment } from '@vegaprotocol/environment';
import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleMarketsSpec';
import { useState } from 'react';
export const OracleProfileTitle = ({ provider }: { provider: Provider }) => {
const { icon, intent } = getVerifiedStatusIcon(provider);
const verifiedProofs = provider.proofs.filter(
(proof) => proof.available === true
);
return (
<span className="flex gap-1">
{provider.url && (
<span>
<span className="pr-1">{provider.name}</span>
<span className="dark:text-vega-light-300 text-vega-dark-300">
({verifiedProofs.length})
</span>
</span>
)}
<span
className={classNames(
{
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'text-vega-green dark:text-vega-green': intent === Intent.Success,
'text-yellow-600 dark:text-yellow': intent === Intent.Warning,
'text-vega-pink': intent === Intent.Danger,
},
'flex items-start align-text-bottom p-1'
)}
>
<Icon size={6} name={icon as IconName} />
</span>
</span>
);
};
export const OracleFullProfile = ({
provider,
id,
markets: oracleMarkets,
}: {
provider: Provider;
id: string;
markets?: OracleMarketSpecFieldsFragment[] | undefined;
}) => {
const { message } = getVerifiedStatusIcon(provider);
const { VEGA_EXPLORER_URL } = useEnvironment();
const [showMore, setShowMore] = useState(false);
const links = provider.proofs
.filter((proof) => proof.format === 'url' && proof.available === true)
.map((proof) => ({
...proof,
url: 'url' in proof ? proof.url : '',
icon: getLinkIcon(proof.type),
}));
return (
<div className="flex flex-col text-sm">
<div className="dark:text-vega-light-300 text-vega-dark-300">
<p className=" pb-2">{message}</p>
{!showMore && (
<p className="pb-2">
{provider.description_markdown.slice(0, 100)}
{'... '}
<span className="ml-2">
<ButtonLink onClick={() => setShowMore(!showMore)}>
Read more
</ButtonLink>
</span>
</p>
)}
{showMore && (
<p className="pb-2">
{provider.description_markdown}
<span className="ml-2">
<ButtonLink onClick={() => setShowMore(!showMore)}>
Show less
</ButtonLink>
</span>
</p>
)}
</div>
<div className="grid grid-cols-2 gap-6">
<div className="col-span-1">
<p
className="dark:text-vega-light-300 text-vega-dark-300 uppercase"
data-testid="verified-accounts"
>
{t('%s proofs of ownership', links.length.toString())}
</p>
{links.length > 0 ? (
<div className="flex flex-col gap-1">
{links.map((link) => (
<ExternalLink
key={link.url}
href={link.url}
className="flex align-items-bottom underline text-sm"
>
<span className="pt-1">
<VegaIcon name={getLinkIcon(link.type)} />
</span>
<span className="underline capitalize">
{link.type}{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</span>
</ExternalLink>
))}
</div>
) : (
<p className="dark:text-vega-light-300 text-vega-dark-300">
{t('This oracle has not proven ownership of any accounts.')}
</p>
)}
</div>
<div className="col-span-1 gap-2 py-2 flex flex-col">
<p className="dark:text-vega-light-300 text-vega-dark-300 uppercase">
{t('Details')}
</p>
{id && (
<ExternalLink
href={`${VEGA_EXPLORER_URL}/oracles/${id}`}
data-testid="block-explorer-link"
>
{t('Block explorer')}
</ExternalLink>
)}
{provider.github_link && (
<ExternalLink href={provider.github_link} data-testid="github-link">
{t('Oracle repository')}
</ExternalLink>
)}
</div>
</div>
<div>
{oracleMarkets && (
<p className="dark:text-vega-light-300 text-vega-dark-300 uppercase mt-4">
{t('Oracle in %s %s', [
oracleMarkets.length.toString(),
oracleMarkets.length === 1 ? 'market' : 'markets',
])}
</p>
)}
</div>
{oracleMarkets && oracleMarkets.length > 0 && (
<div
data-testid="oracle-markets"
className="border-vega-light-200 dark:border-vega-dark-200 border-solid border-2 py-4 px-2 rounded-lg my-2"
>
<div className="grid grid-cols-4 gap-1 uppercase mb-2 font-alpha calt dark:text-vega-light-300 text-vega-dark-300">
<div className="col-span-1">{t('Market')}</div>
<div className="col-span-1">{t('Status')}</div>
<div className="col-span-1">{t('Specifications')}</div>
</div>
<div className="max-h-60 overflow-auto">
{oracleMarkets?.map((market) => (
<div
className="grid grid-cols-4 gap-1 capitalize mb-2 last:mb-0"
key={`oracle-market-${market.id}`}
>
<div className="col-span-1">
{market.tradableInstrument.instrument.code}
</div>
<div
className={classNames('col-span-1', {
'dark:text-vega-light-300 text-vega-dark-300': ![
MarketState.STATE_ACTIVE,
MarketState.STATE_PROPOSED,
].includes(market.state),
})}
>
{MarketStateMapping[market.state]}
</div>
<div className="col-span-1">
{
<ExternalLink
href={`${VEGA_EXPLORER_URL}/oracles/${market.tradableInstrument?.instrument.product?.dataSourceSpecForSettlementData.id}`}
data-testid="block-explorer-link-settlement"
>
{t('Settlement')}
</ExternalLink>
}
</div>
<div className="col-span-1">
{
<ExternalLink
href={`${VEGA_EXPLORER_URL}/oracles/${market.tradableInstrument?.instrument.product?.dataSourceSpecForTradingTermination.id}`}
data-testid="block-explorer-link-termination"
>
{t('Termination')}
</ExternalLink>
}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,3 @@
export * from './use-oracle-markets';
export * from './use-oracle-proofs';
export * from './use-oracle-spec-binding-data';

View File

@ -0,0 +1,50 @@
import type { Provider } from '../oracle-schema';
import type { OracleMarketSpecFieldsFragment } from '../__generated__/OracleMarketsSpec';
import { useOracleMarketsSpecQuery } from '../__generated__/OracleMarketsSpec';
export const useOracleMarkets = (
provider: Provider
): OracleMarketSpecFieldsFragment[] | undefined => {
const signedProofs = provider.proofs.filter(
(proof) => proof.format === 'signed_message' && proof.available === true
);
const { data: markets } = useOracleMarketsSpecQuery();
const oracleMarkets = markets?.marketsConnection?.edges
?.map((edge) => edge.node)
?.filter((node) => {
const p = node.tradableInstrument.instrument.product;
const sourceType = p.dataSourceSpecForSettlementData.data.sourceType;
if (sourceType.__typename !== 'DataSourceDefinitionExternal') {
return false;
}
const signers = sourceType?.sourceType.signers;
const signerKeys = signers?.filter(Boolean).map((signer) => {
if (signer.signer.__typename === 'ETHAddress') {
return signer.signer.address;
}
if (signer.signer.__typename === 'PubKey') {
return signer.signer.key;
}
return undefined;
});
const signedProofsKeys = signedProofs.map((proof) => {
if ('public_key' in proof && proof.public_key) {
return proof.public_key;
}
if ('eth_address' in proof && proof.eth_address) {
return proof.eth_address;
}
return undefined;
});
const key = signedProofsKeys.find((key) => signerKeys?.includes(key));
return !!key;
});
return oracleMarkets;
};

View File

@ -1,5 +1,5 @@
import { renderHook, waitFor } from '@testing-library/react'; import { renderHook, waitFor } from '@testing-library/react';
import type { Provider } from './oracle-schema'; import type { Provider } from '../oracle-schema';
import { useOracleProofs, cache, invalidateCache } from './use-oracle-proofs'; import { useOracleProofs, cache, invalidateCache } from './use-oracle-proofs';
global.fetch = jest.fn(); global.fetch = jest.fn();

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { Provider } from './oracle-schema'; import type { Provider } from '../oracle-schema';
import { providersSchema } from './oracle-schema'; import { providersSchema } from '../oracle-schema';
export let cache: { export let cache: {
[url: string]: Provider[]; [url: string]: Provider[];

View File

@ -4,8 +4,8 @@ import { MockedProvider } from '@apollo/client/testing';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useOracleSpecBindingData } from './use-oracle-spec-binding-data'; import { useOracleSpecBindingData } from './use-oracle-spec-binding-data';
import type { Property } from '@vegaprotocol/types'; import type { Property } from '@vegaprotocol/types';
import type { OracleSpecDataConnectionQuery } from './__generated__/OracleSpecDataConnection'; import type { OracleSpecDataConnectionQuery } from '../__generated__/OracleSpecDataConnection';
import { OracleSpecDataConnectionDocument } from './__generated__/OracleSpecDataConnection'; import { OracleSpecDataConnectionDocument } from '../__generated__/OracleSpecDataConnection';
describe('useSettlementPrice', () => { describe('useSettlementPrice', () => {
const setup = ( const setup = (

View File

@ -1,4 +1,4 @@
import { useOracleSpecDataConnectionQuery } from './__generated__/OracleSpecDataConnection'; import { useOracleSpecDataConnectionQuery } from '../__generated__/OracleSpecDataConnection';
export const useOracleSpecBindingData = ( export const useOracleSpecBindingData = (
oracleSpecId: string | undefined, oracleSpecId: string | undefined,

View File

@ -2,5 +2,4 @@ export * from './__generated__';
export * from './components'; export * from './components';
export * from './oracle-schema'; export * from './oracle-schema';
export * from './oracle-spec-data-connection.mock'; export * from './oracle-spec-data-connection.mock';
export * from './use-oracle-proofs'; export * from './hooks';
export * from './use-oracle-spec-binding-data';

View File

@ -6,7 +6,7 @@
}, },
"files": [ "files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts", "../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts" "../../node_modules/@nrwl/next/typings/image.d.ts"
], ],
"exclude": [ "exclude": [
"**/*.spec.ts", "**/*.spec.ts",

View File

@ -13,7 +13,7 @@ interface DialogProps {
onChange?: (isOpen: boolean) => void; onChange?: (isOpen: boolean) => void;
onCloseAutoFocus?: (e: Event) => void; onCloseAutoFocus?: (e: Event) => void;
onInteractOutside?: (e: Event) => void; onInteractOutside?: (e: Event) => void;
title?: string; title?: string | ReactNode;
icon?: ReactNode; icon?: ReactNode;
intent?: Intent; intent?: Intent;
size?: 'small' | 'medium'; size?: 'small' | 'medium';

View File

@ -40,7 +40,7 @@ describe('ExternalLink', () => {
render(<ExternalLink href="https://vega.xyz/">Go to Vega</ExternalLink>); render(<ExternalLink href="https://vega.xyz/">Go to Vega</ExternalLink>);
const link = screen.getByTestId('external-link'); const link = screen.getByTestId('external-link');
expect(link.children.length).toEqual(2); expect(link.children.length).toEqual(2);
expect(link.children[1].tagName.toUpperCase()).toEqual('SVG'); expect(link.children[1].tagName.toUpperCase()).toEqual('SPAN');
}); });
it('should have an underlined text part', () => { it('should have an underlined text part', () => {

View File

@ -1,7 +1,6 @@
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode, AnchorHTMLAttributes } from 'react'; import type { ReactNode, AnchorHTMLAttributes } from 'react';
import { Icon } from '../icon'; import { VegaIcon, VegaIconNames } from '../icon';
type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & { type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
children?: ReactNode; children?: ReactNode;
@ -24,7 +23,7 @@ export const Link = ({ className, children, ...props }: LinkProps) => {
}; };
// if no href is passed just render a span, this is so that we can wrap an // if no href is passed just render a span, this is so that we can wrap an
// element with our links styles with a react router link compoment // element with our links styles with a react router link component
if (!props.href) { if (!props.href) {
return ( return (
<span {...shared} {...props}> <span {...shared} {...props}>
@ -43,7 +42,10 @@ Link.displayName = 'Link';
export const ExternalLink = ({ children, className, ...props }: LinkProps) => ( export const ExternalLink = ({ children, className, ...props }: LinkProps) => (
<Link <Link
className={classNames(className, 'inline-flex items-baseline')} className={classNames(
className,
'inline-flex items-baseline underline-offset-4'
)}
target="_blank" target="_blank"
data-testid="external-link" data-testid="external-link"
rel="noreferrer nofollow noopener" rel="noreferrer nofollow noopener"
@ -56,7 +58,7 @@ export const ExternalLink = ({ children, className, ...props }: LinkProps) => (
> >
{children} {children}
</span> </span>
<Icon size={3} name={IconNames.SHARE} className="ml-1" /> <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</> </>
) : ( ) : (
children children