Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e2c51f7c0 | ||
|
|
67afff2baf | ||
|
|
ca63a671c8 | ||
|
|
4165f3905f | ||
|
|
a359ccb755 | ||
|
|
e584737005 | ||
|
|
b12c5ed39f | ||
|
|
d3dbd64cf1 | ||
|
|
2df62bca61 | ||
|
|
fd2a434b46 | ||
|
|
4e7a78f988 | ||
|
|
327619652a | ||
|
|
d2339205cc | ||
|
|
63df4cf465 | ||
|
|
5e34178291 | ||
|
|
0c44664aa5 | ||
|
|
7ef1d75529 | ||
|
|
84795b2d6a | ||
|
|
300019f108 | ||
|
|
2d0aebd3fe | ||
|
|
9e86d2aff5 | ||
|
|
0f7e76ae01 | ||
|
|
56732d1503 | ||
|
|
5c19263653 | ||
|
|
0d911b3029 | ||
|
|
9f059e7862 | ||
|
|
92ea93dadb | ||
|
|
6165aefad7 | ||
|
|
c95369d914 | ||
|
|
5dbc5d7997 | ||
|
|
47b3d5b55f | ||
|
|
da7c0b84f7 | ||
|
|
67e602ebb1 | ||
|
|
f407110e95 | ||
|
|
66a85cce2a | ||
|
|
93f8450ecb | ||
|
|
0447a6cf5d | ||
|
|
498be838c7 |
@@ -2,11 +2,11 @@
|
||||
NX_VEGA_ENV=VALIDATOR_TESTNET
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
|
||||
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
|
||||
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
|
||||
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
||||
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
|
||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.xyz/rest
|
||||
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||
|
||||
@@ -10,7 +10,7 @@ export const Footer = () => {
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const showFullFeedbackLabel = useMemo(
|
||||
() => ['lg', 'xl'].includes(screenSize),
|
||||
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,143 +1,52 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
|
||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
|
||||
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
MarketStateMapping,
|
||||
MarketTradingModeMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
InstrumentInfoPanel,
|
||||
KeyDetailsInfoPanel,
|
||||
LiquidityPriceRangeInfoPanel,
|
||||
MetadataInfoPanel,
|
||||
OracleInfoPanel,
|
||||
RiskFactorsInfoPanel,
|
||||
RiskModelInfoPanel,
|
||||
RiskParametersInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
} from '@vegaprotocol/market-info';
|
||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
|
||||
if (!market) return null;
|
||||
|
||||
const keyDetails = {
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
tradingMode: market.tradingMode,
|
||||
state: MarketStateMapping[market.state],
|
||||
};
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const panels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
keyDetails.tradingMode &&
|
||||
MarketTradingModeMapping[keyDetails.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <KeyDetailsInfoPanel noBorder={false} market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Instrument'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType:
|
||||
market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <InstrumentInfoPanel noBorder={false} market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: asset ? (
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={false}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
),
|
||||
content: <SettlementAssetInfoPanel market={market} noBorder={false} />,
|
||||
},
|
||||
{
|
||||
title: t('Metadata'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
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 }), {}),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <MetadataInfoPanel noBorder={false} market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Risk model'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
content: <RiskModelInfoPanel noBorder={false} market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Risk parameters'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
content: <RiskParametersInfoPanel noBorder={false} market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Risk factors'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
/>
|
||||
),
|
||||
content: <RiskFactorsInfoPanel noBorder={false} market={market} />,
|
||||
},
|
||||
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(trigger, i) => ({
|
||||
@@ -158,7 +67,10 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{ referencePrice: trigger.referencePrice }}
|
||||
decimalPlaces={assetDecimals}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
@@ -166,64 +78,26 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
{
|
||||
title: t('Liquidity monitoring parameters'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
<LiquidityMonitoringParametersInfoPanel
|
||||
noBorder={false}
|
||||
data={{
|
||||
triggeringRatio:
|
||||
market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
market={market}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity'),
|
||||
content: <LiquidityInfoPanel market={market} noBorder={false} />,
|
||||
},
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders to count towards a commitment, they must be
|
||||
within the liquidity monitoring bounds.`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</>
|
||||
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={
|
||||
market.tradableInstrument.instrument.product.dataSourceSpecBinding
|
||||
}
|
||||
>
|
||||
<OracleInfoPanel noBorder={false} market={market}>
|
||||
<Link
|
||||
className="text-xs hover:underline"
|
||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
@@ -236,7 +110,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</Link>
|
||||
</MarketInfoTable>
|
||||
</OracleInfoPanel>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -244,7 +118,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
return (
|
||||
<>
|
||||
{panels.map((p) => (
|
||||
<div className="mb-3">
|
||||
<div key={p.title} className="mb-3">
|
||||
<h2 className="font-alpha calt text-xl">{p.title}</h2>
|
||||
{p.content}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { StatusMessage } from '../status-message';
|
||||
|
||||
interface RenderFetchedProps {
|
||||
@@ -7,6 +8,7 @@ interface RenderFetchedProps {
|
||||
loading: boolean | undefined;
|
||||
className?: string;
|
||||
errorMessage?: string;
|
||||
refetch?: () => void;
|
||||
}
|
||||
|
||||
export const RenderFetched = ({
|
||||
@@ -15,6 +17,7 @@ export const RenderFetched = ({
|
||||
children,
|
||||
className,
|
||||
errorMessage = t('Error retrieving data'),
|
||||
refetch,
|
||||
}: RenderFetchedProps) => {
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -23,7 +26,20 @@ export const RenderFetched = ({
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <StatusMessage className={className}>{errorMessage}</StatusMessage>;
|
||||
return (
|
||||
<>
|
||||
<StatusMessage className={className}>{errorMessage}</StatusMessage>
|
||||
{refetch && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
{t('Try again')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import compact from 'lodash/compact';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { marketInfoProvider } from '@vegaprotocol/market-info';
|
||||
import { marketInfoWithDataProvider } from '@vegaprotocol/market-info';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
|
||||
export const MarketPage = () => {
|
||||
@@ -17,7 +17,7 @@ export const MarketPage = () => {
|
||||
const { marketId } = useParams<{ marketId: string }>();
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
dataProvider: marketInfoWithDataProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
|
||||
@@ -22,6 +22,7 @@ const Tx = () => {
|
||||
|
||||
const {
|
||||
state: { data, loading, error },
|
||||
refetch,
|
||||
} = useFetch<BlockExplorerTransaction>(
|
||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(hash)}`
|
||||
);
|
||||
@@ -56,6 +57,7 @@ const Tx = () => {
|
||||
error={error}
|
||||
loading={loading}
|
||||
errorMessage={errorMessage}
|
||||
refetch={refetch}
|
||||
>
|
||||
<TxDetails
|
||||
className="mb-28"
|
||||
|
||||
@@ -21,7 +21,7 @@ import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
ContractAddressLink,
|
||||
EtherscanLink,
|
||||
DApp,
|
||||
TOKEN_VALIDATOR,
|
||||
useLinks,
|
||||
@@ -224,7 +224,7 @@ export const ValidatorsPage = () => {
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Ethereum address')}</div>
|
||||
<div className="break-all text-xs">
|
||||
<ContractAddressLink address={v.ethereumAddress} />{' '}
|
||||
<EtherscanLink address={v.ethereumAddress} />{' '}
|
||||
<CopyWithTooltip text={v.ethereumAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<Icon size={3} name="duplicate" />
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"changes": {
|
||||
"decimalPlaces": "5",
|
||||
"positionDecimalPlaces": "5",
|
||||
"linearSlippageFactor": "0.001",
|
||||
"quadraticSlippageFactor": "0",
|
||||
"lpPriceRange": "10",
|
||||
"instrument": {
|
||||
"name": "Token test market",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"lpPriceRange": "10",
|
||||
"linearSlippageFactor": "0.001",
|
||||
"quadraticSlippageFactor": "0",
|
||||
"instrument": {
|
||||
"code": "TEST.24h",
|
||||
"future": {
|
||||
|
||||
@@ -110,10 +110,9 @@ Cypress.Commands.add('vega_wallet_teardown', function () {
|
||||
}
|
||||
});
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
cy.get('[data-testid="associated-amount"]', { timeout: 30000 }).should(
|
||||
'contain.text',
|
||||
'0.00'
|
||||
);
|
||||
cy.get('[data-testid="vega-wallet-balance-unstaked"]', {
|
||||
timeout: 30000,
|
||||
}).should('contain.text', '0.00');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
NX_VEGA_ENV=VALIDATOR_TESTNET
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
|
||||
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
|
||||
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
|
||||
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
||||
+7
-2
@@ -2,7 +2,7 @@ import type { Dispatch, SetStateAction } from 'react';
|
||||
import { forwardRef, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
@@ -87,7 +87,12 @@ const TopThirdCellRenderer = (
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
data-testid="show-all-validators"
|
||||
rightIcon="arrow-right"
|
||||
rightIcon={
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="fill-current mr-2 align-text-top"
|
||||
/>
|
||||
}
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
{t('Reveal top validators')}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Link,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
@@ -11,7 +10,7 @@ import { useParams } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { TrancheItem } from '../redemption/tranche-item';
|
||||
import Routes from '../routes';
|
||||
import { TrancheLabel } from './tranche-label';
|
||||
@@ -19,7 +18,6 @@ import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
export const Tranche = () => {
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const { trancheId } = useParams<{ trancheId: string; address: string }>();
|
||||
const { chainId } = useWeb3React();
|
||||
@@ -59,25 +57,15 @@ export const Tranche = () => {
|
||||
</KeyValueTableRow>
|
||||
{tranche.users.map((user) => (
|
||||
<KeyValueTableRow key={user}>
|
||||
{
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/address/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
<RouterLink
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
data-testid="redeem-link"
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
}
|
||||
<EtherscanLink address={user} data-testid="link" />
|
||||
<RouterLink
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
data-testid="redeem-link"
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
</KeyValueTableRow>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api.n04.d.vega.xyz/graphql"]
|
||||
"hosts": ["https://api-n04.d.vega.rocks/graphql"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api.n00.mainnet-mirror.vega.xyz/graphql"]
|
||||
"hosts": ["https://api-n00.mainnet-mirror.vega.rocks/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api.n01.sandbox.vega.xyz/graphql"]
|
||||
"hosts": ["https://api-n01.sandbox.vega.rocks/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api.n00.stagnet1.vega.xyz/graphql"]
|
||||
"hosts": ["https://api-n00.stagnet1.vega.rocks/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api.stagnet3.vega.xyz/graphql"]
|
||||
"hosts": ["https://api-stagnet3.vega.rocks/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"hosts": [
|
||||
"https://api.n06.testnet.vega.xyz/graphql",
|
||||
"https://api.n07.testnet.vega.xyz/graphql",
|
||||
"https://api.n08.testnet.vega.xyz/graphql",
|
||||
"https://api.n09.testnet.vega.xyz/graphql",
|
||||
"https://api.n10.testnet.vega.xyz/graphql",
|
||||
"https://api.n11.testnet.vega.xyz/graphql",
|
||||
"https://api.n12.testnet.vega.xyz/graphql"
|
||||
"https://api-n06.testnet.vega.rocks/graphql",
|
||||
"https://api-n07.testnet.vega.rocks/graphql",
|
||||
"https://api-n08.testnet.vega.rocks/graphql",
|
||||
"https://api-n09.testnet.vega.rocks/graphql",
|
||||
"https://api-n10.testnet.vega.rocks/graphql",
|
||||
"https://api-n11.testnet.vega.rocks/graphql",
|
||||
"https://api-n12.testnet.vega.rocks/graphql"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ const externalLink = 'external-link';
|
||||
const accordionContent = 'accordion-content';
|
||||
|
||||
describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
|
||||
@@ -216,6 +216,60 @@ describe('Market trading page', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('market bottom panel', { tags: '@smoke' }, () => {
|
||||
it('on xxl screen should be splitted out into two tables', () => {
|
||||
cy.getByTestId('tab-positions').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'active'
|
||||
);
|
||||
cy.getByTestId('tab-orders').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
|
||||
cy.getByTestId('tab-accounts').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
|
||||
cy.viewport(1801, 1000);
|
||||
cy.getByTestId('tab-positions').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'active'
|
||||
);
|
||||
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'active');
|
||||
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
|
||||
cy.getByTestId('tab-accounts').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
|
||||
cy.getByTestId('Fills').click();
|
||||
cy.getByTestId('Collateral').click();
|
||||
cy.getByTestId('tab-positions').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
cy.getByTestId('tab-orders').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'active');
|
||||
cy.getByTestId('tab-accounts').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'active'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('market states not accepting orders', { tags: '@smoke' }, function () {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
|
||||
const connectEthWalletBtn = 'connect-eth-wallet-btn';
|
||||
|
||||
describe('ethereum wallet', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
// Using portfolio withdrawals tab is it requires Ethereum wallet connection
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('main[data-testid="/portfolio"]').should('exist');
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// 0004-EWAL-001
|
||||
|
||||
cy.wait('@NetworkParams');
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.getByTestId('connect-eth-wallet-btn').click();
|
||||
cy.getByTestId('web3-connector-list').should('exist');
|
||||
cy.getByTestId('web3-connector-MetaMask').click();
|
||||
cy.getByTestId('web3-connector-list').should('not.exist');
|
||||
cy.getByTestId('tab-deposits').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('should see an option to cancel the attempted connection', () => {
|
||||
// 0004-EWAL-003
|
||||
|
||||
cy.wait('@NetworkParams');
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.getByTestId('connect-eth-wallet-btn').click();
|
||||
cy.getByTestId('web3-connector-list').should('exist');
|
||||
cy.getByTestId('web3-connector-WalletConnect').click();
|
||||
cy.get('#walletconnect-qrcode-text').should('exist');
|
||||
cy.get('#walletconnect-qrcode-close').click();
|
||||
});
|
||||
|
||||
it('able to disconnect eth wallet', () => {
|
||||
// 0004-EWAL-004
|
||||
// 0004-EWAL-005
|
||||
// 0004-EWAL-006
|
||||
|
||||
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
|
||||
cy.getByTestId('disconnect-ethereum-wallet')
|
||||
.should('have.text', 'Disconnect')
|
||||
.click();
|
||||
cy.getByTestId(connectEthWalletBtn).should('exist');
|
||||
});
|
||||
});
|
||||
+19
-39
@@ -1,7 +1,8 @@
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import {
|
||||
mockConnectWallet,
|
||||
mockConnectWalletWithUserError,
|
||||
} from '@vegaprotocol/cypress';
|
||||
|
||||
const connectEthWalletBtn = 'connect-eth-wallet-btn';
|
||||
const connectVegaBtn = 'connect-vega-wallet';
|
||||
const manageVegaBtn = 'manage-vega-wallet';
|
||||
const form = 'rest-connector-form';
|
||||
@@ -127,6 +128,21 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
|
||||
it('can not connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
|
||||
mockConnectWalletWithUserError();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.getByTestId('dialog-content')
|
||||
.should('contain.text', 'User error')
|
||||
.and('contain.text', 'the user rejected the wallet connection');
|
||||
});
|
||||
|
||||
it('can change selected public key and disconnect', () => {
|
||||
// 0002-WCON-022
|
||||
// 0002-WCON-023
|
||||
@@ -164,39 +180,3 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ethereum wallet', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
// Using portfolio withdrawals tab is it requires Ethereum wallet connection
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('main[data-testid="/portfolio"]').should('exist');
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
cy.wait('@NetworkParams');
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.getByTestId('connect-eth-wallet-btn').click();
|
||||
cy.getByTestId('web3-connector-list').should('exist');
|
||||
cy.getByTestId('web3-connector-MetaMask').click();
|
||||
cy.getByTestId('web3-connector-list').should('not.exist');
|
||||
cy.getByTestId('tab-deposits').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('able to disconnect eth wallet', () => {
|
||||
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
|
||||
cy.getByTestId('disconnect-ethereum-wallet')
|
||||
.should('have.text', 'Disconnect')
|
||||
.click();
|
||||
cy.getByTestId(connectEthWalletBtn).should('exist');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
useDataProvider,
|
||||
useScreenDimensions,
|
||||
useThrottledDataProvider,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -61,7 +61,8 @@ export const MarketPage = () => {
|
||||
const { marketId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { w } = useWindowSize();
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
|
||||
const update = useGlobalStore((store) => store.update);
|
||||
const lastMarketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
@@ -87,7 +88,7 @@ export const MarketPage = () => {
|
||||
}, [update, lastMarketId, data?.id]);
|
||||
|
||||
const tradeView = useMemo(() => {
|
||||
if (w > 960) {
|
||||
if (largeScreen) {
|
||||
return (
|
||||
<TradeGrid
|
||||
market={data}
|
||||
@@ -105,7 +106,7 @@ export const MarketPage = () => {
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
);
|
||||
}, [w, data, onSelect, navigate]);
|
||||
}, [largeScreen, data, onSelect, navigate]);
|
||||
if (!data && marketId) {
|
||||
return (
|
||||
<Splash>
|
||||
@@ -140,37 +141,3 @@ export const MarketPage = () => {
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
const useWindowSize = () => {
|
||||
const [windowSize, setWindowSize] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return {
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// Something sensible for server rendered page
|
||||
return {
|
||||
w: 1200,
|
||||
h: 900,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = debounce(({ target }) => {
|
||||
setWindowSize({
|
||||
w: target.innerWidth,
|
||||
h: target.innerHeight,
|
||||
});
|
||||
}, 300);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import type { ReactNode, ComponentProps } from 'react';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
@@ -29,6 +29,7 @@ import { LiquidityContainer } from '../liquidity/liquidity';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
@@ -69,129 +70,204 @@ interface TradeGridProps {
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
const MainGrid = ({
|
||||
marketId,
|
||||
onSelect,
|
||||
pinnedAsset,
|
||||
}: {
|
||||
interface BottomPanelProps {
|
||||
marketId: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const onMarketClick = (marketId: string) => {
|
||||
navigate(Links[Routes.MARKET](marketId), {
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ResizableGrid vertical>
|
||||
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
|
||||
<ResizableGrid proportionalLayout={false} minSize={200}>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.High}
|
||||
minSize={200}
|
||||
preferredSize="50%"
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="chart" name={t('Chart')}>
|
||||
<TradingViews.Candles marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="depth" name={t('Depth')}>
|
||||
<TradingViews.Depth marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="liquidity" name={t('Liquidity')}>
|
||||
<TradingViews.Liquidity marketId={marketId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize={330}
|
||||
minSize={300}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="ticket" name={t('Ticket')}>
|
||||
<TradingViews.Ticket
|
||||
}
|
||||
|
||||
const MarketBottomPanel = memo(
|
||||
({ marketId, pinnedAsset }: BottomPanelProps) => {
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const navigate = useNavigate();
|
||||
const onMarketClick = useCallback(
|
||||
(marketId: string) => {
|
||||
navigate(Links[Routes.MARKET](marketId), {
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return 'xxxl' === screenSize ? (
|
||||
<ResizableGrid proportionalLayout minSize={200}>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize="50%"
|
||||
minSize={50}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Orders
|
||||
marketId={marketId}
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab id="info" name={t('Info')}>
|
||||
<TradingViews.Info
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Fills
|
||||
marketId={marketId}
|
||||
onSelect={(id: string) => {
|
||||
onSelect?.(id);
|
||||
}}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize={430}
|
||||
minSize={200}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="orderbook" name={t('Orderbook')}>
|
||||
<TradingViews.Orderbook marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="trades" name={t('Trades')}>
|
||||
<TradingViews.Trades marketId={marketId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
</ResizableGrid>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize="25%"
|
||||
minSize={50}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="positions" name={t('Positions')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Positions onMarketClick={onMarketClick} />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Orders
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Fills
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="accounts" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Collateral
|
||||
pinnedAsset={pinnedAsset}
|
||||
hideButtons
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
</ResizableGrid>
|
||||
);
|
||||
};
|
||||
const MainGridWrapped = memo(MainGrid);
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize="50%"
|
||||
minSize={50}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="positions" name={t('Positions')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Positions
|
||||
onMarketClick={onMarketClick}
|
||||
noBottomPlaceholder
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="accounts" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Collateral
|
||||
pinnedAsset={pinnedAsset}
|
||||
noBottomPlaceholder
|
||||
hideButtons
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
</ResizableGrid>
|
||||
) : (
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="positions" name={t('Positions')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Positions onMarketClick={onMarketClick} />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Orders
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Fills
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="accounts" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Collateral pinnedAsset={pinnedAsset} hideButtons />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
);
|
||||
}
|
||||
);
|
||||
MarketBottomPanel.displayName = 'MarketBottomPanel';
|
||||
|
||||
const MainGrid = memo(
|
||||
({
|
||||
marketId,
|
||||
onSelect,
|
||||
pinnedAsset,
|
||||
}: {
|
||||
marketId: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<ResizableGrid vertical>
|
||||
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
|
||||
<ResizableGrid proportionalLayout={false} minSize={200}>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.High}
|
||||
minSize={200}
|
||||
preferredSize="50%"
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="chart" name={t('Chart')}>
|
||||
<TradingViews.Candles marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="depth" name={t('Depth')}>
|
||||
<TradingViews.Depth marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="liquidity" name={t('Liquidity')}>
|
||||
<TradingViews.Liquidity marketId={marketId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize={330}
|
||||
minSize={300}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="ticket" name={t('Ticket')}>
|
||||
<TradingViews.Ticket
|
||||
marketId={marketId}
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab id="info" name={t('Info')}>
|
||||
<TradingViews.Info
|
||||
marketId={marketId}
|
||||
onSelect={(id: string) => {
|
||||
onSelect?.(id);
|
||||
}}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize={430}
|
||||
minSize={200}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="orderbook" name={t('Orderbook')}>
|
||||
<TradingViews.Orderbook marketId={marketId} />
|
||||
</Tab>
|
||||
<Tab id="trades" name={t('Trades')}>
|
||||
<TradingViews.Trades marketId={marketId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
</ResizableGrid>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize="25%"
|
||||
minSize={50}
|
||||
>
|
||||
<MarketBottomPanel marketId={marketId} pinnedAsset={pinnedAsset} />
|
||||
</ResizableGridPanel>
|
||||
</ResizableGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
MainGrid.displayName = 'MainGrid';
|
||||
|
||||
export const TradeGrid = ({
|
||||
market,
|
||||
@@ -201,7 +277,7 @@ export const TradeGrid = ({
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<MainGridWrapped
|
||||
<MainGrid
|
||||
marketId={market?.id || ''}
|
||||
onSelect={onSelect}
|
||||
pinnedAsset={pinnedAsset}
|
||||
|
||||
@@ -25,7 +25,8 @@ export const DepositsContainer = () => {
|
||||
<div className="h-full relative">
|
||||
<DepositsTable
|
||||
rowData={data || []}
|
||||
noRowsOverlayComponent={() => null}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
ref={gridRef}
|
||||
{...bottomPlaceholderProps}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,8 @@ export const WithdrawalsContainer = () => {
|
||||
<WithdrawalsTable
|
||||
data-testid="withdrawals-history"
|
||||
rowData={data}
|
||||
noRowsOverlayComponent={() => null}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -12,9 +12,11 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||
export const AccountsContainer = ({
|
||||
pinnedAsset,
|
||||
hideButtons,
|
||||
noBottomPlaceholder,
|
||||
}: {
|
||||
pinnedAsset?: PinnedAsset;
|
||||
hideButtons?: boolean;
|
||||
noBottomPlaceholder?: boolean;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
@@ -46,6 +48,7 @@ export const AccountsContainer = ({
|
||||
onClickDeposit={openDepositDialog}
|
||||
isReadOnly={isReadOnly}
|
||||
pinnedAsset={pinnedAsset}
|
||||
noBottomPlaceholder={noBottomPlaceholder}
|
||||
/>
|
||||
{!isReadOnly && !hideButtons && (
|
||||
<div className="flex gap-2 justify-end p-2 px-[11px] absolute lg:fixed bottom-0 right-3 dark:bg-black/75 bg-white/75 rounded">
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
useEnvironment: jest
|
||||
.fn()
|
||||
.mockImplementation(() => ({ VEGA_URL: 'https://vega-url.wtf' })),
|
||||
}));
|
||||
|
||||
const mockSetNodeSwitcher = jest.fn();
|
||||
jest.mock('../../stores', () => ({
|
||||
...jest.requireActual('../../stores'),
|
||||
useGlobalStore: () => mockSetNodeSwitcher,
|
||||
}));
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
it('controls the node switcher dialog', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
render(
|
||||
<NodeHealth
|
||||
onClick={mockOnClick}
|
||||
url={'https://api.n99.somenetwork.vega.xyz'}
|
||||
blockHeight={100}
|
||||
blockDiff={0}
|
||||
/>
|
||||
);
|
||||
render(<NodeHealth />, { wrapper: MockedProvider });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
expect(mockSetNodeSwitcher).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,14 +41,22 @@ describe('NodeUrl', () => {
|
||||
|
||||
describe('HealthIndicator', () => {
|
||||
const cases = [
|
||||
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||
{ diff: null, classname: 'bg-danger', text: 'Non operational' },
|
||||
{
|
||||
intent: Intent.Success,
|
||||
text: 'Operational',
|
||||
classname: 'bg-vega-green-550',
|
||||
},
|
||||
{
|
||||
intent: Intent.Warning,
|
||||
text: '5 Blocks behind',
|
||||
classname: 'bg-warning',
|
||||
},
|
||||
{ intent: Intent.Danger, text: 'Non operational', classname: 'bg-danger' },
|
||||
];
|
||||
it.each(cases)(
|
||||
'renders correct text and indicator color for $diff block difference',
|
||||
(elem) => {
|
||||
render(<HealthIndicator blockDiff={elem.diff} />);
|
||||
render(<HealthIndicator text={elem.text} intent={elem.intent} />);
|
||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
@@ -1,60 +1,45 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Indicator } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
|
||||
export const Footer = () => {
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
|
||||
|
||||
return (
|
||||
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 lg:fixed bottom-0 left-0 border-r bg-white dark:bg-black">
|
||||
{/* Pull left to align with top nav, due to button padding */}
|
||||
<div className="-ml-2">
|
||||
{VEGA_URL && (
|
||||
<NodeHealth
|
||||
url={VEGA_URL}
|
||||
blockHeight={datanodeBlockHeight}
|
||||
blockDiff={blockDiff}
|
||||
onClick={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
)}
|
||||
<NodeHealth />
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
interface NodeHealthProps {
|
||||
url: string;
|
||||
blockHeight: number | undefined;
|
||||
blockDiff: number | null;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const NodeHealth = ({
|
||||
url,
|
||||
blockHeight,
|
||||
blockDiff,
|
||||
onClick,
|
||||
}: NodeHealthProps) => {
|
||||
return (
|
||||
export const NodeHealth = () => {
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const { datanodeBlockHeight, text, intent } = useNodeHealth();
|
||||
const onClick = useCallback(() => {
|
||||
setNodeSwitcher(true);
|
||||
}, [setNodeSwitcher]);
|
||||
return VEGA_URL ? (
|
||||
<FooterButton onClick={onClick} data-testid="node-health">
|
||||
<FooterButtonPart>
|
||||
<HealthIndicator blockDiff={blockDiff} />
|
||||
<HealthIndicator text={text} intent={intent} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<NodeUrl url={url} />
|
||||
<NodeUrl url={VEGA_URL} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<span title={t('Block height')}>{blockHeight}</span>
|
||||
<span title={t('Block height')}>{datanodeBlockHeight}</span>
|
||||
</FooterButtonPart>
|
||||
</FooterButton>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
interface NodeUrlProps {
|
||||
@@ -69,31 +54,11 @@ export const NodeUrl = ({ url }: NodeUrlProps) => {
|
||||
};
|
||||
|
||||
interface HealthIndicatorProps {
|
||||
blockDiff: number | null;
|
||||
text: string;
|
||||
intent: Intent;
|
||||
}
|
||||
|
||||
// How many blocks behind the most advanced block that is
|
||||
// deemed acceptable for "Good" status
|
||||
const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export const HealthIndicator = ({ blockDiff }: HealthIndicatorProps) => {
|
||||
const online = useNavigatorOnline();
|
||||
|
||||
let intent = Intent.Success;
|
||||
let text = 'Operational';
|
||||
|
||||
if (!online) {
|
||||
text = t('Offline');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff === null) {
|
||||
// Block height query failed and null was returned
|
||||
text = t('Non operational');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff >= BLOCK_THRESHOLD) {
|
||||
text = t(`${blockDiff} Blocks behind`);
|
||||
intent = Intent.Warning;
|
||||
}
|
||||
|
||||
export const HealthIndicator = ({ text, intent }: HealthIndicatorProps) => {
|
||||
return (
|
||||
<span title={t('Node health')}>
|
||||
<Indicator variant={intent} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { ETHERSCAN_TX, useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -8,7 +8,7 @@ import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { Panel } from '@vegaprotocol/ui-toolkit';
|
||||
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
@@ -103,7 +103,7 @@ const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
<>
|
||||
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
|
||||
<p>{t('Please wait for your transaction to be confirmed.')}</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
@@ -126,26 +126,12 @@ const EthTxErrorToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EtherscanLink = ({ tx }: EthTxToastContentProps) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
return tx.txHash ? (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Transaction confirmed')}</ToastHeading>
|
||||
<p>{t('Your transaction has been confirmed.')}</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
@@ -162,7 +148,7 @@ const EthTxCompletedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
{t('Your transaction has been completed.')}{' '}
|
||||
{isDeposit && t('Waiting for deposit confirmation.')}
|
||||
</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,51 +15,29 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||
}));
|
||||
|
||||
describe('AccountManager', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseDataProvider
|
||||
.mockImplementationOnce((args) => {
|
||||
return {
|
||||
data: [],
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce((args) => {
|
||||
return {
|
||||
data: [
|
||||
{ asset: { id: 'a1' }, party: { id: 't1' } },
|
||||
{ asset: { id: 'a2' }, party: { id: 't2' } },
|
||||
],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('change partyId should reload data provider', async () => {
|
||||
const { rerender } = render(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
onClickAsset={jest.fn}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
(helpers.useDataProvider as jest.Mock).mock.calls[0][0].variables.partyId
|
||||
).toEqual('partyOne');
|
||||
await act(() => {
|
||||
rerender(
|
||||
<AccountManager
|
||||
partyId="partyTwo"
|
||||
onClickAsset={jest.fn}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
describe('when rerender', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseDataProvider
|
||||
.mockImplementationOnce((args) => {
|
||||
return {
|
||||
data: [],
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce((args) => {
|
||||
return {
|
||||
data: [
|
||||
{ asset: { id: 'a1' }, party: { id: 't1' } },
|
||||
{ asset: { id: 'a2' }, party: { id: 't2' } },
|
||||
],
|
||||
};
|
||||
});
|
||||
});
|
||||
expect(
|
||||
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables.partyId
|
||||
).toEqual('partyTwo');
|
||||
});
|
||||
|
||||
it('update method should return proper result', async () => {
|
||||
let rerenderer: (ui: React.ReactElement) => void;
|
||||
await act(() => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('change partyId should reload data provider', async () => {
|
||||
const { rerender } = render(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
@@ -67,13 +45,67 @@ describe('AccountManager', () => {
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
rerenderer = rerender;
|
||||
expect(
|
||||
(helpers.useDataProvider as jest.Mock).mock.calls[0][0].variables
|
||||
.partyId
|
||||
).toEqual('partyOne');
|
||||
await act(() => {
|
||||
rerender(
|
||||
<AccountManager
|
||||
partyId="partyTwo"
|
||||
onClickAsset={jest.fn}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(
|
||||
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables
|
||||
.partyId
|
||||
).toEqual('partyTwo');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No accounts')).toBeInTheDocument();
|
||||
|
||||
it('update method should return proper result', async () => {
|
||||
let rerenderer: (ui: React.ReactElement) => void;
|
||||
await act(() => {
|
||||
const { rerender } = render(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
onClickAsset={jest.fn}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
rerenderer = rerender;
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No accounts')).toBeInTheDocument();
|
||||
});
|
||||
await act(() => {
|
||||
rerenderer(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
onClickAsset={jest.fn}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
await waitFor(() => {
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('splash loading should be displayed', async () => {
|
||||
mockedUseDataProvider.mockImplementation((args) => {
|
||||
return {
|
||||
loading: true,
|
||||
data: null,
|
||||
};
|
||||
});
|
||||
await act(() => {
|
||||
rerenderer(
|
||||
render(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
onClickAsset={jest.fn}
|
||||
@@ -81,11 +113,15 @@ describe('AccountManager', () => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
await waitFor(() => {
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
(content, element) =>
|
||||
Boolean(
|
||||
element?.className.endsWith('flex items-center justify-center')
|
||||
) && content.startsWith('Loading')
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ interface AccountManagerProps {
|
||||
onClickDeposit?: (assetId?: string) => void;
|
||||
isReadOnly: boolean;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
noBottomPlaceholder?: boolean;
|
||||
}
|
||||
|
||||
export const AccountManager = ({
|
||||
@@ -28,6 +29,7 @@ export const AccountManager = ({
|
||||
partyId,
|
||||
isReadOnly,
|
||||
pinnedAsset,
|
||||
noBottomPlaceholder,
|
||||
}: AccountManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
@@ -45,6 +47,7 @@ export const AccountManager = ({
|
||||
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
|
||||
gridRef,
|
||||
setId,
|
||||
disabled: noBottomPlaceholder,
|
||||
});
|
||||
|
||||
const getRowHeight = useCallback(
|
||||
@@ -60,7 +63,8 @@ export const AccountManager = ({
|
||||
onClickDeposit={onClickDeposit}
|
||||
onClickWithdraw={onClickWithdraw}
|
||||
isReadOnly={isReadOnly}
|
||||
noRowsOverlayComponent={() => null}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
pinnedAsset={pinnedAsset}
|
||||
getRowHeight={getRowHeight}
|
||||
{...bottomPlaceholderProps}
|
||||
|
||||
@@ -59,13 +59,19 @@ export function createClient({
|
||||
const timestamp = r?.headers.get('x-block-timestamp');
|
||||
if (blockHeight && timestamp) {
|
||||
const state = useHeaderStore.getState();
|
||||
useHeaderStore.setState({
|
||||
...state,
|
||||
[r.url]: {
|
||||
blockHeight: Number(blockHeight),
|
||||
timestamp: new Date(Number(timestamp.slice(0, -6))),
|
||||
},
|
||||
});
|
||||
const urlState = state[r.url];
|
||||
if (
|
||||
!urlState?.blockHeight ||
|
||||
urlState.blockHeight !== blockHeight
|
||||
) {
|
||||
useHeaderStore.setState({
|
||||
...state,
|
||||
[r.url]: {
|
||||
blockHeight: Number(blockHeight),
|
||||
timestamp: new Date(Number(timestamp.slice(0, -6))),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContractAddressLink } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
@@ -108,7 +108,7 @@ export const rows: Rows = [
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractAddressLink address={asset.source.contractAddress} />{' '}
|
||||
<EtherscanLink address={asset.source.contractAddress} />{' '}
|
||||
<CopyWithTooltip text={asset.source.contractAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<Icon size={3} name="duplicate" />
|
||||
|
||||
@@ -46,7 +46,10 @@ addVegaWalletSubmitProposal();
|
||||
addVegaWalletSubmitLiquidityProvision();
|
||||
addImportNodeWallets();
|
||||
|
||||
export { mockConnectWallet } from './lib/commands/vega-wallet-connect';
|
||||
export {
|
||||
mockConnectWallet,
|
||||
mockConnectWalletWithUserError,
|
||||
} from './lib/commands/vega-wallet-connect';
|
||||
export type { onMessage } from './lib/mock-ws';
|
||||
export { aliasGQLQuery } from './lib/mock-gql';
|
||||
export { aliasWalletQuery } from './lib/mock-rest';
|
||||
|
||||
@@ -35,6 +35,8 @@ function createNewMarketProposal(): ProposalSubmissionBody {
|
||||
changes: {
|
||||
decimalPlaces: '5',
|
||||
positionDecimalPlaces: '5',
|
||||
linearSlippageFactor: '0.001',
|
||||
quadraticSlippageFactor: '0',
|
||||
lpPriceRange: '10',
|
||||
instrument: {
|
||||
name: 'Test market 1',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { aliasWalletConnectQuery } from '../mock-rest';
|
||||
import {
|
||||
aliasWalletConnectQuery,
|
||||
aliasWalletConnectWithUserError,
|
||||
} from '../mock-rest';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -20,6 +23,12 @@ export const mockConnectWallet = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const mockConnectWalletWithUserError = () => {
|
||||
cy.mockWallet((req) => {
|
||||
aliasWalletConnectWithUserError(req);
|
||||
});
|
||||
};
|
||||
|
||||
export function addVegaWalletConnect() {
|
||||
Cypress.Commands.add('connectVegaWallet', (isMobile) => {
|
||||
mockConnectWallet();
|
||||
|
||||
@@ -63,3 +63,23 @@ export const aliasWalletConnectQuery = (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const aliasWalletConnectWithUserError = (
|
||||
req: CyHttpMessages.IncomingHttpRequest
|
||||
) => {
|
||||
if (hasMethod(req, 'client.connect_wallet')) {
|
||||
req.alias = 'client.connect_wallet';
|
||||
req.reply({
|
||||
statusCode: 400,
|
||||
body: {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: 3001,
|
||||
data: 'the user rejected the wallet connection',
|
||||
message: 'User error',
|
||||
},
|
||||
id: '0',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
@@ -126,14 +126,10 @@ const ApprovalTxFeedback = ({
|
||||
selectedAsset: Asset;
|
||||
allowance?: BigNumber;
|
||||
}) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
|
||||
if (!tx) return null;
|
||||
|
||||
const txLink = tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
<EtherscanLink tx={tx.txHash}>{t('View on Etherscan')}</EtherscanLink>
|
||||
);
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
@@ -16,14 +15,13 @@ import type {
|
||||
TypedDataAgGrid,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { DepositFieldsFragment } from './__generated__/Deposit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { DepositStatusMapping } from '@vegaprotocol/types';
|
||||
|
||||
export const DepositsTable = forwardRef<
|
||||
AgGridReact,
|
||||
TypedDataAgGrid<DepositFieldsFragment>
|
||||
>((props, ref) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
return (
|
||||
<AgGrid
|
||||
ref={ref}
|
||||
@@ -77,14 +75,9 @@ export const DepositsTable = forwardRef<
|
||||
if (!data) return null;
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ETHERSCAN_URL}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
<EtherscanLink tx={value} data-testid="etherscan-link">
|
||||
{truncateByChars(value)}
|
||||
</Link>
|
||||
</EtherscanLink>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
import { getFaucetError } from './get-faucet-error';
|
||||
|
||||
@@ -19,7 +19,6 @@ export const FaucetNotification = ({
|
||||
selectedAsset,
|
||||
faucetTxId,
|
||||
}: FaucetNotificationProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === faucetTxId);
|
||||
});
|
||||
@@ -79,9 +78,9 @@ export const FaucetNotification = ({
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
<p>
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
<EtherscanLink tx={tx.txHash}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</EtherscanLink>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
@@ -107,9 +106,9 @@ export const FaucetNotification = ({
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
<p>
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
<EtherscanLink tx={tx.txHash}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</EtherscanLink>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEtherscanLink } from '../hooks';
|
||||
|
||||
export const ContractAddressLink = ({ address }: { address: string }) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
const href = etherscanLink(`/address/${address}`);
|
||||
return (
|
||||
<Link href={href} target="_blank" title={t('View on etherscan')}>
|
||||
{address}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { ETHERSCAN_ADDRESS, ETHERSCAN_TX, useEtherscanLink } from '../hooks';
|
||||
|
||||
export const EtherscanLink = ({
|
||||
address,
|
||||
tx,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
address?: string;
|
||||
tx?: string;
|
||||
} & ComponentProps<typeof ExternalLink>) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
let href = '';
|
||||
|
||||
if ((!address && !tx) || (address && tx)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (address) {
|
||||
href = etherscanLink(ETHERSCAN_ADDRESS.replace(':hash', address));
|
||||
}
|
||||
if (tx) {
|
||||
href = etherscanLink(ETHERSCAN_TX.replace(':hash', tx));
|
||||
}
|
||||
|
||||
return (
|
||||
<ExternalLink
|
||||
href={href}
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
{...props}
|
||||
>
|
||||
{children || address || tx}
|
||||
</ExternalLink>
|
||||
);
|
||||
};
|
||||
@@ -2,4 +2,4 @@ export * from './network-loader';
|
||||
export * from './network-switcher';
|
||||
export * from './node-guard';
|
||||
export * from './node-switcher';
|
||||
export * from './contract-address-link';
|
||||
export * from './etherscan-link';
|
||||
|
||||
@@ -103,6 +103,7 @@ export const TOKEN_VALIDATOR = '/validators/:id';
|
||||
export const EXPLORER_TX = '/txs/:hash';
|
||||
|
||||
// Etherscan pages
|
||||
export const ETHERSCAN_ADDRESS = '/address/:hash';
|
||||
export const ETHERSCAN_TX = '/tx/:hash';
|
||||
|
||||
// Console pages
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MockedProvider } from '@apollo/react-testing';
|
||||
import type { StatisticsQuery } from '../utils/__generated__/Node';
|
||||
import { StatisticsDocument } from '../utils/__generated__/Node';
|
||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const vegaUrl = 'https://foo.bar.com';
|
||||
|
||||
@@ -55,9 +56,24 @@ function setup(
|
||||
|
||||
describe('useNodeHealth', () => {
|
||||
it.each([
|
||||
{ core: 1, node: 1, expected: 0 },
|
||||
{ core: 1, node: 5, expected: -4 },
|
||||
{ core: 10, node: 5, expected: 5 },
|
||||
{
|
||||
core: 1,
|
||||
node: 1,
|
||||
expectedText: 'Operational',
|
||||
expectedIntent: Intent.Success,
|
||||
},
|
||||
{
|
||||
core: 1,
|
||||
node: 5,
|
||||
expectedText: 'Operational',
|
||||
expectedIntent: Intent.Success,
|
||||
},
|
||||
{
|
||||
core: 10,
|
||||
node: 5,
|
||||
expectedText: '5 Blocks behind',
|
||||
expectedIntent: Intent.Warning,
|
||||
},
|
||||
])(
|
||||
'provides difference core block $core and node block $node',
|
||||
async (cases) => {
|
||||
@@ -65,12 +81,12 @@ describe('useNodeHealth', () => {
|
||||
blockHeight: cases.node,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
||||
await waitFor(() => {
|
||||
expect(result.current.blockDiff).toEqual(cases.expected);
|
||||
expect(result.current.coreBlockHeight).toEqual(cases.core);
|
||||
expect(result.current.text).toEqual(cases.expectedText);
|
||||
expect(result.current.intent).toEqual(cases.expectedIntent);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
||||
});
|
||||
}
|
||||
@@ -90,25 +106,64 @@ describe('useNodeHealth', () => {
|
||||
blockHeight: 1,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(1);
|
||||
await waitFor(() => {
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 0 if no headers are found (waits until stats query resolves)', async () => {
|
||||
const { result } = setup(createStatsMock(1), undefined);
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
||||
await waitFor(() => {
|
||||
expect(result.current.blockDiff).toEqual(0);
|
||||
expect(result.current.coreBlockHeight).toEqual(1);
|
||||
expect(result.current.text).toEqual('Operational');
|
||||
expect(result.current.intent).toEqual(Intent.Success);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('Warning latency', async () => {
|
||||
const now = 1678800900087;
|
||||
const headerTimestamp = now - 4000;
|
||||
const dateNow = new Date(now);
|
||||
const dateHeaderTimestamp = new Date(headerTimestamp);
|
||||
jest.useFakeTimers().setSystemTime(dateNow);
|
||||
|
||||
const { result } = setup(createStatsMock(2), {
|
||||
blockHeight: 2,
|
||||
timestamp: dateHeaderTimestamp,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.text).toEqual('Warning delay ( >3 sec): 4.05 sec');
|
||||
expect(result.current.intent).toEqual(Intent.Warning);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('Erroneous latency', async () => {
|
||||
const now = 1678800900087;
|
||||
const headerTimestamp = now - 11000;
|
||||
const dateNow = new Date(now);
|
||||
const dateHeaderTimestamp = new Date(headerTimestamp);
|
||||
jest.useFakeTimers().setSystemTime(dateNow);
|
||||
|
||||
const { result } = setup(createStatsMock(2), {
|
||||
blockHeight: 2,
|
||||
timestamp: dateHeaderTimestamp,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.text).toEqual(
|
||||
'Erroneous latency ( >10 sec): 11.05 sec'
|
||||
);
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(2);
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,30 +2,35 @@ import { useEffect, useMemo } from 'react';
|
||||
import { useStatisticsQuery } from '../utils/__generated__/Node';
|
||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
import { useEnvironment } from './use-environment';
|
||||
import { fromNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
const POLL_INTERVAL = 1000;
|
||||
const BLOCK_THRESHOLD = 3;
|
||||
const ERROR_LATENCY = 10000;
|
||||
const WARNING_LATENCY = 3000;
|
||||
|
||||
export const useNodeHealth = () => {
|
||||
const online = useNavigatorOnline();
|
||||
const url = useEnvironment((store) => store.VEGA_URL);
|
||||
const headerStore = useHeaderStore();
|
||||
const headers = url ? headerStore[url] : undefined;
|
||||
const { data, error, loading, startPolling, stopPolling } =
|
||||
useStatisticsQuery({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const { data, error, startPolling, stopPolling } = useStatisticsQuery({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const blockDiff = useMemo(() => {
|
||||
if (!data?.statistics.blockHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!headers) {
|
||||
if (!headers?.blockHeight) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(data.statistics.blockHeight) - headers.blockHeight;
|
||||
}, [data, headers]);
|
||||
}, [data?.statistics.blockHeight, headers?.blockHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
@@ -38,17 +43,43 @@ export const useNodeHealth = () => {
|
||||
}
|
||||
}, [error, startPolling, stopPolling]);
|
||||
|
||||
const blockUpdateMsLatency = headers?.timestamp
|
||||
? Date.now() - headers.timestamp.getTime()
|
||||
: 0;
|
||||
|
||||
const [text, intent] = useMemo(() => {
|
||||
let intent = Intent.Success;
|
||||
let text = 'Operational';
|
||||
|
||||
if (!online) {
|
||||
text = t('Offline');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff === null) {
|
||||
// Block height query failed and null was returned
|
||||
text = t('Non operational');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockUpdateMsLatency > ERROR_LATENCY) {
|
||||
text = t('Erroneous latency ( >%s sec): %s sec', [
|
||||
(ERROR_LATENCY / 1000).toString(),
|
||||
(blockUpdateMsLatency / 1000).toFixed(2),
|
||||
]);
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff >= BLOCK_THRESHOLD) {
|
||||
text = t(`%s Blocks behind`, String(blockDiff));
|
||||
intent = Intent.Warning;
|
||||
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
|
||||
text = t('Warning delay ( >%s sec): %s sec', [
|
||||
(WARNING_LATENCY / 1000).toString(),
|
||||
(blockUpdateMsLatency / 1000).toFixed(2),
|
||||
]);
|
||||
intent = Intent.Warning;
|
||||
}
|
||||
return [text, intent];
|
||||
}, [online, blockDiff, blockUpdateMsLatency]);
|
||||
|
||||
return {
|
||||
error,
|
||||
loading,
|
||||
coreBlockHeight: data?.statistics
|
||||
? Number(data.statistics.blockHeight)
|
||||
: undefined,
|
||||
coreVegaTime: data?.statistics
|
||||
? fromNanoSeconds(data?.statistics.vegaTime)
|
||||
: undefined,
|
||||
datanodeBlockHeight: headers?.blockHeight,
|
||||
datanodeVegaTime: headers?.timestamp,
|
||||
blockDiff,
|
||||
text,
|
||||
intent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './info-market';
|
||||
export * from './tooltip-mapping';
|
||||
export * from './__generated__/MarketInfo';
|
||||
export * from './market-info-data-provider';
|
||||
export * from './market-info-panels';
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
totalFeesPercentage,
|
||||
calcCandleVolume,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
removePaginationWrapper,
|
||||
TokenLinks,
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { removePaginationWrapper, TokenLinks } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider, useYesterday } from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -22,15 +10,31 @@ import {
|
||||
Link as UILink,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
|
||||
import { MarketInfoTable } from './info-key-value-table';
|
||||
import { marketInfoWithDataAndCandlesProvider } from './market-info-data-provider';
|
||||
|
||||
import type { MarketInfoWithDataAndCandles } from './market-info-data-provider';
|
||||
import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
CurrentFeesInfoPanel,
|
||||
InstrumentInfoPanel,
|
||||
InsurancePoolInfoPanel,
|
||||
KeyDetailsInfoPanel,
|
||||
LiquidityInfoPanel,
|
||||
LiquidityMonitoringParametersInfoPanel,
|
||||
LiquidityPriceRangeInfoPanel,
|
||||
MarketPriceInfoPanel,
|
||||
MarketVolumeInfoPanel,
|
||||
MetadataInfoPanel,
|
||||
OracleInfoPanel,
|
||||
PriceMonitoringBoundsInfoPanel,
|
||||
RiskFactorsInfoPanel,
|
||||
RiskModelInfoPanel,
|
||||
RiskParametersInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
} from './market-info-panels';
|
||||
|
||||
export interface InfoProps {
|
||||
market: MarketInfoWithDataAndCandles;
|
||||
@@ -80,15 +84,6 @@ export const MarketInfoContainer = ({
|
||||
export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
const { VEGA_TOKEN_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const headerClassName = 'uppercase text-lg';
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
|
||||
if (!market) return null;
|
||||
|
||||
@@ -96,272 +91,75 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
market.accountsConnection?.edges
|
||||
);
|
||||
|
||||
const last24hourVolume = market.candles && calcCandleVolume(market.candles);
|
||||
|
||||
const marketDataPanels = [
|
||||
{
|
||||
title: t('Current fees'),
|
||||
content: (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
...market.fees.factors,
|
||||
totalFees: totalFeesPercentage(market.fees.factors),
|
||||
}}
|
||||
asPercentage={true}
|
||||
/>
|
||||
<p className="text-xs">
|
||||
{t(
|
||||
'All fees are paid by price takers and are a % of the trade notional value. Fees are not paid during auction uncrossing.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
content: <CurrentFeesInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Market price'),
|
||||
content: (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
markPrice: market.data?.markPrice,
|
||||
bestBidPrice: market.data?.bestBidPrice,
|
||||
bestOfferPrice: market.data?.bestOfferPrice,
|
||||
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
content: <MarketPriceInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Market volume'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
'24hourVolume':
|
||||
last24hourVolume && last24hourVolume !== '0'
|
||||
? addDecimalsFormatNumber(
|
||||
last24hourVolume,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
: '-',
|
||||
openInterest: market.data?.openInterest,
|
||||
bestBidVolume: market.data?.bestBidVolume,
|
||||
bestOfferVolume: market.data?.bestOfferVolume,
|
||||
bestStaticBidVolume: market.data?.bestStaticBidVolume,
|
||||
bestStaticOfferVolume: market.data?.bestStaticOfferVolume,
|
||||
}}
|
||||
decimalPlaces={market.positionDecimalPlaces}
|
||||
/>
|
||||
),
|
||||
content: <MarketVolumeInfoPanel market={market} />,
|
||||
},
|
||||
...marketAccounts
|
||||
.filter((a) => a.type === Schema.AccountType.ACCOUNT_TYPE_INSURANCE)
|
||||
.map((a) => ({
|
||||
title: t(`Insurance pool`),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
balance: a.balance,
|
||||
}}
|
||||
assetSymbol={assetSymbol}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals
|
||||
}
|
||||
/>
|
||||
),
|
||||
content: <InsurancePoolInfoPanel market={market} account={a} />,
|
||||
})),
|
||||
];
|
||||
|
||||
const keyDetails = {
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
tradingMode: market.tradingMode,
|
||||
state: Schema.MarketStateMapping[market.state],
|
||||
};
|
||||
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const marketSpecPanels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
keyDetails.tradingMode &&
|
||||
Schema.MarketTradingModeMapping[keyDetails.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <KeyDetailsInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Instrument'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType:
|
||||
market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <InstrumentInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: asset ? (
|
||||
<>
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={true}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
),
|
||||
content: <SettlementAssetInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Metadata'),
|
||||
content: (
|
||||
<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 }), {}),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <MetadataInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Risk model'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
content: <RiskModelInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Risk parameters'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
content: <RiskParametersInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Risk factors'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
/>
|
||||
),
|
||||
content: <RiskFactorsInfoPanel market={market} />,
|
||||
},
|
||||
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(trigger, i) => {
|
||||
const bounds = market.data?.priceMonitoringBounds?.[i];
|
||||
return {
|
||||
title: t(`Price monitoring bounds ${i + 1}`),
|
||||
content: (
|
||||
<div className="text-xs">
|
||||
<div className="grid grid-cols-2 text-xs mb-4">
|
||||
<p className="col-span-1">
|
||||
{t('%s probability price bounds', [
|
||||
formatNumberPercentage(
|
||||
new BigNumber(trigger.probability).times(100)
|
||||
),
|
||||
])}
|
||||
</p>
|
||||
<p className="col-span-1 text-right">
|
||||
{t('Within %s seconds', [formatNumber(trigger.horizonSecs)])}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
{bounds && (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
{t('Results in %s seconds auction if breached', [
|
||||
trigger.auctionExtensionSecs.toString(),
|
||||
])}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
(_, triggerIndex) => ({
|
||||
title: t(`Price monitoring bounds ${triggerIndex + 1}`),
|
||||
content: (
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={market}
|
||||
triggerIndex={triggerIndex}
|
||||
/>
|
||||
),
|
||||
})
|
||||
),
|
||||
{
|
||||
title: t('Liquidity monitoring parameters'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
triggeringRatio:
|
||||
market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
content: <LiquidityMonitoringParametersInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Liquidity'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
targetStake: market.data && market.data.targetStake,
|
||||
suppliedStake: market.data && market.data?.suppliedStake,
|
||||
marketValueProxy: market.data && market.data.marketValueProxy,
|
||||
}}
|
||||
decimalPlaces={assetDecimals}
|
||||
assetSymbol={assetSymbol}
|
||||
>
|
||||
<LiquidityInfoPanel market={market}>
|
||||
<Link
|
||||
to={`/liquidity/${market.id}`}
|
||||
onClick={() => onSelect(market.id)}
|
||||
@@ -369,57 +167,17 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
>
|
||||
<UILink>{t('View liquidity provision table')}</UILink>
|
||||
</Link>
|
||||
</MarketInfoTable>
|
||||
</LiquidityInfoPanel>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders to count towards a commitment, they must be
|
||||
within the liquidity monitoring bounds.`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
content: <LiquidityPriceRangeInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={
|
||||
market.tradableInstrument.instrument.product.dataSourceSpecBinding
|
||||
}
|
||||
>
|
||||
<OracleInfoPanel market={market}>
|
||||
<ExternalLink
|
||||
href={`${VEGA_EXPLORER_URL}/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
>
|
||||
@@ -430,7 +188,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</ExternalLink>
|
||||
</MarketInfoTable>
|
||||
</OracleInfoPanel>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
totalFeesPercentage,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { Get } from 'type-fest';
|
||||
import { MarketInfoTable } from './info-key-value-table';
|
||||
import type {
|
||||
MarketInfo,
|
||||
MarketInfoWithData,
|
||||
MarketInfoWithDataAndCandles,
|
||||
} from './market-info-data-provider';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
|
||||
type PanelProps = Pick<
|
||||
ComponentProps<typeof MarketInfoTable>,
|
||||
'children' | 'noBorder'
|
||||
>;
|
||||
|
||||
type MarketInfoProps = {
|
||||
market: MarketInfo;
|
||||
};
|
||||
|
||||
type MarketInfoWithDataProps = {
|
||||
market: MarketInfoWithData;
|
||||
};
|
||||
|
||||
type MarketInfoWithDataAndCandlesProps = {
|
||||
market: MarketInfoWithDataAndCandles;
|
||||
};
|
||||
|
||||
export const CurrentFeesInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
...market.fees.factors,
|
||||
totalFees: totalFeesPercentage(market.fees.factors),
|
||||
}}
|
||||
asPercentage={true}
|
||||
{...props}
|
||||
/>
|
||||
<p className="text-xs">
|
||||
{t(
|
||||
'All fees are paid by price takers and are a % of the trade notional value. Fees are not paid during auction uncrossing.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export const MarketPriceInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataProps & PanelProps) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
return (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
markPrice: market.data?.markPrice,
|
||||
bestBidPrice: market.data?.bestBidPrice,
|
||||
bestOfferPrice: market.data?.bestOfferPrice,
|
||||
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
{...props}
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarketVolumeInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataAndCandlesProps & PanelProps) => {
|
||||
const last24hourVolume = market.candles && calcCandleVolume(market.candles);
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
'24hourVolume':
|
||||
last24hourVolume && last24hourVolume !== '0'
|
||||
? addDecimalsFormatNumber(
|
||||
last24hourVolume,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
: '-',
|
||||
openInterest: market.data?.openInterest,
|
||||
bestBidVolume: market.data?.bestBidVolume,
|
||||
bestOfferVolume: market.data?.bestOfferVolume,
|
||||
bestStaticBidVolume: market.data?.bestStaticBidVolume,
|
||||
bestStaticOfferVolume: market.data?.bestStaticOfferVolume,
|
||||
}}
|
||||
decimalPlaces={market.positionDecimalPlaces}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InsurancePoolInfoPanel = ({
|
||||
market,
|
||||
account,
|
||||
...props
|
||||
}: {
|
||||
account: NonNullable<
|
||||
Get<MarketInfoWithData, 'accountsConnection.edges[0].node'>
|
||||
>;
|
||||
} & MarketInfoProps &
|
||||
PanelProps) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
balance: account.balance,
|
||||
}}
|
||||
assetSymbol={assetSymbol}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyDetailsInfoPanel = ({
|
||||
market,
|
||||
}: MarketInfoProps & PanelProps) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
market.tradingMode && MarketTradingModeMapping[market.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstrumentInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType: market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SettlementAssetInfoPanel = ({
|
||||
market,
|
||||
noBorder = true,
|
||||
}: MarketInfoProps & PanelProps) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
return asset ? (
|
||||
<>
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={noBorder}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
);
|
||||
};
|
||||
|
||||
export const MetadataInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<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 }), {}),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskModelInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskParametersInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskFactorsInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const PriceMonitoringBoundsInfoPanel = ({
|
||||
market,
|
||||
triggerIndex,
|
||||
...props
|
||||
}: {
|
||||
triggerIndex: number;
|
||||
} & MarketInfoWithDataProps &
|
||||
PanelProps) => {
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const trigger =
|
||||
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
||||
const bounds = market.data?.priceMonitoringBounds?.[triggerIndex];
|
||||
if (!trigger) {
|
||||
console.error(
|
||||
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<div className="grid grid-cols-2 text-xs mb-4">
|
||||
<p className="col-span-1">
|
||||
{t('%s probability price bounds', [
|
||||
formatNumberPercentage(
|
||||
new BigNumber(trigger.probability).times(100)
|
||||
),
|
||||
])}
|
||||
</p>
|
||||
<p className="col-span-1 text-right">
|
||||
{t('Within %s seconds', [formatNumber(trigger.horizonSecs)])}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
{bounds && (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
{t('Results in %s seconds auction if breached', [
|
||||
trigger.auctionExtensionSecs.toString(),
|
||||
])}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityMonitoringParametersInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const LiquidityInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataProps & PanelProps) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
targetStake: market.data && market.data.targetStake,
|
||||
suppliedStake: market.data && market.data?.suppliedStake,
|
||||
marketValueProxy: market.data && market.data.marketValueProxy,
|
||||
}}
|
||||
decimalPlaces={assetDecimals}
|
||||
assetSymbol={assetSymbol}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityPriceRangeInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataProps & PanelProps) => {
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders to count towards a commitment, they must be
|
||||
within the liquidity monitoring bounds.`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const OracleInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.instrument.product.dataSourceSpecBinding}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Position } from '../';
|
||||
import { usePositionsData, PositionsTable } from '../';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
@@ -22,6 +23,7 @@ export const PositionsManager = ({
|
||||
noBottomPlaceholder,
|
||||
}: PositionsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [dataCount, setDataCount] = useState(0);
|
||||
const { data, error, loading, reload } = usePositionsData(
|
||||
partyId,
|
||||
gridRef,
|
||||
@@ -66,8 +68,14 @@ export const PositionsManager = ({
|
||||
const bottomPlaceholderProps = useBottomPlaceholder<Position>({
|
||||
gridRef,
|
||||
setId,
|
||||
disabled: noBottomPlaceholder,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, [data?.length]);
|
||||
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, []);
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<PositionsTable
|
||||
@@ -75,9 +83,11 @@ export const PositionsManager = ({
|
||||
ref={gridRef}
|
||||
onMarketClick={onMarketClick}
|
||||
onClose={onClose}
|
||||
noRowsOverlayComponent={() => null}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
isReadOnly={isReadOnly}
|
||||
{...(noBottomPlaceholder ? null : bottomPlaceholderProps)}
|
||||
onFilterChanged={onFilterChanged}
|
||||
{...bottomPlaceholderProps}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -85,7 +95,7 @@ export const PositionsManager = ({
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No positions')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataCondition={(data) => !dataCount}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
value
|
||||
)
|
||||
}
|
||||
minWidth={190}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Notional')}
|
||||
@@ -141,6 +142,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
minWidth={80}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Open volume')}
|
||||
@@ -174,6 +176,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
);
|
||||
}}
|
||||
cellRenderer={OpenVolumeCell}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark price')}
|
||||
@@ -213,8 +216,13 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Settlement asset')}
|
||||
field="assetSymbol"
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn headerName={t('Settlement asset')} field="assetSymbol" />
|
||||
<AgGridColumn
|
||||
headerName={t('Entry price')}
|
||||
field="averageEntryPrice"
|
||||
@@ -248,6 +256,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Leverage')}
|
||||
@@ -264,6 +273,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
|
||||
value === undefined ? undefined : formatNumber(value.toString(), 1)
|
||||
}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Margin allocated')}
|
||||
@@ -295,6 +305,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
data.decimals
|
||||
);
|
||||
}}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Realised PNL')}
|
||||
@@ -321,6 +332,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
|
||||
)}
|
||||
cellRenderer={PNLCell}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
@@ -347,6 +359,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
|
||||
)}
|
||||
cellRenderer={PNLCell}
|
||||
minWidth={100}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
@@ -361,6 +374,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
minWidth={150}
|
||||
/>
|
||||
{onClose && !props.isReadOnly ? (
|
||||
<AgGridColumn
|
||||
@@ -375,6 +389,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
</ButtonLink>
|
||||
) : null
|
||||
}
|
||||
minWidth={80}
|
||||
/>
|
||||
) : null}
|
||||
</AgGrid>
|
||||
|
||||
@@ -11,11 +11,13 @@ const isFullWidthRow = (params: IsFullWidthRowParams) =>
|
||||
interface Props<T> {
|
||||
gridRef: RefObject<AgGridReact>;
|
||||
setId?: (data: T) => T;
|
||||
disabled?: boolean;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const useBottomPlaceholder = <T extends {}>({
|
||||
gridRef,
|
||||
setId,
|
||||
disabled,
|
||||
}: Props<T>) => {
|
||||
const onBodyScrollEnd = useCallback(() => {
|
||||
const rowCont = gridRef.current?.api.getModel().getRowCount() ?? 0;
|
||||
@@ -58,14 +60,17 @@ export const useBottomPlaceholder = <T extends {}>({
|
||||
}, [gridRef, onBodyScrollEnd]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
onBodyScrollEnd,
|
||||
rowClassRules: NO_HOVER_CSS_RULE,
|
||||
isFullWidthRow,
|
||||
fullWidthCellRenderer,
|
||||
onSortChanged: onRowsChanged,
|
||||
onFilterChange: onRowsChanged,
|
||||
}),
|
||||
[onBodyScrollEnd, onRowsChanged]
|
||||
() =>
|
||||
!disabled
|
||||
? {
|
||||
onBodyScrollEnd,
|
||||
rowClassRules: NO_HOVER_CSS_RULE,
|
||||
isFullWidthRow,
|
||||
fullWidthCellRenderer,
|
||||
onSortChanged: onRowsChanged,
|
||||
onFilterChange: onRowsChanged,
|
||||
}
|
||||
: {},
|
||||
[onBodyScrollEnd, onRowsChanged, disabled]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
|
||||
const SERVER_SIDE_DIMENSIONS = {
|
||||
width: 1200,
|
||||
height: 900,
|
||||
};
|
||||
|
||||
export const useResize = () => {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
const [windowSize, setWindowSize] = useState(
|
||||
typeof window !== undefined
|
||||
? {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
}
|
||||
: { ...SERVER_SIDE_DIMENSIONS }
|
||||
);
|
||||
|
||||
const timeout = useRef(0);
|
||||
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
// @ts-ignore avoid adding declaration file
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
import { useResize } from './use-resize';
|
||||
|
||||
type Screen = keyof typeof theme.screens;
|
||||
export type Screen = keyof typeof theme.screens;
|
||||
|
||||
interface Props {
|
||||
isMobile: boolean;
|
||||
screenSize: Screen;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const useScreenDimensions = (): Props => {
|
||||
const { width } = useResize();
|
||||
const isMobile = width < parseInt(theme.screens.md);
|
||||
const screenSize = Object.entries(theme.screens).reduce(
|
||||
(agg: Screen, entry) => {
|
||||
if (width > parseInt(entry[1])) {
|
||||
agg = entry[0] as Screen;
|
||||
}
|
||||
return agg;
|
||||
},
|
||||
'xs'
|
||||
);
|
||||
return useMemo(
|
||||
() => ({
|
||||
width,
|
||||
isMobile: width < parseInt(theme.screens.md),
|
||||
screenSize: Object.entries(theme.screens).reduce((agg: Screen, entry) => {
|
||||
if (width > parseInt(entry[1])) {
|
||||
agg = entry[0] as Screen;
|
||||
}
|
||||
return agg;
|
||||
}, 'xs'),
|
||||
isMobile,
|
||||
screenSize,
|
||||
}),
|
||||
[width]
|
||||
[isMobile, screenSize]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ module.exports = {
|
||||
lg: '960px',
|
||||
xl: '1280px',
|
||||
xxl: '1536px',
|
||||
xxxl: '1800px',
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
|
||||
@@ -50,7 +50,7 @@ interface Props extends AgGridReactProps {
|
||||
export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%', background: 'red' }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No trades')}
|
||||
getRowId={({ data }) => data.id}
|
||||
ref={ref}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/ui-toolkit",
|
||||
"version": "0.7.0"
|
||||
"version": "0.8.0"
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import type {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { IconName } from '../icon';
|
||||
import { Icon } from '../icon';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export type ButtonVariant = 'default' | 'primary' | 'secondary' | 'ternary';
|
||||
@@ -76,8 +74,8 @@ interface CommonProps {
|
||||
disabled?: boolean;
|
||||
fill?: boolean;
|
||||
size?: ButtonSize;
|
||||
icon?: IconName;
|
||||
rightIcon?: IconName;
|
||||
icon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
}
|
||||
export interface ButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
@@ -151,17 +149,13 @@ export const ButtonLink = forwardRef<HTMLButtonElement, ButtonLinkProps>(
|
||||
|
||||
interface ButtonContentProps {
|
||||
children: ReactNode;
|
||||
icon?: IconName;
|
||||
rightIcon?: IconName;
|
||||
icon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
}
|
||||
|
||||
const ButtonContent = ({ children, icon, rightIcon }: ButtonContentProps) => {
|
||||
const iconEl = icon ? (
|
||||
<Icon name={icon} className="fill-current mr-2 align-text-top" />
|
||||
) : null;
|
||||
const rightIconEl = rightIcon ? (
|
||||
<Icon name={rightIcon} className="fill-current ml-2 align-text-top" />
|
||||
) : null;
|
||||
const iconEl = icon ? icon : null;
|
||||
const rightIconEl = rightIcon ? rightIcon : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -29,7 +29,6 @@ import { countryCodeToFlagEmoji, FALLBACK_FLAG } from './flag-emoji';
|
||||
// 🇿
|
||||
|
||||
const cases = [
|
||||
['AC', '🇦🇨'],
|
||||
['AD', '🇦🇩'],
|
||||
['AE', '🇦🇪'],
|
||||
['AF', '🇦🇫'],
|
||||
@@ -38,11 +37,13 @@ const cases = [
|
||||
['AL', '🇦🇱'],
|
||||
['AM', '🇦🇲'],
|
||||
['AO', '🇦🇴'],
|
||||
['AQ', '🇦🇶'],
|
||||
['AR', '🇦🇷'],
|
||||
['AS', '🇦🇸'],
|
||||
['AT', '🇦🇹'],
|
||||
['AQ', '🇦🇶'],
|
||||
['AU', '🇦🇺'],
|
||||
['AW', '🇦🇼'],
|
||||
['AX', '🇦🇽'],
|
||||
['AZ', '🇦🇿'],
|
||||
['BA', '🇧🇦'],
|
||||
['BB', '🇧🇧'],
|
||||
@@ -57,10 +58,11 @@ const cases = [
|
||||
['BM', '🇧🇲'],
|
||||
['BN', '🇧🇳'],
|
||||
['BO', '🇧🇴'],
|
||||
['BQ', '🇧🇶'],
|
||||
['BR', '🇧🇷'],
|
||||
['BS', '🇧🇸'],
|
||||
['BT', '🇧🇹'],
|
||||
['BQ', '🇧🇶'],
|
||||
['BV', '🇧🇻'],
|
||||
['BW', '🇧🇼'],
|
||||
['BY', '🇧🇾'],
|
||||
['BZ', '🇧🇿'],
|
||||
@@ -76,19 +78,19 @@ const cases = [
|
||||
['CM', '🇨🇲'],
|
||||
['CN', '🇨🇳'],
|
||||
['CO', '🇨🇴'],
|
||||
['CP', '🇨🇵'],
|
||||
['CR', '🇨🇷'],
|
||||
['CU', '🇨🇺'],
|
||||
['CV', '🇨🇻'],
|
||||
['CW', '🇨🇼'],
|
||||
['CX', '🇨🇽'],
|
||||
['CY', '🇨🇾'],
|
||||
['CZ', '🇨🇿'],
|
||||
['DE', '🇩🇪'],
|
||||
['DG', '🇩🇬'],
|
||||
['DJ', '🇩🇯'],
|
||||
['DK', '🇩🇰'],
|
||||
['DM', '🇩🇲'],
|
||||
['DO', '🇩🇴'],
|
||||
['DZ', '🇩🇿'],
|
||||
['EA', '🇪🇦'],
|
||||
['EC', '🇪🇨'],
|
||||
['EE', '🇪🇪'],
|
||||
['EG', '🇪🇬'],
|
||||
@@ -114,10 +116,11 @@ const cases = [
|
||||
['GM', '🇬🇲'],
|
||||
['GN', '🇬🇳'],
|
||||
['GP', '🇬🇵'],
|
||||
['GQ', '🇬🇶'],
|
||||
['GR', '🇬🇷'],
|
||||
['GS', '🇬🇸'],
|
||||
['GT', '🇬🇹'],
|
||||
['GQ', '🇬🇶'],
|
||||
['GU', '🇬🇺'],
|
||||
['GW', '🇬🇼'],
|
||||
['GY', '🇬🇾'],
|
||||
['HK', '🇭🇰'],
|
||||
@@ -125,17 +128,17 @@ const cases = [
|
||||
['HN', '🇭🇳'],
|
||||
['HR', '🇭🇷'],
|
||||
['HT', '🇭🇹'],
|
||||
['IC', '🇮🇨'],
|
||||
['HU', '🇭🇺'],
|
||||
['ID', '🇮🇩'],
|
||||
['IE', '🇮🇪'],
|
||||
['IL', '🇮🇱'],
|
||||
['IM', '🇮🇲'],
|
||||
['IN', '🇮🇳'],
|
||||
['IO', '🇮🇴'],
|
||||
['IQ', '🇮🇶'],
|
||||
['IR', '🇮🇷'],
|
||||
['IS', '🇮🇸'],
|
||||
['IT', '🇮🇹'],
|
||||
['IQ', '🇮🇶'],
|
||||
['JE', '🇯🇪'],
|
||||
['JM', '🇯🇲'],
|
||||
['JO', '🇯🇴'],
|
||||
@@ -159,6 +162,8 @@ const cases = [
|
||||
['LR', '🇱🇷'],
|
||||
['LS', '🇱🇸'],
|
||||
['LT', '🇱🇹'],
|
||||
['LU', '🇱🇺'],
|
||||
['LV', '🇱🇻'],
|
||||
['LY', '🇱🇾'],
|
||||
['MA', '🇲🇦'],
|
||||
['MC', '🇲🇨'],
|
||||
@@ -173,11 +178,14 @@ const cases = [
|
||||
['MN', '🇲🇳'],
|
||||
['MO', '🇲🇴'],
|
||||
['MP', '🇲🇵'],
|
||||
['MQ', '🇲🇶'],
|
||||
['MR', '🇲🇷'],
|
||||
['MS', '🇲🇸'],
|
||||
['MT', '🇲🇹'],
|
||||
['MQ', '🇲🇶'],
|
||||
['MU', '🇲🇺'],
|
||||
['MV', '🇲🇻'],
|
||||
['MW', '🇲🇼'],
|
||||
['MX', '🇲🇽'],
|
||||
['MY', '🇲🇾'],
|
||||
['MZ', '🇲🇿'],
|
||||
['NA', '🇳🇦'],
|
||||
@@ -190,6 +198,7 @@ const cases = [
|
||||
['NO', '🇳🇴'],
|
||||
['NP', '🇳🇵'],
|
||||
['NR', '🇳🇷'],
|
||||
['NU', '🇳🇺'],
|
||||
['NZ', '🇳🇿'],
|
||||
['OM', '🇴🇲'],
|
||||
['PA', '🇵🇦'],
|
||||
@@ -206,9 +215,11 @@ const cases = [
|
||||
['PT', '🇵🇹'],
|
||||
['PW', '🇵🇼'],
|
||||
['PY', '🇵🇾'],
|
||||
['QA', '🇶🇦'],
|
||||
['RE', '🇷🇪'],
|
||||
['RO', '🇷🇴'],
|
||||
['RS', '🇷🇸'],
|
||||
['RU', '🇷🇺'],
|
||||
['RW', '🇷🇼'],
|
||||
['SA', '🇸🇦'],
|
||||
['SB', '🇸🇧'],
|
||||
@@ -227,9 +238,10 @@ const cases = [
|
||||
['SR', '🇸🇷'],
|
||||
['SS', '🇸🇸'],
|
||||
['ST', '🇸🇹'],
|
||||
['SV', '🇸🇻'],
|
||||
['SX', '🇸🇽'],
|
||||
['SY', '🇸🇾'],
|
||||
['SZ', '🇸🇿'],
|
||||
['TA', '🇹🇦'],
|
||||
['TC', '🇹🇨'],
|
||||
['TD', '🇹🇩'],
|
||||
['TF', '🇹🇫'],
|
||||
@@ -243,10 +255,22 @@ const cases = [
|
||||
['TO', '🇹🇴'],
|
||||
['TR', '🇹🇷'],
|
||||
['TT', '🇹🇹'],
|
||||
['TV', '🇹🇻'],
|
||||
['TW', '🇹🇼'],
|
||||
['TZ', '🇹🇿'],
|
||||
['QA', '🇶🇦'],
|
||||
['UA', '🇺🇦'],
|
||||
['UG', '🇺🇬'],
|
||||
['UM', '🇺🇲'],
|
||||
['US', '🇺🇸'],
|
||||
['UY', '🇺🇾'],
|
||||
['UZ', '🇺🇿'],
|
||||
['VA', '🇻🇦'],
|
||||
['VC', '🇻🇨'],
|
||||
['VE', '🇻🇪'],
|
||||
['VG', '🇻🇬'],
|
||||
['VI', '🇻🇮'],
|
||||
['VN', '🇻🇳'],
|
||||
['VU', '🇻🇺'],
|
||||
['WF', '🇼🇫'],
|
||||
['WS', '🇼🇸'],
|
||||
['YE', '🇾🇪'],
|
||||
@@ -254,6 +278,23 @@ const cases = [
|
||||
['ZA', '🇿🇦'],
|
||||
['ZM', '🇿🇲'],
|
||||
['ZW', '🇿🇼'],
|
||||
// UK
|
||||
['ENGLAND', '🏴'],
|
||||
['GB-ENG', '🏴'],
|
||||
['ENG', '🏴'],
|
||||
['SCOTLAND', '🏴'],
|
||||
['GB-SCT', '🏴'],
|
||||
['SCT', '🏴'],
|
||||
['WALES', '🏴'],
|
||||
['GB-WLS', '🏴'],
|
||||
['WLS', '🏴'],
|
||||
['CYMRU', '🏴'],
|
||||
['GB-CYM', '🏴'],
|
||||
['CYM', '🏴'],
|
||||
['NORTHERN IRELAND', '🇬🇧'],
|
||||
['GB-NIR', '🇬🇧'],
|
||||
['NIR', '🇬🇧'],
|
||||
['UK', '🇬🇧'],
|
||||
// unknown
|
||||
['AA', FALLBACK_FLAG],
|
||||
['XX', FALLBACK_FLAG],
|
||||
|
||||
@@ -2,23 +2,303 @@ import compact from 'lodash/compact';
|
||||
|
||||
export const FALLBACK_FLAG = '🏳';
|
||||
|
||||
const KNOWN_CODES = `AC AD AE AF AG AI AL AM AO AR AS AT AQ AW AZ BA BB BD BE BF
|
||||
BG BH BI BJ BL BM BN BO BR BS BT BQ BW BY BZ CA CC CD CF CG CH CI CK CL CM CN
|
||||
CO CP CR CW CY CZ DE DG DJ DK DM DO DZ EA EC EE EG EH ER ES ET FI FJ FK FM FO
|
||||
FR GA GB GD GE GF GG GH GI GL GM GN GP GR GS GT GQ GW GY HK HM HN HR HT IC ID
|
||||
IE IL IM IN IO IR IS IT IQ JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB
|
||||
LC LI LK LR LS LT LY MA MC MD ME MF MG MH MK ML MM MN MO MP MR MS MT MQ MW MY
|
||||
MZ NA NC NE NF NG NI NL NO NP NR NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW
|
||||
PY RE RO RS RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SY SZ TA TC
|
||||
TD TF TG TH TJ TK TL TM TN TO TR TT TW TZ QA VG WF WS YE YT ZA ZM ZW`;
|
||||
const KNOWN_CODES = [
|
||||
'AD',
|
||||
'AE',
|
||||
'AF',
|
||||
'AG',
|
||||
'AI',
|
||||
'AL',
|
||||
'AM',
|
||||
'AO',
|
||||
'AQ',
|
||||
'AR',
|
||||
'AS',
|
||||
'AT',
|
||||
'AU',
|
||||
'AW',
|
||||
'AX',
|
||||
'AZ',
|
||||
'BA',
|
||||
'BB',
|
||||
'BD',
|
||||
'BE',
|
||||
'BF',
|
||||
'BG',
|
||||
'BH',
|
||||
'BI',
|
||||
'BJ',
|
||||
'BL',
|
||||
'BM',
|
||||
'BN',
|
||||
'BO',
|
||||
'BQ',
|
||||
'BR',
|
||||
'BS',
|
||||
'BT',
|
||||
'BV',
|
||||
'BW',
|
||||
'BY',
|
||||
'BZ',
|
||||
'CA',
|
||||
'CC',
|
||||
'CD',
|
||||
'CF',
|
||||
'CG',
|
||||
'CH',
|
||||
'CI',
|
||||
'CK',
|
||||
'CL',
|
||||
'CM',
|
||||
'CN',
|
||||
'CO',
|
||||
'CR',
|
||||
'CU',
|
||||
'CV',
|
||||
'CW',
|
||||
'CX',
|
||||
'CY',
|
||||
'CZ',
|
||||
'DE',
|
||||
'DJ',
|
||||
'DK',
|
||||
'DM',
|
||||
'DO',
|
||||
'DZ',
|
||||
'EC',
|
||||
'EE',
|
||||
'EG',
|
||||
'EH',
|
||||
'ER',
|
||||
'ES',
|
||||
'ET',
|
||||
'FI',
|
||||
'FJ',
|
||||
'FK',
|
||||
'FM',
|
||||
'FO',
|
||||
'FR',
|
||||
'GA',
|
||||
'GB',
|
||||
'GD',
|
||||
'GE',
|
||||
'GF',
|
||||
'GG',
|
||||
'GH',
|
||||
'GI',
|
||||
'GL',
|
||||
'GM',
|
||||
'GN',
|
||||
'GP',
|
||||
'GQ',
|
||||
'GR',
|
||||
'GS',
|
||||
'GT',
|
||||
'GU',
|
||||
'GW',
|
||||
'GY',
|
||||
'HK',
|
||||
'HM',
|
||||
'HN',
|
||||
'HR',
|
||||
'HT',
|
||||
'HU',
|
||||
'ID',
|
||||
'IE',
|
||||
'IL',
|
||||
'IM',
|
||||
'IN',
|
||||
'IO',
|
||||
'IQ',
|
||||
'IR',
|
||||
'IS',
|
||||
'IT',
|
||||
'JE',
|
||||
'JM',
|
||||
'JO',
|
||||
'JP',
|
||||
'KE',
|
||||
'KG',
|
||||
'KH',
|
||||
'KI',
|
||||
'KM',
|
||||
'KN',
|
||||
'KP',
|
||||
'KR',
|
||||
'KW',
|
||||
'KY',
|
||||
'KZ',
|
||||
'LA',
|
||||
'LB',
|
||||
'LC',
|
||||
'LI',
|
||||
'LK',
|
||||
'LR',
|
||||
'LS',
|
||||
'LT',
|
||||
'LU',
|
||||
'LV',
|
||||
'LY',
|
||||
'MA',
|
||||
'MC',
|
||||
'MD',
|
||||
'ME',
|
||||
'MF',
|
||||
'MG',
|
||||
'MH',
|
||||
'MK',
|
||||
'ML',
|
||||
'MM',
|
||||
'MN',
|
||||
'MO',
|
||||
'MP',
|
||||
'MQ',
|
||||
'MR',
|
||||
'MS',
|
||||
'MT',
|
||||
'MU',
|
||||
'MV',
|
||||
'MW',
|
||||
'MX',
|
||||
'MY',
|
||||
'MZ',
|
||||
'NA',
|
||||
'NC',
|
||||
'NE',
|
||||
'NF',
|
||||
'NG',
|
||||
'NI',
|
||||
'NL',
|
||||
'NO',
|
||||
'NP',
|
||||
'NR',
|
||||
'NU',
|
||||
'NZ',
|
||||
'OM',
|
||||
'PA',
|
||||
'PE',
|
||||
'PF',
|
||||
'PG',
|
||||
'PH',
|
||||
'PK',
|
||||
'PL',
|
||||
'PM',
|
||||
'PN',
|
||||
'PR',
|
||||
'PS',
|
||||
'PT',
|
||||
'PW',
|
||||
'PY',
|
||||
'QA',
|
||||
'RE',
|
||||
'RO',
|
||||
'RS',
|
||||
'RU',
|
||||
'RW',
|
||||
'SA',
|
||||
'SB',
|
||||
'SC',
|
||||
'SD',
|
||||
'SE',
|
||||
'SG',
|
||||
'SH',
|
||||
'SI',
|
||||
'SJ',
|
||||
'SK',
|
||||
'SL',
|
||||
'SM',
|
||||
'SN',
|
||||
'SO',
|
||||
'SR',
|
||||
'SS',
|
||||
'ST',
|
||||
'SV',
|
||||
'SX',
|
||||
'SY',
|
||||
'SZ',
|
||||
'TC',
|
||||
'TD',
|
||||
'TF',
|
||||
'TG',
|
||||
'TH',
|
||||
'TJ',
|
||||
'TK',
|
||||
'TL',
|
||||
'TM',
|
||||
'TN',
|
||||
'TO',
|
||||
'TR',
|
||||
'TT',
|
||||
'TV',
|
||||
'TW',
|
||||
'TZ',
|
||||
'UA',
|
||||
'UG',
|
||||
'UM',
|
||||
'US',
|
||||
'UY',
|
||||
'UZ',
|
||||
'VA',
|
||||
'VC',
|
||||
'VE',
|
||||
'VG',
|
||||
'VI',
|
||||
'VN',
|
||||
'VU',
|
||||
'WF',
|
||||
'WS',
|
||||
'YE',
|
||||
'YT',
|
||||
'ZA',
|
||||
'ZM',
|
||||
'ZW',
|
||||
];
|
||||
|
||||
export const countryCodeToFlagEmoji = (countryCode: string) => {
|
||||
const ENGLAND = '🏴';
|
||||
const SCOTLAND = '🏴';
|
||||
const WALES = '🏴';
|
||||
const NORTHERN_IRELAND = '🇬🇧';
|
||||
|
||||
const UK = new Map([
|
||||
['ENGLAND', ENGLAND],
|
||||
['GB-ENG', ENGLAND],
|
||||
['ENG', ENGLAND],
|
||||
['SCOTLAND', SCOTLAND],
|
||||
['GB-SCT', SCOTLAND],
|
||||
['SCT', SCOTLAND],
|
||||
['WALES', WALES],
|
||||
['GB-WLS', WALES],
|
||||
['WLS', WALES],
|
||||
['CYMRU', WALES],
|
||||
['GB-CYM', WALES],
|
||||
['CYM', WALES],
|
||||
['NORTHERN IRELAND', NORTHERN_IRELAND],
|
||||
['GB-NIR', NORTHERN_IRELAND],
|
||||
['NIR', NORTHERN_IRELAND],
|
||||
['UNITED KINGDOM', '🇬🇧'],
|
||||
['UK', '🇬🇧'],
|
||||
]);
|
||||
|
||||
const EU = new Map([['EU', '🇪🇺']]);
|
||||
|
||||
const getCode = (countryCode: string): string => {
|
||||
const code = countryCode.trim().toUpperCase();
|
||||
const known = compact(KNOWN_CODES.split(' ').map((ch) => ch.trim()));
|
||||
return code;
|
||||
};
|
||||
|
||||
export const countryCodeToFlagEmoji = (countryCode: string): string => {
|
||||
const code = getCode(countryCode);
|
||||
const known = compact(KNOWN_CODES.map((ch) => ch.trim()));
|
||||
if (known.includes(code)) {
|
||||
return code.replace(/./g, (char) =>
|
||||
String.fromCodePoint(0x1f1a5 + char.toUpperCase().charCodeAt(0))
|
||||
);
|
||||
}
|
||||
if (UK.has(code)) {
|
||||
return UK.get(code) as string;
|
||||
}
|
||||
if (EU.has(code)) {
|
||||
return EU.get(code) as string;
|
||||
}
|
||||
return FALLBACK_FLAG;
|
||||
};
|
||||
|
||||
@@ -98,6 +98,8 @@ interface ProposalNewMarketTerms {
|
||||
decimalPlaces: string;
|
||||
positionDecimalPlaces: string;
|
||||
lpPriceRange: string;
|
||||
linearSlippageFactor: string;
|
||||
quadraticSlippageFactor: string;
|
||||
instrument: {
|
||||
name: string;
|
||||
code: string;
|
||||
@@ -130,6 +132,8 @@ interface ProposalUpdateMarketTerms {
|
||||
updateMarket: {
|
||||
marketId: string;
|
||||
changes: {
|
||||
linearSlippageFactor: string;
|
||||
quadraticSlippageFactor: string;
|
||||
instrument: {
|
||||
code: string;
|
||||
future: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||
|
||||
const ACTIVE_CLASSES = 'text-black dark:text-white';
|
||||
@@ -62,14 +62,14 @@ export const TxRow = ({
|
||||
}`}
|
||||
>
|
||||
<span>{t('Ethereum transaction complete')}</span>
|
||||
<Link
|
||||
href={`${ETHERSCAN_URL}/tx/${txHash}`}
|
||||
title={t('View on Etherscan')}
|
||||
className="text-vega-pink dark:text-vega-yellow"
|
||||
target="_blank"
|
||||
>
|
||||
{t('View transaction on Etherscan')}
|
||||
</Link>
|
||||
{txHash && (
|
||||
<EtherscanLink
|
||||
tx={txHash}
|
||||
className="text-vega-pink dark:text-vega-yellow"
|
||||
>
|
||||
{t('View transaction on Etherscan')}
|
||||
</EtherscanLink>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Link, ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
TypedDataAgGrid,
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -27,7 +27,6 @@ export const WithdrawalsTable = (
|
||||
props: TypedDataAgGrid<WithdrawalFieldsFragment>
|
||||
) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const createWithdrawApproval = useEthWithdrawApprovalsStore(
|
||||
(store) => store.create
|
||||
);
|
||||
@@ -66,7 +65,6 @@ export const WithdrawalsTable = (
|
||||
headerName={t('Recipient')}
|
||||
field="details.receiverAddress"
|
||||
cellRenderer="RecipientCell"
|
||||
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
@@ -126,7 +124,6 @@ export const WithdrawalsTable = (
|
||||
complete: (withdrawal: WithdrawalFieldsFragment) => {
|
||||
createWithdrawApproval(withdrawal);
|
||||
},
|
||||
ethUrl: ETHERSCAN_URL,
|
||||
}}
|
||||
cellRendererSelector={({
|
||||
data,
|
||||
@@ -160,20 +157,12 @@ export const CompleteCell = ({ data, complete }: CompleteCellProps) => {
|
||||
|
||||
export const EtherscanLinkCell = ({
|
||||
value,
|
||||
ethUrl,
|
||||
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'txHash'> & {
|
||||
ethUrl: string;
|
||||
}) => {
|
||||
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'txHash'>) => {
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ethUrl}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
<EtherscanLink tx={value} data-testid="etherscan-link">
|
||||
{truncateByChars(value)}
|
||||
</Link>
|
||||
</EtherscanLink>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -193,28 +182,17 @@ export const StatusCell = ({ data }: { data: WithdrawalFieldsFragment }) => {
|
||||
return <span>{t('Failed')}</span>;
|
||||
};
|
||||
|
||||
export interface RecipientCellProps
|
||||
extends VegaICellRendererParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'details.receiverAddress'
|
||||
> {
|
||||
ethUrl: string;
|
||||
}
|
||||
|
||||
const RecipientCell = ({
|
||||
ethUrl,
|
||||
value,
|
||||
valueFormatted,
|
||||
}: RecipientCellProps) => {
|
||||
}: VegaICellRendererParams<
|
||||
WithdrawalFieldsFragment,
|
||||
'details.receiverAddress'
|
||||
>) => {
|
||||
return (
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ethUrl}/address/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
<EtherscanLink address={value} data-testid="etherscan-link">
|
||||
{valueFormatted}
|
||||
</Link>
|
||||
</EtherscanLink>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user