diff --git a/apps/explorer/.env b/apps/explorer/.env index 6e2be9609..bcffbed73 100644 --- a/apps/explorer/.env +++ b/apps/explorer/.env @@ -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_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest 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_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo diff --git a/apps/explorer/src/app/components/markets/market-details.tsx b/apps/explorer/src/app/components/markets/market-details.tsx index 3f8c02a00..cbcb783b6 100644 --- a/apps/explorer/src/app/components/markets/market-details.tsx +++ b/apps/explorer/src/app/components/markets/market-details.tsx @@ -14,10 +14,72 @@ import { SettlementAssetInfoPanel, } 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 }) => { 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: ( + + ), + }, + { + title: t('Termination Oracle'), + content: ( + + ), + }, + ] + : [ + { + title: t('Oracle'), + content: ( + + ), + }, + ]; + const panels = [ { title: t('Key details'), @@ -95,22 +157,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => { ), }, - { - title: t('Settlement Oracle'), - content: ( - - ), - }, - { - title: t('Termination Oracle'), - content: ( - - ), - }, + ...oraclePanels, ]; return ( diff --git a/apps/trading-e2e/src/integration/market-info.cy.ts b/apps/trading-e2e/src/integration/market-info.cy.ts index f507a47ee..2798325c7 100644 --- a/apps/trading-e2e/src/integration/market-info.cy.ts +++ b/apps/trading-e2e/src/integration/market-info.cy.ts @@ -184,17 +184,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => { .getByTestId('provider-name') .and('contain', 'Another oracle'); - cy.getByTestId(accordionContent) - .getByTestId('signed-proofs') - .and('contain', '1'); - cy.getByTestId(accordionContent) .getByTestId('verified-proofs') .and('contain', '1'); - - cy.getByTestId(accordionContent) - .getByTestId('signed-proofs') - .and('contain', '1'); }); it('proposal displayed', () => { diff --git a/libs/market-info/src/components/market-info/__generated__/MarketInfo.ts b/libs/market-info/src/components/market-info/__generated__/MarketInfo.ts index 6825b5d7c..51c75dc13 100644 --- a/libs/market-info/src/components/market-info/__generated__/MarketInfo.ts +++ b/libs/market-info/src/components/market-info/__generated__/MarketInfo.ts @@ -198,4 +198,4 @@ export function useMarketInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions } export type MarketInfoQueryHookResult = ReturnType; export type MarketInfoLazyQueryHookResult = ReturnType; -export type MarketInfoQueryResult = Apollo.QueryResult; +export type MarketInfoQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/libs/market-info/src/components/market-info/market-info-accordion.tsx b/libs/market-info/src/components/market-info/market-info-accordion.tsx index 1b8c3cff0..19960ea8f 100644 --- a/libs/market-info/src/components/market-info/market-info-accordion.tsx +++ b/libs/market-info/src/components/market-info/market-info-accordion.tsx @@ -35,6 +35,8 @@ import { RiskParametersInfoPanel, SettlementAssetInfoPanel, } from './market-info-panels'; +import type { DataSourceDefinition } from '@vegaprotocol/types'; +import isEqual from 'lodash/isEqual'; export interface MarketInfoAccordionProps { market: MarketInfo; @@ -70,7 +72,7 @@ export const MarketInfoAccordionContainer = ({ ); }; -const MarketInfoAccordion = ({ +export const MarketInfoAccordion = ({ market, onSelect, }: MarketInfoAccordionProps) => { @@ -103,8 +105,64 @@ const MarketInfoAccordion = ({ content: , })), ]; - 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: ( + + ), + }, + ] + : [ + { + title: t('Settlement Oracle'), + content: ( + + ), + }, + { + title: t('Termination Oracle'), + content: ( + + ), + }, + ]; const marketSpecPanels = [ { title: t('Key details'), @@ -114,14 +172,7 @@ const MarketInfoAccordion = ({ title: t('Instrument'), content: , }, - product.dataSourceSpecForSettlementData && { - title: t('Settlement Oracle'), - content: , - }, - product.dataSourceSpecForTradingTermination && { - title: t('Termination Oracle'), - content: , - }, + ...oraclePanels, { title: t('Settlement asset'), content: , @@ -182,33 +233,37 @@ const MarketInfoAccordion = ({ title: t('Proposal'), content: (
- - {t('View governance proposal')} - - - {t('Propose a change to market')} - + {VEGA_TOKEN_URL && ( + + {t('View governance proposal')} + + )} + {VEGA_TOKEN_URL && ( + + {t('Propose a change to market')} + + )}
), }, @@ -225,7 +280,7 @@ const MarketInfoAccordion = ({

{t('Market specification')}

- {VEGA_TOKEN_URL && market.proposal?.id && ( + {VEGA_TOKEN_URL && marketGovPanels && market.proposal?.id && (

{t('Market governance')}

diff --git a/libs/market-info/src/components/market-info/market-info-panels.spec.tsx b/libs/market-info/src/components/market-info/market-info-panels.spec.tsx index 1ddbd3e83..067795952 100644 --- a/libs/market-info/src/components/market-info/market-info-panels.spec.tsx +++ b/libs/market-info/src/components/market-info/market-info-panels.spec.tsx @@ -5,62 +5,13 @@ import { } from '@vegaprotocol/types'; import { DataSourceProof } from './market-info-panels'; +jest.mock('@vegaprotocol/oracles', () => ({ + useOracleMarkets: () => [], +})); + describe('DataSourceProof', () => { const ORACLE_PUBKEY = '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(); - expect(screen.getByRole('link')).toHaveAttribute( - 'href', - props.providers[0].github_link - ); - }); it('renders message if there are no providers', () => { const props = { @@ -84,7 +35,7 @@ describe('DataSourceProof', () => { providers: [], type: 'termination' as const, }; - render(); + render(); expect( screen.getByText('No oracle proof for termination') ).toBeInTheDocument(); @@ -137,7 +88,7 @@ describe('DataSourceProof', () => { ], type: 'settlementData' as const, }; - render(); + render(); expect( screen.getByText('No oracle proof for settlement data') ).toBeInTheDocument(); @@ -177,7 +128,7 @@ describe('DataSourceProof', () => { providers: [], type: 'termination' as const, }; - render(); + render(); expect(screen.getByText('Internal conditions')).toBeInTheDocument(); expect( screen.getByText( diff --git a/libs/market-info/src/components/market-info/market-info-panels.tsx b/libs/market-info/src/components/market-info/market-info-panels.tsx index ccaccc984..513dc7723 100644 --- a/libs/market-info/src/components/market-info/market-info-panels.tsx +++ b/libs/market-info/src/components/market-info/market-info-panels.tsx @@ -1,4 +1,5 @@ import type { ComponentProps } from 'react'; +import { useState } from 'react'; import { useMemo } from 'react'; import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets'; import { t } from '@vegaprotocol/i18n'; @@ -6,7 +7,7 @@ import { totalFeesPercentage, marketDataProvider, } from '@vegaprotocol/market-list'; -import { ExternalLink, Splash } from '@vegaprotocol/ui-toolkit'; +import { Dialog, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit'; import { addDecimalsFormatNumber, formatNumber, @@ -26,7 +27,12 @@ import { ConditionOperatorMapping } from '@vegaprotocol/types'; import { MarketTradingModeMapping } from '@vegaprotocol/types'; import { useEnvironment } from '@vegaprotocol/environment'; 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'; type PanelProps = Pick< @@ -456,6 +462,11 @@ export const OracleInfoPanel = ({ const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment(); const { data } = useOracleProofs(ORACLE_PROOFS_URL); + const dataSourceSpecId = + type === 'settlementData' + ? product.dataSourceSpecForSettlementData.id + : product.dataSourceSpecForTradingTermination.id; + return (
{ if (data.sourceType.__typename === 'DataSourceDefinitionExternal') { const signers = data.sourceType.sourceType.signers || []; @@ -509,7 +523,7 @@ export const DataSourceProof = ({ providers={providers} signer={signer} type={type} - index={i} + id={id} /> ); })} @@ -540,11 +554,12 @@ const OracleLink = ({ providers, signer, type, + id, }: { providers: Provider[]; signer: SignerKind; type: 'settlementData' | 'termination'; - index: number; + id: string; }) => { const signerProviders = providers.filter((p) => { if (signer.__typename === 'PubKey') { @@ -575,10 +590,7 @@ const OracleLink = ({ return (
{signerProviders.map((provider) => ( - + ))}
); @@ -598,3 +610,36 @@ const NoOracleProof = ({

); }; + +const OracleProfile = ({ + provider, + id, +}: { + provider: Provider; + id: string; +}) => { + const [dialogOpen, setDialogOpen] = useState(false); + const oracleMarkets = useOracleMarkets(provider); + return ( +
+ setDialogOpen(!dialogOpen)} + markets={oracleMarkets} + /> + } + open={dialogOpen} + onChange={() => setDialogOpen(!dialogOpen)} + aria-labelledby="oracle-proof-dialog" + > + + +
+ ); +}; diff --git a/libs/market-info/src/setup-tests.ts b/libs/market-info/src/setup-tests.ts index 7b0828bfa..e62ea0326 100644 --- a/libs/market-info/src/setup-tests.ts +++ b/libs/market-info/src/setup-tests.ts @@ -1 +1,7 @@ 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; diff --git a/libs/oracles/.storybook/styles.scss b/libs/oracles/.storybook/styles.scss new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/libs/oracles/.storybook/styles.scss @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/libs/oracles/src/lib/OracleMarketsSpec.graphql b/libs/oracles/src/lib/OracleMarketsSpec.graphql new file mode 100644 index 000000000..e085ffa54 --- /dev/null +++ b/libs/oracles/src/lib/OracleMarketsSpec.graphql @@ -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 + } + } + } +} diff --git a/libs/oracles/src/lib/__generated__/OracleMarketsSpec.ts b/libs/oracles/src/lib/__generated__/OracleMarketsSpec.ts new file mode 100644 index 000000000..f45568150 --- /dev/null +++ b/libs/oracles/src/lib/__generated__/OracleMarketsSpec.ts @@ -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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(OracleMarketsSpecDocument, options); + } +export function useOracleMarketsSpecLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(OracleMarketsSpecDocument, options); + } +export type OracleMarketsSpecQueryHookResult = ReturnType; +export type OracleMarketsSpecLazyQueryHookResult = ReturnType; +export type OracleMarketsSpecQueryResult = Apollo.QueryResult; diff --git a/libs/oracles/src/lib/components/index.ts b/libs/oracles/src/lib/components/index.ts index 8706f7f57..98884d206 100644 --- a/libs/oracles/src/lib/components/index.ts +++ b/libs/oracles/src/lib/components/index.ts @@ -1 +1,2 @@ export * from './oracle-basic-profile'; +export * from './oracle-full-profile'; diff --git a/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.spec.tsx b/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.spec.tsx index 1a513e7dd..e33f6f853 100644 --- a/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.spec.tsx +++ b/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.spec.tsx @@ -35,11 +35,10 @@ describe('OracleBasicProfile', () => { }); it('should render the name', () => { - render(); + render(); expect(screen.getByTestId('provider-name')).toHaveTextContent( 'Test oracle' ); expect(screen.getByTestId('verified-proofs')).toHaveTextContent('1'); - expect(screen.getByTestId('signed-proofs')).toHaveTextContent('1'); }); }); diff --git a/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx b/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx index 3da3d0ea9..49a19533a 100644 --- a/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx +++ b/libs/oracles/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx @@ -1,18 +1,19 @@ import { t } from '@vegaprotocol/i18n'; import type { Provider } from '../../oracle-schema'; import { + ButtonLink, ExternalLink, Icon, Intent, - Link, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import type { IconName } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; +import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleMarketsSpec'; -const getVerifiedStatusIcon = (provider: Provider) => { +export const getVerifiedStatusIcon = (provider: Provider) => { const getIconIntent = () => { switch (provider.oracle.status) { 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 verifiedProofs = provider.proofs.filter( (proof) => proof.available === true ); - const signedProofs = provider.proofs.filter( - (proof) => proof.format === 'signed_message' && proof.available === true - ); - const links = provider.proofs .filter((proof) => proof.format === 'url' && proof.available === true) .map((proof) => ({ @@ -77,23 +82,20 @@ export const OracleBasicProfile = ({ provider }: { provider: Provider }) => { <> {provider.url && ( - - - - {provider.name} - - - ({verifiedProofs.length}) - + + onClick && onClick(true)} + data-testid="provider-name" + > + {provider.name} + + + ({verifiedProofs.length}) - + )} { data-testid="signed-proofs" className="dark:text-vega-light-300 text-vega-dark-300" > - {t('Involved in %s %s', [ - signedProofs.length.toString(), - signedProofs.length !== 1 ? t('markets') : t('market'), - ])} + {oracleMarkets && + t('Involved in %s %s', [ + oracleMarkets.length.toString(), + oracleMarkets.length !== 1 ? t('markets') : t('market'), + ])}

{links.length > 0 && (
@@ -133,7 +136,7 @@ export const OracleBasicProfile = ({ provider }: { provider: Provider }) => { - {link.type}{' '} + {link.type} diff --git a/libs/oracles/src/lib/components/oracle-full-profile/index.ts b/libs/oracles/src/lib/components/oracle-full-profile/index.ts new file mode 100644 index 000000000..a16060bc4 --- /dev/null +++ b/libs/oracles/src/lib/components/oracle-full-profile/index.ts @@ -0,0 +1,2 @@ +export * from './oracle-full-profile.stories'; +export * from './oracle-full-profile'; diff --git a/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.spec.tsx b/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.spec.tsx new file mode 100644 index 000000000..c495cfef8 --- /dev/null +++ b/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.spec.tsx @@ -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 = ( + + ); + expect(component).toBeTruthy(); + }); + + it('should render the name', () => { + render( + + ); + expect(screen.getByTestId('github-link')).toHaveTextContent( + 'Oracle repository' + ); + expect(screen.getByTestId('block-explorer-link')).toHaveTextContent( + 'Block explorer' + ); + }); +}); diff --git a/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.stories.tsx b/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.stories.tsx new file mode 100644 index 000000000..18ec32975 --- /dev/null +++ b/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.stories.tsx @@ -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) => ( + +); + +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', + }, +}; diff --git a/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.tsx b/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.tsx new file mode 100644 index 000000000..494a522fb --- /dev/null +++ b/libs/oracles/src/lib/components/oracle-full-profile/oracle-full-profile.tsx @@ -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 ( + + {provider.url && ( + + {provider.name} + + ({verifiedProofs.length}) + + + )} + + + + + ); +}; + +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 ( +
+
+

{message}

+ {!showMore && ( +

+ {provider.description_markdown.slice(0, 100)} + {'... '} + + setShowMore(!showMore)}> + Read more + + +

+ )} + {showMore && ( +

+ {provider.description_markdown} + + setShowMore(!showMore)}> + Show less + + +

+ )} +
+
+
+

+ {t('%s proofs of ownership', links.length.toString())} +

+ {links.length > 0 ? ( +
+ {links.map((link) => ( + + + + + + {link.type}{' '} + + + + ))} +
+ ) : ( +

+ {t('This oracle has not proven ownership of any accounts.')} +

+ )} +
+
+

+ {t('Details')} +

+ {id && ( + + {t('Block explorer')} + + )} + {provider.github_link && ( + + {t('Oracle repository')} + + )} +
+
+
+ {oracleMarkets && ( +

+ {t('Oracle in %s %s', [ + oracleMarkets.length.toString(), + oracleMarkets.length === 1 ? 'market' : 'markets', + ])} +

+ )} +
+ {oracleMarkets && oracleMarkets.length > 0 && ( +
+
+
{t('Market')}
+
{t('Status')}
+
{t('Specifications')}
+
+
+ {oracleMarkets?.map((market) => ( +
+
+ {market.tradableInstrument.instrument.code} +
+
+ {MarketStateMapping[market.state]} +
+
+ { + + {t('Settlement')} + + } +
+
+ { + + {t('Termination')} + + } +
+
+ ))} +
+
+ )} +
+ ); +}; diff --git a/libs/oracles/src/lib/hooks/index.ts b/libs/oracles/src/lib/hooks/index.ts new file mode 100644 index 000000000..285081890 --- /dev/null +++ b/libs/oracles/src/lib/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './use-oracle-markets'; +export * from './use-oracle-proofs'; +export * from './use-oracle-spec-binding-data'; diff --git a/libs/oracles/src/lib/hooks/use-oracle-markets.ts b/libs/oracles/src/lib/hooks/use-oracle-markets.ts new file mode 100644 index 000000000..da5b55a05 --- /dev/null +++ b/libs/oracles/src/lib/hooks/use-oracle-markets.ts @@ -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; +}; diff --git a/libs/oracles/src/lib/use-oracle-proofs.spec.ts b/libs/oracles/src/lib/hooks/use-oracle-proofs.spec.ts similarity index 98% rename from libs/oracles/src/lib/use-oracle-proofs.spec.ts rename to libs/oracles/src/lib/hooks/use-oracle-proofs.spec.ts index d377fdb15..750f398d0 100644 --- a/libs/oracles/src/lib/use-oracle-proofs.spec.ts +++ b/libs/oracles/src/lib/hooks/use-oracle-proofs.spec.ts @@ -1,5 +1,5 @@ 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'; global.fetch = jest.fn(); diff --git a/libs/oracles/src/lib/use-oracle-proofs.ts b/libs/oracles/src/lib/hooks/use-oracle-proofs.ts similarity index 92% rename from libs/oracles/src/lib/use-oracle-proofs.ts rename to libs/oracles/src/lib/hooks/use-oracle-proofs.ts index 7953743c2..3669dd260 100644 --- a/libs/oracles/src/lib/use-oracle-proofs.ts +++ b/libs/oracles/src/lib/hooks/use-oracle-proofs.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import type { Provider } from './oracle-schema'; -import { providersSchema } from './oracle-schema'; +import type { Provider } from '../oracle-schema'; +import { providersSchema } from '../oracle-schema'; export let cache: { [url: string]: Provider[]; diff --git a/libs/oracles/src/lib/use-oracle-spec-binding-data.spec.tsx b/libs/oracles/src/lib/hooks/use-oracle-spec-binding-data.spec.tsx similarity index 94% rename from libs/oracles/src/lib/use-oracle-spec-binding-data.spec.tsx rename to libs/oracles/src/lib/hooks/use-oracle-spec-binding-data.spec.tsx index 57c19d63e..23292b63f 100644 --- a/libs/oracles/src/lib/use-oracle-spec-binding-data.spec.tsx +++ b/libs/oracles/src/lib/hooks/use-oracle-spec-binding-data.spec.tsx @@ -4,8 +4,8 @@ import { MockedProvider } from '@apollo/client/testing'; import type { ReactNode } from 'react'; import { useOracleSpecBindingData } from './use-oracle-spec-binding-data'; import type { Property } from '@vegaprotocol/types'; -import type { OracleSpecDataConnectionQuery } from './__generated__/OracleSpecDataConnection'; -import { OracleSpecDataConnectionDocument } from './__generated__/OracleSpecDataConnection'; +import type { OracleSpecDataConnectionQuery } from '../__generated__/OracleSpecDataConnection'; +import { OracleSpecDataConnectionDocument } from '../__generated__/OracleSpecDataConnection'; describe('useSettlementPrice', () => { const setup = ( diff --git a/libs/oracles/src/lib/use-oracle-spec-binding-data.ts b/libs/oracles/src/lib/hooks/use-oracle-spec-binding-data.ts similarity index 86% rename from libs/oracles/src/lib/use-oracle-spec-binding-data.ts rename to libs/oracles/src/lib/hooks/use-oracle-spec-binding-data.ts index fa9afac50..b5ff55cfa 100644 --- a/libs/oracles/src/lib/use-oracle-spec-binding-data.ts +++ b/libs/oracles/src/lib/hooks/use-oracle-spec-binding-data.ts @@ -1,4 +1,4 @@ -import { useOracleSpecDataConnectionQuery } from './__generated__/OracleSpecDataConnection'; +import { useOracleSpecDataConnectionQuery } from '../__generated__/OracleSpecDataConnection'; export const useOracleSpecBindingData = ( oracleSpecId: string | undefined, diff --git a/libs/oracles/src/lib/index.ts b/libs/oracles/src/lib/index.ts index e45917945..28605920d 100644 --- a/libs/oracles/src/lib/index.ts +++ b/libs/oracles/src/lib/index.ts @@ -2,5 +2,4 @@ export * from './__generated__'; export * from './components'; export * from './oracle-schema'; export * from './oracle-spec-data-connection.mock'; -export * from './use-oracle-proofs'; -export * from './use-oracle-spec-binding-data'; +export * from './hooks'; diff --git a/libs/oracles/tsconfig.lib.json b/libs/oracles/tsconfig.lib.json index 6a440c7bd..5ad10446b 100644 --- a/libs/oracles/tsconfig.lib.json +++ b/libs/oracles/tsconfig.lib.json @@ -6,7 +6,7 @@ }, "files": [ "../../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": [ "**/*.spec.ts", diff --git a/libs/ui-toolkit/src/components/dialog/dialog.tsx b/libs/ui-toolkit/src/components/dialog/dialog.tsx index 21918b352..6e719beb9 100644 --- a/libs/ui-toolkit/src/components/dialog/dialog.tsx +++ b/libs/ui-toolkit/src/components/dialog/dialog.tsx @@ -13,7 +13,7 @@ interface DialogProps { onChange?: (isOpen: boolean) => void; onCloseAutoFocus?: (e: Event) => void; onInteractOutside?: (e: Event) => void; - title?: string; + title?: string | ReactNode; icon?: ReactNode; intent?: Intent; size?: 'small' | 'medium'; diff --git a/libs/ui-toolkit/src/components/link/link.spec.tsx b/libs/ui-toolkit/src/components/link/link.spec.tsx index 389105dbf..1988a655e 100644 --- a/libs/ui-toolkit/src/components/link/link.spec.tsx +++ b/libs/ui-toolkit/src/components/link/link.spec.tsx @@ -40,7 +40,7 @@ describe('ExternalLink', () => { render(Go to Vega); const link = screen.getByTestId('external-link'); 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', () => { diff --git a/libs/ui-toolkit/src/components/link/link.tsx b/libs/ui-toolkit/src/components/link/link.tsx index 6329ece62..a557194d9 100644 --- a/libs/ui-toolkit/src/components/link/link.tsx +++ b/libs/ui-toolkit/src/components/link/link.tsx @@ -1,7 +1,6 @@ -import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import type { ReactNode, AnchorHTMLAttributes } from 'react'; -import { Icon } from '../icon'; +import { VegaIcon, VegaIconNames } from '../icon'; type LinkProps = AnchorHTMLAttributes & { 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 - // 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) { return ( @@ -43,7 +42,10 @@ Link.displayName = 'Link'; export const ExternalLink = ({ children, className, ...props }: LinkProps) => ( ( > {children} - + ) : ( children