Compare commits

...
25 changed files with 1085 additions and 183 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: Feature Epic
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'Epic: '
title: 'FEATURE EPIC: '
labels: feature-epic
---
+3 -3
View File
@@ -10,8 +10,8 @@ on:
jobs:
run-tests:
name: run-tests
runs-on: ubuntu-22.04
timeout-minutes: 25
runs-on: 8-cores
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
@@ -125,7 +125,7 @@ jobs:
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest --numprocesses auto
run: poetry run pytest -s --numprocesses auto
- name: Check files
run: |
ls -al .
@@ -1,6 +1,9 @@
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets';
import {
PriceMonitoringBoundsInfoPanel,
SuccessionLineInfoPanel,
} from '@vegaprotocol/markets';
import {
LiquidityInfoPanel,
LiquidityMonitoringParametersInfoPanel,
@@ -103,6 +106,8 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
<OracleInfoPanel market={market} type="settlementData" />
</>
)}
<h2 className={`${headerClassName} mb-4`}>{t('Succession line')}</h2>
<SuccessionLineInfoPanel market={market} />
</div>
);
};
@@ -10,6 +10,8 @@ import Hash from '../../links/hash';
import { t } from '@vegaprotocol/i18n';
import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new';
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links';
import { formatNumber } from '@vegaprotocol/utils';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -53,7 +55,11 @@ export function proposalTypeLabel(terms?: ProposalTerms): string {
} else if (has(terms, 'updateAsset')) {
return t('Update asset proposal');
} else if (has(terms, 'newMarket')) {
return t('New market proposal');
if (terms?.newMarket?.changes?.successor) {
return t('Successor market proposal');
} else {
return t('New market proposal');
}
} else if (has(terms, 'updateMarket')) {
return t('Update market proposal');
} else if (has(terms, 'updateNetworkParameter')) {
@@ -85,6 +91,13 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
}
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
const isSuccessorMarketProposal =
proposal?.terms?.newMarket?.changes?.successor;
const parentMarketId =
isSuccessorMarketProposal &&
proposal?.terms.newMarket?.changes?.successor?.parentMarketId;
const insurancePoolFraction =
proposal?.terms?.newMarket?.changes?.successor?.insurancePoolFraction;
// This component is not rendered if no bundle is required
const SignatureBundleComponent = proposal.terms?.newAsset
@@ -111,6 +124,30 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
<Hash text={deterministicId} />
</TableCell>
</TableRow>
{isSuccessorMarketProposal ? (
<>
{parentMarketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Previous market')}</TableCell>
<TableCell>
<MarketLink
id={
proposal.terms?.newMarket.changes.successor.parentMarketId
}
/>
</TableCell>
</TableRow>
) : null}
{insurancePoolFraction ? (
<TableRow modifier="bordered">
<TableCell>{t('Insurance pool fraction')}</TableCell>
<TableCell>
{formatNumber(Number(insurancePoolFraction) * 100, 0)}%
</TableCell>
</TableRow>
) : null}
</>
) : null}
</TableWithTbody>
<ProposalSummary
id={deterministicId}
@@ -78,7 +78,11 @@ export function getLabelForProposal(
} else if (proposal.terms?.updateAsset) {
return t('Proposal: Update asset');
} else if (proposal.terms?.newMarket) {
return t('Proposal: New market');
if (proposal.terms?.newMarket.changes?.successor) {
return t('Proposal: Successor market');
} else {
return t('Proposal: New market');
}
} else if (proposal.terms?.updateMarket) {
return t('Proposal: Update market');
} else if (proposal.terms?.updateNetworkParameter) {
@@ -730,6 +730,7 @@ context(
.getByTestId('key-value-table-row')
.contains(heading)
.parent()
.parent()
.siblings();
}
}
@@ -45,8 +45,10 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
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 = ({
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
content={
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
content={
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{isEqual(
getSigners(settlementData),
@@ -115,6 +140,11 @@ export const ProposalMarketData = ({
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
@@ -127,6 +157,11 @@ export const ProposalMarketData = ({
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
@@ -135,11 +170,21 @@ export const ProposalMarketData = ({
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
@@ -148,22 +193,42 @@ export const ProposalMarketData = ({
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
content={
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
content={
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
content={
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
content={
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
@@ -174,6 +239,7 @@ export const ProposalMarketData = ({
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
parentMarket={parentMarketData}
triggerIndex={triggerIndex}
/>
}
@@ -183,13 +249,21 @@ export const ProposalMarketData = ({
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
content={
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
</Accordion>
</div>
@@ -32,6 +32,7 @@ export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
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 && (
<div className="mb-4">
<ProposalMarketData marketData={newMarketData} />
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
</div>
)}
@@ -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={
+2 -2
View File
@@ -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,
});
};
@@ -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 (
<KeyValueTableRow
key={field}
@@ -63,10 +78,35 @@ const Row = ({
dtClassName={className}
ddClassName={className}
>
<Tooltip description={tooltipMapping[field]} align="start">
<div tabIndex={-1}>{startCase(t(field))}</div>
</Tooltip>
<span style={{ wordBreak: 'break-word' }}>{formattedValue}</span>
<div className="flex items-center gap-3">
<Tooltip description={tooltipMapping[field]} align="start">
<div tabIndex={-1}>{startCase(t(field))}</div>
</Tooltip>
{valueDiffersFromParentMarket && (
<Lozenge className="py-0" variant={Intent.Primary}>
{t('Updated')}
</Lozenge>
)}
{newValueInSuccessorMarket && (
<Lozenge className="py-0" variant={Intent.Primary}>
{t('Added')}
</Lozenge>
)}
</div>
<div style={{ wordBreak: 'break-word' }}>
{valueDiffersFromParentMarket ? (
<div className="flex items-center gap-3">
<span className="line-through">
{getFormattedValue(parentValue)}
</span>
<span>{formattedValue}</span>
</div>
) : (
formattedValue
)}
</div>
</KeyValueTableRow>
);
};
@@ -79,6 +119,7 @@ export interface MarketInfoTableProps {
children?: ReactNode;
assetSymbol?: string;
noBorder?: boolean;
parentData?: Record<string, ReactNode> | 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 (
<>
<KeyValueTable>
{Object.entries(data).map(([key, value]) => (
<Row
key={key}
field={key}
value={value}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted}
noBorder={noBorder}
/>
))}
<>
{Object.entries(data).map(([key, value]) => (
<Row
key={key}
field={key}
value={value}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted}
noBorder={noBorder}
parentValue={parentData?.[key]}
hasParentData={hasParentData}
/>
))}
</>
</KeyValueTable>
<div className="flex flex-col gap-2">{children}</div>
</>
@@ -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) => (
<MarketInfoTable
data={{
marketName: market.tradableInstrument.instrument.name,
@@ -337,6 +389,16 @@ export const InstrumentInfoPanel = ({ market }: 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) => (
<MarketInfoTable
data={{
expiryDate: getMarketExpiryDateFormatted(
market.tradableInstrument.instrument.metadata.tags
),
...market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
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 <MarketInfoTable data={{ tau, riskAversionParameter }} unformatted />;
let parentData;
if (
parentMarket?.tradableInstrument?.riskModel?.__typename ===
'LogNormalRiskModel'
) {
const {
tau: parentTau,
riskAversionParameter: parentRiskAversionParameter,
} = market.tradableInstrument.riskModel;
parentData = {
tau: parentTau,
riskAversionParameter: parentRiskAversionParameter,
};
}
return (
<MarketInfoTable
data={{ tau, riskAversionParameter }}
parentData={parentData}
unformatted
/>
);
};
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 <MarketInfoTable data={{ r, sigma, mu }} unformatted />;
}
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 <MarketInfoTable data={{ factorLong, factorShort }} unformatted />;
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 <MarketInfoTable data={data} parentData={parentData} unformatted />;
};
export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => {
export const RiskFactorsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
if (!market.riskFactors) {
return null;
}
const { short, long } = market.riskFactors;
return <MarketInfoTable data={{ short, long }} unformatted />;
let parentData;
if (parentMarket?.riskFactors) {
const parentShort = parentMarket.riskFactors.short;
const parentLong = parentMarket.riskFactors.long;
parentData = { short: parentShort, long: parentLong };
}
return (
<MarketInfoTable
data={{ short, long }}
parentData={parentData}
unformatted
/>
);
};
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) => (
<MarketInfoTable
data={{
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
scalingFactor:
market.liquidityMonitoringParameters.targetStakeParameters
.scalingFactor,
}}
/>
);
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 <MarketInfoTable data={marketData} parentData={parentMarketData} />;
};
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 (
<>
<p className="text-sm mb-2">
@@ -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 (
<div className="flex flex-col gap-2">
<DataSourceProof
@@ -584,7 +857,14 @@ export const OracleInfoPanel = ({
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentData={
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
}
parentDataSourceSpecId={
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
@@ -606,14 +886,28 @@ export const DataSourceProof = ({
providers,
type,
dataSourceSpecId,
parentData,
parentDataSourceSpecId,
}: {
data: DataSourceDefinition;
providers: Provider[] | undefined;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentData?: DataSourceDefinition;
parentDataSourceSpecId?: string;
}) => {
// 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 <NoOracleProof type={type} />;
@@ -622,7 +916,14 @@ export const DataSourceProof = ({
return (
<div className="flex flex-col gap-2">
{signers.map(({ signer }, i) => {
return (
const parentSigner = parentSigners?.find(
({ signer: ParentSigner }) =>
ParentSigner.__typename === signer.__typename
)?.signer;
const isParentSignerEqual = isEqual(signer, parentSigner);
return isParentSignerEqual ? (
<OracleLink
key={i}
providers={providers}
@@ -630,6 +931,16 @@ export const DataSourceProof = ({
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentSigner={parentSigner}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
@@ -663,18 +974,8 @@ export const DataSourceProof = ({
return <div>{t('Invalid data source')}</div>;
};
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 <NoOracleProof type={type} />;
}
return (
<div className="mt-2">
{signerProviders.map((provider) => (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
))}
{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 ? (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
parentProvider={parentProvider}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
);
};
@@ -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 (
<div key={props.provider.name}>
<OracleBasicProfile
provider={props.provider}
onClick={() => onChange(!open)}
parentProvider={props.parentProvider}
/>
<OracleDialog {...props} open={open} onChange={onChange} />
</div>
@@ -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 && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
<span className="flex gap-1">
{provider.url && (
<span className="flex align-items-bottom text-md gap-1">
@@ -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 (
<Dialog
title={<OracleProfileTitle provider={provider} />}
title={
<OracleProfileTitle
provider={provider}
parentProvider={parentProvider}
/>
}
aria-labelledby="oracle-proof-dialog"
open={open}
onChange={onChange}
@@ -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 (
<span className="flex gap-1">
{parentProvider && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
{provider.url && (
<span>
{parentProvider && parentProvider.name && (
<span className="line-through">{parentProvider.name}</span>
)}
<span className="pr-1">{provider.name}</span>
<span className="dark:text-vega-light-300 text-vega-dark-300">
({verifiedProofs.length})
@@ -150,3 +150,9 @@ query StopOrders($partyId: ID!) {
}
}
}
query StopOrderById($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...StopOrderFields
}
}
@@ -32,7 +32,7 @@ export type OrdersUpdateSubscriptionVariables = Types.Exact<{
export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', id: string, marketId: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null }> | null };
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null};
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null };
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
@@ -43,6 +43,13 @@ export type StopOrdersQueryVariables = Types.Exact<{
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null };
export type StopOrderByIdQueryVariables = Types.Exact<{
stopOrderId: Types.Scalars['ID'];
}>;
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null };
export const OrderFieldsFragmentDoc = gql`
fragment OrderFields on Order {
id
@@ -309,4 +316,39 @@ export function useStopOrdersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
}
export type StopOrdersQueryHookResult = ReturnType<typeof useStopOrdersQuery>;
export type StopOrdersLazyQueryHookResult = ReturnType<typeof useStopOrdersLazyQuery>;
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
export const StopOrderByIdDocument = gql`
query StopOrderById($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...StopOrderFields
}
}
${StopOrderFieldsFragmentDoc}`;
/**
* __useStopOrderByIdQuery__
*
* To run a query within a React component, call `useStopOrderByIdQuery` and pass it any options that fit your needs.
* When your component renders, `useStopOrderByIdQuery` 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 } = useStopOrderByIdQuery({
* variables: {
* stopOrderId: // value for 'stopOrderId'
* },
* });
*/
export function useStopOrderByIdQuery(baseOptions: Apollo.QueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
}
export function useStopOrderByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
}
export type StopOrderByIdQueryHookResult = ReturnType<typeof useStopOrderByIdQuery>;
export type StopOrderByIdLazyQueryHookResult = ReturnType<typeof useStopOrderByIdLazyQuery>;
export type StopOrderByIdQueryResult = Apollo.QueryResult<StopOrderByIdQuery, StopOrderByIdQueryVariables>;
@@ -3,6 +3,7 @@ import {
getDateTimeFormat,
isNumeric,
toBigNum,
formatTrigger,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
@@ -55,29 +56,8 @@ export const StopOrdersTable = memo<
sortable: false,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
if (data && value?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(
value.price,
data.market.decimalPlaces
)}`;
}
if (data && value?.__typename === 'StopOrderTrailingPercentOffset') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return '-';
},
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
data ? formatTrigger(data, data.market.decimalPlaces) : '',
minWidth: 100,
},
{
+1 -1
View File
@@ -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
+1
View File
@@ -3,3 +3,4 @@ export * from './number';
export * from './range';
export * from './size';
export * from './strings';
export * from './trigger';
+27
View File
@@ -0,0 +1,27 @@
import * as Schema from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { addDecimalsFormatNumber } from './number';
export const formatTrigger = (
data: Pick<Schema.StopOrder, 'trigger' | 'triggerDirection'> | undefined,
marketDecimalPlaces: number,
defaultValue = '-'
) => {
if (data && data?.trigger?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(data.trigger.price, marketDecimalPlaces)}`;
}
if (data && data?.trigger?.__typename === 'StopOrderTrailingPercentOffset') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(data?.trigger.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return defaultValue;
};
@@ -12,6 +12,7 @@ import {
getVegaTransactionContentIntent,
} from './use-vega-transaction-toasts';
import { Intent } from '@vegaprotocol/ui-toolkit';
import type { OrderByIdQuery, StopOrderByIdQuery } from '@vegaprotocol/orders';
jest.mock('@vegaprotocol/assets', () => {
const A1 = {
@@ -23,7 +24,7 @@ jest.mock('@vegaprotocol/assets', () => {
};
return {
...jest.requireActual('@vegaprotocol/assets'),
useAssetsDataProvider: jest.fn(() => ({ data: [A1] })),
useAssetsMapProvider: jest.fn(() => ({ data: { [A1.id]: A1 } })),
};
});
@@ -50,7 +51,7 @@ jest.mock('@vegaprotocol/markets', () => {
};
return {
...jest.requireActual('@vegaprotocol/markets'),
useMarketList: jest.fn(() => ({ data: [M1] })),
useMarketsMapProvider: jest.fn(() => ({ data: { [M1.id]: M1 } })),
};
});
@@ -59,20 +60,64 @@ jest.mock('@vegaprotocol/orders', () => {
...jest.requireActual('@vegaprotocol/orders'),
useOrderByIdQuery: jest.fn(({ variables: { orderId } }) => {
if (orderId === '0') {
return {
data: {
orderByID: {
id: '0',
side: 'SIDE_BUY',
size: '10',
timeInForce: 'TIME_IN_FORCE_FOK',
type: 'TYPE_MARKET',
price: '1234',
createdAt: new Date(),
status: 'STATUS_ACTIVE',
market: { id: 'market-1' },
},
const data: OrderByIdQuery = {
orderByID: {
id: '0',
side: 'SIDE_BUY',
size: '10',
remaining: '10',
timeInForce: 'TIME_IN_FORCE_FOK',
type: 'TYPE_MARKET',
price: '1234',
createdAt: new Date(),
status: 'STATUS_ACTIVE',
market: { id: 'market-1' },
},
} as OrderByIdQuery;
return {
data,
};
} else {
return { data: undefined };
}
}),
useStopOrderByIdQuery: jest.fn(({ variables: { stopOrderId } }) => {
if (stopOrderId === '0') {
const data: StopOrderByIdQuery = {
stopOrder: {
id: '0',
ocoLinkId: null,
expiresAt: null,
expiryStrategy: null,
triggerDirection: 'TRIGGER_DIRECTION_RISES_ABOVE',
status: 'STATUS_CANCELLED',
createdAt: '2023-08-03T07:12:36.325927Z',
updatedAt: null,
partyId: 'party-id',
marketId: 'market-1',
trigger: {
price: '1234',
__typename: 'StopOrderPrice',
},
submission: {
marketId: 'market-1',
price: '1234',
size: '10',
side: 'SIDE_SELL',
timeInForce: 'TIME_IN_FORCE_FOK',
expiresAt: null,
type: 'TYPE_MARKET',
reference: '',
peggedOrder: null,
postOnly: false,
reduceOnly: true,
__typename: 'OrderSubmission',
},
__typename: 'StopOrder',
},
} as StopOrderByIdQuery;
return {
data,
};
} else {
return { data: undefined };
@@ -143,6 +188,31 @@ const submitOrder: VegaStoredTxState = {
},
};
const submitStopOrder: VegaStoredTxState = {
id: 0,
createdAt: new Date(),
updatedAt: new Date(),
body: {
stopOrdersSubmission: {
risesAbove: {
price: '1234',
orderSubmission: {
marketId: 'market-1',
side: Side.SIDE_BUY,
size: '10',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
type: OrderType.TYPE_MARKET,
},
},
},
},
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
dialogOpen: false,
};
const editOrder: VegaStoredTxState = {
id: 0,
createdAt: new Date(),
@@ -208,6 +278,23 @@ const cancelAll: VegaStoredTxState = {
dialogOpen: false,
};
const cancelStopOrder: VegaStoredTxState = {
id: 0,
createdAt: new Date(),
updatedAt: new Date(),
body: {
stopOrdersCancellation: {
marketId: 'market-1',
stopOrderId: '0',
},
},
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
dialogOpen: false,
};
const closePosition: VegaStoredTxState = {
id: 0,
createdAt: new Date(),
@@ -269,12 +356,20 @@ describe('VegaTransactionDetails', () => {
it.each([
{ tx: withdraw, details: 'Withdraw 12.34 $A' },
{ tx: submitOrder, details: 'Submit order - activeM1+0.10 @ 12.34 $A' },
{
tx: submitStopOrder,
details: 'Submit stop orderM1+0.10 @ ~ $AMark > 12.34',
},
{
tx: editOrder,
details: 'Edit order - activeM1+0.10 @ 12.34 $A+0.11 @ 10.00 $A',
},
{ tx: cancelOrder, details: 'Cancel orderM1+0.10 @ 12.34 $A' },
{ tx: cancelAll, details: 'Cancel all orders' },
{
tx: cancelStopOrder,
details: 'Cancel stop orderM1-0.10 @ 12.34 $AMark > 12.34',
},
{ tx: closePosition, details: 'Close position for M1' },
{ tx: batch, details: 'Batch market instruction' },
])('display details for transaction', ({ tx, details }) => {
@@ -291,12 +386,18 @@ describe('getVegaTransactionContentIntent', () => {
expect(getVegaTransactionContentIntent(submitOrder).intent).toBe(
Intent.Success
);
expect(getVegaTransactionContentIntent(submitStopOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(editOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelStopOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelAll).intent).toBe(
Intent.Primary
);
+153 -39
View File
@@ -5,10 +5,10 @@ import type {
BatchMarketInstructionSubmissionBody,
OrderAmendment,
OrderTxUpdateFieldsFragment,
OrderCancellationBody,
OrderSubmission,
VegaStoredTxState,
WithdrawalBusEventFieldsFragment,
StopOrdersSubmission,
} from '@vegaprotocol/wallet';
import {
isTransferTransaction,
@@ -36,9 +36,10 @@ import {
formatNumber,
toBigNum,
truncateByChars,
formatTrigger,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store';
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import {
@@ -46,8 +47,9 @@ import {
getOrderToastTitle,
getRejectionReason,
useOrderByIdQuery,
useStopOrderByIdQuery,
} from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/markets';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
import type { Side } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
import { Size } from '@vegaprotocol/datagrid';
@@ -136,8 +138,8 @@ const SubmitOrderDetails = ({
data: OrderSubmission;
order?: OrderTxUpdateFieldsFragment;
}) => {
const { data: markets } = useMarketList();
const market = markets?.find((m) => m.id === order?.marketId);
const { data: markets } = useMarketsMapProvider();
const market = markets?.[order?.marketId || ''];
if (!market) return null;
const price = order ? order.price : data.price;
@@ -172,6 +174,58 @@ const SubmitOrderDetails = ({
);
};
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
const { data: markets } = useMarketsMapProvider();
const stopOrderSetup = data.risesAbove || data.fallsBelow;
if (!stopOrderSetup) return null;
const market = markets?.[stopOrderSetup?.orderSubmission.marketId];
if (!market || !stopOrderSetup) return null;
const { price, size, side } = stopOrderSetup.orderSubmission;
let trigger: Schema.StopOrderTrigger | null = null;
if (stopOrderSetup.price) {
trigger = { price: stopOrderSetup.price, __typename: 'StopOrderPrice' };
} else if (stopOrderSetup.trailingPercentOffset) {
trigger = {
trailingPercentOffset: stopOrderSetup.trailingPercentOffset,
__typename: 'StopOrderTrailingPercentOffset',
};
}
const triggerDirection = data.risesAbove
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
return (
<Panel>
<h4>{t('Submit stop order')}</h4>
<p>{market?.tradableInstrument.instrument.code}</p>
<p>
<SizeAtPrice
meta={{
positionDecimalPlaces: market.positionDecimalPlaces,
decimalPlaces: market.decimalPlaces,
asset:
market.tradableInstrument.instrument.product.settlementAsset
.symbol,
}}
side={side}
size={size}
price={price}
/>
<br />
{trigger &&
formatTrigger(
{
triggerDirection,
trigger,
},
market.decimalPlaces,
''
)}
</p>
</Panel>
);
};
const EditOrderDetails = ({
data,
order,
@@ -183,13 +237,12 @@ const EditOrderDetails = ({
variables: { orderId: data.orderId },
fetchPolicy: 'no-cache',
});
const { data: markets } = useMarketList();
const { data: markets } = useMarketsMapProvider();
const originalOrder = order || orderById?.orderByID;
const marketId = order?.marketId || orderById?.orderByID.market.id;
if (!originalOrder) return null;
const market = markets?.find((m) => m.id === marketId);
if (!market) return null;
const market = markets?.[marketId || ''];
if (!originalOrder || !market) return null;
const original = (
<SizeAtPrice
@@ -245,11 +298,11 @@ const CancelOrderDetails = ({
const { data: orderById } = useOrderByIdQuery({
variables: { orderId },
});
const { data: markets } = useMarketList();
const { data: markets } = useMarketsMapProvider();
const originalOrder = orderById?.orderByID;
if (!originalOrder) return null;
const market = markets?.find((m) => m.id === originalOrder.market.id);
const market = markets?.[originalOrder.market.id];
if (!market) return null;
const original = (
@@ -282,15 +335,52 @@ const CancelOrderDetails = ({
);
};
const CancelStopOrderDetails = ({ stopOrderId }: { stopOrderId: string }) => {
const { data: orderById } = useStopOrderByIdQuery({
variables: { stopOrderId },
});
const { data: markets } = useMarketsMapProvider();
const originalOrder = orderById?.stopOrder;
if (!originalOrder) return null;
const market = markets?.[originalOrder.marketId];
if (!market) return null;
const original = (
<>
<SizeAtPrice
side={originalOrder.submission.side}
size={originalOrder.submission.size}
price={originalOrder.submission.price}
meta={{
positionDecimalPlaces: market.positionDecimalPlaces,
decimalPlaces: market.decimalPlaces,
asset:
market.tradableInstrument.instrument.product.settlementAsset.symbol,
}}
/>
<br />
{formatTrigger(originalOrder, market.decimalPlaces, '')}
</>
);
return (
<Panel title={stopOrderId}>
<h4>{t('Cancel stop order')}</h4>
<p>{market?.tradableInstrument.instrument.code}</p>
<p>
<s>{original}</s>
</p>
</Panel>
);
};
export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
const { data: assets } = useAssetsDataProvider();
const { data: markets } = useMarketList();
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
if (isWithdrawTransaction(tx.body)) {
const transactionDetails = tx.body;
const asset = assets?.find(
(a) => a.id === transactionDetails.withdrawSubmission.asset
);
const asset = assets?.[transactionDetails.withdrawSubmission.asset];
if (asset) {
const num = formatNumber(
toBigNum(transactionDetails.withdrawSubmission.amount, asset.decimals),
@@ -312,15 +402,11 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
);
}
if (isOrderCancellationTransaction(tx.body)) {
// CANCEL ALL (from Portfolio)
if (
tx.body.orderCancellation.marketId === undefined &&
tx.body.orderCancellation.orderId === undefined
) {
return <Panel>{t('Cancel all orders')}</Panel>;
}
if (isStopOrdersSubmissionTransaction(tx.body)) {
return <SubmitStopOrderDetails data={tx.body.stopOrdersSubmission} />;
}
if (isOrderCancellationTransaction(tx.body)) {
// CANCEL
if (
tx.body.orderCancellation.orderId &&
@@ -336,22 +422,50 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
// CANCEL ALL (from Trading)
if (tx.body.orderCancellation.marketId) {
const marketName = markets?.find(
(m) =>
m.id === (tx.body as OrderCancellationBody).orderCancellation.marketId
)?.tradableInstrument.instrument.code;
const marketName =
markets?.[tx.body.orderCancellation.marketId]?.tradableInstrument
.instrument.code;
if (marketName) {
return (
<Panel>
{t('Cancel all orders for')} <strong>{marketName}</strong>
</Panel>
);
}
}
// CANCEL ALL (from Portfolio)
return <Panel>{t('Cancel all orders')}</Panel>;
}
if (isStopOrdersCancellationTransaction(tx.body)) {
// CANCEL
if (
tx.body.stopOrdersCancellation.stopOrderId &&
tx.body.stopOrdersCancellation.marketId
) {
return (
<Panel>
{marketName ? (
<>
{t('Cancel all orders for')} <strong>{marketName}</strong>
</>
) : (
t('Cancel all orders')
)}
</Panel>
<CancelStopOrderDetails
stopOrderId={String(tx.body.stopOrdersCancellation.stopOrderId)}
/>
);
}
// CANCEL ALL for market
if (tx.body.stopOrdersCancellation.marketId) {
const marketName =
markets?.[tx.body.stopOrdersCancellation.marketId]?.tradableInstrument
.instrument.code;
if (marketName) {
return (
<Panel>
{t('Cancel all stop orders for')} <strong>{marketName}</strong>
</Panel>
);
}
}
// CANCEL ALL
return <Panel>{t('Cancel all stop orders')}</Panel>;
}
if (isOrderAmendmentTransaction(tx.body)) {
@@ -368,7 +482,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
const marketId = first(
transaction.batchMarketInstructions.cancellations
)?.marketId;
const market = marketId && markets?.find((m) => m.id === marketId);
const market = markets?.[marketId || ''];
if (market) {
return (
<Panel>
@@ -385,7 +499,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
if (isTransferTransaction(tx.body)) {
const { amount, to, asset } = tx.body.transfer;
const transferAsset = assets?.find((a) => a.id === asset);
const transferAsset = assets?.[asset];
// only render if we have an asset to avoid unformatted amounts showing
if (transferAsset) {
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
+23
View File
@@ -0,0 +1,23 @@
# First use & get started steps
## When first enter the app
- **Must** When I open Console for the first time I can see what it is i.e. a short description and key features in auto opened dialog window (first use popup) (<a name="0007-FUGS-001" href="#0007-FUGS-001">0007-FUGS-001</a>)
- **Must** If my wallet is already connected I don't see the first use popup (<a name="0007-FUGS-002" href="#0007-FUGS-002">0007-FUGS-002</a>)
- - **Must** If window.vega is detected (browser wallet is installed), don't open first use popup (<a name="0007-FUGS-003" href="#0007-FUGS-003">0007-FUGS-003</a>)
- - **Must** If we detect previous connection using localStorage for desktop/cli wallet, don't open first use popup (<a name="0007-FUGS-004" href="#0007-FUGS-004">0007-FUGS-004</a>)
- **Must** There is a call to action to browse markets, linking to the market view market/all (<a name="0007-FUGS-005" href="#0007-FUGS-005">0007-FUGS-005</a>)
- **Must** I can see the steps I need to take to get started trading (<a name="0007-FUGS-007" href="#0007-FUGS-006">0007-FUGS-006</a>)
- **Must** There is a call to action to get started, triggering the connect modal (<a name="0007-FUGS-007" href="#0007-FUGS-007">0007-FUGS-007</a>)
- **Must** There is a link to try out trading on Fairground when I'm on Mainnet (<a name="0007-FUGS-008" href="#0007-FUGS-008">0007-FUGS-008</a>)
- **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (<a name="0007-FUGS-010" href="#0007-FUGS-010">0007-FUGS-010</a>)
- **Must** When I am on the Fairground version, I can see a warning / call out that this is Fairground meaning I can try out with virtual assets at no risk (<a name="0007-FUGS-011" href="#0007-FUGS-011">0007-FUGS-011</a>)
- If I dismiss the popup, I land on the default market (<a name="0007-FUGS-012" href="#0007-FUGS-012">0007-FUGS-012</a>)
## When first use popup has been seen, but no browser wallet is installed
- **Must** I can see the steps to get started with a visible call to action to "get started" in the context of the deal ticket, deposit, withdraw, transfer components in the sidebar (<a name="0007-FUGS-013" href="#0007-FUGS-013">0007-FUGS-013</a>)
- **Must** Remove buttons from pane containers that prompt to connect wallet (<a name="0007-FUGS-014" href="#0007-FUGS-014">0007-FUGS-014</a>)
- **Must** We've replaced "connect wallet" in the top right with "get started" (<a name="0007-FUGS-015" href="#0007-FUGS-015">0007-FUGS-015</a>)
- **Must** When I press the get started CTA, I see the wallet connect popup (<a name="0007-FUGS-016" href="#0007-FUGS-016">0007-FUGS-016</a>)
- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (<a name="0007-FUGS-017" href="#0007-FUGS-017">0007-FUGS-017</a>)
+7 -2
View File
@@ -14,8 +14,13 @@ domain = 'vega.rocks'
bucket_name = ''
if 'release/' in args.github_ref:
# remove prefixing release/ and take the first string limited by - which is supposed to be name of the environment for releasing (format: release/testnet-trading)
env_name = args.github_ref.replace('refs/heads/release/', '').split('-')[0]
if 'mainnet-mirror' in args.github_ref:
env_name = 'mainnet-mirror'
if 'validators-testnet' in args.github_ref:
env_name = 'validators-testnet'
else:
# remove prefixing release/ and take the first string limited by - which is supposed to be name of the environment for releasing (format: release/testnet-trading)
env_name = args.github_ref.replace('refs/heads/release/', '').split('-')[0]
elif 'develop' in args.github_ref:
env_name = 'stagnet1'
apps_deployed_from_develop_to_mainnet = {