From 8d31510d5e438f0bfec73f5a4b4d3b4c39604b37 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Wed, 9 Aug 2023 14:05:37 +0100 Subject: [PATCH] feat(governance,markets,ui-toolkit): highlight successor market changes (#4449) Co-authored-by: Joe --- .../src/integration/flow/proposal-forms.cy.ts | 1 + .../proposal-market-data.tsx | 92 +++- .../components/proposal/proposal.tsx | 7 +- .../proposals/proposal/proposal-container.tsx | 47 +- libs/assets/src/lib/asset-data-provider.ts | 4 +- .../market-info/info-key-value-table.tsx | 83 ++- .../market-info/market-info-panels.tsx | 471 +++++++++++++++--- .../oracle-basic-profile.tsx | 9 + .../oracle-dialog/oracle-dialog.tsx | 13 +- .../oracle-full-profile.tsx | 18 +- libs/ui-toolkit/src/utils/intent.ts | 2 +- 11 files changed, 652 insertions(+), 95 deletions(-) diff --git a/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts index 7a7157101..4e9ec9784 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts @@ -730,6 +730,7 @@ context( .getByTestId('key-value-table-row') .contains(heading) .parent() + .parent() .siblings(); } } diff --git a/apps/governance/src/routes/proposals/components/proposal-market-data/proposal-market-data.tsx b/apps/governance/src/routes/proposals/components/proposal-market-data/proposal-market-data.tsx index c46d4903c..6cce4d6fe 100644 --- a/apps/governance/src/routes/proposals/components/proposal-market-data/proposal-market-data.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-market-data/proposal-market-data.tsx @@ -45,8 +45,10 @@ export const useMarketDataDialogStore = create( export const ProposalMarketData = ({ marketData, + parentMarketData, }: { marketData: MarketInfoWithData; + parentMarketData?: MarketInfoWithData; }) => { const { t } = useTranslation(); const { isOpen, open, close } = useMarketDataDialogStore(); @@ -58,8 +60,21 @@ export const ProposalMarketData = ({ const settlementData = marketData.tradableInstrument.instrument.product .dataSourceSpecForSettlementData.data as DataSourceDefinition; + const parentSettlementData = + parentMarketData?.tradableInstrument.instrument?.product + ?.dataSourceSpecForSettlementData?.data; const terminationData = marketData.tradableInstrument.instrument.product .dataSourceSpecForTradingTermination.data as DataSourceDefinition; + const parentTerminationData = + parentMarketData?.tradableInstrument.instrument?.product + ?.dataSourceSpecForTradingTermination?.data; + + const isParentSettlementDataEqual = + parentSettlementData !== undefined && + isEqual(settlementData, parentSettlementData); + const isParentTerminationDataEqual = + parentTerminationData !== undefined && + isEqual(terminationData, parentTerminationData); const getSigners = (data: DataSourceDefinition) => { if (data.sourceType.__typename === 'DataSourceDefinitionExternal') { @@ -97,12 +112,22 @@ export const ProposalMarketData = ({ } + content={ + + } /> } + content={ + + } /> {isEqual( getSigners(settlementData), @@ -115,6 +140,11 @@ export const ProposalMarketData = ({ } /> @@ -127,6 +157,11 @@ export const ProposalMarketData = ({ } /> @@ -135,11 +170,21 @@ export const ProposalMarketData = ({ itemId="termination-oracle" title={t('Termination Oracle')} content={ - + } /> )} + {/*Note: successor markets will not differ in their settlement*/} + {/*assets, so no need to pass in parent market data for comparison.*/} } + content={ + + } /> } + content={ + + } /> } + content={ + + } /> } + content={ + + } /> {( marketData.priceMonitoringSettings?.parameters?.triggers || [] @@ -174,6 +239,7 @@ export const ProposalMarketData = ({ content={ } @@ -183,13 +249,21 @@ export const ProposalMarketData = ({ itemId="liqudity-monitoring-parameters" title={t('Liquidity monitoring parameters')} content={ - + } /> } + content={ + + } /> diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx index 6b86800fb..5839e08c3 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx @@ -32,6 +32,7 @@ export interface ProposalProps { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; networkParams: Partial; newMarketData?: MarketInfoWithData | null; + parentMarketData?: MarketInfoWithData | null; assetData?: AssetQuery | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any restData: any; @@ -46,6 +47,7 @@ export const Proposal = ({ networkParams, restData, newMarketData, + parentMarketData, assetData, originalMarketProposalRestData, mostRecentlyEnactedAssociatedMarketProposal, @@ -157,7 +159,10 @@ export const Proposal = ({ {newMarketData && (
- +
)} diff --git a/apps/governance/src/routes/proposals/proposal/proposal-container.tsx b/apps/governance/src/routes/proposals/proposal/proposal-container.tsx index f9986624d..a97fcbd69 100644 --- a/apps/governance/src/routes/proposals/proposal/proposal-container.tsx +++ b/apps/governance/src/routes/proposals/proposal/proposal-container.tsx @@ -14,6 +14,9 @@ import { NetworkParams, useNetworkParams, } from '@vegaprotocol/network-parameters'; +import { useParentMarketIdQuery } from '@vegaprotocol/markets'; +import { FLAGS } from '@vegaprotocol/environment'; +import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals'; export const ProposalContainer = () => { const [ @@ -54,6 +57,10 @@ export const ProposalContainer = () => { skip: !params.proposalId, }); + const successor = useSuccessorMarketProposalDetails(params.proposalId); + + const isSuccessor = !!successor?.parentMarketId || !!successor.code; + const { state: { data: originalMarketProposalRestData, @@ -96,6 +103,36 @@ export const ProposalContainer = () => { }, }); + const { + data: parentMarketId, + loading: parentMarketIdLoading, + error: parentMarketIdError, + } = useParentMarketIdQuery({ + variables: { + marketId: newMarketData?.data?.market?.id || '', + }, + skip: + !FLAGS.SUCCESSOR_MARKETS || + !isSuccessor || + !newMarketData?.data?.market?.id, + }); + + const { + data: parentMarketData, + loading: parentMarketLoading, + error: parentMarketError, + } = useDataProvider({ + dataProvider: marketInfoWithDataProvider, + skipUpdates: true, + variables: { + marketId: parentMarketId?.market?.parentMarketID || '', + skip: + !FLAGS.SUCCESSOR_MARKETS || + !isSuccessor || + !parentMarketId?.market?.parentMarketID, + }, + }); + const { data: assetData, loading: assetLoading, @@ -160,6 +197,8 @@ export const ProposalContainer = () => { newMarketLoading || assetLoading || networkParamsLoading || + parentMarketIdLoading || + parentMarketLoading || (restLoading ? (restLoading as boolean) : false) || (originalMarketProposalRestLoading ? (originalMarketProposalRestLoading as boolean) @@ -172,15 +211,18 @@ export const ProposalContainer = () => { error || newMarketError || assetError || + networkParamsError || + parentMarketIdError || + parentMarketError || restError || originalMarketProposalRestError || - previouslyEnactedMarketProposalsRestError || - networkParamsError + previouslyEnactedMarketProposalsRestError } data={{ ...data, ...networkParams, ...(newMarketData ? { newMarketData } : {}), + ...(parentMarketData ? { parentMarketData } : {}), ...(assetData ? { assetData } : {}), ...(restData ? { restData } : {}), ...(originalMarketProposalRestData @@ -197,6 +239,7 @@ export const ProposalContainer = () => { networkParams={networkParams} restData={restData} newMarketData={newMarketData} + parentMarketData={parentMarketData} assetData={assetData} originalMarketProposalRestData={originalMarketProposalRestData} mostRecentlyEnactedAssociatedMarketProposal={ diff --git a/libs/assets/src/lib/asset-data-provider.ts b/libs/assets/src/lib/asset-data-provider.ts index e117bb30f..9c9018860 100644 --- a/libs/assets/src/lib/asset-data-provider.ts +++ b/libs/assets/src/lib/asset-data-provider.ts @@ -28,10 +28,10 @@ export const assetProvider = makeDataProvider< getData, }); -export const useAssetDataProvider = (assetId: string) => { +export const useAssetDataProvider = (assetId: string, skip?: boolean) => { return useDataProvider({ dataProvider: assetProvider, variables: { assetId: assetId || '' }, - skip: !assetId, + skip: !assetId || skip, }); }; diff --git a/libs/markets/src/lib/components/market-info/info-key-value-table.tsx b/libs/markets/src/lib/components/market-info/info-key-value-table.tsx index ec81c5f0c..e717453bb 100644 --- a/libs/markets/src/lib/components/market-info/info-key-value-table.tsx +++ b/libs/markets/src/lib/components/market-info/info-key-value-table.tsx @@ -4,8 +4,10 @@ import { } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { + Intent, KeyValueTable, KeyValueTableRow, + Lozenge, Tooltip, } from '@vegaprotocol/ui-toolkit'; import BigNumber from 'bignumber.js'; @@ -22,9 +24,11 @@ interface RowProps { unformatted?: boolean; assetSymbol?: string; noBorder?: boolean; + parentValue?: ReactNode; + hasParentData?: boolean; } -const Row = ({ +export const Row = ({ field, value, decimalPlaces, @@ -32,7 +36,14 @@ const Row = ({ unformatted, assetSymbol = '', noBorder = true, + parentValue, + hasParentData, }: RowProps) => { + // Note: we need both 'parentValue' and 'hasParentData' to do a conditional + // check to differentiate between when parentData itself is missing and when + // a specific parentValue is missing. These values are only used when we + // have successor market parent data. + const className = 'text-sm'; const getFormattedValue = (value: ReactNode) => { @@ -55,6 +66,10 @@ const Row = ({ const formattedValue = getFormattedValue(value); if (!formattedValue) return null; + + const newValueInSuccessorMarket = hasParentData && value && !parentValue; + const valueDiffersFromParentMarket = parentValue && parentValue !== value; + return ( - -
{startCase(t(field))}
-
- {formattedValue} +
+ +
{startCase(t(field))}
+
+ + {valueDiffersFromParentMarket && ( + + {t('Updated')} + + )} + + {newValueInSuccessorMarket && ( + + {t('Added')} + + )} +
+
+ {valueDiffersFromParentMarket ? ( +
+ + {getFormattedValue(parentValue)} + + {formattedValue} +
+ ) : ( + formattedValue + )} +
); }; @@ -79,6 +119,7 @@ export interface MarketInfoTableProps { children?: ReactNode; assetSymbol?: string; noBorder?: boolean; + parentData?: Record | null | undefined; } export const MarketInfoTable = ({ @@ -89,25 +130,33 @@ export const MarketInfoTable = ({ children, assetSymbol, noBorder, + parentData, }: MarketInfoTableProps) => { if (!data || typeof data !== 'object') { return null; } + + const hasParentData = parentData !== undefined; + return ( <> - {Object.entries(data).map(([key, value]) => ( - - ))} + <> + {Object.entries(data).map(([key, value]) => ( + + ))} +
{children}
diff --git a/libs/markets/src/lib/components/market-info/market-info-panels.tsx b/libs/markets/src/lib/components/market-info/market-info-panels.tsx index 001f562c6..1c84223d3 100644 --- a/libs/markets/src/lib/components/market-info/market-info-panels.tsx +++ b/libs/markets/src/lib/components/market-info/market-info-panels.tsx @@ -1,3 +1,4 @@ +import isEqual from 'lodash/isEqual'; import type { ReactNode } from 'react'; import { Fragment, useState } from 'react'; import { useMemo } from 'react'; @@ -47,11 +48,14 @@ import { useSuccessorMarketQuery, } from '../../__generated__'; import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals'; +import type { MarketTradingMode } from '@vegaprotocol/types'; +import type { Signer } from '@vegaprotocol/types'; import classNames from 'classnames'; import compact from 'lodash/compact'; type MarketInfoProps = { market: MarketInfo; + parentMarket?: MarketInfo; children?: ReactNode; }; @@ -156,21 +160,43 @@ export const InsurancePoolInfoPanel = ({ ); }; -export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => { - const { data: parentData } = useParentMarketIdQuery({ +export const KeyDetailsInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => { + const { data: parentMarketIdData } = useParentMarketIdQuery({ variables: { marketId: market.id, }, skip: !FLAGS.SUCCESSOR_MARKETS, }); - const { data: successor } = useSuccessorMarketProposalDetailsQuery({ + const { data: successorProposalDetails } = + useSuccessorMarketProposalDetailsQuery({ + variables: { + proposalId: market.proposal?.id || '', + }, + skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id, + }); + + // The following queries are needed as the parent market could also have been a successor market. + // Note: the parent market is only passed to this component if the successor markets flag is enabled, + // so that check is not needed in the skip. + const { data: grandparentMarketIdData } = useParentMarketIdQuery({ variables: { - proposalId: market.proposal?.id || '', + marketId: parentMarket?.id || '', }, - skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id, + skip: !parentMarket?.id, }); + const { data: parentSuccessorProposalDetails } = + useSuccessorMarketProposalDetailsQuery({ + variables: { + proposalId: parentMarket?.proposal?.id || '', + }, + skip: !parentMarket?.proposal?.id, + }); + const assetDecimals = market.tradableInstrument.instrument.product.settlementAsset.decimals; @@ -181,11 +207,12 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => { ? { name: market.tradableInstrument.instrument.name, marketID: market.id, - parentMarketID: parentData?.market?.parentMarketID || '-', + parentMarketID: parentMarketIdData?.market?.parentMarketID || '-', insurancePoolFraction: - (successor?.proposal?.terms.change.__typename === 'NewMarket' && - successor.proposal.terms.change.successorConfiguration - ?.insurancePoolFraction) || + (successorProposalDetails?.proposal?.terms.change.__typename === + 'NewMarket' && + successorProposalDetails.proposal.terms.change + .successorConfiguration?.insurancePoolFraction) || '-', tradingMode: market.tradingMode && @@ -205,6 +232,28 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => { settlementAssetDecimalPlaces: assetDecimals, } } + parentData={ + parentMarket && { + name: parentMarket?.tradableInstrument?.instrument?.name, + marketID: parentMarket?.id, + parentMarketID: grandparentMarketIdData?.market?.parentMarketID, + insurancePoolFraction: + parentSuccessorProposalDetails?.proposal?.terms.change + .__typename === 'NewMarket' && + parentSuccessorProposalDetails.proposal.terms.change + .successorConfiguration?.insurancePoolFraction, + tradingMode: + parentMarket?.tradingMode && + MarketTradingModeMapping[ + parentMarket.tradingMode as MarketTradingMode + ], + marketDecimalPlaces: parentMarket?.decimalPlaces, + positionDecimalPlaces: parentMarket?.positionDecimalPlaces, + settlementAssetDecimalPlaces: + parentMarket?.tradableInstrument?.instrument?.product + ?.settlementAsset?.decimals, + } + } /> ); }; @@ -329,7 +378,10 @@ export const SuccessionLineInfoPanel = ({ ); }; -export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => ( +export const InstrumentInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => ( ( productType: market.tradableInstrument.instrument.product.__typename, quoteName: market.tradableInstrument.instrument.product.quoteName, }} + parentData={ + parentMarket && { + marketName: parentMarket?.tradableInstrument?.instrument?.name, + code: parentMarket?.tradableInstrument?.instrument?.code, + productType: + parentMarket?.tradableInstrument?.instrument?.product?.__typename, + quoteName: + parentMarket?.tradableInstrument?.instrument?.product?.quoteName, + } + } /> ); @@ -349,6 +411,7 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => { () => market?.tradableInstrument.instrument.product?.settlementAsset.id, [market] ); + const { data: asset } = useAssetDataProvider(assetId ?? ''); return asset ? ( <> @@ -371,54 +434,148 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => { ); }; -export const MetadataInfoPanel = ({ market }: MarketInfoProps) => ( +const getMarketMetadata = (market: MarketInfo) => + market.tradableInstrument.instrument.metadata.tags + ?.map((tag) => { + const [key, value] = tag.split(':'); + return { [key]: value }; + }) + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); + +export const MetadataInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => ( { - const [key, value] = tag.split(':'); - return { [key]: value }; - }) - .reduce((acc, curr) => ({ ...acc, ...curr }), {}), + ...(getMarketMetadata(market) || {}), }} + parentData={ + parentMarket && { + expiryDate: getMarketExpiryDateFormatted( + parentMarket.tradableInstrument.instrument.metadata.tags + ), + ...(getMarketMetadata(parentMarket) || {}), + } + } /> ); -export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => { +export const RiskModelInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => { if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') { return null; } + const { tau, riskAversionParameter } = market.tradableInstrument.riskModel; - return ; + + let parentData; + + if ( + parentMarket?.tradableInstrument?.riskModel?.__typename === + 'LogNormalRiskModel' + ) { + const { + tau: parentTau, + riskAversionParameter: parentRiskAversionParameter, + } = market.tradableInstrument.riskModel; + + parentData = { + tau: parentTau, + riskAversionParameter: parentRiskAversionParameter, + }; + } + + return ( + + ); }; -export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => { - if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') { +export const RiskParametersInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => { + const marketType = market.tradableInstrument.riskModel.__typename; + + let data, parentData; + + if (marketType === 'LogNormalRiskModel') { const { r, sigma, mu } = market.tradableInstrument.riskModel.params; - return ; - } - if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') { + data = { r, sigma, mu }; + + if ( + parentMarket?.tradableInstrument?.riskModel.__typename === + 'LogNormalRiskModel' + ) { + const parentParams = parentMarket.tradableInstrument.riskModel.params; + parentData = { + r: parentParams.r, + sigma: parentParams.sigma, + mu: parentParams.mu, + }; + } + } else if (marketType === 'SimpleRiskModel') { const { factorLong, factorShort } = market.tradableInstrument.riskModel.params; - return ; + data = { factorLong, factorShort }; + + if ( + parentMarket?.tradableInstrument?.riskModel.__typename === + 'SimpleRiskModel' + ) { + const parentParams = parentMarket.tradableInstrument.riskModel.params; + parentData = { + factorLong: parentParams.factorLong, + factorShort: parentParams.factorShort, + }; + } } - return null; + + if (!data) return null; + + return ; }; -export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => { +export const RiskFactorsInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => { if (!market.riskFactors) { return null; } + const { short, long } = market.riskFactors; - return ; + + let parentData; + + if (parentMarket?.riskFactors) { + const parentShort = parentMarket.riskFactors.short; + const parentLong = parentMarket.riskFactors.long; + parentData = { short: parentShort, long: parentLong }; + } + + return ( + + ); }; export const PriceMonitoringBoundsInfoPanel = ({ market, triggerIndex, + parentMarket, }: MarketInfoProps & { triggerIndex: number; }) => { @@ -426,11 +583,35 @@ export const PriceMonitoringBoundsInfoPanel = ({ dataProvider: marketDataProvider, variables: { marketId: market.id }, }); + + const { data: parentData } = useDataProvider({ + dataProvider: marketDataProvider, + variables: { marketId: parentMarket?.id || '' }, + skip: + !parentMarket || + !parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[ + triggerIndex + ], + }); + const quoteUnit = market?.tradableInstrument.instrument.product?.quoteName || ''; + const parentQuoteUnit = + parentMarket?.tradableInstrument.instrument.product?.quoteName || ''; + const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit; + const trigger = market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex]; + const parentTrigger = + parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex]; + const isParentTriggerEqual = isEqual(trigger, parentTrigger); + const bounds = data?.priceMonitoringBounds?.[triggerIndex]; + const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex]; + + const shouldShowParentData = + isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds; + if (!trigger) { console.error( `Could not find data for trigger ${triggerIndex} (market id: ${market.id})` @@ -457,6 +638,14 @@ export const PriceMonitoringBoundsInfoPanel = ({ highestPrice: bounds.maxValidPrice, lowestPrice: bounds.minValidPrice, }} + parentData={ + shouldShowParentData + ? { + highestPrice: parentBounds.maxValidPrice, + lowestPrice: parentBounds.minValidPrice, + } + : undefined + } decimalPlaces={market.decimalPlaces} assetSymbol={quoteUnit} /> @@ -472,18 +661,31 @@ export const PriceMonitoringBoundsInfoPanel = ({ export const LiquidityMonitoringParametersInfoPanel = ({ market, -}: MarketInfoProps) => ( - -); + parentMarket, +}: MarketInfoProps) => { + const marketData = { + triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio, + timeWindow: + market.liquidityMonitoringParameters.targetStakeParameters.timeWindow, + scalingFactor: + market.liquidityMonitoringParameters.targetStakeParameters.scalingFactor, + }; + + const parentMarketData = parentMarket + ? { + triggeringRatio: + parentMarket.liquidityMonitoringParameters.triggeringRatio, + timeWindow: + parentMarket.liquidityMonitoringParameters.targetStakeParameters + .timeWindow, + scalingFactor: + parentMarket.liquidityMonitoringParameters.targetStakeParameters + .scalingFactor, + } + : {}; + + return ; +}; export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => { const assetDecimals = @@ -510,16 +712,61 @@ export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => { ); }; -export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => { +export const LiquidityPriceRangeInfoPanel = ({ + market, + parentMarket, +}: MarketInfoProps) => { const quoteUnit = market?.tradableInstrument.instrument.product?.quoteName || ''; + const parentQuoteUnit = + parentMarket?.tradableInstrument.instrument.product?.quoteName || ''; + const liquidityPriceRange = formatNumberPercentage( new BigNumber(market.lpPriceRange).times(100) ); + const parentLiquidityPriceRange = parentMarket + ? formatNumberPercentage( + new BigNumber(parentMarket.lpPriceRange).times(100) + ) + : null; + const { data } = useDataProvider({ dataProvider: marketDataProvider, variables: { marketId: market.id }, }); + + const { data: parentMarketData } = useDataProvider({ + dataProvider: marketDataProvider, + variables: { marketId: parentMarket?.id || '' }, + skip: !parentMarket, + }); + + let parentData; + + if (parentMarket && parentMarketData && quoteUnit === parentQuoteUnit) { + parentData = { + liquidityPriceRange: `${parentLiquidityPriceRange} of mid price`, + lowestPrice: + parentMarketData?.midPrice && + `${addDecimalsFormatNumber( + new BigNumber(1) + .minus(parentMarket.lpPriceRange) + .times(parentMarketData.midPrice) + .toString(), + parentMarket.decimalPlaces + )} ${quoteUnit}`, + highestPrice: + parentMarketData?.midPrice && + `${addDecimalsFormatNumber( + new BigNumber(1) + .plus(parentMarket.lpPriceRange) + .times(parentMarketData.midPrice) + .toString(), + parentMarket.decimalPlaces + )} ${quoteUnit}`, + }; + } + return ( <>

@@ -552,6 +799,7 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => { market.decimalPlaces )} ${quoteUnit}`, }} + parentData={parentData} /> ); @@ -560,8 +808,12 @@ export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => { export const OracleInfoPanel = ({ market, type, + parentMarket, }: MarketInfoProps & { type: 'settlementData' | 'termination' }) => { + // If this is a successor market, this component will only receive parent market + // data if the termination or settlement data is different from the parent. const product = market.tradableInstrument.instrument.product; + const parentProduct = parentMarket?.tradableInstrument?.instrument?.product; const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment(); const { data } = useOracleProofs(ORACLE_PROOFS_URL); @@ -570,12 +822,33 @@ export const OracleInfoPanel = ({ ? product.dataSourceSpecForSettlementData.id : product.dataSourceSpecForTradingTermination.id; + const parentDataSourceSpecId = + type === 'settlementData' + ? parentProduct?.dataSourceSpecForSettlementData?.id + : parentProduct?.dataSourceSpecForTradingTermination?.id; + const dataSourceSpec = ( type === 'settlementData' ? product.dataSourceSpecForSettlementData.data : product.dataSourceSpecForTradingTermination.data ) as DataSourceDefinition; + const parentDataSourceSpec = + type === 'settlementData' + ? parentProduct?.dataSourceSpecForSettlementData?.data + : (parentProduct?.dataSourceSpecForTradingTermination + ?.data as DataSourceDefinition); + + const isParentDataSourceSpecEqual = + parentDataSourceSpec !== undefined && + dataSourceSpec === parentDataSourceSpec; + const isParentDataSourceSpecIdEqual = + parentDataSourceSpecId !== undefined && + dataSourceSpecId === parentDataSourceSpecId; + + // We'll only provide successor parent data (if it differs) to the + // DataSourceProof component. Having an old external link struck through + // is unlikely to be useful. return (

+ { + // If this is a successor market, we'll only pass parent data to child + // components for comparison if the data differs from the parent market. if (data.sourceType.__typename === 'DataSourceDefinitionExternal') { const signers = data.sourceType.sourceType.signers || []; + let parentSigners: Signer[]; + + if ( + parentData && + parentData.sourceType.__typename === 'DataSourceDefinitionExternal' + ) { + parentSigners = parentData.sourceType.sourceType?.signers || []; + } if (!providers?.length) { return ; @@ -622,7 +916,14 @@ export const DataSourceProof = ({ return (
{signers.map(({ signer }, i) => { - return ( + const parentSigner = parentSigners?.find( + ({ signer: ParentSigner }) => + ParentSigner.__typename === signer.__typename + )?.signer; + + const isParentSignerEqual = isEqual(signer, parentSigner); + + return isParentSignerEqual ? ( + ) : ( + ); })}
@@ -663,18 +974,8 @@ export const DataSourceProof = ({ return
{t('Invalid data source')}
; }; -const OracleLink = ({ - providers, - signer, - type, - dataSourceSpecId, -}: { - providers: Provider[]; - signer: SignerKind; - type: 'settlementData' | 'termination'; - dataSourceSpecId: string; -}) => { - const signerProviders = providers.filter((p) => { +const getSignerProviders = (signer: SignerKind, providers: Provider[]) => + providers.filter((p) => { if (signer.__typename === 'PubKey') { if ( p.oracle.type === 'public_key' && @@ -696,19 +997,62 @@ const OracleLink = ({ return false; }); +const OracleLink = ({ + providers, + signer, + type, + dataSourceSpecId, + parentSigner, + parentDataSourceSpecId, +}: { + providers: Provider[]; + signer: SignerKind; + type: 'settlementData' | 'termination'; + dataSourceSpecId: string; + parentSigner?: SignerKind; + parentDataSourceSpecId?: string; +}) => { + // If this is a successor market, the parent market data will only have been passed + // in if it differs from the current data. + const signerProviders = getSignerProviders(signer, providers); + const parentSignerProviders = parentSigner + ? getSignerProviders(parentSigner, providers) + : []; + if (!signerProviders.length) { return ; } return (
- {signerProviders.map((provider) => ( - - ))} + {signerProviders.map((provider) => { + // Making the assumption here that if the provider name is the same, + // that it is the same provider that the parent market used. + const parentProvider = parentSignerProviders.find( + (p) => p.name === provider.name + ); + + const isParentProviderEqual = + parentProvider !== undefined && isEqual(provider, parentProvider); + + // We only want to pass the parent data to the child component if the + // data differs from the parent market. + return isParentProviderEqual ? ( + + ) : ( + + ); + })}
); }; @@ -731,13 +1075,18 @@ const NoOracleProof = ({ const OracleProfile = (props: { provider: Provider; dataSourceSpecId: string; + parentProvider?: Provider; + parentDataSourceSpecId?: string; }) => { + // If this is a successor market, the parent market data will only have been passed + // in if it differs from the current data. const [open, onChange] = useState(false); return (
onChange(!open)} + parentProvider={props.parentProvider} />
diff --git a/libs/markets/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx b/libs/markets/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx index a8afd2966..b9243ffc7 100644 --- a/libs/markets/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx +++ b/libs/markets/src/lib/components/oracle-basic-profile/oracle-basic-profile.tsx @@ -5,6 +5,7 @@ import { ExternalLink, Icon, Intent, + Lozenge, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; @@ -59,10 +60,12 @@ export const OracleBasicProfile = ({ provider, onClick, markets: oracleMarkets, + parentProvider, }: { provider: Provider; markets?: OracleMarketSpecFieldsFragment[] | undefined; onClick?: (value?: boolean) => void; + parentProvider?: Provider; }) => { const { icon, message, intent } = getVerifiedStatusIcon(provider); @@ -78,8 +81,14 @@ export const OracleBasicProfile = ({ icon: getLinkIcon(proof.type), })); + // If this is a successor market and there's a different parent provider, + // we'll just show that there's been a change, rather than add old data + // in alongside the new provider. return ( <> + {parentProvider && ( + {t('Updated')} + )} {provider.url && ( diff --git a/libs/markets/src/lib/components/oracle-dialog/oracle-dialog.tsx b/libs/markets/src/lib/components/oracle-dialog/oracle-dialog.tsx index 4e79d743e..cb004211c 100644 --- a/libs/markets/src/lib/components/oracle-dialog/oracle-dialog.tsx +++ b/libs/markets/src/lib/components/oracle-dialog/oracle-dialog.tsx @@ -11,16 +11,27 @@ export const OracleDialog = ({ dataSourceSpecId, open, onChange, + parentProvider, }: { dataSourceSpecId: string; provider: Provider; open: boolean; onChange?: (isOpen: boolean) => void; + parentProvider?: Provider; }) => { + // If this is a successor market, the parent market data will only have been passed + // in if it differs from the current data. We'll pass this on to the title component + // to show a change, but the full profile showing changes is unwieldy - it's enough + // to know from the title that the oracle has changed. const oracleMarkets = useOracleMarkets(provider); return ( } + title={ + + } aria-labelledby="oracle-proof-dialog" open={open} onChange={onChange} diff --git a/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx b/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx index 2affe9f92..7e06ec87e 100644 --- a/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx +++ b/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx @@ -6,6 +6,7 @@ import { ExternalLink, Icon, Intent, + Lozenge, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; @@ -18,15 +19,30 @@ import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleM import ReactMarkdown from 'react-markdown'; import { useState } from 'react'; -export const OracleProfileTitle = ({ provider }: { provider: Provider }) => { +export const OracleProfileTitle = ({ + provider, + parentProvider, +}: { + provider: Provider; + parentProvider?: Provider; +}) => { + // If this is a successor market, the parent provider will only have been passed + // in if it differs from the current provider. If it is different, we'll just + // show the change in name, not icons and proofs. const { icon, intent } = getVerifiedStatusIcon(provider); const verifiedProofs = provider.proofs.filter( (proof) => proof.available === true ); return ( + {parentProvider && ( + {t('Updated')} + )} {provider.url && ( + {parentProvider && parentProvider.name && ( + {parentProvider.name} + )} {provider.name} ({verifiedProofs.length}) diff --git a/libs/ui-toolkit/src/utils/intent.ts b/libs/ui-toolkit/src/utils/intent.ts index 93443afea..011f98edd 100644 --- a/libs/ui-toolkit/src/utils/intent.ts +++ b/libs/ui-toolkit/src/utils/intent.ts @@ -22,7 +22,7 @@ export const getIntentBackground = (intent?: Intent) => { return { 'bg-neutral-200 dark:bg-neutral-800': intent === undefined, 'bg-black dark:bg-white': intent === Intent.None, - 'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary, + 'bg-vega-blue-300 dark:bg-vega-blue-650': intent === Intent.Primary, 'bg-danger': intent === Intent.Danger, 'bg-warning': intent === Intent.Warning, // contrast issues with light mode