diff --git a/apps/explorer/src/app/components/price-in-asset/price-in-asset.spec.tsx b/apps/explorer/src/app/components/price-in-asset/price-in-asset.spec.tsx new file mode 100644 index 0000000000..4c04511d54 --- /dev/null +++ b/apps/explorer/src/app/components/price-in-asset/price-in-asset.spec.tsx @@ -0,0 +1,17 @@ +import { MemoryRouter } from 'react-router-dom'; +import { render } from '@testing-library/react'; +import PriceInAsset from './price-in-asset'; + +function renderComponent(price: string, decimals: number, symbol: string) { + return ( + + + + ); +} +describe('Price in Market component', () => { + it('Renders the raw price before there is no market data', () => { + const res = render(renderComponent('100', 8, 'USDT')); + expect(res.getByText('0.000001')).toBeInTheDocument(); + }); +}); diff --git a/apps/explorer/src/app/components/price-in-asset/price-in-asset.tsx b/apps/explorer/src/app/components/price-in-asset/price-in-asset.tsx new file mode 100644 index 0000000000..ab3c86b317 --- /dev/null +++ b/apps/explorer/src/app/components/price-in-asset/price-in-asset.tsx @@ -0,0 +1,29 @@ +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; + +export type DecimalSource = 'MARKET' | 'SETTLEMENT_ASSET'; + +export type PriceInMarketProps = { + price: string; + decimals: number; + symbol: string; +}; + +/** + * An alternative to PriceInMarket for when you need the price + * with an asset's decimal places. + */ +export const PriceInAsset = ({ + price, + decimals, + symbol, +}: PriceInMarketProps) => { + const label = addDecimalsFormatNumber(price, decimals); + + return ( + + ); +}; + +export default PriceInAsset; diff --git a/apps/explorer/src/app/components/price-in-market/price-in-market.tsx b/apps/explorer/src/app/components/price-in-market/price-in-market.tsx index 2d08520f03..46f82d9747 100644 --- a/apps/explorer/src/app/components/price-in-market/price-in-market.tsx +++ b/apps/explorer/src/app/components/price-in-market/price-in-market.tsx @@ -21,7 +21,6 @@ export const PriceInMarket = ({ marketId, price, decimalSource = 'MARKET', - assetIcon = true, }: PriceInMarketProps) => { const { data } = useExplorerMarketQuery({ variables: { id: marketId }, diff --git a/apps/explorer/src/app/components/proposals/proposals-table.tsx b/apps/explorer/src/app/components/proposals/proposals-table.tsx index 49a9a3843e..97c9aac151 100644 --- a/apps/explorer/src/app/components/proposals/proposals-table.tsx +++ b/apps/explorer/src/app/components/proposals/proposals-table.tsx @@ -25,7 +25,9 @@ type ProposalTermsDialog = { content: unknown; }; type ProposalsTableProps = { - data: ProposalListFieldsFragment[] | null; + data: Array< + ProposalListFieldsFragment | BatchproposalListFieldsFragment + > | null; }; export const ProposalsTable = ({ data }: ProposalsTableProps) => { const tokenLink = useLinks(DApp.Governance); diff --git a/apps/explorer/src/app/routes/assets/asset-page.tsx b/apps/explorer/src/app/routes/assets/asset-page.tsx index bf1424c020..e4fb3c5b11 100644 --- a/apps/explorer/src/app/routes/assets/asset-page.tsx +++ b/apps/explorer/src/app/routes/assets/asset-page.tsx @@ -46,7 +46,13 @@ export const AssetPage = () => { >
- {assetId && } + {data && assetId && data.decimals && data.symbol && ( + + )}
diff --git a/apps/explorer/src/app/routes/assets/components/Asset-Markets.graphql b/apps/explorer/src/app/routes/assets/components/Asset-Markets.graphql index 01d8977844..923de44d51 100644 --- a/apps/explorer/src/app/routes/assets/components/Asset-Markets.graphql +++ b/apps/explorer/src/app/routes/assets/components/Asset-Markets.graphql @@ -1,8 +1,9 @@ query AssetMarkets { - marketsConnection(includeSettled: false) { + marketsConnection(includeSettled: true) { edges { node { id + state tradableInstrument { instrument { name @@ -14,6 +15,8 @@ query AssetMarkets { type asset { id + decimals + symbol } balance } diff --git a/apps/explorer/src/app/routes/assets/components/__generated__/Asset-Markets.ts b/apps/explorer/src/app/routes/assets/components/__generated__/Asset-Markets.ts index 990eba390b..738d8ec226 100644 --- a/apps/explorer/src/app/routes/assets/components/__generated__/Asset-Markets.ts +++ b/apps/explorer/src/app/routes/assets/components/__generated__/Asset-Markets.ts @@ -6,15 +6,16 @@ const defaultOptions = {} as const; export type AssetMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type AssetMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string } } } | null> | null } | null } }> } | null }; +export type AssetMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string, decimals: number, symbol: string } } } | null> | null } | null } }> } | null }; export const AssetMarketsDocument = gql` query AssetMarkets { - marketsConnection(includeSettled: false) { + marketsConnection(includeSettled: true) { edges { node { id + state tradableInstrument { instrument { name @@ -26,6 +27,8 @@ export const AssetMarketsDocument = gql` type asset { id + decimals + symbol } balance } diff --git a/apps/explorer/src/app/routes/assets/components/asset-markets.spec.tsx b/apps/explorer/src/app/routes/assets/components/asset-markets.spec.tsx index cea877473e..a07ca4f462 100644 --- a/apps/explorer/src/app/routes/assets/components/asset-markets.spec.tsx +++ b/apps/explorer/src/app/routes/assets/components/asset-markets.spec.tsx @@ -3,7 +3,7 @@ import { AssetMarkets, transformAssetMarketsQuery } from './asset-markets'; import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import type { AssetMarketsQuery } from './__generated__/Asset-Markets'; -import { AccountType } from '@vegaprotocol/types'; +import { AccountType, MarketState } from '@vegaprotocol/types'; import { MockAssetMarkets } from '../../../mocks/links'; jest.mock('../../../components/links'); @@ -34,6 +34,7 @@ describe('transformAssetMarketsQuery', () => { name: 'instrument1', }, }, + state: MarketState.STATE_ACTIVE, id: 'market1', accountsConnection: { edges: [], @@ -59,6 +60,7 @@ describe('transformAssetMarketsQuery', () => { name: 'instrument1', }, }, + state: MarketState.STATE_ACTIVE, accountsConnection: { edges: [ { @@ -66,6 +68,8 @@ describe('transformAssetMarketsQuery', () => { type: AccountType.ACCOUNT_TYPE_INSURANCE, asset: { id: 'assetId', + decimals: 8, + symbol: 'ASSET', }, balance: '100', }, @@ -75,6 +79,8 @@ describe('transformAssetMarketsQuery', () => { type: AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE, asset: { id: 'assetId', + decimals: 8, + symbol: 'ASSET', }, balance: '200', }, @@ -91,6 +97,7 @@ describe('transformAssetMarketsQuery', () => { name: 'instrument1', }, }, + state: MarketState.STATE_ACTIVE, accountsConnection: { edges: [ { @@ -98,6 +105,8 @@ describe('transformAssetMarketsQuery', () => { type: AccountType.ACCOUNT_TYPE_INSURANCE, asset: { id: 'assetId', + decimals: 8, + symbol: 'ASSET', }, balance: '300', }, @@ -114,6 +123,7 @@ describe('transformAssetMarketsQuery', () => { name: 'instrument1', }, }, + state: MarketState.STATE_ACTIVE, accountsConnection: { edges: [ { @@ -121,6 +131,8 @@ describe('transformAssetMarketsQuery', () => { type: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE, asset: { id: 'any-other-asset-id', + decimals: 8, + symbol: 'ASSET', }, balance: '300', }, @@ -134,13 +146,15 @@ describe('transformAssetMarketsQuery', () => { }; const result = transformAssetMarketsQuery(data, 'assetId'); expect(result).toEqual([ - { - marketId: 'market1', - balance: '100', - }, { marketId: 'market2', balance: '300', + state: MarketState.STATE_ACTIVE, + }, + { + marketId: 'market1', + balance: '100', + state: MarketState.STATE_ACTIVE, }, ]); }); @@ -151,7 +165,7 @@ describe('AssetMarkets', () => { render( - + ); @@ -163,7 +177,7 @@ describe('AssetMarkets', () => { render( - + ); diff --git a/apps/explorer/src/app/routes/assets/components/asset-markets.tsx b/apps/explorer/src/app/routes/assets/components/asset-markets.tsx index a2a4313c7c..85b0ddaa09 100644 --- a/apps/explorer/src/app/routes/assets/components/asset-markets.tsx +++ b/apps/explorer/src/app/routes/assets/components/asset-markets.tsx @@ -1,12 +1,15 @@ -import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import { MarketLink } from '../../../components/links'; -import { PriceInMarket } from '../../../components/price-in-market/price-in-market'; +import PriceInAsset from '../../../components/price-in-asset/price-in-asset'; +import { Table, TableCell, TableRow } from '../../../components/table'; import type { AssetMarketsQuery } from './__generated__/Asset-Markets'; import { useAssetMarketsQuery } from './__generated__/Asset-Markets'; import { t } from '@vegaprotocol/i18n'; +import { MarketState, MarketStateMapping } from '@vegaprotocol/types'; type AssetMarketProps = { asset: string; + symbol: string; + decimals: number; }; /** @@ -16,7 +19,7 @@ type AssetMarketProps = { * @param param0 * @returns */ -export const AssetMarkets = ({ asset }: AssetMarketProps) => { +export const AssetMarkets = ({ asset, decimals, symbol }: AssetMarketProps) => { const { data } = useAssetMarketsQuery(); const markets = transformAssetMarketsQuery(data, asset); @@ -31,26 +34,30 @@ export const AssetMarkets = ({ asset }: AssetMarketProps) => { )}

) : ( - + {markets.map((market) => { return ( market.marketId && market.balance !== '0' && ( - -
+ + -
-
- + + {market.state ? MarketStateMapping[market.state] : ''} + + + -
-
+ + ) ); })} - +
)} ); @@ -59,6 +66,7 @@ export const AssetMarkets = ({ asset }: AssetMarketProps) => { export type AssetMarketInsuranceAccount = { marketId: string; balance: string; + state: MarketState; }; /** @@ -86,15 +94,19 @@ export function transformAssetMarketsQuery( : []; // Now reshape the data to only include the market ID and the balance of the insurance account - return marketsWithAsset.map((market) => { - const accountsWithAsset = market.node?.accountsConnection?.edges?.filter( - (e) => - e?.node.type === 'ACCOUNT_TYPE_INSURANCE' && e?.node.asset?.id === asset - ); + return marketsWithAsset + .map((market) => { + const accountsWithAsset = market.node?.accountsConnection?.edges?.filter( + (e) => + e?.node.type === 'ACCOUNT_TYPE_INSURANCE' && + e?.node.asset?.id === asset + ); - return { - marketId: market.node?.id || '', - balance: accountsWithAsset?.[0]?.node?.balance || '0', - }; - }); + return { + marketId: market.node?.id || '', + balance: accountsWithAsset?.[0]?.node?.balance || '0', + state: market.node?.state || null, + }; + }) + .sort((a, b) => (a.state === MarketState.STATE_ACTIVE ? -1 : 1)); } diff --git a/apps/governance-e2e/src/integration/view/wallet-eth.cy.ts b/apps/governance-e2e/src/integration/view/wallet-eth.cy.ts index 5e1f3276db..c143356a1a 100644 --- a/apps/governance-e2e/src/integration/view/wallet-eth.cy.ts +++ b/apps/governance-e2e/src/integration/view/wallet-eth.cy.ts @@ -141,7 +141,7 @@ context( cy.get(vegaInVesting).within(() => { cy.get(currencyTitle) .should('be.visible') - .and('have.text', 'VEGAIn vesting contract'); + .and('contain.text', 'VEGAIn vesting contract'); }); }); @@ -214,7 +214,7 @@ context( cy.get(vegaInWallet).within(() => { cy.get(currencyTitle) .should('be.visible') - .and('have.text', 'VEGAIn Wallet'); + .and('contain.text', 'VEGAIn Wallet'); }); }); diff --git a/apps/governance/src/components/vega-wallet/Delegations.graphql b/apps/governance/src/components/vega-wallet/Delegations.graphql index a0cd374d3b..650a36fc26 100644 --- a/apps/governance/src/components/vega-wallet/Delegations.graphql +++ b/apps/governance/src/components/vega-wallet/Delegations.graphql @@ -35,6 +35,7 @@ query Delegations($partyId: ID!, $delegationsPagination: Pagination) { __typename ... on ERC20 { contractAddress + chainId } } } diff --git a/apps/governance/src/components/vega-wallet/__generated__/Delegations.ts b/apps/governance/src/components/vega-wallet/__generated__/Delegations.ts index 6076e18fc4..d3c2e5f068 100644 --- a/apps/governance/src/components/vega-wallet/__generated__/Delegations.ts +++ b/apps/governance/src/components/vega-wallet/__generated__/Delegations.ts @@ -11,7 +11,7 @@ export type DelegationsQueryVariables = Types.Exact<{ }>; -export type DelegationsQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, party?: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } } } } | null> | null } | null } | null }; +export type DelegationsQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, party?: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, chainId: string } } } } | null> | null } | null } | null }; export const WalletDelegationFieldsFragmentDoc = gql` fragment WalletDelegationFields on Delegation { @@ -52,6 +52,7 @@ export const DelegationsDocument = gql` __typename ... on ERC20 { contractAddress + chainId } } } diff --git a/apps/governance/src/components/vega-wallet/hooks.ts b/apps/governance/src/components/vega-wallet/hooks.ts index 405aae13a8..995eb279f5 100644 --- a/apps/governance/src/components/vega-wallet/hooks.ts +++ b/apps/governance/src/components/vega-wallet/hooks.ts @@ -109,6 +109,10 @@ export const usePollForDelegations = () => { isAssetTypeERC20(a.asset) && a.asset.source.contractAddress === vegaToken.address; + const chainId = isAssetTypeERC20(a.asset) + ? a.asset.source.chainId + : undefined; + const isVesting = a.type === Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS || a.type === Schema.AccountType.ACCOUNT_TYPE_VESTING_REWARDS; @@ -126,6 +130,7 @@ export const usePollForDelegations = () => { symbol: a.asset.symbol, decimals: a.asset.decimals, assetId: a.asset.id, + chainId: chainId, balance: new BigNumber( addDecimal(a.balance, a.asset.decimals) ), diff --git a/apps/governance/src/components/wallet-card/wallet-card.tsx b/apps/governance/src/components/wallet-card/wallet-card.tsx index abc736d080..a78853f2a1 100644 --- a/apps/governance/src/components/wallet-card/wallet-card.tsx +++ b/apps/governance/src/components/wallet-card/wallet-card.tsx @@ -11,10 +11,14 @@ import { CONSOLE_TRANSFER_ASSET, DApp, DocsLinks, + getExternalChainShortLabel, useLinks, } from '@vegaprotocol/environment'; import { useNetworkParam } from '@vegaprotocol/network-parameters'; import { formatNumberPercentage } from '@vegaprotocol/utils'; +import noIcon from '../../images/token-no-icon.png'; +import { EmblemByAsset } from '@vegaprotocol/emblem'; +import { useWallet } from '@vegaprotocol/wallet-react'; interface WalletCardProps { children: React.ReactNode; @@ -112,6 +116,7 @@ export type WalletCardAssetProps = { balance: BigNumber; decimals: number; assetId?: string; + chainId?: string; border?: boolean; subheading?: string; type?: Schema.AccountType; @@ -131,6 +136,7 @@ export const WalletCardAsset = ({ symbol, decimals, assetId, + chainId, border, subheading, allowZeroBalance = false, @@ -169,17 +175,30 @@ export const WalletCardAsset = ({ /> )); + const vegaChainId = useWallet((store) => store.chainId); if (!values || values.length === 0) return; + let img = ( + Vega + ); + + if (image === noIcon && assetId) { + img = ( +
+ +
+ ); + } + return (
- Vega + {img}
{name}
- {subheading || symbol} + {subheading || symbol}{' '} + {chainId && ( + + {getExternalChainShortLabel(chainId)} + + )}
{values} diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx index 5e55043f8f..cb99494988 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.spec.tsx @@ -1,6 +1,8 @@ import { render } from '@testing-library/react'; import { AppStateProvider } from '../../../contexts/app-state/app-state-provider'; import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table'; +import { useWallet } from '@vegaprotocol/wallet-react'; +jest.mock('@vegaprotocol/wallet-react'); const mockData = { epoch: 4441, @@ -40,6 +42,11 @@ const mockData = { }; describe('EpochIndividualRewardsTable', () => { + beforeAll(() => { + (useWallet as jest.Mock).mockReturnValue(() => () => ({ + chainId: 'vega-chain-id', + })); + }); it('should render correctly', () => { const { getByTestId } = render( diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.tsx b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.tsx index 68c9ea3d3d..1e6cd1aa2d 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.tsx +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards-table.tsx @@ -5,6 +5,8 @@ import { RewardsTable, } from '../shared-rewards-table-assets/shared-rewards-table-assets'; import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list'; +import { useWallet } from '@vegaprotocol/wallet-react'; +import { EmblemByAsset } from '@vegaprotocol/emblem'; interface EpochIndividualRewardsGridProps { data: EpochIndividualReward; @@ -72,39 +74,46 @@ export const EpochIndividualRewardsTable = ({ data, marketCreationQuantumMultiple, }: EpochIndividualRewardsGridProps) => { + const vegaChainId = useWallet((store) => store.chainId); return ( - {data.rewards.map(({ asset, rewardTypes, totalAmount, decimals }, i) => ( -
-
- {asset} + {data.rewards.map( + ({ assetId, asset, rewardTypes, totalAmount, decimals }, i) => ( +
+
+
+ {assetId && ( +
+ +
+ )} + {asset} +
+
+ {Object.entries(rewardTypes).map( + ([key, { amount, percentageOfTotal }]) => ( + + ) + )} +
- {Object.entries(rewardTypes).map( - ([key, { amount, percentageOfTotal }]) => ( - - ) - )} - -
- ))} + ) + )} ); }; diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts index 487f85beca..5fe29d49ad 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.spec.ts @@ -83,6 +83,7 @@ describe('generateEpochIndividualRewardsList', () => { rewards: [ { asset: 'USD', + assetId: 'usd', decimals: 6, totalAmount: '100', rewardTypes: { @@ -137,6 +138,7 @@ describe('generateEpochIndividualRewardsList', () => { rewards: [ { asset: 'EUR', + assetId: 'eur', totalAmount: '50', decimals: 5, rewardTypes: { @@ -157,6 +159,7 @@ describe('generateEpochIndividualRewardsList', () => { rewards: [ { asset: 'USD', + assetId: 'usd', totalAmount: '100', decimals: 6, rewardTypes: { @@ -195,6 +198,7 @@ describe('generateEpochIndividualRewardsList', () => { rewards: [ { asset: 'EUR', + assetId: 'eur', totalAmount: '50', decimals: 5, rewardTypes: { @@ -226,6 +230,7 @@ describe('generateEpochIndividualRewardsList', () => { rewards: [ { asset: 'USD', + assetId: 'usd', totalAmount: '100', decimals: 6, rewardTypes: { diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts index 536d92f367..4bd49d9e37 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/generate-epoch-individual-rewards-list.ts @@ -11,6 +11,7 @@ export interface EpochIndividualReward { epoch: number; rewards: { asset: string; + assetId?: string; totalAmount: string; decimals: number; rewardTypes: { @@ -81,6 +82,7 @@ export const generateEpochIndividualRewardsList = ({ if (!asset) { asset = { asset: assetName, + assetId: reward.asset.id, decimals: assetDecimals, totalAmount: '0', rewardTypes: Object.fromEntries(emptyRowAccountTypes), diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx index 55a8dec3ae..7f7f0878ab 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.spec.tsx @@ -7,6 +7,8 @@ import type { RewardItem, } from './generate-epoch-total-rewards-list'; import { AccountType } from '@vegaprotocol/types'; +import { useWallet } from '@vegaprotocol/wallet-react'; +jest.mock('@vegaprotocol/wallet-react'); const assetId = 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663'; @@ -63,6 +65,11 @@ const mockData = { }; describe('EpochTotalRewardsTable', () => { + beforeAll(() => { + (useWallet as jest.Mock).mockReturnValue(() => () => ({ + chainId: 'vega-chain-id', + })); + }); it('should render correctly', () => { const { getByTestId } = render( diff --git a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx index c0d4a30e83..ac80c449bd 100644 --- a/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx +++ b/apps/governance/src/routes/rewards/epoch-total-rewards/epoch-total-rewards-table.tsx @@ -5,6 +5,8 @@ import { RewardsTable, } from '../shared-rewards-table-assets/shared-rewards-table-assets'; import type { EpochTotalSummary } from './generate-epoch-total-rewards-list'; +import { EmblemByAsset } from '@vegaprotocol/emblem'; +import { useWallet } from '@vegaprotocol/wallet-react'; interface EpochTotalRewardsGridProps { data: EpochTotalSummary; @@ -55,6 +57,7 @@ export const EpochTotalRewardsTable = ({ data, marketCreationQuantumMultiple, }: EpochTotalRewardsGridProps) => { + const vegaChainId = useWallet((store) => store.chainId); return ( {Array.from(data.assetRewards.values()).map( - ({ name, rewards, totalAmount, decimals }, i) => ( + ({ assetId, name, rewards, totalAmount, decimals }, i) => (
-
- {name} +
+
+
+ +
+ {name} +
{Array.from(rewards.values()).map(({ rewardType, amount }, i) => ( data.id; const defaultColDef = { sortable: true, - filter: true, + filter: false, resizable: true, filterParams: { buttons: ['reset'] }, minWidth: 100, @@ -20,7 +22,9 @@ const components = { PriceFlashCell, }; -type Props = TypedDataAgGrid; +type Props = TypedDataAgGrid & { + filterSummary?: ReactNode; +}; export type DataGridSlice = { gridStore: DataGridStore; @@ -45,7 +49,7 @@ export const useMarketsStore = create()( }) ); -export const MarketListTable = (props: Props) => { +export const MarketListTable = ({ filterSummary, ...props }: Props) => { const columnDefs = useMarketsColumnDefs(); return ( @@ -55,14 +59,49 @@ export const MarketListTable = (props: Props) => { columnDefs={columnDefs} components={components} rowHeight={60} + rowClass={ + '!border-b !last:border-b-0 mb-1 border-vega-clight-600 dark:border-vega-cdark-600' + } headerHeight={40} domLayout="autoHeight" autoSizeStrategy={{ type: 'fitGridWidth', }} + isFullWidthRow={(params) => { + const data = params.rowNode.data; + return isFilterSummaryRow(data); + }} + fullWidthCellRenderer={FullWidthCellRenderer} + getRowHeight={(params) => { + const data = params.data; + if (isFilterSummaryRow(data)) { + return 30; + } + return 60; + }} + pinnedTopRowData={ + filterSummary + ? [ + { + id: 'summary', + filterSummary, + }, + ] + : [] + } {...props} /> ); }; +const isFilterSummaryRow = (data: unknown) => + Boolean(data && typeof data === 'object' && 'filterSummary' in data); + +const FullWidthCellRenderer = ({ data }: ICellRendererParams) => { + if (isFilterSummaryRow(data)) { + return
{data.filterSummary}
; + } + return null; +}; + export default MarketListTable; diff --git a/apps/trading/client-pages/markets/markets-page.tsx b/apps/trading/client-pages/markets/markets-page.tsx index 992da314bd..3da013bc8e 100644 --- a/apps/trading/client-pages/markets/markets-page.tsx +++ b/apps/trading/client-pages/markets/markets-page.tsx @@ -1,19 +1,26 @@ -import { Sparkline, TinyScroll } from '@vegaprotocol/ui-toolkit'; -import { OpenMarkets } from './open-markets'; -import { Proposed } from './proposed'; -import { Closed } from './closed'; +import { + MultiSelect, + MultiSelectOption, + Sparkline, + TinyScroll, + TradingInput, + VegaIcon, + VegaIconNames, +} from '@vegaprotocol/ui-toolkit'; import { useT } from '../../lib/use-t'; import { ErrorBoundary } from '../../components/error-boundary'; import { usePageTitle } from '../../lib/hooks/use-page-title'; import { Card } from '../../components/card'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { - activeMarketsWithCandlesProvider, + type MarketMaybeWithData, calcCandleVolumePrice, + marketsWithCandlesProvider, + retrieveAssets, type MarketMaybeWithCandles, } from '@vegaprotocol/markets'; import { useYesterday } from '@vegaprotocol/react-helpers'; -import { useEffect, useState } from 'react'; +import { type ReactNode, useEffect } from 'react'; import { Interval } from '@vegaprotocol/types'; import { formatNumber } from '@vegaprotocol/utils'; import { TopMarketList } from './top-market-list'; @@ -24,18 +31,41 @@ import { useTotalVolume24hCandles, } from '../../lib/hooks/use-markets-stats'; import { useTotalValueLocked } from '../../lib/hooks/use-total-volume-locked'; +import uniq from 'lodash/uniq'; +import trim from 'lodash/trim'; +import flatten from 'lodash/flatten'; +import compact from 'lodash/compact'; +import uniqBy from 'lodash/uniqBy'; +import orderBy from 'lodash/orderBy'; +import { getChainName } from '@vegaprotocol/web3'; +import { type AssetFieldsFragment } from '@vegaprotocol/assets'; +import { + DEFAULT_FILTERS, + type IMarketState, + type IMarketType, + MarketType, + MarketState, + filterMarkets, + useMarketFiltersStore, + filterMarket, +} from '../../lib/hooks/use-market-filters'; +import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; +import MarketListTable from './market-list-table'; +import type { CellClickedEvent, IRowNode } from 'ag-grid-community'; +import { FilterSummary } from '../../components/market-selector/filter-summary'; const POLLING_TIME = 2000; export const MarketsPage = () => { const t = useT(); const yesterday = useYesterday(); + const { - data: activeMarkets, + data: allMarkets, error, reload, } = useDataProvider({ - dataProvider: activeMarketsWithCandlesProvider, + dataProvider: marketsWithCandlesProvider, variables: { since: new Date(yesterday).toISOString(), interval: Interval.INTERVAL_I1H, @@ -52,16 +82,16 @@ export const MarketsPage = () => { usePageTitle(t('Markets')); - const topGainers = useTopGainers(activeMarkets); - const newListings = useNewListings(activeMarkets); - const totalVolume24hCandles = useTotalVolume24hCandles(activeMarkets); + const topGainers = useTopGainers(allMarkets); + const newListings = useNewListings(allMarkets); + const totalVolume24hCandles = useTotalVolume24hCandles(allMarkets); const totalVolumeSparkline = ( ); const { tvl, loading: tvlLoading, error: tvlError } = useTotalValueLocked(); - const totalVolume24h = activeMarkets?.reduce((acc, market) => { + const totalVolume24h = allMarkets?.reduce((acc, market) => { return ( acc + Number( @@ -74,10 +104,24 @@ export const MarketsPage = () => { ); }, 0); + const marketAssets = uniqBy( + compact( + flatten( + allMarkets?.map((m) => { + const product = m.tradableInstrument?.instrument?.product; + if (product) { + return retrieveAssets(product); + } + }) + ) + ), + (a) => a.id + ); + return ( -
+
@@ -125,78 +169,279 @@ export const MarketsPage = () => {
- +
); }; -export const MarketTables = ({ - activeMarkets, +export const MarketTable = ({ + markets, + marketAssets, error, }: { - activeMarkets: MarketMaybeWithCandles[] | null; + markets: MarketMaybeWithCandles[] | null; + marketAssets: AssetFieldsFragment[]; error: Error | undefined; }) => { const t = useT(); - const [activeTab, setActiveTab] = useState('open-markets'); - - const marketTabs: { - [key: string]: { id: string; name: string }; - } = { - open: { - id: 'open-markets', - name: t('Open'), - }, - proposed: { - id: 'proposed-markets', - name: t('Proposed'), - }, - closed: { - id: 'closed-markets', - name: t('Closed'), - }, + + const { + marketTypes, + marketStates, + assets, + searchTerm, + setMarketTypes, + setMarketStates, + setAssets, + setSearchTerm, + reset, + } = useMarketFiltersStore((state) => ({ + marketTypes: state.marketTypes, + setMarketTypes: state.setMarketTypes, + marketStates: state.marketStates, + setMarketStates: state.setMarketStates, + assets: state.assets, + setAssets: state.setAssets, + searchTerm: state.searchTerm, + setSearchTerm: state.setSearchTerm, + reset: state.reset, + })); + + const isMarketTypeSelected = (marketType: IMarketType) => + marketTypes.includes(marketType); + + const marketTypeFilterOptions: Record = { + [MarketType.PERPETUAL]: t('Perpetuals'), + [MarketType.FUTURE]: t('Futures'), + [MarketType.SPOT]: t('Spot'), + }; + + const marketStateFilterOptions: Record = { + [MarketState.OPEN]: t('Open'), + [MarketState.CLOSED]: t('Closed'), + [MarketState.PROPOSED]: t('Proposed'), + }; + let marketStateTrigger: string | undefined = undefined; + if (marketStates.length > 0) { + if (marketStates.length === 1) { + marketStateTrigger = marketStateFilterOptions[marketStates[0]]; + } + if (marketStates.length > 1) { + marketStateTrigger = t('Status ({{count}})', { + count: marketStates.length, + }); + } + } + + const assetFilterOptions = orderBy(marketAssets, (a) => a.symbol, 'asc'); + let assetTrigger: string | undefined = undefined; + if (assets.length > 0) { + if (assets.length === 1) { + assetTrigger = marketAssets?.find((a) => a.id === assets[0])?.symbol; + } + if (assets.length > 1) { + assetTrigger = t('Assets ({{count}})', { count: assets.length }); + } + } + + const filteredMarkets = filterMarkets(markets || [], { + marketTypes, + marketStates, + assets, + searchTerm, + }); + const defaultMarkets = filterMarkets(markets || [], DEFAULT_FILTERS); + + let filterSummary: ReactNode = undefined; + if (filteredMarkets.length != defaultMarkets.length) { + const diff = defaultMarkets.length - filteredMarkets.length; + filterSummary = ; + } + + const handleOnSelect = useMarketClickHandler(); + + const isExternalFilterPresent = () => true; + const doesExternalFilterPass = ( + rowData: IRowNode + ) => { + const market = rowData.data; + if (!market) return false; + return filterMarket(market, { + marketTypes, + marketStates, + assets, + searchTerm, + }); }; return (
-
- {Object.keys(marketTabs).map((key: string) => ( +
+ {/** MARKET TYPE FILTER */} +
- ))} + {Object.keys(marketTypeFilterOptions).map((key) => { + const marketType = key as IMarketType; + return ( + + ); + })} +
+ +
+
+ + {Object.keys(marketStateFilterOptions).map((key) => { + const marketState = key as IMarketState; + const isChecked = marketStates.includes(marketState); + return ( + { + if (checked) { + setMarketStates(uniq([...marketStates, marketState])); + } else { + setMarketStates( + marketStates.filter((s) => s !== marketState) + ); + } + }} + key={key} + > + {marketStateFilterOptions[marketState]} + + ); + })} + +
+
+ + {assetFilterOptions.map((asset) => { + const chainName = getChainName( + asset.source.__typename === 'ERC20' + ? Number(asset.source.chainId) + : undefined + ); + const isChecked = assets.includes(asset.id); + return ( + { + if (checked) { + setAssets(uniq([...assets, asset.id])); + } else { + setAssets(assets.filter((a) => a !== asset.id)); + } + }} + key={asset.id} + > + {asset.symbol}{' '} + ({chainName}) + + ); + })} + +
+ {/** MARKET NAME FILTER */} +
+ } + placeholder={t('Search by market')} + className="text-sm border !placeholder:text-secondary" + onChange={(ev) => { + const value = trim(ev.target.value); + setSearchTerm(value); + }} + value={searchTerm} + defaultValue={''} + /> +
+
- {activeTab === 'open-markets' && ( - - - - )} - {activeTab === 'proposed-markets' && ( - - - - )} - {activeTab === 'closed-markets' && ( - - - - )} + + ) => { + if (!data) return; + + // prevent navigating to the market page if any of the below cells are clicked + // event.preventDefault or event.stopPropagation do not seem to apply for ag-grid + const colId = column.getColId(); + + if ( + [ + 'tradableInstrument.instrument.product.settlementAsset.symbol', + 'market-actions', + ].includes(colId) + ) { + return; + } + + // @ts-ignore metaKey exists + handleOnSelect(data.id, event ? event.metaKey : false); + }} + overlayNoRowsTemplate={error ? error.message : t('No markets')} + suppressNoRowsOverlay + isExternalFilterPresent={isExternalFilterPresent} + doesExternalFilterPass={doesExternalFilterPass} + /> +
); diff --git a/apps/trading/client-pages/markets/open-markets.tsx b/apps/trading/client-pages/markets/open-markets.tsx index 33f39e173e..6e2b27beea 100644 --- a/apps/trading/client-pages/markets/open-markets.tsx +++ b/apps/trading/client-pages/markets/open-markets.tsx @@ -41,6 +41,7 @@ export const OpenMarkets = ({ handleOnSelect(data.id, event ? event.metaKey : false); }} overlayNoRowsTemplate={error ? error.message : t('No markets')} + suppressNoRowsOverlay /> ); }; diff --git a/apps/trading/client-pages/markets/use-column-defs.tsx b/apps/trading/client-pages/markets/use-column-defs.tsx index 89631ba001..9126ea1f53 100644 --- a/apps/trading/client-pages/markets/use-column-defs.tsx +++ b/apps/trading/client-pages/markets/use-column-defs.tsx @@ -117,7 +117,7 @@ export const priceValueFormatter = ( data: MarketMaybeWithData | undefined, formatDecimalPlaces?: number ): string => { - if (data?.tradableInstrument.instrument.product.__typename === 'Spot') { + if (isSpotMarket(data)) { const quoteAsset = data && getQuoteAsset(data); return data?.data?.lastTradedPrice === undefined ? '-' @@ -126,7 +126,10 @@ export const priceValueFormatter = ( data.decimalPlaces )} ${quoteAsset?.symbol}`; } - const quoteName = data && getQuoteName(data); + let quoteName: string | undefined = undefined; + if (data?.tradableInstrument?.instrument?.product) { + quoteName = data && getQuoteName(data); + } return data?.data?.bestOfferPrice === undefined ? '-' @@ -166,7 +169,7 @@ export const useMarketsColumnDefs = () => { const state = data?.data?.marketState; const tooltip = getMarketStateTooltip(state, tradingMode); const productType = - data?.tradableInstrument.instrument.product.__typename; + data?.tradableInstrument?.instrument.product.__typename; return ( @@ -179,7 +182,7 @@ export const useMarketsColumnDefs = () => { } - secondary={data?.tradableInstrument.instrument.name} + secondary={data?.tradableInstrument?.instrument.name} /> @@ -194,7 +197,7 @@ export const useMarketsColumnDefs = () => { maxWidth: 150, cellClass: 'text-sm text-right', valueGetter: ({ data }: VegaValueGetterParams) => { - if (data && isSpot(data.tradableInstrument.instrument.product)) { + if (isSpotMarket(data)) { return data?.data?.lastTradedPrice === undefined ? undefined : toBigNum( @@ -264,7 +267,12 @@ export const useMarketsColumnDefs = () => { if (!data) return '-'; const candles = data.candles; const vol = candles ? calcCandleVolume(candles) : '0'; - const quoteName = getQuoteName(data); + let quoteName: string | undefined = undefined; + try { + quoteName = getQuoteName(data); + } catch { + quoteName = ''; + } const volPrice = candles ? calcCandleVolumePrice( candles, @@ -312,7 +320,7 @@ export const useMarketsColumnDefs = () => { MarketMaybeWithData, 'data.openInterest' >) => { - if (!data || isSpot(data.tradableInstrument.instrument.product)) { + if (!data || isSpotMarket(data) || !openInterestValues(data)) { return -; } return ( @@ -326,3 +334,10 @@ export const useMarketsColumnDefs = () => { [chainId, t] ); }; + +const isSpotMarket = ( + market: Pick | undefined | null +) => { + const product = market?.tradableInstrument?.instrument?.product; + return product && isSpot(product); +}; diff --git a/apps/trading/components/accounts-container/sidebar-accounts-container.tsx b/apps/trading/components/accounts-container/sidebar-accounts-container.tsx index 1f47c4ddee..967c3d4007 100644 --- a/apps/trading/components/accounts-container/sidebar-accounts-container.tsx +++ b/apps/trading/components/accounts-container/sidebar-accounts-container.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { useT } from '../../lib/use-t'; import { SwapContainer } from '../swap'; import { WithdrawContainer } from '../withdraw-container'; @@ -10,21 +9,36 @@ import React from 'react'; import { DepositContainer } from '../deposit-container'; import { TransferContainer } from '@vegaprotocol/accounts'; import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; +import { create } from 'zustand'; -enum View { +export enum SidebarAccountsViewType { Deposit = 'Deposit', Swap = 'Swap', Transfer = 'Transfer', Withdraw = 'Withdraw', } -type InnerView = [view: View, assetId: string]; +type InnerView = [view: SidebarAccountsViewType, assetId: string]; + +type SidebarAccountsInnerViewStore = { + view: InnerView | undefined; + setView: (view: InnerView | undefined) => void; +}; +export const useSidebarAccountsInnerView = + create()((set) => ({ + view: undefined, + setView: (view) => set({ view }), + })); export const SidebarAccountsContainer = ({ pinnedAssets, }: Pick) => { const t = useT(); - const [innerView, setInnerView] = useState(undefined); + + const [innerView, setInnerView] = useSidebarAccountsInnerView((state) => [ + state.view, + state.setView, + ]); return ( <> @@ -54,16 +68,16 @@ export const SidebarAccountsContainer = ({ orderByBalance hideZeroBalance onClickDeposit={(assetId) => { - setInnerView([View.Deposit, assetId]); + setInnerView([SidebarAccountsViewType.Deposit, assetId]); }} onClickSwap={(assetId) => { - setInnerView([View.Swap, assetId]); + setInnerView([SidebarAccountsViewType.Swap, assetId]); }} onClickTransfer={(assetId) => { - setInnerView([View.Transfer, assetId]); + setInnerView([SidebarAccountsViewType.Transfer, assetId]); }} onClickWithdraw={(assetId) => { - setInnerView([View.Withdraw, assetId]); + setInnerView([SidebarAccountsViewType.Withdraw, assetId]); }} />
@@ -80,24 +94,24 @@ const InnerContainer = ({ }) => { const [view, assetId] = innerView; switch (view) { - case View.Deposit: + case SidebarAccountsViewType.Deposit: return ; - case View.Swap: + case SidebarAccountsViewType.Swap: return ( { if (!assetId) return; - setInnerView?.([View.Deposit, assetId]); + setInnerView?.([SidebarAccountsViewType.Deposit, assetId]); }} /> ); - case View.Transfer: + case SidebarAccountsViewType.Transfer: return ; - case View.Withdraw: + case SidebarAccountsViewType.Withdraw: return ; } }; diff --git a/apps/trading/components/asset-card/asset-card.tsx b/apps/trading/components/asset-card/asset-card.tsx index 7b88fb79da..3fccea7229 100644 --- a/apps/trading/components/asset-card/asset-card.tsx +++ b/apps/trading/components/asset-card/asset-card.tsx @@ -67,7 +67,8 @@ export const AssetCard = ({ asset.decimals, asset.quantum )} - {allocatedRatio && ` (${formatNumber(allocatedRatio)}%)`} + {allocatedRatio && + ` (${formatNumber(allocatedRatio.times(100))}%)`} )} diff --git a/apps/trading/components/deposit-container/deposit-container.tsx b/apps/trading/components/deposit-container/deposit-container.tsx index b3cea584e5..7e09b98f69 100644 --- a/apps/trading/components/deposit-container/deposit-container.tsx +++ b/apps/trading/components/deposit-container/deposit-container.tsx @@ -147,6 +147,7 @@ const DepositForm = ({ return (
{ const asset = assets?.find((a) => a.id === fields.assetId); diff --git a/apps/trading/components/margin-mode/margin-mode.tsx b/apps/trading/components/margin-mode/margin-mode.tsx index 6a561098a6..1dcaaa3658 100644 --- a/apps/trading/components/margin-mode/margin-mode.tsx +++ b/apps/trading/components/margin-mode/margin-mode.tsx @@ -76,21 +76,22 @@ const Toggle = ({ onValueChange: (value: string) => void; }) => { const t = useT(); - const itemClass = 'relative py-px px-2 text-xs data-[state=on]:px-1'; + const itemClass = + 'relative py-0.5 px-2 text-xs data-[state=on]:text-vega-clight-900 data-[state=on]:dark:text-vega-cdark-900'; const indicator = ( - + ); return ( onValueChange(MarginMode.MARGIN_MODE_CROSS_MARGIN)} data-testid="cross-margin" > {mode === MarginMode.MARGIN_MODE_CROSS_MARGIN && indicator} @@ -99,12 +100,13 @@ const Toggle = ({ onValueChange(MarginMode.MARGIN_MODE_ISOLATED_MARGIN)} data-testid="isolated-margin" > {mode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN && indicator} {mode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN ? ( - {t('Isolated')} + {t('Isolated')}: ) : ( @@ -122,9 +124,5 @@ const Leverage = ({ factor }: { factor: string | undefined }) => { leverage = (1 / Number(factor)).toFixed(1); } - return ( - - {leverage}x - - ); + return {leverage}x; }; diff --git a/apps/trading/components/market-selector/asset-dropdown.spec.tsx b/apps/trading/components/market-selector/asset-dropdown.spec.tsx index 28872e64d9..5a90093c41 100644 --- a/apps/trading/components/market-selector/asset-dropdown.spec.tsx +++ b/apps/trading/components/market-selector/asset-dropdown.spec.tsx @@ -6,6 +6,7 @@ const createAssets = (count = 3) => { return new Array(count).fill(null).map((_, i) => ({ id: i.toString(), symbol: 'asset-1', + chainId: 1, })); }; @@ -25,7 +26,7 @@ describe('AssetDropdown', () => { const items = screen.getAllByRole('menuitemcheckbox'); expect(items).toHaveLength(assets.length); expect(items.map((i) => i.textContent)).toEqual( - assets.map((a) => a.symbol) + assets.map((a) => `${a.symbol} (Ethereum)`) ); await userEvent.click(items[0]); expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, true); diff --git a/apps/trading/components/market-selector/asset-dropdown.tsx b/apps/trading/components/market-selector/asset-dropdown.tsx index 6a13756988..aa07fb25df 100644 --- a/apps/trading/components/market-selector/asset-dropdown.tsx +++ b/apps/trading/components/market-selector/asset-dropdown.tsx @@ -1,14 +1,8 @@ -import { - TradingDropdown, - TradingDropdownCheckboxItem, - TradingDropdownContent, - TradingDropdownItemIndicator, - TradingDropdownTrigger, -} from '@vegaprotocol/ui-toolkit'; -import { MarketSelectorButton } from './market-selector-button'; +import { MultiSelect, MultiSelectOption } from '@vegaprotocol/ui-toolkit'; import { useT } from '../../lib/use-t'; +import { getChainName } from '@vegaprotocol/web3'; -type Assets = Array<{ id: string; symbol: string }>; +type Assets = Array<{ id: string; symbol: string; chainId?: number }>; export const AssetDropdown = ({ assets, @@ -25,37 +19,25 @@ export const AssetDropdown = ({ } return ( - assets && ( - - - {triggerText({ assets, checkedAssets }, t)} - - - } - > - - {assets.filter(Boolean).map((a) => { - return ( - { - if (typeof checked === 'boolean') { - onSelect(a.id, checked); - } - }} - data-testid={`asset-id-${a.id}`} - > - {a.symbol} - - - ); - })} - - - ) + + {assets.map((a) => { + return ( + { + if (typeof checked === 'boolean') { + onSelect(a.id, checked); + } + }} + data-testid={`asset-id-${a.id}`} + > + {a.symbol}{' '} + ({getChainName(a.chainId)}) + + ); + })} + ); }; @@ -74,10 +56,10 @@ const triggerText = ( if (checkedAssets.length === 1) { const assetId = checkedAssets[0]; const asset = assets.find((a) => a.id === assetId); - text = asset ? asset.symbol : t('Asset (1)'); + text = asset ? asset.symbol : t('Assets (1)'); } else if (checkedAssets.length > 1) { - text = t('{{checkedAssets}} Assets', { - checkedAssets: checkedAssets.length, + text = t('Assets ({{count}})', { + count: checkedAssets.length, }); } diff --git a/apps/trading/components/market-selector/filter-summary.tsx b/apps/trading/components/market-selector/filter-summary.tsx new file mode 100644 index 0000000000..b6a6140ccd --- /dev/null +++ b/apps/trading/components/market-selector/filter-summary.tsx @@ -0,0 +1,31 @@ +import { Trans } from 'react-i18next'; + +export const FilterSummary = ({ + diff, + resetFilters, +}: { + diff: number; + resetFilters: () => void; +}) => ( +
+ 0 + ? '{{count}} results excluded due to the applied filters. <0>Remove filters.' + : '{{count}} results included due to the applied filters. <0>Remove filters.' + } + values={{ count: Math.abs(diff) }} + components={[ + , + ]} + /> +
+); diff --git a/apps/trading/components/market-selector/market-selector-item.spec.tsx b/apps/trading/components/market-selector/market-selector-item.spec.tsx index 613ee14f00..7b7838bb61 100644 --- a/apps/trading/components/market-selector/market-selector-item.spec.tsx +++ b/apps/trading/components/market-selector/market-selector-item.spec.tsx @@ -106,7 +106,6 @@ describe('MarketSelectorItem', () => { currentMarketId={market.id} style={{}} onSelect={jest.fn()} - allProducts /> diff --git a/apps/trading/components/market-selector/market-selector-item.tsx b/apps/trading/components/market-selector/market-selector-item.tsx index 5419898d6f..4b35160e45 100644 --- a/apps/trading/components/market-selector/market-selector-item.tsx +++ b/apps/trading/components/market-selector/market-selector-item.tsx @@ -18,13 +18,11 @@ export const MarketSelectorItem = ({ style, currentMarketId, onSelect, - allProducts, }: { market: MarketMaybeWithDataAndCandles; style: CSSProperties; currentMarketId?: string; onSelect: (marketId: string) => void; - allProducts: boolean; }) => { return (
@@ -41,19 +39,13 @@ export const MarketSelectorItem = ({ )} onClick={() => onSelect(market.id)} > - +
); }; -const MarketData = ({ - market, - allProducts, -}: { - market: MarketMaybeWithDataAndCandles; - allProducts: boolean; -}) => { +const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => { const t = useT(); const { data } = useMarketDataUpdateSubscription({ variables: { diff --git a/apps/trading/components/market-selector/market-selector.spec.tsx b/apps/trading/components/market-selector/market-selector.spec.tsx index 4691bcfbe6..e247a0a116 100644 --- a/apps/trading/components/market-selector/market-selector.spec.tsx +++ b/apps/trading/components/market-selector/market-selector.spec.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MarketSelector } from './market-selector'; -import { useMarketList } from '@vegaprotocol/markets'; import { createMarketFragment, createMarketsDataFragment, @@ -9,14 +8,19 @@ import { import { MarketState } from '@vegaprotocol/types'; import { MemoryRouter } from 'react-router-dom'; import type { ReactNode } from 'react'; -import type { SortType } from './sort-dropdown'; import { SortTypeMapping } from './sort-dropdown'; -import { Sort } from './sort-dropdown'; import { subDays } from 'date-fns'; import { isMarketActive } from '../../lib/utils'; +import { + type ISortOption, + SortOption, + useMarketFiltersStore, + DEFAULT_FILTERS, +} from '../../lib/hooks/use-market-filters'; +import { useDataProvider } from '@vegaprotocol/data-provider'; -jest.mock('@vegaprotocol/markets'); -const mockUseMarketList = useMarketList as jest.Mock; +jest.mock('@vegaprotocol/data-provider'); +const mockUseMarketList = useDataProvider as jest.Mock; // mock market list items to avoid subscriptions starting jest.mock('./market-selector-item', () => ({ @@ -36,6 +40,9 @@ jest.mock('react-virtualized-auto-sizer', () => { }); describe('MarketSelector', () => { + beforeEach(() => { + useMarketFiltersStore.setState(DEFAULT_FILTERS); + }); const markets = [ createMarketFragment({ id: 'market-0', @@ -164,14 +171,14 @@ describe('MarketSelector', () => { }); it('Button "All" should be selected by default', () => { - const buttons = ['All', 'Futures', 'Spot', 'Perpetuals']; + const buttons = ['All', 'Perpetuals', 'Futures', 'Spot']; render( ); screen - .getAllByTestId(/^product-(All|Future|Spot|Perpetual)$/) + .getAllByTestId(/^product-(ALL|FUTURE|SPOT|PERPETUAL)$/) .forEach((elem, i) => { expect(elem.textContent).toEqual(buttons[i]); }); @@ -195,20 +202,20 @@ describe('MarketSelector', () => { ); - await userEvent.click(screen.getByTestId('product-Spot')); + await userEvent.click(screen.getByTestId('product-SPOT')); expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0); expect(screen.getByTestId('no-items')).toHaveTextContent( 'No spot markets.' ); - await userEvent.click(screen.getByTestId('product-Perpetual')); + await userEvent.click(screen.getByTestId('product-PERPETUAL')); expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(1); - await userEvent.click(screen.getByTestId('product-Future')); + await userEvent.click(screen.getByTestId('product-FUTURE')); expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(3); expect(screen.queryByTestId('no-items')).not.toBeInTheDocument(); - await userEvent.click(screen.getByTestId('product-All')); + await userEvent.click(screen.getByTestId('product-ALL')); expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(4); expect(screen.queryByTestId('no-items')).not.toBeInTheDocument(); }); @@ -251,9 +258,11 @@ describe('MarketSelector', () => { await userEvent.click(screen.getByTestId('sort-trigger')); const options = screen.getAllByTestId(/sort-item/); expect(options.map((o) => o.textContent?.trim())).toEqual( - Object.entries(Sort).map(([key]) => SortTypeMapping[key as SortType]) + Object.entries(SortOption).map( + ([key]) => SortTypeMapping[key as ISortOption] + ) ); - await userEvent.click(screen.getByTestId('sort-item-Gained')); + await userEvent.click(screen.getByTestId('sort-item-GAINED')); expect( screen .getAllByTestId(/market-\d/) @@ -269,7 +278,7 @@ describe('MarketSelector', () => { ); await userEvent.click(screen.getByTestId('sort-trigger')); - await userEvent.click(screen.getByTestId('sort-item-Lost')); + await userEvent.click(screen.getByTestId('sort-item-LOST')); expect( screen .getAllByTestId(/market-\d/) @@ -285,7 +294,7 @@ describe('MarketSelector', () => { ); await userEvent.click(screen.getByTestId('sort-trigger')); - await userEvent.click(screen.getByTestId('sort-item-New')); + await userEvent.click(screen.getByTestId('sort-item-NEW')); expect( screen .getAllByTestId(/market-\d/) diff --git a/apps/trading/components/market-selector/market-selector.tsx b/apps/trading/components/market-selector/market-selector.tsx index 1119311170..7763799255 100644 --- a/apps/trading/components/market-selector/market-selector.tsx +++ b/apps/trading/components/market-selector/market-selector.tsx @@ -1,6 +1,8 @@ +import compact from 'lodash/compact'; import uniqBy from 'lodash/uniqBy'; import { - getAsset, + marketsWithCandlesProvider, + retrieveAssets, type MarketMaybeWithDataAndCandles, } from '@vegaprotocol/markets'; import { @@ -9,25 +11,28 @@ import { VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; -import type { CSSProperties } from 'react'; -import { useCallback, useState, useMemo, useRef, useEffect } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import { useCallback, useMemo, useRef, useEffect } from 'react'; import { FixedSizeList } from 'react-window'; -import { useMarketSelectorList } from './use-market-selector-list'; -import type { ProductType } from './product-selector'; -import { Product, ProductSelector } from './product-selector'; +import { ProductSelector } from './product-selector'; import { AssetDropdown } from './asset-dropdown'; -import type { SortType } from './sort-dropdown'; -import { Sort, SortDropdown } from './sort-dropdown'; +import { SortDropdown } from './sort-dropdown'; import { MarketSelectorItem } from './market-selector-item'; import classNames from 'classnames'; import { useT } from '../../lib/use-t'; - -export type Filter = { - searchTerm: string; - product: ProductType; - sort: SortType; - assets: string[]; -}; +import flatten from 'lodash/flatten'; +import { + MarketType, + useMarketFiltersStore, + filterMarkets, + orderMarkets, + DEFAULT_FILTERS, +} from '../../lib/hooks/use-market-filters'; +import { useDataProvider } from '@vegaprotocol/data-provider'; +import { useYesterday } from '@vegaprotocol/react-helpers'; +import { Interval } from '@vegaprotocol/types'; +import uniq from 'lodash/uniq'; +import { FilterSummary } from './filter-summary'; /** * Fetches market data and filters it given a set of filter properties @@ -41,36 +46,94 @@ export const MarketSelector = ({ onSelect: (marketId: string) => void; }) => { const t = useT(); - const [filter, setFilter] = useState({ - searchTerm: '', - product: Product.All, - sort: Sort.TopTraded, - assets: [], + + const { + marketTypes, + marketStates, + assets, + searchTerm, + sortOrder, + setMarketTypes, + setAssets, + setSearchTerm, + setSortOrder, + reset, + } = useMarketFiltersStore((state) => ({ + marketTypes: state.marketTypes, + setMarketTypes: state.setMarketTypes, + marketStates: state.marketStates, + assets: state.assets, + setAssets: state.setAssets, + searchTerm: state.searchTerm, + sortOrder: state.sortOrder, + setSearchTerm: state.setSearchTerm, + setSortOrder: state.setSortOrder, + reset: state.reset, + })); + + const yesterday = useYesterday(); + const { data, loading, error, reload } = useDataProvider({ + dataProvider: marketsWithCandlesProvider, + variables: { + since: new Date(yesterday).toISOString(), + interval: Interval.INTERVAL_I1H, + }, }); - const allProducts = filter.product === Product.All; - const { markets, data, loading, error, reload } = - useMarketSelectorList(filter); + const markets = orderMarkets( + filterMarkets(data || [], { + marketTypes, + marketStates, + assets, + searchTerm, + }), + sortOrder + ); + const defaultMarkets = filterMarkets(data || [], DEFAULT_FILTERS); + let filterSummary: ReactNode = undefined; + if (markets.length != defaultMarkets.length) { + const diff = defaultMarkets.length - markets.length; + filterSummary = ; + } useEffect(() => { reload(); }, [reload]); + const marketAssets = uniqBy( + compact( + flatten( + data?.map((d) => { + const product = d.tradableInstrument?.instrument?.product; + if (product) return retrieveAssets(product); + }) + ) + ), + (a) => a.id + ).map((a) => ({ + id: a.id, + symbol: a.symbol, + chainId: + a.source.__typename === 'ERC20' ? Number(a.source.chainId) : undefined, + })); + return (
{ - setFilter((curr) => ({ ...curr, product })); + marketTypes={marketTypes} + onSelect={(marketType) => { + if (marketType) setMarketTypes([marketType]); + else setMarketTypes([]); }} />
- setFilter((curr) => ({ ...curr, searchTerm: e.target.value })) - } - value={filter.searchTerm} + onChange={(e) => { + const searchTerm = e.target.value; + setSearchTerm(searchTerm); + }} + value={searchTerm} type="text" placeholder={t('Search')} data-testid="search-term" @@ -79,40 +142,20 @@ export const MarketSelector = ({ />
getAsset(d)), - 'id' - )} - checkedAssets={filter.assets} + assets={marketAssets} + checkedAssets={assets} onSelect={(id: string, checked) => { - setFilter((curr) => { - if (checked) { - if (curr.assets.includes(id)) { - return curr; - } else { - return { ...curr, assets: [...curr.assets, id] }; - } - } else { - if (curr.assets.includes(id)) { - return { - ...curr, - assets: curr.assets.filter((x) => x !== id), - }; - } - } - return curr; - }); + if (checked) { + setAssets(uniq([...assets, id])); + } else { + setAssets(assets.filter((a) => a !== id)); + } }} /> { - setFilter((curr) => { - return { - ...curr, - sort, - }; - }); + currentSort={sortOrder} + onSelect={(sortOrder) => { + setSortOrder(sortOrder); }} />
@@ -122,19 +165,18 @@ export const MarketSelector = ({ data={markets} loading={loading && !data} error={error} - searchTerm={filter.searchTerm} currentMarketId={currentMarketId} onSelect={onSelect} noItems={ - filter.product === Product.Perpetual + marketTypes.includes(MarketType.PERPETUAL) ? t('No perpetual markets.') - : filter.product === Product.Spot + : marketTypes.includes(MarketType.SPOT) ? t('No spot markets.') - : filter.product === Product.Future + : marketTypes.includes(MarketType.FUTURE) ? t('No future markets.') : t('No markets.') } - allProducts={allProducts} + filterSummary={filterSummary} />
@@ -148,16 +190,15 @@ const MarketList = ({ currentMarketId, onSelect, noItems, - allProducts, + filterSummary, }: { data: MarketMaybeWithDataAndCandles[]; error: Error | undefined; loading: boolean; - searchTerm: string; currentMarketId?: string; onSelect: (marketId: string) => void; noItems: string; - allProducts: boolean; + filterSummary: ReactNode; }) => { const t = useT(); const itemSize = 45; @@ -199,6 +240,9 @@ const MarketList = ({
+ {filterSummary ? ( +
{filterSummary}
+ ) : null}
@@ -219,7 +262,6 @@ interface ListItemData { data: MarketMaybeWithDataAndCandles[]; onSelect: (marketId: string) => void; currentMarketId?: string; - allProducts: boolean; } const ListItem = ({ @@ -236,7 +278,6 @@ const ListItem = ({ currentMarketId={data.currentMarketId} style={style} onSelect={data.onSelect} - allProducts={data.allProducts} /> ); @@ -248,21 +289,19 @@ const List = ({ onSelect, noItems, currentMarketId, - allProducts, }: ListItemData & { loading: boolean; height: number; itemSize: number; noItems: string; - allProducts: boolean; }) => { const itemKey = useCallback( (index: number, data: ListItemData) => data.data[index].id, [] ); const itemData = useMemo( - () => ({ data, onSelect, currentMarketId, allProducts }), - [data, onSelect, currentMarketId, allProducts] + () => ({ data, onSelect, currentMarketId }), + [data, onSelect, currentMarketId] ); if (!data || loading) { return ( diff --git a/apps/trading/components/market-selector/product-selector.tsx b/apps/trading/components/market-selector/product-selector.tsx index 67ee4adb67..cd4a3aa5e6 100644 --- a/apps/trading/components/market-selector/product-selector.tsx +++ b/apps/trading/components/market-selector/product-selector.tsx @@ -3,54 +3,65 @@ import { Link } from 'react-router-dom'; import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import { Links } from '../../lib/links'; import { useT } from '../../lib/use-t'; - -// Make sure these match the available __typename properties on product -export const Product = { - All: 'All', - Future: 'Future', - Spot: 'Spot', - Perpetual: 'Perpetual', -} as const; - -export type ProductType = keyof typeof Product; +import { + type IMarketType, + MarketType, +} from '../../lib/hooks/use-market-filters'; export const ProductSelector = ({ - product, + marketTypes, onSelect, }: { - product: ProductType; - onSelect: (product: ProductType) => void; + marketTypes: IMarketType[]; + onSelect: (marketType?: IMarketType) => void; }) => { const t = useT(); + const ProductTypeMapping: { - [key in ProductType]: string; + [key in IMarketType]: string; } = { - [Product.All]: t('All'), - [Product.Future]: t('Futures'), - [Product.Spot]: t('Spot'), - [Product.Perpetual]: t('Perpetuals'), + [MarketType.PERPETUAL]: t('Perpetuals'), + [MarketType.FUTURE]: t('Futures'), + [MarketType.SPOT]: t('Spot'), }; + + const buttons = [MarketType.PERPETUAL, MarketType.FUTURE, MarketType.SPOT]; + + const getStyles = (selected: boolean) => + classNames( + 'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50', + { + 'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default': selected, + 'text-secondary': !selected, + } + ); + return (
- {Object.keys(Product).map((t) => { - const classes = classNames( - 'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50', - { - 'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default': - t === product, - 'text-secondary': t !== product, - } - ); + + {buttons.map((t) => { return ( ); })} diff --git a/apps/trading/components/market-selector/sort-dropdown.tsx b/apps/trading/components/market-selector/sort-dropdown.tsx index c2ba4097fb..942d496b02 100644 --- a/apps/trading/components/market-selector/sort-dropdown.tsx +++ b/apps/trading/components/market-selector/sort-dropdown.tsx @@ -9,41 +9,43 @@ import { VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import { MarketSelectorButton } from './market-selector-button'; - -export const Sort = { - Gained: 'Gained', - Lost: 'Lost', - New: 'New', - TopTraded: 'TopTraded', -} as const; - -export type SortType = keyof typeof Sort; +import { + type ISortOption, + SortOption, +} from '../../lib/hooks/use-market-filters'; export const SortTypeMapping: { - [key in SortType]: string; + [key in ISortOption]: string; } = { - [Sort.TopTraded]: 'Top traded', - [Sort.Gained]: 'Top gaining', - [Sort.Lost]: 'Top losing', - [Sort.New]: 'New markets', + [SortOption.TOP_TRADED]: 'Top traded', + [SortOption.GAINED]: 'Top gaining', + [SortOption.LOST]: 'Top losing', + [SortOption.NEW]: 'New markets', }; const SortIconMapping: { - [key in SortType]: VegaIconNames; + [key in ISortOption]: VegaIconNames; } = { - [Sort.Gained]: VegaIconNames.TREND_UP, - [Sort.Lost]: VegaIconNames.TREND_DOWN, - [Sort.New]: VegaIconNames.STAR, - [Sort.TopTraded]: VegaIconNames.ARROW_UP, + [SortOption.TOP_TRADED]: VegaIconNames.TREND_UP, + [SortOption.GAINED]: VegaIconNames.TREND_DOWN, + [SortOption.LOST]: VegaIconNames.STAR, + [SortOption.NEW]: VegaIconNames.ARROW_UP, }; export const SortDropdown = ({ currentSort, onSelect, }: { - currentSort: SortType; - onSelect: (sort: SortType) => void; + currentSort: ISortOption; + onSelect: (sortOrder: ISortOption) => void; }) => { + const options = [ + SortOption.GAINED, + SortOption.LOST, + SortOption.NEW, + SortOption.TOP_TRADED, + ]; + return ( onSelect(value as SortType)} + onValueChange={(value) => onSelect(value as ISortOption)} > - {Object.keys(Sort).map((key) => { + {options.map((option) => { return ( - {' '} - {SortTypeMapping[key as SortType]} + {' '} + {SortTypeMapping[option]} diff --git a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx b/apps/trading/components/market-selector/use-market-selector-list.spec.tsx deleted file mode 100644 index 11ccae40c8..0000000000 --- a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx +++ /dev/null @@ -1,605 +0,0 @@ -import merge from 'lodash/merge'; -import { renderHook } from '@testing-library/react'; -import { useMarketSelectorList } from './use-market-selector-list'; -import { isMarketActive } from '../../lib/utils'; -import { Product } from './product-selector'; -import { Sort } from './sort-dropdown'; -import { - createMarketFragment, - createMarketsDataFragment, -} from '@vegaprotocol/mock'; -import { MarketState } from '@vegaprotocol/types'; -import { useMarketList } from '@vegaprotocol/markets'; -import type { Filter } from './market-selector'; -import { subDays } from 'date-fns'; - -jest.mock('@vegaprotocol/markets', () => ({ - ...jest.requireActual('@vegaprotocol/markets'), - useMarketList: jest.fn(), -})); -const mockUseMarketList = useMarketList as jest.Mock; - -describe('useMarketSelectorList', () => { - const setup = (initialArgs?: Partial) => { - const defaultArgs: Filter = { - searchTerm: '', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }; - return renderHook((args) => useMarketSelectorList(args), { - initialProps: merge(defaultArgs, initialArgs), - }); - }; - - it('returns all markets active and suspended markets', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - }), - createMarketFragment({ - id: 'market-1', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_SUSPENDED, - }), - }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_CLOSED, - }), - }), - createMarketFragment({ - id: 'market-3', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_CLOSED, - }), - }), - createMarketFragment({ - id: 'market-4', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_PENDING, - }), - }), - ]; - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - const { result } = setup(); - const expectedFilteredMarkets = markets.filter((m) => - // @ts-ignore candles get joined outside this type - isMarketActive(m.data.marketState) - ); - expect(result.current).toEqual({ - data: markets, - markets: expectedFilteredMarkets, - loading: false, - error: undefined, - }); - }); - - it('filters by product', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - product: { - __typename: 'Future', - }, - }, - }, - }), - // createMarketFragment({ - // id: 'market-1', - // tradableInstrument: { - // instrument: { - // product: { - // __typename: 'Spot', - // }, - // }, - // }, - // }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - product: { - __typename: 'Perpetual', - }, - }, - }, - }), - ]; - - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - const { result, rerender } = setup(); - expect(result.current.markets).toEqual([markets[0]]); - // rerender({ - // searchTerm: '', - // product: Product.Spot as 'Future', - // sort: Sort.TopTraded, - // assets: [], - // }); - // expect(result.current.markets).toEqual([markets[1]]); - rerender({ - searchTerm: '', - product: Product.Perpetual as 'Future', - sort: Sort.TopTraded, - assets: [], - }); - // expect(result.current.markets).toEqual([markets[2]]); - rerender({ - searchTerm: '', - product: Product.All, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual(markets); - }); - - // eslint-disable-next-line jest/no-disabled-tests - it.skip('filters by asset', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - product: { - __typename: 'Future', - settlementAsset: { - id: 'asset-0', - }, - }, - }, - }, - }), - createMarketFragment({ - id: 'market-1', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - product: { - __typename: 'Future', - settlementAsset: { - id: 'asset-0', - }, - }, - }, - }, - }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - product: { - __typename: 'Future', - settlementAsset: { - id: 'asset-1', - }, - }, - }, - }, - }), - createMarketFragment({ - id: 'market-3', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - product: { - __typename: 'Future', - settlementAsset: { - id: 'asset-2', - }, - }, - }, - }, - }), - ]; - - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - const { result, rerender } = setup({ - searchTerm: '', - product: Product.Future, - sort: Sort.TopTraded, - assets: ['asset-0'], - }); - - expect(result.current.markets).toEqual([markets[0], markets[1]]); - - rerender({ - searchTerm: '', - product: Product.Future, - sort: Sort.TopTraded, - assets: ['asset-0', 'asset-1'], - }); - - expect(result.current.markets).toEqual([ - markets[0], - markets[1], - markets[2], - ]); - - rerender({ - searchTerm: '', - product: Product.Future, - sort: Sort.TopTraded, - assets: ['asset-0', 'asset-1', 'asset-2'], - }); - - // all assets selected - expect(result.current.markets).toEqual(markets); - - rerender({ - searchTerm: '', - product: Product.Future, - sort: Sort.TopTraded, - assets: ['asset-invalid'], - }); - - expect(result.current.markets).toEqual([]); - }); - - it('filters by search term', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - code: 'abc', - name: 'aaa', - }, - }, - }), - createMarketFragment({ - id: 'market-1', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - code: 'def', - name: 'ggg', - }, - }, - }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - code: 'defg', - name: 'gggh', - }, - }, - }), - createMarketFragment({ - id: 'market-3', - // @ts-ignore candles get joined outside this type - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - tradableInstrument: { - instrument: { - code: 'ggg', - name: 'foo', - }, - }, - }), - ]; - - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - const { result, rerender } = setup({ - searchTerm: 'abc', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual([markets[0]]); - rerender({ - searchTerm: 'def', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual([markets[1], markets[2]]); - rerender({ - searchTerm: 'defg', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual([markets[2]]); - rerender({ - searchTerm: 'zzz', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual([]); - - // by name - rerender({ - searchTerm: 'aaa', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual([markets[0]]); - rerender({ - searchTerm: 'ggg', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - expect(result.current.markets).toEqual([ - markets[1], - markets[2], - markets[3], - ]); - }); - - it('sorts by top traded by default', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - markPrice: '1', - }), - // @ts-ignore candles not on fragment - candles: [ - { - volume: '200', - }, - ], - }), - createMarketFragment({ - id: 'market-1', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - markPrice: '1', - }), - // @ts-ignore candles not on fragment - candles: [ - { - volume: '100', - }, - ], - }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - markPrice: '1', - }), - // @ts-ignore candles not on fragment - candles: [ - { - volume: '300', - }, - ], - }), - createMarketFragment({ - id: 'market-3', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - markPrice: '1', - }), - // @ts-ignore candles not on fragment - candles: [ - { - volume: '400', - }, - ], - }), - ]; - - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - - const { result } = setup({ - searchTerm: '', - product: Product.Future, - sort: Sort.TopTraded, - assets: [], - }); - - expect(result.current.markets).toEqual([ - markets[3], - markets[2], - markets[0], - markets[1], - ]); - }); - - it('sorts by gained', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - // @ts-ignore actual fragment doesn't contain candles and is joined later - candles: [ - { - close: '100', - }, - { - close: '200', - }, - ], - }), - createMarketFragment({ - id: 'market-1', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - // @ts-ignore actual fragment doesn't contain candles and is joined later - candles: [ - { - close: '100', - }, - { - close: '1000', - }, - ], - }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - // @ts-ignore actual fragment doesn't contain candles and is joined later - candles: [ - { - close: '100', - }, - { - close: '400', - }, - ], - }), - ]; - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - const { result, rerender } = setup({ - searchTerm: '', - product: Product.Future, - sort: Sort.Gained, - assets: [], - }); - expect(result.current.markets).toEqual([ - markets[1], - markets[2], - markets[0], - ]); - rerender({ - searchTerm: '', - product: Product.Future, - sort: Sort.Lost as 'Gained', - assets: [], - }); - expect(result.current.markets).toEqual([ - markets[0], - markets[2], - markets[1], - ]); - }); - - it('sorts by open timestamp', () => { - const markets = [ - createMarketFragment({ - id: 'market-0', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - marketTimestamps: { - open: subDays(new Date(), 3).toISOString(), - }, - }), - createMarketFragment({ - id: 'market-1', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - marketTimestamps: { - open: subDays(new Date(), 1).toISOString(), - }, - }), - createMarketFragment({ - id: 'market-2', - // @ts-ignore data not on fragment - data: createMarketsDataFragment({ - marketState: MarketState.STATE_ACTIVE, - }), - marketTimestamps: { - open: subDays(new Date(), 2).toISOString(), - }, - }), - ]; - mockUseMarketList.mockReturnValue({ - data: markets, - loading: false, - error: undefined, - }); - const { result } = setup({ - searchTerm: '', - product: Product.Future, - sort: Sort.New, - assets: [], - }); - expect(result.current.markets).toEqual([ - markets[1], - markets[2], - markets[0], - ]); - }); -}); diff --git a/apps/trading/components/market-selector/use-market-selector-list.ts b/apps/trading/components/market-selector/use-market-selector-list.ts deleted file mode 100644 index 82f28db48c..0000000000 --- a/apps/trading/components/market-selector/use-market-selector-list.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { useMemo } from 'react'; -import orderBy from 'lodash/orderBy'; -import { - calcTradedFactor, - getAsset, - useMarketList, -} from '@vegaprotocol/markets'; -import { priceChangePercentage } from '@vegaprotocol/utils'; -import type { Filter } from '../../components/market-selector/market-selector'; -import { Sort } from './sort-dropdown'; -import { Product } from './product-selector'; -import { isMarketActive } from '../../lib/utils'; - -export const useMarketSelectorList = ({ - product, - assets, - sort, - searchTerm, -}: Filter) => { - const { data, loading, error, reload } = useMarketList(); - - const markets = useMemo(() => { - if (!data?.length) return []; - const markets = data - // show only active markets, using m.data.marketState as this will be - // data that will get refreshed when calling reload - .filter((m) => { - if (!m.data) return false; - return isMarketActive(m.data.marketState); - }) - // only selected product type - .filter((m) => { - if ( - product === Product.All || - m.tradableInstrument.instrument.product.__typename === product - ) { - return true; - } - return false; - }) - .filter((m) => { - if (assets.length === 0) return true; - const asset = getAsset(m); - return assets.includes(asset.id); - }) - // filter based on search term - .filter((m) => { - const code = m.tradableInstrument.instrument.code.toLowerCase(); - const name = m.tradableInstrument.instrument.name.toLowerCase(); - if ( - code.includes(searchTerm.toLowerCase()) || - name.includes(searchTerm.toLowerCase()) - ) { - return true; - } - return false; - }); - - if (sort === Sort.Gained || sort === Sort.Lost) { - const dir = sort === Sort.Gained ? 'desc' : 'asc'; - return orderBy( - markets, - [ - (m) => { - if (!m.candles?.length) return 0; - return Number( - priceChangePercentage( - m.candles.filter((c) => c.close !== '').map((c) => c.close) - ) - ); - }, - ], - [dir] - ); - } - - if (sort === Sort.New) { - return orderBy( - markets, - [(m) => new Date(m.marketTimestamps.open).getTime()], - ['desc'] - ); - } - - if (sort === Sort.TopTraded) { - return orderBy(markets, [(m) => calcTradedFactor(m)], ['desc']); - } - - return markets; - }, [data, product, searchTerm, assets, sort]); - - return { markets, data, loading, error, reload }; -}; diff --git a/apps/trading/components/sidebar/sidebar.spec.tsx b/apps/trading/components/sidebar/sidebar.spec.tsx index 02bb2da610..45c9852b3b 100644 --- a/apps/trading/components/sidebar/sidebar.spec.tsx +++ b/apps/trading/components/sidebar/sidebar.spec.tsx @@ -27,6 +27,8 @@ jest.mock('../asset-card', () => ({ jest.mock('../accounts-container/sidebar-accounts-container.tsx', () => ({ SidebarAccountsContainer: () =>
, + SidebarAccountsViewType: '', + useSidebarAccountsInnerView: () => () => ({}), })); jest.mock('../margin-mode', () => ({ diff --git a/apps/trading/components/sidebar/sidebar.tsx b/apps/trading/components/sidebar/sidebar.tsx index af4e108312..9b4fc52616 100644 --- a/apps/trading/components/sidebar/sidebar.tsx +++ b/apps/trading/components/sidebar/sidebar.tsx @@ -9,13 +9,16 @@ import { import * as AccordionPrimitive from '@radix-ui/react-accordion'; import { DealTicketContainer } from '@vegaprotocol/deal-ticket'; import { MarketInfoAccordionContainer } from '@vegaprotocol/markets'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { ErrorBoundary } from '../error-boundary'; import { NodeHealthContainer } from '../node-health'; import { AssetCard } from '../asset-card'; -import { Links } from '../../lib/links'; import { useT } from '../../lib/use-t'; -import { SidebarAccountsContainer } from '../accounts-container/sidebar-accounts-container'; +import { + SidebarAccountsContainer, + SidebarAccountsViewType, + useSidebarAccountsInnerView, +} from '../accounts-container/sidebar-accounts-container'; import classNames from 'classnames'; import { MarginModeToggle } from '../margin-mode'; @@ -28,8 +31,8 @@ export enum ViewType { export const Sidebar = ({ pinnedAssets }: { pinnedAssets?: string[] }) => { const t = useT(); const params = useParams(); - const navigate = useNavigate(); const { view, setView } = useSidebar(); + const setInnerView = useSidebarAccountsInnerView((state) => state.setView); return (
@@ -58,7 +61,10 @@ export const Sidebar = ({ pinnedAssets }: { pinnedAssets?: string[] }) => { {params.marketId && ( navigate(Links.DEPOSIT())} + onDeposit={(assetId) => { + setView(ViewType.Assets); + setInnerView([SidebarAccountsViewType.Deposit, assetId]); + }} /> )} diff --git a/apps/trading/components/welcome-dialog/proposed-markets.tsx b/apps/trading/components/welcome-dialog/proposed-markets.tsx index 389d85b855..4993076999 100644 --- a/apps/trading/components/welcome-dialog/proposed-markets.tsx +++ b/apps/trading/components/welcome-dialog/proposed-markets.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import compact from 'lodash/compact'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { proposalsDataProvider } from '@vegaprotocol/proposals'; import take from 'lodash/take'; @@ -35,12 +36,29 @@ export const ProposedMarkets = () => { ].includes(proposal.state) ), 3 - ).map((proposal) => ({ - id: proposal.id, - displayName: - proposal.terms.change.__typename === 'NewMarket' && - proposal.terms.change.instrument.code, - })); + ).map((proposal) => { + if (proposal.__typename === 'Proposal') { + return { + id: proposal.id, + displayName: + proposal.terms.change.__typename === 'NewMarket' && + proposal.terms.change.instrument.code, + }; + } + + if (proposal.__typename === 'BatchProposal') { + const subProposal = proposal.subProposals?.find( + (p) => p?.terms?.change.__typename === 'NewMarket' + ); + + if (subProposal?.terms?.change.__typename === 'NewMarket') { + return { + id: proposal.id, + displayName: subProposal.terms?.change.instrument.code, + }; + } + } + }); const tokenLink = useLinks(DApp.Governance); return useMemo( @@ -52,7 +70,7 @@ export const ProposedMarkets = () => { {t('Proposed markets')}
- {newMarkets.map(({ displayName, id }, i) => ( + {compact(newMarkets).map(({ displayName, id }, i) => (
{displayName}
diff --git a/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py b/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py index b83b671c1b..46af28eeb0 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py +++ b/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py @@ -38,7 +38,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag "1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI" ) page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click() - expect(page.get_by_test_id("pathname-/portfolio/assets/deposit") + expect(page.get_by_test_id("deposit-form") ).to_be_visible() diff --git a/apps/trading/e2e/tests/successor_market/test_succession_line.py b/apps/trading/e2e/tests/successor_market/test_succession_line.py index 15d96d61bd..e29dd8ecd2 100644 --- a/apps/trading/e2e/tests/successor_market/test_succession_line.py +++ b/apps/trading/e2e/tests/successor_market/test_succession_line.py @@ -9,6 +9,8 @@ market_banner = "market-banner" +# TODO: fix this flakey tests +@pytest.mark.skip("flakey") @pytest.mark.usefixtures("risk_accepted") def test_succession_line(vega: VegaServiceNull, page: Page): parent_market_id = setup_continuous_market(vega) diff --git a/apps/trading/lib/hooks/use-market-filters.ts b/apps/trading/lib/hooks/use-market-filters.ts new file mode 100644 index 0000000000..76890b41e3 --- /dev/null +++ b/apps/trading/lib/hooks/use-market-filters.ts @@ -0,0 +1,211 @@ +import type * as Types from '@vegaprotocol/types'; +import { + type MarketMaybeWithCandles, + isFuture, + isPerpetual, + isSpot, + OPEN_MARKETS_STATES, + CLOSED_MARKETS_STATES, + PROPOSED_MARKETS_STATES, + retrieveAssets, + calcTradedFactor, +} from '@vegaprotocol/markets'; +import { create } from 'zustand'; +import intersection from 'lodash/intersection'; +import orderBy from 'lodash/orderBy'; +import { priceChangePercentage } from '@vegaprotocol/utils'; +import omit from 'lodash/omit'; + +export const MarketType = { + FUTURE: 'FUTURE', + PERPETUAL: 'PERPETUAL', + SPOT: 'SPOT', +} as const; + +export type IMarketType = keyof typeof MarketType; + +export const MarketState = { + OPEN: 'OPEN', + PROPOSED: 'PROPOSED', + CLOSED: 'CLOSED', +} as const; + +export type IMarketState = keyof typeof MarketState; + +export const SortOption = { + GAINED: 'GAINED', + LOST: 'LOST', + NEW: 'NEW', + TOP_TRADED: 'TOP_TRADED', +} as const; + +export type ISortOption = keyof typeof SortOption; + +export type Filters = { + marketTypes: IMarketType[]; + marketStates: IMarketState[]; + assets: string[]; + searchTerm: string | undefined; + sortOrder: ISortOption; +}; + +type Actions = { + setMarketTypes: (marketTypes: IMarketType[]) => void; + setMarketStates: (marketSates: IMarketState[]) => void; + setAssets: (assets: string[]) => void; + setSearchTerm: (searchTerm: string) => void; + setSortOrder: (sortOrder: ISortOption) => void; + reset: () => void; +}; + +export const DEFAULT_FILTERS: Filters = { + marketTypes: [], + marketStates: ['OPEN'], + assets: [], + searchTerm: '', + sortOrder: SortOption.TOP_TRADED, +}; + +export const useMarketFiltersStore = create()((set) => ({ + ...DEFAULT_FILTERS, + setMarketTypes: (marketTypes) => set({ marketTypes }), + setMarketStates: (marketStates) => set({ marketStates }), + setAssets: (assets) => set({ assets }), + setSearchTerm: (searchTerm) => set({ searchTerm }), + setSortOrder: (sortOrder) => set({ sortOrder }), + reset: () => set(omit(DEFAULT_FILTERS, 'sortOrder')), +})); + +const isOfTypes = ( + market: MarketMaybeWithCandles, + marketTypes: IMarketType[] +) => { + let marketType: IMarketType | undefined = undefined; + const product = market?.tradableInstrument?.instrument?.product; + if (product) { + if (isFuture(product)) marketType = MarketType.FUTURE; + if (isPerpetual(product)) marketType = MarketType.PERPETUAL; + if (isSpot(product)) marketType = MarketType.SPOT; + } + return marketType && marketTypes.includes(marketType); +}; + +const isOfStates = ( + market: MarketMaybeWithCandles, + marketStates: IMarketState[] +) => { + let states: Types.MarketState[] = []; + if (marketStates.includes('OPEN')) { + states = [...states, ...OPEN_MARKETS_STATES]; + } + if (marketStates.includes('CLOSED')) { + states = [...states, ...CLOSED_MARKETS_STATES]; + } + if (marketStates.includes('PROPOSED')) { + states = [...states, ...PROPOSED_MARKETS_STATES]; + } + + const marketState = market.data?.marketState; + return marketState && states.includes(marketState); +}; + +const isOfAssets = (market: MarketMaybeWithCandles, assets: string[]) => { + const product = market.tradableInstrument?.instrument?.product; + if (product) { + const marketAssets = retrieveAssets(product).map((a) => a.id); + return intersection(assets, marketAssets).length > 0; + } + return false; +}; + +const nameOrCodeMatches = (market: MarketMaybeWithCandles, term: string) => { + const name = market.tradableInstrument.instrument.name; + const code = market.tradableInstrument.instrument.code; + const re = new RegExp(term, 'ig'); + return re.test(name) || re.test(code); +}; + +export const filterMarket = ( + market: MarketMaybeWithCandles, + filters: Partial +) => { + let passes = true; + const { marketTypes, marketStates, assets, searchTerm } = filters; + + // filter by market type + if ( + marketTypes && + marketTypes.length > 0 && + !isOfTypes(market, marketTypes) + ) { + passes = false; + } + + // filter by state + if ( + marketStates && + marketStates.length > 0 && + !isOfStates(market, marketStates) + ) { + passes = false; + } + + // filter by asset + if (assets && assets.length > 0 && !isOfAssets(market, assets)) { + passes = false; + } + + // filter by name or code + if ( + searchTerm && + searchTerm.length > 0 && + !nameOrCodeMatches(market, searchTerm) + ) { + passes = false; + } + + return passes; +}; + +export const filterMarkets = ( + markets: MarketMaybeWithCandles[], + filters: Partial +) => markets.filter((m) => filterMarket(m, filters)); + +export const orderMarkets = ( + markets: MarketMaybeWithCandles[], + sortOrder?: ISortOption +) => { + if (!sortOrder) return markets; + + switch (sortOrder) { + case SortOption.GAINED: + case SortOption.LOST: { + const dir = sortOrder === SortOption.GAINED ? 'desc' : 'asc'; + return orderBy( + markets, + [ + (m) => { + if (!m.candles?.length) return 0; + return Number( + priceChangePercentage( + m.candles.filter((c) => c.close !== '').map((c) => c.close) + ) + ); + }, + ], + [dir] + ); + } + case SortOption.NEW: { + return orderBy( + markets, + [(m) => new Date(m.marketTimestamps.open).getTime()], + ['desc'] + ); + } + case SortOption.TOP_TRADED: { + return orderBy(markets, [(m) => calcTradedFactor(m)], ['desc']); + } + } +}; diff --git a/apps/trading/lib/hooks/use-markets-stats.spec.tsx b/apps/trading/lib/hooks/use-markets-stats.spec.tsx index d66b4d1a9b..5125c1bb98 100644 --- a/apps/trading/lib/hooks/use-markets-stats.spec.tsx +++ b/apps/trading/lib/hooks/use-markets-stats.spec.tsx @@ -2,6 +2,7 @@ import type { Candle, MarketMaybeWithCandles } from '@vegaprotocol/markets'; import { useNewListings } from './use-markets-stats'; import { useTotalVolume24hCandles } from './use-markets-stats'; +import { MarketState } from '@vegaprotocol/types'; describe('Hooks for market stats', () => { describe('useTotalVolume24hCandles', () => { @@ -24,6 +25,9 @@ describe('Hooks for market stats', () => { ), decimalPlaces: 2, positionDecimalPlaces: 1, + data: { + marketState: MarketState.STATE_ACTIVE, + }, tradableInstrument: { instrument: { product: { @@ -53,9 +57,18 @@ describe('Hooks for market stats', () => { it('returns latest three markets by open time', () => { const markets = [ - { marketTimestamps: { open: '2021-01-01T00:00:00Z' } }, - { marketTimestamps: { open: '2022-01-01T00:00:00Z' } }, - { marketTimestamps: { open: '2023-01-01T00:00:00Z' } }, + { + marketTimestamps: { open: '2021-01-01T00:00:00Z' }, + data: { marketState: MarketState.STATE_ACTIVE }, + }, + { + marketTimestamps: { open: '2022-01-01T00:00:00Z' }, + data: { marketState: MarketState.STATE_ACTIVE }, + }, + { + marketTimestamps: { open: '2023-01-01T00:00:00Z' }, + data: { marketState: MarketState.STATE_ACTIVE }, + }, ] as MarketMaybeWithCandles[]; expect(useNewListings(markets)).toEqual([ markets[2], diff --git a/apps/trading/lib/hooks/use-markets-stats.tsx b/apps/trading/lib/hooks/use-markets-stats.tsx index 5ca114118b..846d585b2d 100644 --- a/apps/trading/lib/hooks/use-markets-stats.tsx +++ b/apps/trading/lib/hooks/use-markets-stats.tsx @@ -1,7 +1,12 @@ -import { getAsset, type MarketMaybeWithCandles } from '@vegaprotocol/markets'; +import { + filterAndSortMarkets, + getAsset, + type MarketMaybeWithCandles, +} from '@vegaprotocol/markets'; import { priceChangePercentage, toBigNum, toQUSD } from '@vegaprotocol/utils'; import BigNumber from 'bignumber.js'; -import { orderBy } from 'lodash'; +import compact from 'lodash/compact'; +import orderBy from 'lodash/orderBy'; /** * useTotalVolume24hCandles returns 24 hr candles with total volume @@ -11,9 +16,10 @@ import { orderBy } from 'lodash'; * @returns */ export const useTotalVolume24hCandles = ( - activeMarkets: MarketMaybeWithCandles[] | null + markets: MarketMaybeWithCandles[] | null ): number[] => { const candles = []; + const activeMarkets = filterAndSortMarkets(compact(markets)); if (!activeMarkets || activeMarkets.length === 0) return []; for (let i = 0; i < 24; i++) { const totalVolume24hr = activeMarkets.reduce((acc, market) => { @@ -34,12 +40,13 @@ export const useTotalVolume24hCandles = ( /** * useTopGainers returns the top 3 markets with highest gains, i.e. sorted by biggest 24h change * - * @param activeMarkets + * @param markets * @returns MarketMaybeWithCandles[] */ export const useTopGainers = ( - activeMarkets: MarketMaybeWithCandles[] | null + markets: MarketMaybeWithCandles[] | null ): MarketMaybeWithCandles[] => { + const activeMarkets = filterAndSortMarkets(compact(markets)); return orderBy( activeMarkets, [ @@ -63,8 +70,9 @@ export const useTopGainers = ( * @returns MarketMaybeWithCandles[] */ export const useNewListings = ( - activeMarkets: MarketMaybeWithCandles[] | null + markets: MarketMaybeWithCandles[] | null ): MarketMaybeWithCandles[] => { + const activeMarkets = filterAndSortMarkets(compact(markets)); return orderBy( activeMarkets, [(m) => new Date(m.marketTimestamps.open).getTime()], diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index e9bc196fcd..69da82b047 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -6,6 +6,8 @@ "{{amount}} $VEGA staked": "{{amount}} $VEGA staked", "{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot", "{{checkedAssets}} Assets": "{{checkedAssets}} Assets", + "{{count}} results excluded due to the applied filters. <0>Remove filters.": "{{count}} results excluded due to the applied filters. <0>Remove filters.", + "{{count}} results included due to the applied filters. <0>Remove filters.": "{{count}} results included due to the applied filters. <0>Remove filters.", "{{distance}} ago": "{{distance}} ago", "{{entity}} scope": "{{entity}} scope", "{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision", @@ -42,6 +44,7 @@ "Asset": "Asset", "Asset (1)": "Asset (1)", "Assets": "Assets", + "Assets ({{count}})": "Assets ({{count}})", "Available to withdraw this epoch": "Available to withdraw this epoch", "Avatar URL": "Avatar URL", "Average position": "Average position", @@ -453,6 +456,7 @@ "Search": "Search", "See all markets": "See all markets", "See all the live games on the cards below. <0>Find out how to create one.": "See all the live games on the cards below. <0>Find out how to create one.", + "Search by market": "Search by market", "See details of {{count}} rewards": "See details of {{count}} rewards", "Select from wallet": "Select from wallet", "Select market": "Select market", @@ -484,6 +488,7 @@ "Start trading on the worlds most advanced decentralised exchange.": "Start trading on the worlds most advanced decentralised exchange.", "Start trading": "Start trading", "State": "State", + "State ({{count}})": "State ({{count}})", "Status": "Status", "Stop orders": "Stop orders", "Stop": "Stop", diff --git a/libs/markets/src/lib/market-utils.ts b/libs/markets/src/lib/market-utils.ts index a9c803672f..e1125bec31 100644 --- a/libs/markets/src/lib/market-utils.ts +++ b/libs/markets/src/lib/market-utils.ts @@ -118,7 +118,9 @@ export const totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => { : undefined; }; -export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => { +type MarketsFilter = (markets: T[]) => T[]; + +export const filterAndSortMarkets: MarketsFilter = (markets) => { const tradingModesOrdering = [ MarketTradingMode.TRADING_MODE_CONTINUOUS, MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, @@ -148,39 +150,43 @@ export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => { ); }; -export const filterActiveMarkets = (markets: MarketMaybeWithData[]) => { +export const OPEN_MARKETS_STATES = [ + MarketState.STATE_ACTIVE, + MarketState.STATE_SUSPENDED, + MarketState.STATE_SUSPENDED_VIA_GOVERNANCE, + MarketState.STATE_PENDING, +]; + +export const CLOSED_MARKETS_STATES = [ + MarketState.STATE_SETTLED, + MarketState.STATE_TRADING_TERMINATED, + MarketState.STATE_CLOSED, + MarketState.STATE_CANCELLED, +]; + +export const PROPOSED_MARKETS_STATES = [MarketState.STATE_PROPOSED]; + +export const filterActiveMarkets: MarketsFilter = (markets) => { return markets.filter((m) => { return ( - m.data?.marketState && - [ - MarketState.STATE_ACTIVE, - MarketState.STATE_SUSPENDED, - MarketState.STATE_SUSPENDED_VIA_GOVERNANCE, - MarketState.STATE_PENDING, - ].includes(m.data.marketState) + m.data?.marketState && OPEN_MARKETS_STATES.includes(m.data.marketState) ); }); }; -export const filterClosedMarkets = (markets: MarketMaybeWithData[]) => { +export const filterClosedMarkets: MarketsFilter = (markets) => { return markets.filter((m) => { return ( - m.data?.marketState && - [ - MarketState.STATE_SETTLED, - MarketState.STATE_TRADING_TERMINATED, - MarketState.STATE_CLOSED, - MarketState.STATE_CANCELLED, - ].includes(m.data.marketState) + m.data?.marketState && CLOSED_MARKETS_STATES.includes(m.data.marketState) ); }); }; -export const filterProposedMarkets = (markets: MarketMaybeWithData[]) => { +export const filterProposedMarkets: MarketsFilter = (markets) => { return markets.filter((m) => { return ( m.data?.marketState && - [MarketState.STATE_PROPOSED].includes(m.data?.marketState) + PROPOSED_MARKETS_STATES.includes(m.data?.marketState) ); }); }; diff --git a/libs/markets/src/lib/markets-provider.ts b/libs/markets/src/lib/markets-provider.ts index 5017b5eea9..adc16e929e 100644 --- a/libs/markets/src/lib/markets-provider.ts +++ b/libs/markets/src/lib/markets-provider.ts @@ -136,9 +136,10 @@ export const proposedMarketsProvider = makeDerivedDataProvider< never >([marketsWithDataProvider], ([markets]) => filterProposedMarkets(markets)); -export type MarketMaybeWithCandles = MarketFieldsWithAccountsFragment & { - candles?: Candle[]; -}; +export type MarketMaybeWithCandles = MarketFieldsWithAccountsFragment & + MarketMaybeWithData & { + candles?: Candle[]; + }; const addCandles = ( markets: T[], @@ -162,6 +163,18 @@ export const activeMarketsWithCandlesProvider = makeDerivedDataProvider< (parts) => addCandles(parts[0] as Market[], parts[1] as MarketCandles[]) ); +export const marketsWithCandlesProvider = makeDerivedDataProvider< + MarketMaybeWithCandles[], + never, + MarketsCandlesQueryVariables +>( + [ + (callback, client) => marketsWithDataProvider(callback, client, undefined), + marketsCandlesProvider, + ], + (parts) => addCandles(parts[0] as Market[], parts[1] as MarketCandles[]) +); + export type MarketMaybeWithData = Market & { data?: MarketData }; const addData = (markets: T[], marketsData: MarketData[]) => diff --git a/libs/markets/src/lib/product.ts b/libs/markets/src/lib/product.ts index 89ad61d7eb..de11465e2f 100644 --- a/libs/markets/src/lib/product.ts +++ b/libs/markets/src/lib/product.ts @@ -17,6 +17,16 @@ export const isFuture = (product: Product): product is FutureFragment => export const isPerpetual = (product: Product): product is PerpetualFragment => product.__typename === 'Perpetual'; +export const retrieveAssets = (product: Product) => { + if (isSpot(product)) { + return [product.baseAsset, product.quoteAsset]; + } + if (isPerpetual(product) || isFuture(product)) { + return [product.settlementAsset]; + } + return []; +}; + export const getDataSourceSpecForSettlementData = (product: Product) => { if (isFuture(product)) { return product.dataSourceSpecForSettlementData; diff --git a/libs/proposals/src/components/asset-proposal-notification.tsx b/libs/proposals/src/components/asset-proposal-notification.tsx index 7c5d9dd9ab..e5bff90e6c 100644 --- a/libs/proposals/src/components/asset-proposal-notification.tsx +++ b/libs/proposals/src/components/asset-proposal-notification.tsx @@ -1,7 +1,7 @@ import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import * as Schema from '@vegaprotocol/types'; import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit'; -import { useUpdateProposal } from '../lib'; +import { useAssetUpdateProposal } from '../lib'; import { useT } from '../use-t'; type AssetProposalNotificationProps = { @@ -12,7 +12,7 @@ export const AssetProposalNotification = ({ }: AssetProposalNotificationProps) => { const t = useT(); const tokenLink = useLinks(DApp.Governance); - const { data: proposal } = useUpdateProposal({ + const { data: proposal } = useAssetUpdateProposal({ id: assetId, proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET, }); diff --git a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts index cecb0f511e..0f0fccd8c4 100644 --- a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts +++ b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts @@ -796,4 +796,4 @@ export function useMarketViewLiveProposalsSubscription(baseOptions?: Apollo.Subs return Apollo.useSubscription(MarketViewLiveProposalsDocument, options); } export type MarketViewLiveProposalsSubscriptionHookResult = ReturnType; -export type MarketViewLiveProposalsSubscriptionResult = Apollo.SubscriptionResult; +export type MarketViewLiveProposalsSubscriptionResult = Apollo.SubscriptionResult; \ No newline at end of file diff --git a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx index a6aa611407..424378e301 100644 --- a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx +++ b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx @@ -15,6 +15,7 @@ import { type MarketViewProposalsQueryVariables, type MarketViewLiveProposalsSubscriptionVariables, type SubProposalFragment, + type BatchproposalListFieldsFragment, } from './__generated__/Proposals'; export type ProposalFragment = @@ -25,11 +26,16 @@ export type ProposalFragments = Array; const getData = (responseData: ProposalsListQuery | null) => responseData?.proposalsConnection?.edges ?.filter((edge) => Boolean(edge?.proposalNode)) - .map((edge) => edge?.proposalNode as ProposalListFieldsFragment) || null; + .map( + (edge) => + edge?.proposalNode as + | ProposalListFieldsFragment + | BatchproposalListFieldsFragment + ) || null; export const proposalsDataProvider = makeDataProvider< ProposalsListQuery, - ProposalListFieldsFragment[], + Array, never, never, ProposalsListQueryVariables diff --git a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts index 3bd75346df..d4846596fb 100644 --- a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts +++ b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts @@ -2,21 +2,13 @@ import { renderHook } from '@testing-library/react'; import * as Schema from '@vegaprotocol/types'; -import type { - ProposalListFieldsFragment, - UpdateAssetFieldsFragment, - UpdateMarketFieldsFragment, -} from '../proposals-data-provider'; -import { - isChangeProposed, - UpdateAssetFields, - UpdateMarketFields, - useUpdateProposal, -} from './use-update-proposal'; +import type { ProposalListFieldsFragment } from '../proposals-data-provider'; +import { useAssetUpdateProposal } from './use-update-proposal'; -type Proposal = Pick & - Pick & - Pick; +type Proposal = Pick< + ProposalListFieldsFragment, + '__typename' | 'id' | 'terms' | 'state' +>; const generateUpdateAssetProposal = ( id: string, @@ -24,6 +16,7 @@ const generateUpdateAssetProposal = ( lifetimeLimit = '', withdrawThreshold = '' ): Proposal => ({ + __typename: 'Proposal', id, state: Schema.ProposalState.STATE_OPEN, terms: { @@ -43,140 +36,19 @@ const generateUpdateAssetProposal = ( }, }); -type RiskParameters = - | { - __typename: 'UpdateMarketLogNormalRiskModel'; - logNormal?: { - __typename?: 'LogNormalRiskModel'; - riskAversionParameter: number; - tau: number; - params: { - __typename?: 'LogNormalModelParams'; - mu: number; - r: number; - sigma: number; - }; - } | null; - } - | { - __typename: 'UpdateMarketSimpleRiskModel'; - simple?: { - __typename?: 'SimpleRiskModelParams'; - factorLong: number; - factorShort: number; - } | null; - }; - -const generateRiskParameters = ( - type: - | 'UpdateMarketLogNormalRiskModel' - | 'UpdateMarketSimpleRiskModel' = 'UpdateMarketLogNormalRiskModel' -): RiskParameters => { - if (type === 'UpdateMarketSimpleRiskModel') - return { - __typename: 'UpdateMarketSimpleRiskModel', - simple: { - __typename: 'SimpleRiskModelParams', - factorLong: 0, - factorShort: 0, - }, - }; - - return { - __typename: 'UpdateMarketLogNormalRiskModel', - logNormal: { - __typename: 'LogNormalRiskModel', - params: { - __typename: 'LogNormalModelParams', - mu: 0, - r: 0, - sigma: 0, - }, - riskAversionParameter: 0, - tau: 0, - }, - }; -}; - -const generateUpdateMarketProposal = ( - id: string, - code = '', - quoteName = '', - priceMonitoring = false, - liquidityMonitoring = false, - riskParameters = false, - riskParametersType: - | 'UpdateMarketLogNormalRiskModel' - | 'UpdateMarketSimpleRiskModel' = 'UpdateMarketLogNormalRiskModel' -): Proposal => ({ - state: Schema.ProposalState.STATE_OPEN, - terms: { - __typename: 'ProposalTerms', - closingDatetime: '', - enactmentDatetime: undefined, - change: { - __typename: 'UpdateMarket', - marketId: id, - updateMarketConfiguration: { - __typename: undefined, - instrument: { - __typename: - code.length > 0 || quoteName.length > 0 - ? 'UpdateInstrumentConfiguration' - : undefined, - code, - product: { - dataSourceSpecBinding: { - settlementDataProperty: '', - tradingTerminationProperty: '', - }, - dataSourceSpecForSettlementData: { - sourceType: { - __typename: 'DataSourceDefinitionInternal', - }, - }, - dataSourceSpecForTradingTermination: { - sourceType: { - __typename: 'DataSourceDefinitionInternal', - }, - }, - __typename: - quoteName.length > 0 ? 'UpdateFutureProduct' : undefined, - quoteName, - }, - }, - priceMonitoringParameters: { - __typename: priceMonitoring ? 'PriceMonitoringParameters' : undefined, - triggers: priceMonitoring - ? [ - { - auctionExtensionSecs: 1, - horizonSecs: 2, - probability: 3, - __typename: 'PriceMonitoringTrigger', - }, - ] - : [], - }, - liquidityMonitoringParameters: { - __typename: liquidityMonitoring - ? 'LiquidityMonitoringParameters' - : undefined, - targetStakeParameters: { - __typename: undefined, - scalingFactor: 0, - timeWindow: 0, - }, - }, - riskParameters: riskParameters - ? generateRiskParameters(riskParametersType) - : { - __typename: riskParametersType, - }, +const generateUpdateMarketProposal = (id: string) => + ({ + state: Schema.ProposalState.STATE_OPEN, + terms: { + __typename: 'ProposalTerms', + closingDatetime: '', + enactmentDatetime: undefined, + change: { + __typename: 'UpdateMarket', + marketId: id, }, }, - }, -}); + } as Proposal); const mockDataProviderData: { data: Proposal[]; @@ -199,36 +71,25 @@ jest.mock('@vegaprotocol/data-provider', () => ({ useDataProvider: jest.fn((args) => mockDataProvider()), })); -describe('useUpdateProposal', () => { +describe('useAssetUpdateProposal', () => { it('returns update proposal for a given asset', () => { const { result } = renderHook(() => - useUpdateProposal({ + useAssetUpdateProposal({ id: '456', proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET, }) ); - const change = result.current.data?.terms - .change as UpdateAssetFieldsFragment; - expect(change.__typename).toEqual('UpdateAsset'); - expect(change.assetId).toEqual('456'); - }); - it('returns update proposal for a given market', () => { - const { result } = renderHook(() => - useUpdateProposal({ - id: '123', - proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET, - }) - ); - const change = result.current.data?.terms - .change as UpdateMarketFieldsFragment; - expect(change.__typename).toEqual('UpdateMarket'); - expect(change.marketId).toEqual('123'); + expect(result.current.data).toMatchObject({ + __typename: 'Proposal', + }); + // @ts-expect-error terms present as mock only includes a normal proposal + expect(result.current.data?.terms?.change?.assetId).toEqual('456'); }); it('does not return a proposal if not found', () => { const { result } = renderHook(() => - useUpdateProposal({ + useAssetUpdateProposal({ id: '789', proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET, }) @@ -236,68 +97,3 @@ describe('useUpdateProposal', () => { expect(result.current.data).toBeFalsy(); }); }); - -describe('isChangeProposed', () => { - it('returns false if a change for the specified asset field is not proposed', () => { - const proposal = generateUpdateAssetProposal('123'); - expect(isChangeProposed(proposal, UpdateAssetFields.Quantum)).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateAssetFields.LifetimeLimit) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateAssetFields.WithdrawThreshold) - ).toBeFalsy(); - }); - - it('returns true if a change for the specified asset field is proposed', () => { - const proposal = generateUpdateAssetProposal('123', '100', '100', '100'); - expect(isChangeProposed(proposal, UpdateAssetFields.Quantum)).toBeTruthy(); - expect( - isChangeProposed(proposal, UpdateAssetFields.LifetimeLimit) - ).toBeTruthy(); - expect( - isChangeProposed(proposal, UpdateAssetFields.WithdrawThreshold) - ).toBeTruthy(); - }); - - it('returns false if a change for the specified market field is not proposed', () => { - const proposal = generateUpdateMarketProposal('123'); - expect(isChangeProposed(proposal, UpdateMarketFields.Code)).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.QuoteName) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.PriceMonitoring) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.LiquidityMonitoring) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.RiskParameters) - ).toBeFalsy(); - }); - - it('returns true if a change for the specified market field is proposed', () => { - const proposal = generateUpdateMarketProposal( - '123', - 'ABCDEF', - 'qABCDEFq', - true, - true, - true - ); - expect(isChangeProposed(proposal, UpdateMarketFields.Code)).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.QuoteName) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.PriceMonitoring) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.LiquidityMonitoring) - ).toBeFalsy(); - expect( - isChangeProposed(proposal, UpdateMarketFields.RiskParameters) - ).toBeFalsy(); - }); -}); diff --git a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts index a59f8438df..6509949cfa 100644 --- a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts +++ b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts @@ -3,7 +3,10 @@ import { useDataProvider } from '@vegaprotocol/data-provider'; import { useMemo } from 'react'; import first from 'lodash/first'; import { proposalsDataProvider } from '../proposals-data-provider'; -import type { ProposalListFieldsFragment } from '../proposals-data-provider'; +import type { + BatchproposalListFieldsFragment, + ProposalListFieldsFragment, +} from '../proposals-data-provider'; type UseUpdateProposalProps = { id?: string; @@ -13,23 +16,15 @@ type UseUpdateProposalProps = { }; type UseUpdateProposal = { - data: ProposalListFieldsFragment | undefined; + data: + | ProposalListFieldsFragment + | BatchproposalListFieldsFragment + | undefined; loading: boolean; error: Error | undefined; }; -const changeCondition = { - [Schema.ProposalType.TYPE_UPDATE_ASSET]: ( - id: string, - change: ProposalListFieldsFragment['terms']['change'] - ) => change.__typename === 'UpdateAsset' && change.assetId === id, - [Schema.ProposalType.TYPE_UPDATE_MARKET]: ( - id: string, - change: ProposalListFieldsFragment['terms']['change'] - ) => change.__typename === 'UpdateMarket' && change.marketId === id, -}; - -export const useUpdateProposal = ({ +export const useAssetUpdateProposal = ({ id, proposalType, }: UseUpdateProposalProps): UseUpdateProposal => { @@ -46,159 +41,44 @@ export const useUpdateProposal = ({ variables, }); - const proposal = id - ? first( - (data || []).filter( - (proposal) => - [ - Schema.ProposalState.STATE_OPEN, - Schema.ProposalState.STATE_PASSED, - Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE, - ].includes(proposal.state) && - changeCondition[proposalType](id, proposal.terms.change) - ) - ) - : undefined; - - return { data: proposal, loading, error }; -}; - -export enum UpdateMarketFields { - Code, - QuoteName, - PriceMonitoring, - LiquidityMonitoring, - RiskParameters, -} - -export enum UpdateAssetFields { - Quantum, - LifetimeLimit, - WithdrawThreshold, -} - -export type UpdateProposalField = UpdateAssetFields | UpdateMarketFields; + const openAssetProposals = (data || []).filter((proposal) => { + if ( + ![ + Schema.ProposalState.STATE_OPEN, + Schema.ProposalState.STATE_PASSED, + Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE, + ].includes(proposal.state) + ) { + return false; + } -const fieldGetters = { - [UpdateMarketFields.Code]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateMarket') { - const proposed = - change.updateMarketConfiguration.__typename !== undefined && - change.updateMarketConfiguration.instrument.__typename !== undefined; - return ( - proposed && change.updateMarketConfiguration.instrument.code.length > 0 - ); + if (proposal.__typename === 'Proposal') { + if ( + proposal.terms.change.__typename === 'UpdateAsset' && + proposal.terms.change.assetId === id + ) { + return true; + } } - return false; - }, - [UpdateMarketFields.QuoteName]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateMarket') { - const proposed = - change.updateMarketConfiguration.__typename !== undefined && - change.updateMarketConfiguration.instrument.__typename !== undefined && - change.updateMarketConfiguration.instrument.product.__typename !== - undefined; - return ( - proposed && - 'quoteName' in change.updateMarketConfiguration.instrument.product && - change.updateMarketConfiguration.instrument.product.quoteName.length > 0 + + if (proposal.__typename === 'BatchProposal') { + const assetChange = proposal?.subProposals?.find( + (p) => p?.terms?.change.__typename === 'UpdateAsset' ); + + if ( + assetChange && + assetChange.terms?.change.__typename === 'UpdateAsset' && + assetChange.terms.change.assetId === id + ) { + return true; + } } + return false; - }, - [UpdateMarketFields.PriceMonitoring]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateMarket') { - const proposed = - change.updateMarketConfiguration.__typename !== undefined && - change.updateMarketConfiguration.priceMonitoringParameters - .__typename !== undefined && - change.updateMarketConfiguration.priceMonitoringParameters.triggers - ?.length; - return proposed; - } - return false; - }, - [UpdateMarketFields.LiquidityMonitoring]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateMarket') { - const proposed = - change.updateMarketConfiguration.__typename !== undefined && - change.updateMarketConfiguration.liquidityMonitoringParameters - .__typename !== undefined; - return proposed; - } - return false; - }, - [UpdateMarketFields.RiskParameters]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateMarket') { - const proposed = - change.updateMarketConfiguration.__typename !== undefined && - change.updateMarketConfiguration.riskParameters.__typename !== - undefined; - const log = - change.updateMarketConfiguration.riskParameters.__typename === - 'UpdateMarketLogNormalRiskModel' && - change.updateMarketConfiguration.riskParameters.logNormal !== undefined; - const simple = - change.updateMarketConfiguration.riskParameters.__typename === - 'UpdateMarketSimpleRiskModel' && - change.updateMarketConfiguration.riskParameters.simple !== undefined; - return proposed && (log || simple); - } - return false; - }, - [UpdateAssetFields.Quantum]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateAsset') { - const proposed = change.quantum.length > 0; - return proposed; - } - return false; - }, - [UpdateAssetFields.LifetimeLimit]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateAsset') { - const proposed = - change.source.__typename === 'UpdateERC20' && - change.source.lifetimeLimit.length > 0; - return proposed; - } - return false; - }, - [UpdateAssetFields.WithdrawThreshold]: ( - change: ProposalListFieldsFragment['terms']['change'] - ) => { - if (change.__typename === 'UpdateAsset') { - const proposed = - change.source.__typename === 'UpdateERC20' && - change.source.withdrawThreshold.length > 0; - return proposed; - } - return false; - }, -}; + }); + + const proposal = first(openAssetProposals); -export const isChangeProposed = ( - proposal: Pick, - field: UpdateProposalField -) => { - if (proposal) { - return ( - (proposal.terms.change.__typename === 'UpdateAsset' || - proposal.terms.change.__typename === 'UpdateMarket') && - fieldGetters[field](proposal.terms.change) - ); - } - return false; + return { data: proposal, loading, error }; }; diff --git a/libs/ui-toolkit/src/components/index.ts b/libs/ui-toolkit/src/components/index.ts index 504bbdc228..868013a8e6 100644 --- a/libs/ui-toolkit/src/components/index.ts +++ b/libs/ui-toolkit/src/components/index.ts @@ -66,5 +66,6 @@ export * from './trading-dropdown'; export * from './trading-form-group'; export * from './trading-input-error'; export * from './trading-input'; +export * from './trading-multi-select'; export * from './trading-radio-group'; export * from './trading-select'; diff --git a/libs/ui-toolkit/src/components/trading-multi-select/index.ts b/libs/ui-toolkit/src/components/trading-multi-select/index.ts new file mode 100644 index 0000000000..dbd80422ea --- /dev/null +++ b/libs/ui-toolkit/src/components/trading-multi-select/index.ts @@ -0,0 +1 @@ +export * from './multi-select'; diff --git a/libs/ui-toolkit/src/components/trading-multi-select/multi-select.stories.tsx b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.stories.tsx new file mode 100644 index 0000000000..6627a1e5ae --- /dev/null +++ b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.stories.tsx @@ -0,0 +1,27 @@ +import type { StoryFn, Meta } from '@storybook/react'; +import { FormGroup } from '../form-group'; +import { MultiSelect, MultiSelectOption } from './multi-select'; + +export default { + component: MultiSelect, + title: 'Multi select component', +} as Meta; + +const Template: StoryFn = (props) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = { + placeholder: 'Select an option', + children: ( + <> + Option A + Option B + Option C + Option D + + ), +}; diff --git a/libs/ui-toolkit/src/components/trading-multi-select/multi-select.tsx b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.tsx new file mode 100644 index 0000000000..80b7532f5f --- /dev/null +++ b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.tsx @@ -0,0 +1,97 @@ +import * as DropdownPrimitive from '@radix-ui/react-dropdown-menu'; +import classNames from 'classnames'; +import { forwardRef, type ReactNode } from 'react'; +import { VegaIcon, VegaIconNames } from '../icon'; + +type MultiSelectProps = React.ComponentProps & { + trigger?: ReactNode; + placeholder?: string; +}; + +export const MultiSelect = ({ + children, + trigger, + placeholder, + ...props +}: MultiSelectProps) => ( + + + + + + + {children} + + + +); + +export const MultiSelectOption = forwardRef< + React.ElementRef, + React.ComponentProps +>(({ children, checked, ...props }, forwardedRef) => ( + { + e.preventDefault(); + }} + ref={forwardedRef} + {...props} + > + +
{children}
+
+)); + +export const PseudoCheckbox = ({ + checked, +}: { + checked?: boolean | 'indeterminate'; +}) => ( +
+ {checked && ( + + )} +
+); diff --git a/package.json b/package.json index 21c4b27e33..920c20e956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nx-monorepo", - "version": "0.26.18", + "version": "0.26.19", "license": "MIT", "scripts": { "start": "nx serve",