feat(oracles): oracle full profile (#3545)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
This commit is contained in:
parent
023a83e0f7
commit
bf63f9d46d
@ -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
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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>;
|
@ -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} />
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
|
3
libs/oracles/.storybook/styles.scss
Normal file
3
libs/oracles/.storybook/styles.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
63
libs/oracles/src/lib/OracleMarketsSpec.graphql
Normal file
63
libs/oracles/src/lib/OracleMarketsSpec.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
109
libs/oracles/src/lib/__generated__/OracleMarketsSpec.ts
generated
Normal file
109
libs/oracles/src/lib/__generated__/OracleMarketsSpec.ts
generated
Normal 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>;
|
@ -1 +1,2 @@
|
|||||||
export * from './oracle-basic-profile';
|
export * from './oracle-basic-profile';
|
||||||
|
export * from './oracle-full-profile';
|
||||||
|
@ -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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './oracle-full-profile.stories';
|
||||||
|
export * from './oracle-full-profile';
|
@ -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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -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',
|
||||||
|
},
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
3
libs/oracles/src/lib/hooks/index.ts
Normal file
3
libs/oracles/src/lib/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './use-oracle-markets';
|
||||||
|
export * from './use-oracle-proofs';
|
||||||
|
export * from './use-oracle-spec-binding-data';
|
50
libs/oracles/src/lib/hooks/use-oracle-markets.ts
Normal file
50
libs/oracles/src/lib/hooks/use-oracle-markets.ts
Normal 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;
|
||||||
|
};
|
@ -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();
|
@ -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[];
|
@ -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 = (
|
@ -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,
|
@ -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';
|
|
||||||
|
@ -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",
|
||||||
|
@ -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';
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user