From 2a40d9ec4d206330f883da6739b9c797a6d8a567 Mon Sep 17 00:00:00 2001 From: Edd Date: Wed, 31 Jan 2024 10:36:58 +0000 Subject: [PATCH 1/7] feat(explorer): support for more external chains (#5687) --- apps/explorer/.env | 2 - apps/explorer/.env.capsule | 1 - apps/explorer/.env.devnet | 1 - apps/explorer/.env.mainnet | 1 - apps/explorer/.env.mainnet-mirror | 1 - apps/explorer/.env.testnet | 1 - apps/explorer/.env.validators-testnet | 1 - apps/explorer/.env.vegacapsule | 1 - .../eth-explorer-link/eth-explorer-link.tsx | 34 -- .../external-chain-icon.tsx | 34 ++ .../external-explorer-link/external-chain.ts | 38 ++ .../external-explorer-link.tsx | 41 ++ .../details/chain-events/tx-contract-call.tsx | 34 +- .../tx-erc20-asset-limits-updated.tsx | 6 +- .../chain-events/tx-erc20-asset-list.tsx | 6 +- .../details/chain-events/tx-erc20-deposit.tsx | 6 +- .../chain-events/tx-erc20-withdrawal.tsx | 6 +- .../details/chain-events/tx-stake-deposit.tsx | 6 +- .../details/chain-events/tx-stake-remove.tsx | 6 +- .../chain-events/tx-stake-totalsupply.tsx | 6 +- .../transfer/blocks/transfer-participants.tsx | 1 + .../txs/details/tx-eth-key-rotate.tsx | 10 +- .../txs/details/tx-issue-signatures.tsx | 6 +- .../txs/details/tx-node-announce.tsx | 6 +- .../components/txs/details/tx-node-vote.tsx | 6 +- .../txs/details/tx-undelegation.tsx | 2 +- .../txs/details/tx-withdraw-submission.tsx | 6 +- .../src/app/components/txs/tx-order-type.tsx | 9 + apps/explorer/src/app/config/env.ts | 1 - .../src/app/routes/oracles/Oracles.graphql | 2 + .../routes/oracles/OraclesForMarkets.graphql | 1 + .../routes/oracles/__generated__/Oracles.ts | 8 +- .../__generated__/OraclesForMarkets.ts | 5 +- .../components/oracle-details-type.tsx | 2 +- .../oracles/components/oracle-eth-source.tsx | 25 +- .../oracles/components/oracle-signers.tsx | 15 +- .../app/routes/oracles/components/oracle.tsx | 7 +- apps/explorer/src/assets/chain-arb-logo.svg | 38 ++ apps/explorer/src/assets/chain-dcn-logo.svg | 1 + apps/explorer/src/assets/chain-eth-logo.svg | 21 ++ apps/explorer/src/assets/chain-gno-logo.svg | 15 + apps/explorer/src/types/explorer.d.ts | 350 ++++++++++++++++-- 42 files changed, 626 insertions(+), 143 deletions(-) delete mode 100644 apps/explorer/src/app/components/links/eth-explorer-link/eth-explorer-link.tsx create mode 100644 apps/explorer/src/app/components/links/external-explorer-link/external-chain-icon.tsx create mode 100644 apps/explorer/src/app/components/links/external-explorer-link/external-chain.ts create mode 100644 apps/explorer/src/app/components/links/external-explorer-link/external-explorer-link.tsx create mode 100644 apps/explorer/src/assets/chain-arb-logo.svg create mode 100644 apps/explorer/src/assets/chain-dcn-logo.svg create mode 100644 apps/explorer/src/assets/chain-eth-logo.svg create mode 100644 apps/explorer/src/assets/chain-gno-logo.svg diff --git a/apps/explorer/.env b/apps/explorer/.env index d79b3a9a5..899fb1f1f 100644 --- a/apps/explorer/.env +++ b/apps/explorer/.env @@ -1,5 +1,4 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.rocks NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml NX_VEGA_ENV=STAGNET1 @@ -9,7 +8,6 @@ NX_VEGA_WALLET_URL=http://localhost:1789 NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_BLOCK_EXPLORER=https://be.stagnet1.vega.rocks/rest -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json diff --git a/apps/explorer/.env.capsule b/apps/explorer/.env.capsule index 39ab79e82..bf553dad8 100644 --- a/apps/explorer/.env.capsule +++ b/apps/explorer/.env.capsule @@ -1,6 +1,5 @@ # App configuration variables NX_VEGA_ENV=CUSTOM -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json NX_VEGA_EXPLORER_URL=/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json diff --git a/apps/explorer/.env.devnet b/apps/explorer/.env.devnet index abe211865..2a40fda87 100644 --- a/apps/explorer/.env.devnet +++ b/apps/explorer/.env.devnet @@ -2,7 +2,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml NX_VEGA_ENV=DEVNET NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_VEGA_GOVERNANCE_URL=https://dev.governance.vega.xyz NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml diff --git a/apps/explorer/.env.mainnet b/apps/explorer/.env.mainnet index 21423c3db..0cb69279e 100644 --- a/apps/explorer/.env.mainnet +++ b/apps/explorer/.env.mainnet @@ -4,7 +4,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_ENV=MAINNET NX_BLOCK_EXPLORER=https://be.vega.community/rest -NX_ETHERSCAN_URL=https://etherscan.io NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/ diff --git a/apps/explorer/.env.mainnet-mirror b/apps/explorer/.env.mainnet-mirror index 527870b91..64bb4280e 100644 --- a/apps/explorer/.env.mainnet-mirror +++ b/apps/explorer/.env.mainnet-mirror @@ -4,7 +4,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-inter NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql NX_VEGA_ENV=MAINNET_MIRROR NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/ diff --git a/apps/explorer/.env.testnet b/apps/explorer/.env.testnet index eaa7350d5..8089573d2 100644 --- a/apps/explorer/.env.testnet +++ b/apps/explorer/.env.testnet @@ -4,7 +4,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-inter NX_VEGA_ENV=TESTNET NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf diff --git a/apps/explorer/.env.validators-testnet b/apps/explorer/.env.validators-testnet index 348f6ce45..8b58d3b8f 100644 --- a/apps/explorer/.env.validators-testnet +++ b/apps/explorer/.env.validators-testnet @@ -4,7 +4,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql NX_VEGA_REST=https://api-validators-testnet.vega.rocks/ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest diff --git a/apps/explorer/.env.vegacapsule b/apps/explorer/.env.vegacapsule index ad5d2d29e..6bc2a0df8 100644 --- a/apps/explorer/.env.vegacapsule +++ b/apps/explorer/.env.vegacapsule @@ -3,7 +3,6 @@ NX_TENDERMINT_URL=http://localhost:26607/ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket NX_VEGA_ENV=CUSTOM NX_BLOCK_EXPLORER= -NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json NX_VEGA_EXPLORER_URL=/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json diff --git a/apps/explorer/src/app/components/links/eth-explorer-link/eth-explorer-link.tsx b/apps/explorer/src/app/components/links/eth-explorer-link/eth-explorer-link.tsx deleted file mode 100644 index 92273980b..000000000 --- a/apps/explorer/src/app/components/links/eth-explorer-link/eth-explorer-link.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -import { DATA_SOURCES } from '../../../config'; -import Hash from '../hash'; - -export enum EthExplorerLinkTypes { - block = 'block', - address = 'address', - tx = 'tx', -} - -export type EthExplorerLinkProps = Partial & { - id: string; - type: EthExplorerLinkTypes; -}; - -export const EthExplorerLink = ({ - id, - type, - ...props -}: EthExplorerLinkProps) => { - const link = `${DATA_SOURCES.ethExplorerUrl}/${type}/${id}`; - return ( - - - - ); -}; diff --git a/apps/explorer/src/app/components/links/external-explorer-link/external-chain-icon.tsx b/apps/explorer/src/app/components/links/external-explorer-link/external-chain-icon.tsx new file mode 100644 index 000000000..27f86d034 --- /dev/null +++ b/apps/explorer/src/app/components/links/external-explorer-link/external-chain-icon.tsx @@ -0,0 +1,34 @@ +import type { ChainIdMapping } from './external-chain'; +import { SUPPORTED_CHAIN_IDS, SUPPORTED_CHAIN_LABELS } from './external-chain'; + +export const SUPPORTED_CHAIN_ICON_URLS: ChainIdMapping = { + '1': '/assets/chain-eth-logo.svg', + '100': '/assets/chain-gno-logo.svg', + '42161': '/assets/chain-arb-logo.svg', + '11155111': '/assets/chain-eth-logo.svg', +}; + +export type ExternalChainIconProps = { + chainId?: string; +}; + +export const ExternalChainIcon = ({ + // If chainID is not provided, default to a non-existent chain + chainId = '-1', +}: ExternalChainIconProps) => { + if (SUPPORTED_CHAIN_IDS.includes(chainId)) { + const url = SUPPORTED_CHAIN_ICON_URLS[chainId]; + const alt = SUPPORTED_CHAIN_LABELS[chainId]; + + return ( + {alt} + ); + } else { + return null; + } +}; diff --git a/apps/explorer/src/app/components/links/external-explorer-link/external-chain.ts b/apps/explorer/src/app/components/links/external-explorer-link/external-chain.ts new file mode 100644 index 000000000..6a0a77ac1 --- /dev/null +++ b/apps/explorer/src/app/components/links/external-explorer-link/external-chain.ts @@ -0,0 +1,38 @@ +export type ChainIdMapping = { + [K in typeof SUPPORTED_CHAIN_IDS[number]]: string; +}; +export const SUPPORTED_CHAIN_IDS: string[] = ['1', '100', '42161', '11155111']; + +export const SUPPORTED_CHAIN_LABELS: ChainIdMapping = { + '1': 'Ethereum', + '100': 'Gnosis', + '42161': 'Arbitrum', + '11155111': 'Sepolia', +}; + +export function getExternalExplorerLink(chainId: string, type: string) { + if (SUPPORTED_CHAIN_IDS.includes(chainId)) { + switch (chainId) { + case '1': + return 'https://etherscan.io'; + case '100': + return 'https://gnosisscan.io'; + case '42161': + return 'https://arbiscan.io'; + case '11155111': + return 'https://sepolia.etherscan.io'; + default: + return '#'; + } + } else { + return '#'; + } +} + +export function getExternalChainLabel(chainId?: string) { + if (chainId && SUPPORTED_CHAIN_IDS.includes(chainId)) { + return SUPPORTED_CHAIN_LABELS[chainId]; + } else { + return 'Custom Chain'; + } +} diff --git a/apps/explorer/src/app/components/links/external-explorer-link/external-explorer-link.tsx b/apps/explorer/src/app/components/links/external-explorer-link/external-explorer-link.tsx new file mode 100644 index 000000000..29b0e54b6 --- /dev/null +++ b/apps/explorer/src/app/components/links/external-explorer-link/external-explorer-link.tsx @@ -0,0 +1,41 @@ +import Hash from '../hash'; +import { getExternalExplorerLink } from './external-chain'; +import { ExternalChainIcon } from './external-chain-icon'; + +export enum EthExplorerLinkTypes { + block = 'block', + address = 'address', + tx = 'tx', +} + +export type ExternalExplorerLinkProps = Partial & { + id: string; + type: EthExplorerLinkTypes; + // Defaults to Ethereum Mainnet, as chain support was added late + chain?: string; + code?: boolean; +}; + +export const ExternalExplorerLink = ({ + id, + type, + chain = '1', + code = false, + ...props +}: ExternalExplorerLinkProps) => { + const link = `${getExternalExplorerLink(chain, type)}/${type}/${id}${ + code ? '#code' : '' + }`; + return ( + + + + + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-contract-call.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-contract-call.tsx index f7c367eaf..2b7fc7927 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-contract-call.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-contract-call.tsx @@ -1,9 +1,10 @@ import { TableCell, TableRow } from '../../../table'; import { t } from '@vegaprotocol/i18n'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; +import { getExternalChainLabel } from '../../../links/external-explorer-link/external-chain'; import type { components } from '../../../../../types/explorer'; import { defaultAbiCoder, base64 } from 'ethers/lib/utils'; import { BigNumber } from 'ethers'; @@ -44,10 +45,10 @@ export const TxDetailsContractCall = ({ }, }); - if (!contractCall || !contractCall.result) { + if (!contractCall) { return null; } - + const chainLabel = getExternalChainLabel(contractCall.sourceChainId); return ( <> {contractCall.specId && ( @@ -64,9 +65,12 @@ export const TxDetailsContractCall = ({ )} {contractCall.blockHeight && ( - {t('ETH block')} - + + @@ -75,14 +79,24 @@ export const TxDetailsContractCall = ({ )} {data?.oracleSpec?.dataSourceSpec && ( )} - - {t('Result')} - {decodeEthCallResult(contractCall.result)} - + {contractCall.error && ( + + {t('Call error')} + {contractCall.error} + + )} + + {contractCall.result && ( + + {t('Result')} + {decodeEthCallResult(contractCall.result)} + + )} ); }; diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-limits-updated.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-limits-updated.tsx index 394a16158..a8efcc27a 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-limits-updated.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-limits-updated.tsx @@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { AssetLink } from '../../../links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventErc20AssetLimitsUpdatedProps { assetLimitsUpdated: components['schemas']['vegaERC20AssetLimitsUpdated']; @@ -42,7 +42,7 @@ export const TxDetailsChainEventErc20AssetLimitsUpdated = ({ {t('ERC20 asset')} - diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-list.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-list.tsx index 597e317db..6e044f566 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-list.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-asset-list.tsx @@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { AssetLink } from '../../../links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventErc20AssetListProps { assetList: components['schemas']['vegaERC20AssetList']; @@ -32,7 +32,7 @@ export const TxDetailsChainEventErc20AssetList = ({ {t('Source')} - diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-deposit.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-deposit.tsx index 9615a2537..4a1f9524e 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-deposit.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-deposit.tsx @@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { AssetLink, PartyLink } from '../../../links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventProps { deposit: components['schemas']['vegaERC20Deposit']; @@ -36,7 +36,7 @@ export const TxDetailsChainEventDeposit = ({ {t('Source')} - diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-withdrawal.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-withdrawal.tsx index e34ab1a73..2c9aa9851 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-withdrawal.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-erc20-withdrawal.tsx @@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { AssetLink } from '../../../links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventWithdrawalProps { withdrawal: components['schemas']['vegaERC20Withdrawal']; @@ -35,7 +35,7 @@ export const TxDetailsChainEventWithdrawal = ({ {t('Recipient')} - diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-deposit.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-deposit.tsx index 356f70f27..1c72c2b3b 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-deposit.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-deposit.tsx @@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { PartyLink } from '../../../links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventStakeDepositProps { deposit: components['schemas']['vegaStakeDeposited']; @@ -39,7 +39,7 @@ export const TxDetailsChainEventStakeDeposit = ({ {t('Source')} - diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-remove.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-remove.tsx index 54dcb3695..e8075782d 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-remove.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-remove.tsx @@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { PartyLink } from '../../../links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventStakeRemoveProps { remove: components['schemas']['vegaStakeRemoved']; @@ -39,7 +39,7 @@ export const TxDetailsChainEventStakeRemove = ({ {t('Source')} - diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-totalsupply.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-totalsupply.tsx index 81ab43d86..8bfef5709 100644 --- a/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-totalsupply.tsx +++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-stake-totalsupply.tsx @@ -3,9 +3,9 @@ import { t } from '@vegaprotocol/i18n'; import { TableRow, TableCell } from '../../../table'; import type { components } from '../../../../../types/explorer'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../links/eth-explorer-link/eth-explorer-link'; +} from '../../../links/external-explorer-link/external-explorer-link'; interface TxDetailsChainEventStakeTotalSupplyProps { update: components['schemas']['vegaStakeTotalSupply']; @@ -38,7 +38,7 @@ export const TxDetailsChainEventStakeTotalSupply = ({ {t('Source')} - diff --git a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx index 1a689b8e7..e84e79f57 100644 --- a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx +++ b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx @@ -41,6 +41,7 @@ const AccountType: Record = { ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY: 'Reward Return Volatility', ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING: 'Reward Validator Ranking', ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD: 'Pending Fee Referral Reward', + ACCOUNT_TYPE_ORDER_MARGIN: 'Order Margin', }; interface TransferParticipantsProps { diff --git a/apps/explorer/src/app/components/txs/details/tx-eth-key-rotate.tsx b/apps/explorer/src/app/components/txs/details/tx-eth-key-rotate.tsx index ecd49e6c6..88e75f5e9 100644 --- a/apps/explorer/src/app/components/txs/details/tx-eth-key-rotate.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-eth-key-rotate.tsx @@ -5,9 +5,9 @@ import { TxDetailsShared } from './shared/tx-details-shared'; import { TableCell, TableRow, TableWithTbody } from '../../table'; import type { components } from '../../../../types/explorer'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../links/eth-explorer-link/eth-explorer-link'; +} from '../../links/external-explorer-link/external-explorer-link'; import { BlockLink } from '../../links'; type EthKeyRotate = components['schemas']['v1EthereumKeyRotateSubmission']; @@ -46,7 +46,7 @@ export const TxDetailsEthKeyRotate = ({ {t('Old Address')} - @@ -57,7 +57,7 @@ export const TxDetailsEthKeyRotate = ({ {t('New Address')} - @@ -68,7 +68,7 @@ export const TxDetailsEthKeyRotate = ({ {t('Submitter address')} - diff --git a/apps/explorer/src/app/components/txs/details/tx-issue-signatures.tsx b/apps/explorer/src/app/components/txs/details/tx-issue-signatures.tsx index b39b8800f..23c74bed6 100644 --- a/apps/explorer/src/app/components/txs/details/tx-issue-signatures.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-issue-signatures.tsx @@ -6,9 +6,9 @@ import { TableRow, TableCell, TableWithTbody } from '../../table'; import type { components } from '../../../../types/explorer'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../links/eth-explorer-link/eth-explorer-link'; +} from '../../links/external-explorer-link/external-explorer-link'; import { NodeLink } from '../../links'; type Command = components['schemas']['v1IssueSignatures']; @@ -57,7 +57,7 @@ export const TxDetailsIssueSignatures = ({ {t('ETH key')} - diff --git a/apps/explorer/src/app/components/txs/details/tx-node-announce.tsx b/apps/explorer/src/app/components/txs/details/tx-node-announce.tsx index 46a9806d8..4d35bacaa 100644 --- a/apps/explorer/src/app/components/txs/details/tx-node-announce.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-node-announce.tsx @@ -6,9 +6,9 @@ import { TableRow, TableCell, TableWithTbody } from '../../table'; import type { components } from '../../../../types/explorer'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../links/eth-explorer-link/eth-explorer-link'; +} from '../../links/external-explorer-link/external-explorer-link'; import { PartyLink } from '../../links'; import Hash from '../../links/hash'; import { ExternalLink } from '@vegaprotocol/ui-toolkit'; @@ -72,7 +72,7 @@ export const TxDetailsNodeAnnounce = ({ {t('Ethereum Address')} - diff --git a/apps/explorer/src/app/components/txs/details/tx-node-vote.tsx b/apps/explorer/src/app/components/txs/details/tx-node-vote.tsx index f5159cae2..94f4896ba 100644 --- a/apps/explorer/src/app/components/txs/details/tx-node-vote.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-node-vote.tsx @@ -8,9 +8,9 @@ import { useExplorerNodeVoteQuery } from './__generated__/Node-vote'; import { PartyLink } from '../../links'; import { Time } from '../../time'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../links/eth-explorer-link/eth-explorer-link'; +} from '../../links/external-explorer-link/external-explorer-link'; interface TxDetailsNodeVoteProps { txData: BlockExplorerTransactionResult | undefined; @@ -143,7 +143,7 @@ export function TxHash({ hash }: TxDetailsEthTxHashProps) { Ethereum TX: - + ); diff --git a/apps/explorer/src/app/components/txs/details/tx-undelegation.tsx b/apps/explorer/src/app/components/txs/details/tx-undelegation.tsx index bd35129da..82392f7c1 100644 --- a/apps/explorer/src/app/components/txs/details/tx-undelegation.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-undelegation.tsx @@ -8,7 +8,7 @@ import GovernanceAssetBalance from '../../asset-balance/governance-asset-balance import type { components } from '../../../../types/explorer'; export const methodText: Record< - components['schemas']['UndelegateSubmissionMethod'], + components['schemas']['v1UndelegateSubmissionMethod'], string > = { METHOD_NOW: 'Immediate', diff --git a/apps/explorer/src/app/components/txs/details/tx-withdraw-submission.tsx b/apps/explorer/src/app/components/txs/details/tx-withdraw-submission.tsx index 2377e29e8..ecac69598 100644 --- a/apps/explorer/src/app/components/txs/details/tx-withdraw-submission.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-withdraw-submission.tsx @@ -4,9 +4,9 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint import { TxDetailsShared } from './shared/tx-details-shared'; import { TableCell, TableRow, TableWithTbody } from '../../table'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../links/eth-explorer-link/eth-explorer-link'; +} from '../../links/external-explorer-link/external-explorer-link'; import { txSignatureToDeterministicId } from '../lib/deterministic-ids'; import AssetBalance from '../../asset-balance/asset-balance'; import { useScrollToLocation } from '../../../hooks/scroll-to-location'; @@ -57,7 +57,7 @@ export const TxDetailsWithdrawSubmission = ({ {t('Recipient')} - diff --git a/apps/explorer/src/app/components/txs/tx-order-type.tsx b/apps/explorer/src/app/components/txs/tx-order-type.tsx index 62ba4fffb..239c55de1 100644 --- a/apps/explorer/src/app/components/txs/tx-order-type.tsx +++ b/apps/explorer/src/app/components/txs/tx-order-type.tsx @@ -1,6 +1,7 @@ import { t } from '@vegaprotocol/i18n'; import type { components } from '../../../types/explorer'; import { VoteIcon } from '../vote-icon/vote-icon'; +import { ExternalChainIcon } from '../links/external-explorer-link/external-chain-icon'; interface TxOrderTypeProps { orderType: string; @@ -188,6 +189,9 @@ export function getLabelForChainEvent( } return t('Multisig update'); } else if (chainEvent.contractCall) { + if (chainEvent.contractCall.error) { + return t('Call error'); + } return t('Contract call'); } return t('Chain Event'); @@ -257,6 +261,11 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => { data-testid="tx-type" className={`text-sm rounded-md leading-tight px-2 inline-block whitespace-nowrap ${colours}`} > + {command?.chainEvent && ( + + )} {type} ); diff --git a/apps/explorer/src/app/config/env.ts b/apps/explorer/src/app/config/env.ts index 50e6998f8..f06125d76 100644 --- a/apps/explorer/src/app/config/env.ts +++ b/apps/explorer/src/app/config/env.ts @@ -14,7 +14,6 @@ export const ENV = { blockExplorerUrl: windowOrDefault('NX_BLOCK_EXPLORER'), tendermintUrl: windowOrDefault('NX_TENDERMINT_URL'), tendermintWebsocketUrl: windowOrDefault('NX_TENDERMINT_WEBSOCKET_URL'), - ethExplorerUrl: windowOrDefault('NX_ETHERSCAN_URL'), governanceUrl: windowOrDefault('NX_VEGA_GOVERNANCE_URL'), vegaRepoUrl: windowOrDefault('NX_VEGA_REPO_URL'), }, diff --git a/apps/explorer/src/app/routes/oracles/Oracles.graphql b/apps/explorer/src/app/routes/oracles/Oracles.graphql index d3bb6caa7..91a9e2121 100644 --- a/apps/explorer/src/app/routes/oracles/Oracles.graphql +++ b/apps/explorer/src/app/routes/oracles/Oracles.graphql @@ -78,6 +78,7 @@ fragment ExplorerOracleDataSource on OracleSpec { } } } + sourceChainId filters { key { name @@ -118,6 +119,7 @@ fragment ExplorerOracleDataSource on OracleSpec { address requiredConfirmations method + sourceChainId filters { key { type diff --git a/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql b/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql index ccfa2f4c5..7116f6356 100644 --- a/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql +++ b/apps/explorer/src/app/routes/oracles/OraclesForMarkets.graphql @@ -67,6 +67,7 @@ fragment ExplorerOracleDataSourceSpec on ExternalDataSourceSpec { sourceType { ... on EthCallSpec { address + sourceChainId } ... on DataSourceSpecConfiguration { signers { diff --git a/apps/explorer/src/app/routes/oracles/__generated__/Oracles.ts b/apps/explorer/src/app/routes/oracles/__generated__/Oracles.ts index 69bd18f2f..d94ecdda4 100644 --- a/apps/explorer/src/app/routes/oracles/__generated__/Oracles.ts +++ b/apps/explorer/src/app/routes/oracles/__generated__/Oracles.ts @@ -5,19 +5,19 @@ import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } }; -export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array | null, args?: Array | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } }; +export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array | null, args?: Array | null, method: string, requiredConfirmations: number, address: string, sourceChainId: number, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } }; export type ExplorerOracleSpecsQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array | null, args?: Array | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null }; +export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array | null, args?: Array | null, method: string, requiredConfirmations: number, address: string, sourceChainId: number, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null }; export type ExplorerOracleSpecByIdQueryVariables = Types.Exact<{ id: Types.Scalars['ID']; }>; -export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array | null, args?: Array | null, method: string, requiredConfirmations: number, address: string, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null }; +export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } | { __typename?: 'EthCallSpec', abi?: Array | null, args?: Array | null, method: string, requiredConfirmations: number, address: string, sourceChainId: number, normalisers?: Array<{ __typename?: 'Normaliser', name: string, expression: string }> | null, trigger: { __typename?: 'EthCallTrigger', trigger: { __typename?: 'EthTimeTrigger', initial?: any | null, every?: number | null, until?: any | null } }, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null }; export const ExplorerOracleDataConnectionFragmentDoc = gql` fragment ExplorerOracleDataConnection on OracleSpec { @@ -101,6 +101,7 @@ export const ExplorerOracleDataSourceFragmentDoc = gql` } } } + sourceChainId filters { key { name @@ -141,6 +142,7 @@ export const ExplorerOracleDataSourceFragmentDoc = gql` address requiredConfirmations method + sourceChainId filters { key { type diff --git a/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts b/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts index 0a2cbfc37..0a1a543c1 100644 --- a/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts +++ b/apps/explorer/src/app/routes/oracles/__generated__/OraclesForMarkets.ts @@ -9,12 +9,12 @@ export type ExplorerOracleFutureFragment = { __typename?: 'Future', dataSourceSp export type ExplorerOracleForMarketsMarketFragment = { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } }; -export type ExplorerOracleDataSourceSpecFragment = { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }; +export type ExplorerOracleDataSourceSpecFragment = { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string, sourceChainId: number } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }; export type ExplorerOracleFormMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type ExplorerOracleFormMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } } }> } | null, oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null }; +export type ExplorerOracleFormMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Perpetual', dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus } } | { __typename?: 'Spot' } } } } }> } | null, oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } | { __typename?: 'EthCallSpec', address: string, sourceChainId: number } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } | { __typename?: 'DataSourceSpecConfigurationTimeTrigger', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null>, triggers: Array<{ __typename?: 'InternalTimeTrigger', initial?: number | null, every?: number | null } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null }; export const ExplorerOracleFutureFragmentDoc = gql` fragment ExplorerOracleFuture on Future { @@ -90,6 +90,7 @@ export const ExplorerOracleDataSourceSpecFragmentDoc = gql` sourceType { ... on EthCallSpec { address + sourceChainId } ... on DataSourceSpecConfiguration { signers { diff --git a/apps/explorer/src/app/routes/oracles/components/oracle-details-type.tsx b/apps/explorer/src/app/routes/oracles/components/oracle-details-type.tsx index 4fb821b7b..1c267d0f9 100644 --- a/apps/explorer/src/app/routes/oracles/components/oracle-details-type.tsx +++ b/apps/explorer/src/app/routes/oracles/components/oracle-details-type.tsx @@ -28,7 +28,7 @@ export function isInternalSourceType(s: SourceType) { export function getExternalType(s: SourceType) { if (s.sourceType.__typename === 'EthCallSpec') { - return 'Ethereum Contract Call'; + return 'Contract Call'; } else { return 'External Data'; } diff --git a/apps/explorer/src/app/routes/oracles/components/oracle-eth-source.tsx b/apps/explorer/src/app/routes/oracles/components/oracle-eth-source.tsx index ad71bdc64..30cb7e06b 100644 --- a/apps/explorer/src/app/routes/oracles/components/oracle-eth-source.tsx +++ b/apps/explorer/src/app/routes/oracles/components/oracle-eth-source.tsx @@ -1,18 +1,24 @@ import { TableRow, TableCell, TableHeader } from '../../../components/table'; import type { SourceType } from './oracle'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../components/links/eth-explorer-link/eth-explorer-link'; +} from '../../../components/links/external-explorer-link/external-explorer-link'; +import { getExternalChainLabel } from '../../../components/links/external-explorer-link/external-chain'; +import { t } from 'i18next'; interface OracleDetailsEthSourceProps { sourceType: SourceType; + chain?: string; } /** * Given an Oracle that sources data from Ethereum, this component will render * a link to the smart contract and some basic details */ -export function OracleEthSource({ sourceType }: OracleDetailsEthSourceProps) { +export function OracleEthSource({ + sourceType, + chain = '1', +}: OracleDetailsEthSourceProps) { if ( sourceType.__typename !== 'DataSourceDefinitionExternal' || sourceType.sourceType.__typename !== 'EthCallSpec' @@ -26,11 +32,20 @@ export function OracleEthSource({ sourceType }: OracleDetailsEthSourceProps) { return null; } + const chainLabel = getExternalChainLabel(chain); + return ( - Ethereum Contract + + {chainLabel} {t('Contract')} + - + {sourceType.sourceType.method} diff --git a/apps/explorer/src/app/routes/oracles/components/oracle-signers.tsx b/apps/explorer/src/app/routes/oracles/components/oracle-signers.tsx index 3df060c82..e56052710 100644 --- a/apps/explorer/src/app/routes/oracles/components/oracle-signers.tsx +++ b/apps/explorer/src/app/routes/oracles/components/oracle-signers.tsx @@ -1,8 +1,8 @@ import { PartyLink } from '../../../components/links'; import { - EthExplorerLink, + ExternalExplorerLink, EthExplorerLinkTypes, -} from '../../../components/links/eth-explorer-link/eth-explorer-link'; +} from '../../../components/links/external-explorer-link/external-explorer-link'; import { TableRow, TableCell, TableHeader } from '../../../components/table'; import { remove0x } from '@vegaprotocol/utils'; @@ -37,13 +37,15 @@ export function getAddressLink(signer: Signer) { } if (signer.__typename === 'ETHAddress') { - return ; + return ( + + ); } else if (signer.__typename === 'PubKey' && address.length !== 64) { // This is a hack: some older oracles were submitted before proper checks stopped // ETH addresses being returned as Vega addresses // Hacky 0x prefixing as a bonus return ( - @@ -61,7 +63,10 @@ interface OracleDetailsSignersProps { /** * Given an Oracle, this component will render either a link to Ethereum - * or the Vega party depending on which type is specified + * or the Vega party depending on which type is specified. + * + * Note that this won't be shown for external contract calls as they do not + * have a signer in the same way. */ export function OracleSigners({ sourceType }: OracleDetailsSignersProps) { if (sourceType.__typename !== 'DataSourceDefinitionExternal') { diff --git a/apps/explorer/src/app/routes/oracles/components/oracle.tsx b/apps/explorer/src/app/routes/oracles/components/oracle.tsx index 1ff08fdc7..b4e8b3117 100644 --- a/apps/explorer/src/app/routes/oracles/components/oracle.tsx +++ b/apps/explorer/src/app/routes/oracles/components/oracle.tsx @@ -42,6 +42,11 @@ export const OracleDetails = ({ dataConnection, }: OracleDetailsProps) => { const sourceType = dataSource.dataSourceSpec.spec.data.sourceType; + const chain = + dataSource.dataSourceSpec.spec.data.sourceType.sourceType.__typename === + 'EthCallSpec' + ? dataSource.dataSourceSpec.spec.data.sourceType.sourceType.sourceChainId.toString() + : undefined; return (
@@ -60,7 +65,7 @@ export const OracleDetails = ({ - + {t('Filter')} diff --git a/apps/explorer/src/assets/chain-arb-logo.svg b/apps/explorer/src/assets/chain-arb-logo.svg new file mode 100644 index 000000000..dd17407f5 --- /dev/null +++ b/apps/explorer/src/assets/chain-arb-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/explorer/src/assets/chain-dcn-logo.svg b/apps/explorer/src/assets/chain-dcn-logo.svg new file mode 100644 index 000000000..0095585d6 --- /dev/null +++ b/apps/explorer/src/assets/chain-dcn-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/explorer/src/assets/chain-eth-logo.svg b/apps/explorer/src/assets/chain-eth-logo.svg new file mode 100644 index 000000000..684e96873 --- /dev/null +++ b/apps/explorer/src/assets/chain-eth-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/explorer/src/assets/chain-gno-logo.svg b/apps/explorer/src/assets/chain-gno-logo.svg new file mode 100644 index 000000000..97842427e --- /dev/null +++ b/apps/explorer/src/assets/chain-gno-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/apps/explorer/src/types/explorer.d.ts b/apps/explorer/src/types/explorer.d.ts index 92bd34b15..156c15fd1 100644 --- a/apps/explorer/src/types/explorer.d.ts +++ b/apps/explorer/src/types/explorer.d.ts @@ -28,7 +28,7 @@ export interface paths { '/transactions': { /** * List transactions - * @description List transactions from the Vega blockchain + * @description List transactions from the Vega blockchain from the newest to the oldest transactions. */ get: operations['BlockExplorer_ListTransactions']; }; @@ -117,15 +117,31 @@ export interface components { | 'EXPIRY_STRATEGY_CANCELS' | 'EXPIRY_STRATEGY_SUBMIT'; /** - * @description - METHOD_NOW: Undelegate straight away, losing all rewards for the current epoch. - * - METHOD_AT_END_OF_EPOCH: Undelegate at the end of an epoch, retaining all rewards for the current epoch. - * @default METHOD_UNSPECIFIED + * - SIZE_OVERRIDE_SETTING_UNSPECIFIED: Never valid + * - SIZE_OVERRIDE_SETTING_NONE: No override, the size within the contained normal order submission will be used + * - SIZE_OVERRIDE_SETTING_POSITION: Use the total position of the trader + * @default SIZE_OVERRIDE_SETTING_UNSPECIFIED * @enum {string} */ - readonly UndelegateSubmissionMethod: - | 'METHOD_UNSPECIFIED' - | 'METHOD_NOW' - | 'METHOD_AT_END_OF_EPOCH'; + readonly StopOrderSizeOverrideSetting: + | 'SIZE_OVERRIDE_SETTING_UNSPECIFIED' + | 'SIZE_OVERRIDE_SETTING_NONE' + | 'SIZE_OVERRIDE_SETTING_POSITION'; + readonly StopOrderSizeOverrideValue: { + /** Scaling percentage of the current position’s size */ + readonly percentage?: string; + }; + /** + * - MODE_UNSPECIFIED: Never valid. + * - MODE_CROSS_MARGIN: Cross margin mode - margin is dynamically acquired and released as a position is marked to market + * - MODE_ISOLATED_MARGIN: Isolated margin mode - margin for any newly opened position volume is transferred to the margin account when the trade is executed + * @default MODE_UNSPECIFIED + * @enum {string} + */ + readonly UpdateMarginModeMode: + | 'MODE_UNSPECIFIED' + | 'MODE_CROSS_MARGIN' + | 'MODE_ISOLATED_MARGIN'; readonly blockexplorerapiv1Transaction: { /** * Height of the block the transaction was found in @@ -307,6 +323,30 @@ export interface components { readonly stopOrdersSubmission?: readonly components['schemas']['v1StopOrdersSubmission'][]; /** @description List of order submissions to be processed sequentially. */ readonly submissions?: readonly components['schemas']['v1OrderSubmission'][]; + /** Update margin mode instruction */ + readonly updateMarginMode?: readonly components['schemas']['v1UpdateMarginMode'][]; + }; + /** + * @description Command that allows a token holder to submit a batch governance proposal that can be voted on by any other token holders, and eventually enacted on the Vega network. + * For example this command can be used to propose a new market and a network parameter change with it. + */ + readonly v1BatchProposalSubmission: { + /** @description Rationale behind a proposal. */ + readonly rationale?: components['schemas']['vegaProposalRationale']; + /** @description Arbitrary human-readable reference identifying the proposal. */ + readonly reference?: string; + /** @description Proposal terms containing the type and details of the proposal, as well as time spans for voting and enactment. */ + readonly terms?: components['schemas']['v1BatchProposalSubmissionTerms']; + }; + /** Terms for a batch governance proposal submission */ + readonly v1BatchProposalSubmissionTerms: { + /** @description List of individual changes included in the batch proposal. */ + readonly changes?: readonly components['schemas']['vegaBatchProposalTermsChange'][]; + /** + * Format: int64 + * @description Closing timestamp in Unix time; adheres to `minClose` and `maxClose` limits. + */ + readonly closingTimestamp?: string; }; /** @description A validator command sent automatically that provides information of events that have happened on foreign chains. */ readonly v1ChainEvent: { @@ -347,6 +387,12 @@ export interface components { readonly team?: components['schemas']['v1CreateReferralSetTeam']; }; readonly v1CreateReferralSetTeam: { + /** + * @description List of public keys that are allowed to join the team. + * Only applicable to closed teams. Removing a party from the allow list does not remove + * the party from the team. + */ + readonly allowList?: readonly string[]; /** @description External link to an avatar for the team. */ readonly avatarUrl?: string; /** @description Whether or not the team is closed to new party members. */ @@ -432,6 +478,8 @@ export interface components { readonly applyReferralCode?: components['schemas']['v1ApplyReferralCode']; /** @description Command to submit a batch of order instructions. */ readonly batchMarketInstructions?: components['schemas']['v1BatchMarketInstructions']; + /** @description Command to submit a batch governance proposal. */ + readonly batchProposalSubmission?: components['schemas']['v1BatchProposalSubmission']; /** * Format: uint64 * @description Block height which has been used to calculate the transaction proof-of-work. @@ -449,6 +497,8 @@ export interface components { readonly ethereumKeyRotateSubmission?: components['schemas']['v1EthereumKeyRotateSubmission']; /** @description Command to request signatures to amend the multisig-control contract. */ readonly issueSignatures?: components['schemas']['v1IssueSignatures']; + /** @description Command to join a team. */ + readonly joinTeam?: components['schemas']['v1JoinTeam']; /** @description Validator command sent manually by a node operator to rotate their node's Vega keys. */ readonly keyRotateSubmission?: components['schemas']['v1KeyRotateSubmission']; /** @description Command to amend a liquidity commitment. */ @@ -489,6 +539,10 @@ export interface components { readonly transfer?: components['schemas']['commandsv1Transfer']; /** @description Command to remove tokens delegated to a validator. */ readonly undelegateSubmission?: components['schemas']['v1UndelegateSubmission']; + /** @description Command to update the margin mode of a party in a market. */ + readonly updateMarginMode?: components['schemas']['v1UpdateMarginMode']; + /** @description Command to update a party's profile. */ + readonly updatePartyProfile?: components['schemas']['v1UpdatePartyProfile']; /** @description Command to update a referral set. */ readonly updateReferralSet?: components['schemas']['v1UpdateReferralSet']; /** @description Validator command sent automatically to signal regular participation in the network. */ @@ -525,6 +579,11 @@ export interface components { /** @description Node ID of the validator node that will be signed in or out of the smart contract. */ readonly validatorNodeId?: string; }; + /** @description Command that allows the submitter to join a team or change teams if they are already a member of a team. */ + readonly v1JoinTeam: { + /** @description ID of the team to join, this is the same as the referral code used to generate the team. */ + readonly id?: string; + }; /** @description A validator command sent manually that allows a node operator to indicate to the network that their node's Vega key will be rotated. */ readonly v1KeyRotateSubmission: { /** @description Hash of the node's current Vega public key. */ @@ -686,6 +745,17 @@ export interface components { readonly peggedReference?: components['schemas']['vegaPeggedReference']; /** @description New price for the order. This field is an unsigned integer scaled to the market's decimal places. */ readonly price?: string; + /** + * Format: uint64 + * @description New size for the order. + * Amending the size causes the size and remaining part of the order to be changed by the difference between the original and amended size. + * - Specifying a size smaller than the current size leaves the order at its current order book position. + * - Specifying a size larger than the current size removes and reinserts the order at the back of the price level. + * - Specifying a size that results in the remaining part of the order being reduced to zero cancels the order. + * This field is an unsigned integer scaled to the market's decimal places. + * If specified, size_delta must be set to 0. + */ + readonly size?: string; /** * Format: int64 * @description Amend the size for the order by the delta specified: @@ -693,6 +763,7 @@ export interface components { * - To increase the size from the current value, set a positive integer value * - To leave the size unchanged set a value of zero * This field needs to be scaled using the market's position decimal places. + * If specified, size must not be set. */ readonly sizeDelta?: string; /** @description New time in force for the order. */ @@ -863,6 +934,10 @@ export interface components { readonly orderSubmission?: components['schemas']['v1OrderSubmission']; /** @description Order will be submitted if the last traded price on the market breaches the given price. */ readonly price?: string; + /** Indicates if this order is linked to an order or position to derive the order size */ + readonly sizeOverrideSetting?: components['schemas']['StopOrderSizeOverrideSetting']; + /** If this order is linked to a position, provide an optional scaling factor */ + readonly sizeOverrideValue?: components['schemas']['StopOrderSizeOverrideValue']; /** @description Order will be submitted if the last traded price has moved the given percent from the highest/lowest mark price since the stop order was submitted. */ readonly trailingPercentOffset?: string; }; @@ -909,10 +984,42 @@ export interface components { */ readonly amount?: string; /** @description Method of delegation. */ - readonly method?: components['schemas']['UndelegateSubmissionMethod']; + readonly method?: components['schemas']['v1UndelegateSubmissionMethod']; /** @description Node ID to undelegate stake from. */ readonly nodeId?: string; }; + /** + * @description - METHOD_NOW: Undelegate straight away, losing all rewards for the current epoch. + * - METHOD_AT_END_OF_EPOCH: Undelegate at the end of an epoch, retaining all rewards for the current epoch. + * @default METHOD_UNSPECIFIED + * @enum {string} + */ + readonly v1UndelegateSubmissionMethod: + | 'METHOD_UNSPECIFIED' + | 'METHOD_NOW' + | 'METHOD_AT_END_OF_EPOCH'; + readonly v1UpdateMarginMode: { + /** Margin factor to use for margin in isolated mode. It is a multiplier that defines how much margin needs to be set aside */ + readonly marginFactor?: string; + /** @description Market to change margin mode for. */ + readonly marketId?: string; + /** @description Margin mode to use. */ + readonly mode?: components['schemas']['UpdateMarginModeMode']; + }; + /** + * @description Command to associate metadata to a public key, known as a party ID. + * Partial update is not supported, meaning previous values must be included in + * the update, otherwise they are removed. + */ + readonly v1UpdatePartyProfile: { + /** @description Alias given to the party. It must be unique network-wide. */ + readonly alias?: string; + /** + * @description Freeform data to associate to the party. + * Support a maximum of 10 entries. + */ + readonly metadata?: readonly components['schemas']['vegaMetadata'][]; + }; /** * @description A command that allows the referrer of a referral set to update team details for a referral set. * Any field that is left unset or has a default value indicates that this field on the original referral set will be left unchanged. @@ -926,9 +1033,18 @@ export interface components { readonly team?: components['schemas']['v1UpdateReferralSetTeam']; }; readonly v1UpdateReferralSetTeam: { + /** + * @description List of public keys that are allowed to join the team. + * Only applicable to closed teams. Removing a party from the allow list does not remove + * the party from the team. + */ + readonly allowList?: readonly string[]; /** @description New link to an avatar for the team. */ readonly avatarUrl?: string; - /** @description Whether or not the team is closed to new party members. */ + /** + * @description Whether or not the team is closed to new party members. When closed, only parties specified in the allow list can + * join the team. + */ readonly closed?: boolean; /** @description New name of the team. */ readonly name?: string; @@ -1014,6 +1130,7 @@ export interface components { * - ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY: Per asset market reward account given for return volatility * - ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING: Per asset market reward account given to validators by their ranking * - ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD: Per asset account for pending fee referral reward payouts + * - ACCOUNT_TYPE_ORDER_MARGIN: Per asset market account for party in isolated margin mode * @default ACCOUNT_TYPE_UNSPECIFIED * @enum {string} */ @@ -1045,7 +1162,8 @@ export interface components { | 'ACCOUNT_TYPE_REWARD_RELATIVE_RETURN' | 'ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY' | 'ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING' - | 'ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD'; + | 'ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD' + | 'ACCOUNT_TYPE_ORDER_MARGIN'; /** Vega representation of an external asset */ readonly vegaAssetDetails: { /** @description Vega built-in asset. */ @@ -1071,6 +1189,42 @@ export interface components { /** @description Minimum economically meaningful amount in the asset. */ readonly quantum?: string; }; + /** Terms change for a batch governance proposal */ + readonly vegaBatchProposalTermsChange: { + /** @description Proposal change to cancel a governance initiated transfe. */ + readonly cancelTransfer?: components['schemas']['vegaCancelTransfer']; + /** + * Format: int64 + * @description Timestamp as Unix time in seconds when proposal terms gets enacted if proposal passed the vote, + * constrained by `minEnact` and `maxEnact` network parameters. + */ + readonly enactmentTimestamp?: string; + /** + * @description Proposal change for a freeform request, which can be voted on but does not change the behaviour of the system, + * and can be used to gauge community sentiment. + */ + readonly newFreeform?: components['schemas']['vegaNewFreeform']; + /** @description Proposal change for creating new futures market. */ + readonly newMarket?: components['schemas']['vegaNewMarket']; + /** @description Proposal change for creating new spot market. */ + readonly newSpotMarket?: components['schemas']['vegaNewSpotMarket']; + /** @description Proposal change for a governance initiated transfer. */ + readonly newTransfer?: components['schemas']['vegaNewTransfer']; + /** @description Proposal change for updating an asset. */ + readonly updateAsset?: components['schemas']['vegaUpdateAsset']; + /** @description Proposal change for modifying an existing futures market. */ + readonly updateMarket?: components['schemas']['vegaUpdateMarket']; + /** @description Proposal change for updating the state of a market. */ + readonly updateMarketState?: components['schemas']['vegaUpdateMarketState']; + /** @description Proposal change for updating Vega network parameters. */ + readonly updateNetworkParameter?: components['schemas']['vegaUpdateNetworkParameter']; + /** @description Proposal change for updating the referral program. */ + readonly updateReferralProgram?: components['schemas']['vegaUpdateReferralProgram']; + /** @description Proposal change for modifying an existing spot market. */ + readonly updateSpotMarket?: components['schemas']['vegaUpdateSpotMarket']; + /** @description Proposal change for updating the volume discount program. */ + readonly updateVolumeDiscountProgram?: components['schemas']['vegaUpdateVolumeDiscountProgram']; + }; readonly vegaBenefitTier: { /** * @description Required number of epochs a party must have been in a referral set to @@ -1125,6 +1279,44 @@ export interface components { /** @description ID of the governance transfer proposal. */ readonly transferId?: string; }; + /** @description Mark price configuration parameters. */ + readonly vegaCompositePriceConfiguration: { + /** @description Cash amount, in asset decimals, used for the calculation of the mark price from the order book. */ + readonly cashAmount?: string; + /** @description Which method is used for the calculation of the composite price for the market. */ + readonly compositePriceType?: components['schemas']['vegaCompositePriceType']; + /** @description Additional price sources to be used for index price calculation. */ + readonly dataSourcesSpec?: readonly components['schemas']['vegaDataSourceDefinition'][]; + /** List of each price source and its corresponding binding */ + readonly dataSourcesSpecBinding?: readonly components['schemas']['vegaSpecBindingForCompositePrice'][]; + /** + * Format: uint64 + * @description Decay power used for the calculation of mark price. + */ + readonly decayPower?: string; + /** @description Decay weight used for calculation of mark price. */ + readonly decayWeight?: string; + /** + * @description For how long a price source is considered valid. One entry for each data source + * such that the first is for the trade based mark price, the second is for the book based price + * the third is for the first oracle, followed by more oracle data source staleness tolerance. + */ + readonly sourceStalenessTolerance?: readonly string[]; + /** @description Weights for each composite price data source. */ + readonly sourceWeights?: readonly string[]; + }; + /** + * @description - COMPOSITE_PRICE_TYPE_WEIGHTED: Composite price is calculated as a weighted average of the underlying mark prices. + * - COMPOSITE_PRICE_TYPE_MEDIAN: Composite price is calculated as a median of the underlying mark prices. + * - COMPOSITE_PRICE_TYPE_LAST_TRADE: Composite price is calculated as the last trade price. + * @default COMPOSITE_PRICE_TYPE_UNSPECIFIED + * @enum {string} + */ + readonly vegaCompositePriceType: + | 'COMPOSITE_PRICE_TYPE_UNSPECIFIED' + | 'COMPOSITE_PRICE_TYPE_WEIGHTED' + | 'COMPOSITE_PRICE_TYPE_MEDIAN' + | 'COMPOSITE_PRICE_TYPE_LAST_TRADE'; /** * @description Represents the top level object that handles data sources. * Data source definition can be external or internal, with whatever @@ -1197,21 +1389,17 @@ export interface components { readonly tradingTerminationProperty?: string; }; /** - * Describes which property of the data source data is to be - * used as settlement data and which to use as the trading terminated trigger + * @description Describes which properties of the data source data is to be + * used for settlement. */ readonly vegaDataSourceSpecToPerpetualBinding: { /** - * @description Name of the property in the source data that should be used as settlement data. - * If it is set to "prices.BTC.value", then the perpetual market will use the value of - * this property as settlement data. + * @description Name of the property in the source data that should be used for settlement data. + * If it is set to "prices.BTC.value" for example, then the perpetual market will use the value of + * this property to get settlement data. */ readonly settlementDataProperty?: string; - /** - * @description Name of the property in the source data that should be used as settlement data. - * If it is set to "prices.BTC.value", then the perpetual market will use the value of - * this property as settlement data. - */ + /** @description Name of the property in the source data that should be used to determine the perpetual's settlement schedule. */ readonly settlementScheduleProperty?: string; }; /** @@ -1493,6 +1681,11 @@ export interface components { * Format: uint64 */ readonly requiredConfirmations?: string; + /** + * Format: uint64 + * @description The ID of the EVM based chain which is to be used to source the oracle data. + */ + readonly sourceChainId?: string; /** @description Conditions for determining when to call the contract method. */ readonly trigger?: components['schemas']['vegaEthCallTrigger']; }; @@ -1519,6 +1712,11 @@ export interface components { * @description Result of contract call, packed according to the ABI stored in the associated data source spec. */ readonly result?: string; + /** + * Format: uint64 + * @description Source chain for this chain event. + */ + readonly sourceChainId?: string; /** @description ID of the data source spec that triggered this contract call. */ readonly specId?: string; }; @@ -1597,6 +1795,42 @@ export interface components { readonly tolerance?: string; readonly value?: components['schemas']['vegaStateVarValue']; }; + /** @description Liquidation strategy used when the network holds a position resulting from position resolution. */ + readonly vegaLiquidationStrategy: { + /** @description Fraction of the open position the market will try to close in a single attempt; range 0 through 1. */ + readonly disposalFraction?: string; + /** + * Format: int64 + * @description Interval, in seconds, at which the network will attempt to close its position. + */ + readonly disposalTimeStep?: string; + /** + * Format: uint64 + * @description Size of the position that the network will try to close in a single attempt. + */ + readonly fullDisposalSize?: string; + /** @description Max fraction of the total volume of the orderbook, within liquidity bounds, that the network can use to close its position; range 0 through 1. */ + readonly maxFractionConsumed?: string; + }; + /** @description Market settings that describe how the liquidity fee is calculated. */ + readonly vegaLiquidityFeeSettings: { + /** @description Constant liquidity fee used when using the constant fee method. */ + readonly feeConstant?: string; + /** @description Method used to calculate the market's liquidity fee. */ + readonly method?: components['schemas']['vegaLiquidityFeeSettingsMethod']; + }; + /** + * @description - METHOD_MARGINAL_COST: Fee is the smallest value of all bids, such that liquidity providers with nominated fees less than or equal to this value still have sufficient commitment to fulfil the market's target stake. + * - METHOD_WEIGHTED_AVERAGE: Fee is the weighted average of all liquidity providers' nominated fees, weighted by their committment. + * - METHOD_CONSTANT: Fee is set by the market to a constant value irrespective of any liquidity provider's nominated fee. + * @default METHOD_UNSPECIFIED + * @enum {string} + */ + readonly vegaLiquidityFeeSettingsMethod: + | 'METHOD_UNSPECIFIED' + | 'METHOD_MARGINAL_COST' + | 'METHOD_WEIGHTED_AVERAGE' + | 'METHOD_CONSTANT'; /** LiquidityMonitoringParameters contains settings used for liquidity monitoring */ readonly vegaLiquidityMonitoringParameters: { /** @@ -1674,6 +1908,13 @@ export interface components { readonly vegaMatrixValue: { readonly value?: readonly components['schemas']['vegaVectorValue'][]; }; + /** @description Generic structure holding a key/value pair. */ + readonly vegaMetadata: { + /** @description Key of the metadata. */ + readonly key?: string; + /** @description Value of the metadata. */ + readonly value?: string; + }; /** Represents a network parameter on Vega */ readonly vegaNetworkParameter: { /** @description Unique key of the network parameter. */ @@ -1708,6 +1949,10 @@ export interface components { readonly instrument?: components['schemas']['vegaInstrumentConfiguration']; /** @description Linear slippage factor is used to cap the slippage component of maintenance margin - it is applied to the slippage volume. */ readonly linearSlippageFactor?: string; + /** @description Liquidation strategy for this market. */ + readonly liquidationStrategy?: components['schemas']['vegaLiquidationStrategy']; + /** @description Specifies how the liquidity fee for the market will be calculated. */ + readonly liquidityFeeSettings?: components['schemas']['vegaLiquidityFeeSettings']; /** @description Liquidity monitoring parameters. */ readonly liquidityMonitoringParameters?: components['schemas']['vegaLiquidityMonitoringParameters']; /** Liquidity SLA parameters */ @@ -1720,6 +1965,8 @@ export interface components { * price levels over which automated liquidity provisions will be deployed. */ readonly lpPriceRange?: string; + /** @description Mark price configuration. */ + readonly markPriceConfiguration?: components['schemas']['vegaCompositePriceConfiguration']; /** @description Optional new futures market metadata, tags. */ readonly metadata?: readonly string[]; /** @@ -1750,6 +1997,8 @@ export interface components { readonly decimalPlaces?: string; /** @description New spot market instrument configuration. */ readonly instrument?: components['schemas']['vegaInstrumentConfiguration']; + /** @description Specifies how the liquidity fee for the market will be calculated. */ + readonly liquidityFeeSettings?: components['schemas']['vegaLiquidityFeeSettings']; /** @description Log normal risk model parameters, valid only if MODEL_LOG_NORMAL is selected. */ readonly logNormal?: components['schemas']['vegaLogNormalRiskModel']; /** @description Optional new spot market metadata, tags. */ @@ -1865,6 +2114,14 @@ export interface components { readonly dataSourceSpecForSettlementData?: components['schemas']['vegaDataSourceDefinition']; /** @description Data source spec describing the data source for settlement schedule. */ readonly dataSourceSpecForSettlementSchedule?: components['schemas']['vegaDataSourceDefinition']; + /** @description Lower bound for the funding-rate such that the funding-rate will never be lower than this value. */ + readonly fundingRateLowerBound?: string; + /** @description Factor applied to funding-rates. This scales the impact that spot price deviations have on funding payments. */ + readonly fundingRateScalingFactor?: string; + /** @description Upper bound for the funding-rate such that the funding-rate will never be higher than this value. */ + readonly fundingRateUpperBound?: string; + /** @description Composite price configuration to drive the calculation of the index price used for funding payments. If undefined the default mark price of the market is used. */ + readonly indexPriceConfiguration?: components['schemas']['vegaCompositePriceConfiguration']; /** @description Continuously compounded interest rate used in funding rate calculation, in the range [-1, 1]. */ readonly interestRate?: string; /** @description Controls how much the upcoming funding payment liability contributes to party's margin, in the range [0, 1]. */ @@ -2042,6 +2299,14 @@ export interface components { */ readonly probabilityOfTrading?: number; }; + /** + * @description Describes which property of the data source data is to be + * used for price source. + */ + readonly vegaSpecBindingForCompositePrice: { + /** @description The property name of price. */ + readonly priceSourceProperty?: string; + }; /** Spot product configuration */ readonly vegaSpotProduct: { /** @description Base asset ID. */ @@ -2172,6 +2437,8 @@ export interface components { readonly code?: string; /** @description Future. */ readonly future?: components['schemas']['vegaUpdateFutureProduct']; + /** Instrument name */ + readonly name?: string; /** @description Perpetual. */ readonly perpetual?: components['schemas']['vegaUpdatePerpetualProduct']; }; @@ -2188,6 +2455,10 @@ export interface components { readonly instrument?: components['schemas']['vegaUpdateInstrumentConfiguration']; /** @description Linear slippage factor is used to cap the slippage component of maintenance margin - it is applied to the slippage volume. */ readonly linearSlippageFactor?: string; + /** Liquidation strategy parameters */ + readonly liquidationStrategy?: components['schemas']['vegaLiquidationStrategy']; + /** @description Specifies how the liquidity fee for the market will be calculated. */ + readonly liquidityFeeSettings?: components['schemas']['vegaLiquidityFeeSettings']; /** @description Liquidity monitoring parameters. */ readonly liquidityMonitoringParameters?: components['schemas']['vegaLiquidityMonitoringParameters']; /** Liquidity SLA parameters */ @@ -2200,6 +2471,8 @@ export interface components { * price levels over which automated liquidity provisions will be deployed. */ readonly lpPriceRange?: string; + /** @description Mark price configuration. */ + readonly markPriceConfiguration?: components['schemas']['vegaCompositePriceConfiguration']; /** @description Optional futures market metadata, tags. */ readonly metadata?: readonly string[]; /** @description Price monitoring parameters. */ @@ -2238,6 +2511,14 @@ export interface components { readonly dataSourceSpecForSettlementData?: components['schemas']['vegaDataSourceDefinition']; /** @description Data source spec describing the data source for settlement schedule. */ readonly dataSourceSpecForSettlementSchedule?: components['schemas']['vegaDataSourceDefinition']; + /** @description Lower bound for the funding-rate such that the funding-rate will never be lower than this value. */ + readonly fundingRateLowerBound?: string; + /** @description Factor applied to funding-rates. This scales the impact that spot price deviations have on funding payments. */ + readonly fundingRateScalingFactor?: string; + /** @description Upper bound for the funding-rate such that the funding-rate will never be higher than this value. */ + readonly fundingRateUpperBound?: string; + /** @description Configuration for the index price used in funding payment calculation. */ + readonly indexPriceConfiguration?: components['schemas']['vegaCompositePriceConfiguration']; /** @description Continuously compounded interest rate used in funding rate calculation, in the range [-1, 1]. */ readonly interestRate?: string; /** @description Controls how much the upcoming funding payment liability contributes to party's margin, in the range [0, 1]. */ @@ -2258,6 +2539,8 @@ export interface components { }; /** Configuration to update a spot market on Vega */ readonly vegaUpdateSpotMarketConfiguration: { + /** @description Specifies how the liquidity fee for the market will be calculated. */ + readonly liquidityFeeSettings?: components['schemas']['vegaLiquidityFeeSettings']; /** @description Log normal risk model parameters, valid only if MODEL_LOG_NORMAL is selected. */ readonly logNormal?: components['schemas']['vegaLogNormalRiskModel']; /** @description Optional spot market metadata, tags. */ @@ -2356,19 +2639,14 @@ export interface operations { }; /** * List transactions - * @description List transactions from the Vega blockchain + * @description List transactions from the Vega blockchain from the newest to the oldest transactions. */ BlockExplorer_ListTransactions: { parameters: { query?: { - /** - * @description Number of transactions to be returned from the blockchain. - * This is deprecated, use first and last instead. - */ - limit?: number; - /** @description Optional cursor to paginate the request. */ + /** @description Cursor to paginate the request. It can be used in conjunction with the `after` cursor. */ before?: string; - /** @description Optional cursor to paginate the request. */ + /** @description Cursor to paginate the request. It can be used in conjunction with the `before` cursor. */ after?: string; /** @description Transaction command types filter, for listing transactions with specified command types. */ cmdTypes?: readonly string[]; @@ -2377,13 +2655,19 @@ export interface operations { /** @description Party IDs filter, can be sender or receiver. */ parties?: readonly string[]; /** - * @description Number of transactions to be returned from the blockchain. Use in conjunction with the `after` cursor to paginate forwards. - * On its own, this will return the first `first` transactions. + * @description Number of transactions to be returned from the blockchain. + * Use in conjunction with the `after` cursor to paginate forwards. Paginating forwards means toward the most recent + * transactions. + * It cannot be used in conjunction with the `before` cursor. + * On its own, this will return the `first` most recent transactions. */ first?: number; /** - * @description Number of transactions to be returned from the blockchain. Use in conjunction with the `before` cursor to paginate backwards. - * On its own, this will return the last `last` transactions. + * @description Number of transactions to be returned from the blockchain. + * Use in conjunction with the `before` cursor to paginate backwards. Paginating forwards means toward the least recent + * transactions. + * It cannot be used in conjunction with the `after` cursor. + * On its own, this will return the `last` oldest transactions. */ last?: number; }; From 508274268d07704410b8c6376aa3292d1535b9aa Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:54:16 +0200 Subject: [PATCH 2/7] fix(trading): show user inactive lose streak in remaining epochs (#5667) --- .../streaks/activity-streaks.spec.tsx | 11 +++++ .../streaks/activity-streaks.tsx | 44 ++++++++++++++++--- libs/i18n/src/locales/en/trading.json | 1 + .../src/use-network-params.ts | 2 + 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/apps/trading/components/rewards-container/streaks/activity-streaks.spec.tsx b/apps/trading/components/rewards-container/streaks/activity-streaks.spec.tsx index 1e6330649..7d4aeee39 100644 --- a/apps/trading/components/rewards-container/streaks/activity-streaks.spec.tsx +++ b/apps/trading/components/rewards-container/streaks/activity-streaks.spec.tsx @@ -1,6 +1,17 @@ import { render, screen } from '@testing-library/react'; import { ActivityStreak } from './activity-streaks'; +jest.mock('@vegaprotocol/network-parameters', () => ({ + ...jest.requireActual('@vegaprotocol/network-parameters'), + useNetworkParams: jest.fn(() => ({ + params: { + rewards_activityStreak_inactivityLimit: '3', + }, + loading: false, + error: null, + })), +})); + describe('ActivityStreak', () => { it('renders null when streak is not active', () => { const tiers: { diff --git a/apps/trading/components/rewards-container/streaks/activity-streaks.tsx b/apps/trading/components/rewards-container/streaks/activity-streaks.tsx index 5744762a5..1526e4321 100644 --- a/apps/trading/components/rewards-container/streaks/activity-streaks.tsx +++ b/apps/trading/components/rewards-container/streaks/activity-streaks.tsx @@ -3,6 +3,10 @@ import { useT } from '../../../lib/use-t'; import classNames from 'classnames'; import BigNumber from 'bignumber.js'; import type { PartyActivityStreak } from '@vegaprotocol/types'; +import { + NetworkParams, + useNetworkParams, +} from '@vegaprotocol/network-parameters'; export const safeProgress = ( i: number, @@ -69,7 +73,12 @@ export const ActivityStreak = ({ }) => { const t = useT(); const userTierIndex = useGetUserTier(tiers, streak?.activeFor); - + const { params } = useNetworkParams([ + NetworkParams.rewards_activityStreak_inactivityLimit, + ]); + const remaining = new BigNumber(params.rewards_activityStreak_inactivityLimit) + .minus(streak?.inactiveFor || 0) + .toNumber(); if (!tiers || tiers.length === 0) return null; const progressBarHeight = 'h-10'; @@ -203,12 +212,35 @@ export const ActivityStreak = ({ - {streak?.isActive && ( + {streak && ( - {t('userActive', '{{active}} trader: {{count}} epochs so far', { - active: streak?.isActive ? 'Active' : 'Inactive', - count: streak?.activeFor || 0, - })}{' '} + {streak.isActive + ? t( + 'userActive', + '{{active}} trader: {{count}} epochs so far', + { + active: 'Active', + count: streak.activeFor || 0, + } + ) + : remaining > 0 + ? t( + 'userInactive', + '{{active}} trader: {{count}} epochs so far, you will lose your streak in {{remaining}} epochs!', + { + active: 'Inactive', + count: streak.inactiveFor || 0, + remaining, + } + ) + : t( + 'userActive', + '{{active}} trader: {{count}} epochs so far', + { + active: 'Inactive', + count: streak.inactiveFor || 0, + } + )}{' '} {userTierIndex > 0 && new BigNumber( tiers[0].minimum_activity_streak diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index 4543c0f62..887212695 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -364,6 +364,7 @@ "totalCommission_one": "Total commission (<0>last {{count}} epoch)", "totalCommission_other": "Total commission (<0>last {{count}} epochs)", "userActive": "{{active}} trader: {{count}} epochs so far", + "userInactive": "{{active}} trader: {{count}} epochs so far, you will lose your streak in {{remaining}} epochs!", "volumeLastEpochs": "Volume (last {{count}} epochs)", "volumeLastEpochs_one": "Volume (last {{count}} epoch)", "volumeLastEpochs_other": "Volume (last {{count}} epochs)", diff --git a/libs/network-parameters/src/use-network-params.ts b/libs/network-parameters/src/use-network-params.ts index d90b0d833..de6dfb18b 100644 --- a/libs/network-parameters/src/use-network-params.ts +++ b/libs/network-parameters/src/use-network-params.ts @@ -9,6 +9,8 @@ export const NetworkParams = { blockchains_ethereumConfig: 'blockchains_ethereumConfig', reward_asset: 'reward_asset', rewards_activityStreak_benefitTiers: 'rewards_activityStreak_benefitTiers', + rewards_activityStreak_inactivityLimit: + 'rewards_activityStreak_inactivityLimit', rewards_vesting_benefitTiers: 'rewards_vesting_benefitTiers', rewards_marketCreationQuantumMultiple: 'rewards_marketCreationQuantumMultiple', From 8b91592b934642ff9049d524a335987e66b330d5 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 31 Jan 2024 11:42:29 +0000 Subject: [PATCH 3/7] chore(trading): update to preview 7 binaries (#5689) --- .github/workflows/console-test-run.yml | 2 +- apps/trading/e2e/.env | 2 +- apps/trading/e2e/tests/rewards/test_filtered_cards.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/console-test-run.yml b/.github/workflows/console-test-run.yml index 7060d4637..9943ab54c 100644 --- a/.github/workflows/console-test-run.yml +++ b/.github/workflows/console-test-run.yml @@ -205,7 +205,7 @@ jobs: # run tests #---------------------------------------------- - name: Run tests - run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 1 --dist loadfile --durations=45 + run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45 working-directory: apps/trading/e2e #---------------------------------------------- # upload traces diff --git a/apps/trading/e2e/.env b/apps/trading/e2e/.env index 7e13db6ac..f17582175 100644 --- a/apps/trading/e2e/.env +++ b/apps/trading/e2e/.env @@ -1,3 +1,3 @@ CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest -VEGA_VERSION=v0.74.0-preview.6 +VEGA_VERSION=v0.74.0-preview.7 LOCAL_SERVER=false diff --git a/apps/trading/e2e/tests/rewards/test_filtered_cards.py b/apps/trading/e2e/tests/rewards/test_filtered_cards.py index 7fddd7b24..78eecb3dc 100644 --- a/apps/trading/e2e/tests/rewards/test_filtered_cards.py +++ b/apps/trading/e2e/tests/rewards/test_filtered_cards.py @@ -7,7 +7,7 @@ from wallet_config import MM_WALLET, PARTY_A, PARTY_B from vega_sim.service import MarketStateUpdateType import vega_sim.api.governance as governance -@pytest.mark.skip("Skipping to unblock CI, working on fix") + @pytest.mark.usefixtures("risk_accepted", "auth") def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page): tDAI_asset_id = vega.find_asset_id(symbol="tDAI") From 1780f6fa7f4e93ef7bb45b66b7d5c97800d8a32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 31 Jan 2024 15:05:30 +0100 Subject: [PATCH 4/7] feat(trading): margin estimate update (#5664) Co-authored-by: Dariusz Majcherczyk Co-authored-by: daro-maj <119658839+daro-maj@users.noreply.github.com> --- .../deal_ticket/test_isolated_cross_margin.py | 2 +- ...test_trading_deal_ticket_submit_account.py | 1 - libs/accounts/src/lib/get-market-account.ts | 15 - libs/accounts/src/lib/index.ts | 3 +- libs/accounts/src/lib/margin-data-provider.ts | 22 + libs/accounts/src/lib/margin-health-chart.tsx | 253 ------------ .../src/lib/margin-heath-chart.spec.tsx | 158 -------- .../src/lib/use-margin-account-balance.tsx | 68 ++++ .../src/lib/use-market-account-balance.tsx | 41 -- .../deal-ticket/deal-ticket-fee-details.tsx | 375 +----------------- .../deal-ticket-margin-details.tsx | 68 +++- .../components/deal-ticket/deal-ticket.tsx | 90 +++-- .../deal-ticket/margin-mode-selector.tsx | 224 +++++++++-- libs/deal-ticket/src/constants.ts | 2 +- .../src/hooks/use-position-estimate.ts | 32 +- libs/i18n/src/locales/en/deal-ticket.json | 13 +- libs/positions/src/lib/Positions.graphql | 16 +- .../src/lib/__generated__/Positions.ts | 23 +- .../src/lib/estimate-position.mock.ts | 11 + .../src/lib/liquidation-price.spec.tsx | 33 +- libs/positions/src/lib/liquidation-price.tsx | 48 +-- .../src/lib/positions-data-providers.ts | 83 ++-- libs/positions/src/lib/positions-table.tsx | 97 +++-- libs/positions/src/lib/positions.mock.ts | 5 +- libs/positions/src/lib/use-open-volume.ts | 11 +- .../src/components/slider/leverage-slider.tsx | 5 +- 26 files changed, 616 insertions(+), 1083 deletions(-) delete mode 100644 libs/accounts/src/lib/get-market-account.ts delete mode 100644 libs/accounts/src/lib/margin-health-chart.tsx delete mode 100644 libs/accounts/src/lib/margin-heath-chart.spec.tsx create mode 100644 libs/accounts/src/lib/use-margin-account-balance.tsx delete mode 100644 libs/accounts/src/lib/use-market-account-balance.tsx diff --git a/apps/trading/e2e/tests/deal_ticket/test_isolated_cross_margin.py b/apps/trading/e2e/tests/deal_ticket/test_isolated_cross_margin.py index c592b2712..88db44a67 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_isolated_cross_margin.py +++ b/apps/trading/e2e/tests/deal_ticket/test_isolated_cross_margin.py @@ -38,7 +38,7 @@ def test_switch_cross_isolated_margin( expect(page.get_by_test_id("toast-content")).to_have_text( "ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x") expect(page.locator(margin_row).nth(1) - ).to_have_text("11,109.99996Isolated1.0x") + ).to_have_text("22,109.99996Isolated1.0x") # tbd - tooltip is not visible without this wait page.wait_for_timeout(1000) page.get_by_test_id(tab_positions).get_by_text("Isolated").hover() 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 e51d808ee..fc94d69b6 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 @@ -23,7 +23,6 @@ def continuous_market(vega): return setup_continuous_market(vega) -@pytest.mark.skip("marked id issue #5681") @pytest.mark.usefixtures("auth", "risk_accepted") def test_should_display_info_and_button_for_deposit(continuous_market, page: Page): page.goto(f"/#/markets/{continuous_market}") diff --git a/libs/accounts/src/lib/get-market-account.ts b/libs/accounts/src/lib/get-market-account.ts deleted file mode 100644 index 5a85f9a61..000000000 --- a/libs/accounts/src/lib/get-market-account.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Account } from './accounts-data-provider'; -import * as Schema from '@vegaprotocol/types'; - -interface Props { - accounts: Account[] | null; - marketId: string; -} - -export const getMarketAccount = ({ accounts, marketId }: Props) => - accounts?.find((account) => { - return ( - account.market?.id === marketId && - account.type === Schema.AccountType.ACCOUNT_TYPE_MARGIN - ); - }) || null; diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts index 99d751fdd..2c29fc7c5 100644 --- a/libs/accounts/src/lib/index.ts +++ b/libs/accounts/src/lib/index.ts @@ -6,8 +6,7 @@ export * from './accounts-manager'; export * from './breakdown-table'; export * from './use-account-balance'; export * from './get-settlement-account'; -export * from './use-market-account-balance'; +export * from './use-margin-account-balance'; export * from './__generated__/Margins'; -export { MarginHealthChart } from './margin-health-chart'; export * from './margin-data-provider'; export * from './transfer-container'; diff --git a/libs/accounts/src/lib/margin-data-provider.ts b/libs/accounts/src/lib/margin-data-provider.ts index 9ea84a417..b6cda8725 100644 --- a/libs/accounts/src/lib/margin-data-provider.ts +++ b/libs/accounts/src/lib/margin-data-provider.ts @@ -83,3 +83,25 @@ export const marketMarginDataProvider = makeDerivedDataProvider< (margin) => margin.market.id === marketId ) || null ); + +export type MarginModeData = Pick< + MarginFieldsFragment, + 'marginMode' | 'marginFactor' +>; + +export const marginModeDataProvider = makeDerivedDataProvider< + MarginModeData, + never, + MarginsQueryVariables & { marketId: string } +>([marketMarginDataProvider], ([data], variables, previousData) => + produce(previousData, (draft) => { + if (!data) { + return data; + } + const newData = { + marginMode: (data as MarginFieldsFragment).marginMode, + marginFactor: (data as MarginFieldsFragment).marginFactor, + }; + return draft ? Object.assign(draft, newData) : newData; + }) +); diff --git a/libs/accounts/src/lib/margin-health-chart.tsx b/libs/accounts/src/lib/margin-health-chart.tsx deleted file mode 100644 index fd32569e5..000000000 --- a/libs/accounts/src/lib/margin-health-chart.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import { marketMarginDataProvider } from './margin-data-provider'; -import { useAssetsMapProvider } from '@vegaprotocol/assets'; -import { useT, ns } from './use-t'; -import { useAccountBalance } from './use-account-balance'; -import { useMarketAccountBalance } from './use-market-account-balance'; -import { Trans } from 'react-i18next'; - -const MarginHealthChartTooltipRow = ({ - label, - value, - decimals, - href, -}: { - label: string; - value: string; - decimals: number; - href?: string; -}) => ( - <> -
- {href ? ( - - {label} - - ) : ( - label - )} -
-
- {addDecimalsFormatNumber(value, decimals)} -
- -); - -export const MarginHealthChartTooltip = ({ - maintenanceLevel, - searchLevel, - initialLevel, - collateralReleaseLevel, - decimals, - marginAccountBalance, -}: { - maintenanceLevel: string; - searchLevel: string; - initialLevel: string; - collateralReleaseLevel: string; - decimals: number; - marginAccountBalance?: string; -}) => { - const t = useT(); - const tooltipContent = [ - , - , - , - , - ]; - - if (marginAccountBalance) { - const balance = ( - - ); - if (BigInt(marginAccountBalance) < BigInt(searchLevel)) { - tooltipContent.splice(1, 0, balance); - } else if (BigInt(marginAccountBalance) < BigInt(initialLevel)) { - tooltipContent.splice(2, 0, balance); - } else if (BigInt(marginAccountBalance) < BigInt(collateralReleaseLevel)) { - tooltipContent.splice(3, 0, balance); - } else { - tooltipContent.push(balance); - } - } - return ( -
- {tooltipContent} -
- ); -}; - -export const MarginHealthChart = ({ - marketId, - assetId, -}: { - marketId: string; - assetId: string; -}) => { - const { data: assetsMap } = useAssetsMapProvider(); - const { pubKey: partyId } = useVegaWallet(); - const { data } = useDataProvider({ - dataProvider: marketMarginDataProvider, - variables: { marketId, partyId: partyId ?? '' }, - skip: !partyId, - }); - const { accountBalance: rawGeneralAccountBalance } = - useAccountBalance(assetId); - const { accountBalance: rawMarginAccountBalance } = - useMarketAccountBalance(marketId); - const asset = assetsMap && assetsMap[assetId]; - if (!data || !asset) { - return null; - } - const { decimals } = asset; - - const collateralReleaseLevel = Number(data.collateralReleaseLevel); - const initialLevel = Number(data.initialLevel); - const maintenanceLevel = Number(data.maintenanceLevel); - const searchLevel = Number(data.searchLevel); - const marginAccountBalance = Number(rawMarginAccountBalance); - const generalAccountBalance = Number(rawGeneralAccountBalance); - const max = Math.max( - marginAccountBalance + generalAccountBalance, - collateralReleaseLevel - ); - - const red = maintenanceLevel / max; - const orange = (searchLevel - maintenanceLevel) / max; - const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max; - const green = (collateralReleaseLevel - initialLevel) / max + yellow; - const balanceMarker = marginAccountBalance / max; - - const tooltip = ( - - ); - - return ( -
- - maintenance level - , - ]} - values={{ - balance: addDecimalsFormatNumber( - ( - BigInt(marginAccountBalance) - BigInt(maintenanceLevel) - ).toString(), - decimals - ), - }} - ns={ns} - /> - -
-
-
-
-
- {balanceMarker > 0 && balanceMarker < 100 && ( -
- )} -
-
-
- ); -}; diff --git a/libs/accounts/src/lib/margin-heath-chart.spec.tsx b/libs/accounts/src/lib/margin-heath-chart.spec.tsx deleted file mode 100644 index 18e232933..000000000 --- a/libs/accounts/src/lib/margin-heath-chart.spec.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { - MarginHealthChart, - MarginHealthChartTooltip, -} from './margin-health-chart'; -import { act, render, screen } from '@testing-library/react'; -import type { MarginFieldsFragment } from './__generated__/Margins'; -import type { AssetFieldsFragment } from '@vegaprotocol/assets'; -import { MarginMode } from '@vegaprotocol/types'; - -const asset: AssetFieldsFragment = { - id: 'assetId', - decimals: 2, -} as AssetFieldsFragment; -const margins: MarginFieldsFragment = { - asset: { - id: 'assetId', - }, - collateralReleaseLevel: '1000', - initialLevel: '800', - searchLevel: '600', - maintenanceLevel: '400', - marginFactor: '', - marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, - orderMarginLevel: '', - market: { - id: 'marketId', - }, -}; - -const mockGetMargins = jest.fn(() => margins); -const mockGetBalance = jest.fn(() => '0'); - -jest.mock('./margin-data-provider', () => ({})); - -jest.mock('@vegaprotocol/assets', () => ({ - useAssetsMapProvider: () => { - return { - data: { - assetId: asset, - }, - }; - }, -})); - -jest.mock('@vegaprotocol/wallet', () => ({ - useVegaWallet: () => { - return { - pubKey: 'partyId', - }; - }, -})); - -jest.mock('@vegaprotocol/data-provider', () => ({ - useDataProvider: () => { - return { - data: mockGetMargins(), - }; - }, -})); - -jest.mock('./use-account-balance', () => ({ - useAccountBalance: () => { - return { - accountBalance: mockGetBalance(), - }; - }, -})); - -jest.mock('./use-market-account-balance', () => ({ - useMarketAccountBalance: () => { - return { - accountBalance: '700', - }; - }, -})); - -describe('MarginHealthChart', () => { - it('should render correct values', async () => { - render(); - const chart = screen.getByTestId('margin-health-chart'); - expect(chart).toHaveTextContent('3.00 above maintenance level'); - const red = screen.getByTestId('margin-health-chart-red'); - const orange = screen.getByTestId('margin-health-chart-orange'); - const yellow = screen.getByTestId('margin-health-chart-yellow'); - const green = screen.getByTestId('margin-health-chart-green'); - const balance = screen.getByTestId('margin-health-chart-balance'); - expect(parseInt(red.style.width)).toBe(40); - expect(parseInt(orange.style.width)).toBe(20); - expect(parseInt(yellow.style.width)).toBe(10); - expect(parseInt(green.style.width)).toBe(30); - expect(parseInt(balance.style.left)).toBe(70); - }); - - it('should use correct scale', async () => { - mockGetBalance.mockReturnValueOnce('1300'); - await act(async () => { - render(); - }); - await screen.findByTestId('margin-health-chart'); - const red = screen.getByTestId('margin-health-chart-red'); - expect(parseInt(red.style.width)).toBe(20); - }); -}); - -describe('MarginHealthChartTooltip', () => { - it('renders correct values and labels', async () => { - await act(async () => { - render( - - ); - }); - const labels = await screen.findAllByTestId('margin-health-tooltip-label'); - const expectedLabels = [ - 'maintenance level', - 'balance', - 'search level', - 'initial level', - 'release level', - ]; - labels.forEach((value, i) => { - expect(value).toHaveTextContent(expectedLabels[i]); - }); - const values = await screen.findAllByTestId('margin-health-tooltip-value'); - const expectedValues = ['4.00', '5.00', '6.00', '8.00', '10.00']; - values.forEach((value, i) => { - expect(value).toHaveTextContent(expectedValues[i]); - }); - }); - - it('renders balance in correct place', async () => { - const { rerender } = render( - - ); - - let values = await screen.findAllByTestId('margin-health-tooltip-value'); - expect(values[2]).toHaveTextContent('7.00'); - - rerender( - - ); - - values = await screen.findAllByTestId('margin-health-tooltip-value'); - expect(values.length).toBe(5); - expect(values[3]).toHaveTextContent('9.00'); - }); -}); diff --git a/libs/accounts/src/lib/use-margin-account-balance.tsx b/libs/accounts/src/lib/use-margin-account-balance.tsx new file mode 100644 index 000000000..87403b2a0 --- /dev/null +++ b/libs/accounts/src/lib/use-margin-account-balance.tsx @@ -0,0 +1,68 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useDataProvider } from '@vegaprotocol/data-provider'; +import { accountsDataProvider } from './accounts-data-provider'; +import type { Account } from './accounts-data-provider'; +import { AccountType } from '@vegaprotocol/types'; + +export const useMarginAccountBalance = (marketId: string) => { + const { pubKey } = useVegaWallet(); + const [marginAccountBalance, setMarginAccountBalance] = useState(''); + const [orderMarginAccountBalance, setOrderMarginAccountBalance] = + useState(''); + const [accountDecimals, setAccountDecimals] = useState(null); + const update = useCallback( + ({ data }: { data: Account[] | null }) => { + const marginAccount = data?.find((account) => { + return ( + account.market?.id === marketId && + account.type === AccountType.ACCOUNT_TYPE_MARGIN + ); + }); + const orderMarginAccount = data?.find((account) => { + return ( + account.market?.id === marketId && + account.type === AccountType.ACCOUNT_TYPE_ORDER_MARGIN + ); + }); + if (marginAccount?.balance) { + setMarginAccountBalance(marginAccount?.balance || ''); + } + if (orderMarginAccount?.balance) { + setOrderMarginAccountBalance(orderMarginAccount?.balance || ''); + } + + const decimals = + orderMarginAccount?.asset.decimals || marginAccount?.asset.decimals; + if (decimals) { + setAccountDecimals(decimals); + } + return true; + }, + [marketId] + ); + const { loading, error } = useDataProvider({ + dataProvider: accountsDataProvider, + variables: { partyId: pubKey || '' }, + skip: !pubKey || !marketId, + update, + }); + + return useMemo( + () => ({ + marginAccountBalance: pubKey ? marginAccountBalance : '', + orderMarginAccountBalance: pubKey ? orderMarginAccountBalance : '', + accountDecimals: pubKey ? accountDecimals : null, + loading, + error, + }), + [ + marginAccountBalance, + orderMarginAccountBalance, + accountDecimals, + pubKey, + loading, + error, + ] + ); +}; diff --git a/libs/accounts/src/lib/use-market-account-balance.tsx b/libs/accounts/src/lib/use-market-account-balance.tsx deleted file mode 100644 index e13877dbb..000000000 --- a/libs/accounts/src/lib/use-market-account-balance.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import { accountsDataProvider } from './accounts-data-provider'; -import type { Account } from './accounts-data-provider'; -import { getMarketAccount } from './get-market-account'; - -export const useMarketAccountBalance = (marketId: string) => { - const { pubKey } = useVegaWallet(); - const [accountBalance, setAccountBalance] = useState(''); - const [accountDecimals, setAccountDecimals] = useState(null); - const update = useCallback( - ({ data }: { data: Account[] | null }) => { - const account = getMarketAccount({ accounts: data, marketId }); - if (account?.balance) { - setAccountBalance(account?.balance || ''); - } - if (account?.asset.decimals) { - setAccountDecimals(account?.asset.decimals || null); - } - return true; - }, - [marketId] - ); - const { loading, error } = useDataProvider({ - dataProvider: accountsDataProvider, - variables: { partyId: pubKey || '' }, - skip: !pubKey || !marketId, - update, - }); - - return useMemo( - () => ({ - accountBalance: pubKey ? accountBalance : '', - accountDecimals: pubKey ? accountDecimals : null, - loading, - error, - }), - [accountBalance, accountDecimals, pubKey, loading, error] - ); -}; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx index 739d5ce14..bcefdb6e5 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx @@ -1,47 +1,14 @@ -import { useCallback, useState } from 'react'; -import { getAsset, getQuoteName } from '@vegaprotocol/markets'; +import { getAsset } from '@vegaprotocol/markets'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; -import { useVegaWallet } from '@vegaprotocol/wallet'; - import type { Market } from '@vegaprotocol/markets'; -import type { EstimatePositionQuery } from '@vegaprotocol/positions'; -import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; - -import { - formatNumberPercentage, - formatRange, - formatValue, -} from '@vegaprotocol/utils'; -import { marketMarginDataProvider } from '@vegaprotocol/accounts'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import * as AccordionPrimitive from '@radix-ui/react-accordion'; -import * as Schema from '@vegaprotocol/types'; - -import { - MARGIN_DIFF_TOOLTIP_TEXT, - DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT, - TOTAL_MARGIN_AVAILABLE, - LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, - EST_TOTAL_MARGIN_TOOLTIP_TEXT, - MARGIN_ACCOUNT_TOOLTIP_TEXT, -} from '../../constants'; +import { formatNumberPercentage, formatValue } from '@vegaprotocol/utils'; import { useEstimateFees } from '../../hooks/use-estimate-fees'; import { KeyValue } from './key-value'; -import { - Accordion, - AccordionChevron, - AccordionPanel, - Intent, - ExternalLink, - Pill, - Tooltip, -} from '@vegaprotocol/ui-toolkit'; -import classNames from 'classnames'; +import { Intent, Pill } from '@vegaprotocol/ui-toolkit'; import BigNumber from 'bignumber.js'; import { FeesBreakdown } from '../fees-breakdown'; import { getTotalDiscountFactor, getDiscountedFee } from '../discounts'; -import { useT, ns } from '../../use-t'; -import { Trans } from 'react-i18next'; +import { useT } from '../../use-t'; export const emptyValue = '-'; @@ -119,337 +86,3 @@ export const DealTicketFeeDetails = ({ /> ); }; - -export interface DealTicketMarginDetailsProps { - generalAccountBalance?: string; - marginAccountBalance?: string; - market: Market; - onMarketClick?: (marketId: string, metaKey?: boolean) => void; - assetSymbol: string; - positionEstimate: EstimatePositionQuery['estimatePosition']; - side: Schema.Side; -} - -export const DealTicketMarginDetails = ({ - marginAccountBalance, - generalAccountBalance, - assetSymbol, - market, - onMarketClick, - positionEstimate, - side, -}: DealTicketMarginDetailsProps) => { - const t = useT(); - const [breakdownDialog, setBreakdownDialog] = useState(false); - const { pubKey: partyId } = useVegaWallet(); - const { data: currentMargins } = useDataProvider({ - dataProvider: marketMarginDataProvider, - variables: { marketId: market.id, partyId: partyId || '' }, - skip: !partyId, - }); - const liquidationEstimate = positionEstimate?.liquidation; - const marginEstimate = positionEstimate?.margin; - const totalBalance = - BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0'); - const asset = getAsset(market); - const { decimals: assetDecimals, quantum } = asset; - let marginRequiredBestCase: string | undefined = undefined; - let marginRequiredWorstCase: string | undefined = undefined; - if (marginEstimate) { - if (currentMargins) { - marginRequiredBestCase = ( - BigInt(marginEstimate.bestCase.initialLevel) - - BigInt(currentMargins.initialLevel) - ).toString(); - if (marginRequiredBestCase.startsWith('-')) { - marginRequiredBestCase = '0'; - } - marginRequiredWorstCase = ( - BigInt(marginEstimate.worstCase.initialLevel) - - BigInt(currentMargins.initialLevel) - ).toString(); - if (marginRequiredWorstCase.startsWith('-')) { - marginRequiredWorstCase = '0'; - } - } else { - marginRequiredBestCase = marginEstimate.bestCase.initialLevel; - marginRequiredWorstCase = marginEstimate.worstCase.initialLevel; - } - } - - const totalMarginAvailable = ( - currentMargins - ? totalBalance - BigInt(currentMargins.maintenanceLevel) - : totalBalance - ).toString(); - - let deductionFromCollateral = null; - let projectedMargin = null; - if (marginAccountBalance) { - const deductionFromCollateralBestCase = - BigInt(marginEstimate?.bestCase.initialLevel ?? 0) - - BigInt(marginAccountBalance); - - const deductionFromCollateralWorstCase = - BigInt(marginEstimate?.worstCase.initialLevel ?? 0) - - BigInt(marginAccountBalance); - - deductionFromCollateral = ( - 0 - ? deductionFromCollateralBestCase.toString() - : '0', - deductionFromCollateralWorstCase > 0 - ? deductionFromCollateralWorstCase.toString() - : '0', - assetDecimals - )} - formattedValue={formatValue( - deductionFromCollateralWorstCase > 0 - ? deductionFromCollateralWorstCase.toString() - : '0', - assetDecimals, - quantum - )} - symbol={assetSymbol} - labelDescription={t( - 'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT', - DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT, - { assetSymbol } - )} - /> - ); - projectedMargin = ( - - ); - } - - let liquidationPriceEstimate = emptyValue; - let liquidationPriceEstimateRange = emptyValue; - - if (liquidationEstimate) { - const liquidationEstimateBestCaseIncludingBuyOrders = BigInt( - liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '') - ); - const liquidationEstimateBestCaseIncludingSellOrders = BigInt( - liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '') - ); - const liquidationEstimateBestCase = - side === Schema.Side.SIDE_BUY - ? liquidationEstimateBestCaseIncludingBuyOrders - : liquidationEstimateBestCaseIncludingSellOrders; - - const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt( - liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '') - ); - const liquidationEstimateWorstCaseIncludingSellOrders = BigInt( - liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '') - ); - const liquidationEstimateWorstCase = - side === Schema.Side.SIDE_BUY - ? liquidationEstimateWorstCaseIncludingBuyOrders - : liquidationEstimateWorstCaseIncludingSellOrders; - - liquidationPriceEstimate = formatValue( - liquidationEstimateWorstCase.toString(), - market.decimalPlaces, - undefined, - market.decimalPlaces - ); - liquidationPriceEstimateRange = formatRange( - (liquidationEstimateBestCase < liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - (liquidationEstimateBestCase > liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - market.decimalPlaces, - undefined, - market.decimalPlaces - ); - } - - const onAccountBreakdownDialogClose = useCallback( - () => setBreakdownDialog(false), - [] - ); - - const quoteName = getQuoteName(market); - - return ( -
- - -
-
- - {t('Margin required')} - - - -
- -
- {formatValue( - marginRequiredWorstCase, - assetDecimals, - quantum - )}{' '} - {assetSymbol || ''} -
-
-
- - } - > -
- - {deductionFromCollateral} - setBreakdownDialog(true) - : undefined - } - value={formatValue(marginAccountBalance, assetDecimals)} - symbol={assetSymbol} - labelDescription={t( - 'MARGIN_ACCOUNT_TOOLTIP_TEXT', - MARGIN_ACCOUNT_TOOLTIP_TEXT - )} - formattedValue={formatValue( - marginAccountBalance, - assetDecimals, - quantum - )} - /> -
-
-
- {projectedMargin} - - - {t( - 'LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT', - LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT - )} - {' '} - - - liquidation price estimate documentation - , - ]} - ns={ns} - /> - - - } - /> - {partyId && ( - - )} -
- ); -}; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx index ac8c0f4a7..d0094c3ff 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx @@ -26,12 +26,25 @@ import { import classNames from 'classnames'; import { useT, ns } from '../../use-t'; import { Trans } from 'react-i18next'; -import type { DealTicketMarginDetailsProps } from './deal-ticket-fee-details'; +import type { Market } from '@vegaprotocol/markets'; import { emptyValue } from './deal-ticket-fee-details'; +import type { EstimatePositionQuery } from '@vegaprotocol/positions'; + +export interface DealTicketMarginDetailsProps { + generalAccountBalance?: string; + marginAccountBalance?: string; + orderMarginAccountBalance?: string; + market: Market; + onMarketClick?: (marketId: string, metaKey?: boolean) => void; + assetSymbol: string; + positionEstimate: EstimatePositionQuery['estimatePosition']; + side: Schema.Side; +} export const DealTicketMarginDetails = ({ marginAccountBalance, generalAccountBalance, + orderMarginAccountBalance, assetSymbol, market, onMarketClick, @@ -48,31 +61,44 @@ export const DealTicketMarginDetails = ({ }); const liquidationEstimate = positionEstimate?.liquidation; const marginEstimate = positionEstimate?.margin; + const totalMarginAccountBalance = + BigInt(marginAccountBalance || '0') + + BigInt(orderMarginAccountBalance || '0'); const totalBalance = - BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0'); + BigInt(generalAccountBalance || '0') + totalMarginAccountBalance; const asset = getAsset(market); const { decimals: assetDecimals, quantum } = asset; let marginRequiredBestCase: string | undefined = undefined; let marginRequiredWorstCase: string | undefined = undefined; + const marginEstimateBestCase = + BigInt(marginEstimate?.bestCase.initialLevel ?? 0) + + BigInt(marginEstimate?.bestCase.orderMarginLevel ?? 0); + const marginEstimateWorstCase = + BigInt(marginEstimate?.worstCase.initialLevel ?? 0) + + BigInt(marginEstimate?.worstCase.orderMarginLevel ?? 0); if (marginEstimate) { if (currentMargins) { + const currentMargin = + BigInt(currentMargins.initialLevel) + + BigInt(currentMargins.orderMarginLevel); + marginRequiredBestCase = ( - BigInt(marginEstimate.bestCase.initialLevel) - - BigInt(currentMargins.initialLevel) + marginEstimateBestCase - currentMargin ).toString(); if (marginRequiredBestCase.startsWith('-')) { marginRequiredBestCase = '0'; } + marginRequiredWorstCase = ( - BigInt(marginEstimate.worstCase.initialLevel) - - BigInt(currentMargins.initialLevel) + marginEstimateWorstCase - currentMargin ).toString(); + if (marginRequiredWorstCase.startsWith('-')) { marginRequiredWorstCase = '0'; } } else { - marginRequiredBestCase = marginEstimate.bestCase.initialLevel; - marginRequiredWorstCase = marginEstimate.worstCase.initialLevel; + marginRequiredBestCase = marginEstimateBestCase.toString(); + marginRequiredWorstCase = marginEstimateWorstCase.toString(); } } @@ -84,14 +110,12 @@ export const DealTicketMarginDetails = ({ let deductionFromCollateral = null; let projectedMargin = null; - if (marginAccountBalance) { + if (totalMarginAccountBalance) { const deductionFromCollateralBestCase = - BigInt(marginEstimate?.bestCase.initialLevel ?? 0) - - BigInt(marginAccountBalance); + marginEstimateBestCase - totalMarginAccountBalance; const deductionFromCollateralWorstCase = - BigInt(marginEstimate?.worstCase.initialLevel ?? 0) - - BigInt(marginAccountBalance); + marginEstimateWorstCase - totalMarginAccountBalance; deductionFromCollateral = ( setBreakdownDialog(true) : undefined } - value={formatValue(marginAccountBalance, assetDecimals)} + value={formatValue( + totalMarginAccountBalance.toString(), + assetDecimals + )} symbol={assetSymbol} labelDescription={t( 'MARGIN_ACCOUNT_TOOLTIP_TEXT', MARGIN_ACCOUNT_TOOLTIP_TEXT )} formattedValue={formatValue( - marginAccountBalance, + totalMarginAccountBalance.toString(), assetDecimals, quantum )} diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index ee3e9abcf..e5e558308 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -58,8 +58,9 @@ import type { } from '@vegaprotocol/markets'; import { MarginWarning } from '../deal-ticket-validation/margin-warning'; import { - useMarketAccountBalance, + useMarginAccountBalance, useAccountBalance, + marginModeDataProvider, } from '@vegaprotocol/accounts'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { type OrderFormValues } from '../../hooks'; @@ -166,9 +167,10 @@ export const DealTicket = ({ const asset = getAsset(market); const { - accountBalance: marginAccountBalance, + orderMarginAccountBalance, + marginAccountBalance, loading: loadingMarginAccountBalance, - } = useMarketAccountBalance(market.id); + } = useMarginAccountBalance(market.id); const { accountBalance: generalAccountBalance, @@ -176,7 +178,9 @@ export const DealTicket = ({ } = useAccountBalance(asset.id); const balance = ( - BigInt(marginAccountBalance) + BigInt(generalAccountBalance) + BigInt(marginAccountBalance) + + BigInt(generalAccountBalance) + + BigInt(orderMarginAccountBalance) ).toString(); const { marketState, marketTradingMode } = marketData; @@ -241,7 +245,19 @@ export const DealTicket = ({ variables: { partyId: pubKey || '', marketId: market.id }, skip: !pubKey, }); - const openVolume = useOpenVolume(pubKey, market.id) ?? '0'; + const { data: margin } = useDataProvider({ + dataProvider: marginModeDataProvider, + variables: { partyId: pubKey || '', marketId: market.id }, + skip: !pubKey, + }); + + const { openVolume, averageEntryPrice } = useOpenVolume( + pubKey, + market.id + ) || { + openVolume: '0', + averageEntryPrice: '0', + }; const orders = activeOrders ? activeOrders.map((order) => ({ isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET, @@ -259,21 +275,25 @@ export const DealTicket = ({ }); } - const positionEstimate = usePositionEstimate({ - marketId: market.id, - openVolume, - orders, - marginAccountBalance: marginAccountBalance, - generalAccountBalance: generalAccountBalance, - orderMarginAccountBalance: '0', // TODO: Get real balance - marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, // TODO: unhardcode this and get users margin mode for the market - averageEntryPrice: marketPrice || '0', // TODO: This assumes the order will be entirely filled at the current market price - skip: - !normalizedOrder || + const positionEstimate = usePositionEstimate( + { + marketId: market.id, + openVolume, + averageEntryPrice, + orders, + marginAccountBalance: marginAccountBalance || '0', + generalAccountBalance: generalAccountBalance || '0', + orderMarginAccountBalance: orderMarginAccountBalance || '0', + marginFactor: margin?.marginFactor || '1', + marginMode: + margin?.marginMode || Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, + includeCollateralIncreaseInAvailableCollateral: true, + }, + !normalizedOrder || (normalizedOrder.type !== Schema.OrderType.TYPE_MARKET && (!normalizedOrder.price || normalizedOrder.price === '0')) || - normalizedOrder.size === '0', - }); + normalizedOrder.size === '0' + ); const assetSymbol = getAsset(market).symbol; @@ -319,7 +339,9 @@ export const DealTicket = ({ } const hasNoBalance = - !BigInt(generalAccountBalance) && !BigInt(marginAccountBalance); + !BigInt(generalAccountBalance) && + !BigInt(marginAccountBalance) && + !BigInt(orderMarginAccountBalance); if ( hasNoBalance && !(loadingMarginAccountBalance || loadingGeneralAccountBalance) @@ -349,6 +371,7 @@ export const DealTicket = ({ marketTradingMode, generalAccountBalance, marginAccountBalance, + orderMarginAccountBalance, loadingMarginAccountBalance, loadingGeneralAccountBalance, pubKey, @@ -707,10 +730,16 @@ export const DealTicket = ({ asset={asset} marketTradingMode={marketData.marketTradingMode} balance={balance} - margin={ - positionEstimate?.estimatePosition?.margin.bestCase.initialLevel || - '0' - } + margin={( + BigInt( + positionEstimate?.estimatePosition?.margin.bestCase.initialLevel || + '0' + ) + + BigInt( + positionEstimate?.estimatePosition?.margin.bestCase + .orderMarginLevel || '0' + ) + ).toString()} isReadOnly={isReadOnly} pubKey={pubKey} onDeposit={onDeposit} @@ -743,6 +772,7 @@ export const DealTicket = ({ onMarketClick={onMarketClick} assetSymbol={asset.symbol} marginAccountBalance={marginAccountBalance} + orderMarginAccountBalance={orderMarginAccountBalance} generalAccountBalance={generalAccountBalance} positionEstimate={positionEstimate?.estimatePosition} market={market} @@ -768,8 +798,20 @@ interface SummaryMessageProps { export const NoWalletWarning = ({ isReadOnly, -}: Pick) => { + noWalletConnected, +}: Pick & { + noWalletConnected?: boolean; +}) => { const t = useT(); + if (noWalletConnected) { + return ( +
+ + {t('You need a Vega wallet to start trading on this market')} + +
+ ); + } if (isReadOnly) { return (
diff --git a/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx index 5373c9551..6abae606b 100644 --- a/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx @@ -1,9 +1,12 @@ import { useDataProvider } from '@vegaprotocol/data-provider'; +import * as Schema from '@vegaprotocol/types'; import { TradingButton as Button, TradingInput as Input, FormGroup, LeverageSlider, + Notification, + Intent, } from '@vegaprotocol/ui-toolkit'; import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet'; import * as Types from '@vegaprotocol/types'; @@ -15,15 +18,151 @@ import { Dialog } from '@vegaprotocol/ui-toolkit'; import { useEffect, useState } from 'react'; import { useT } from '../../use-t'; import classnames from 'classnames'; -import { marketMarginDataProvider } from '@vegaprotocol/accounts'; -import { useMaxLeverage } from '@vegaprotocol/positions'; +import { + marginModeDataProvider, + useAccountBalance, + useMarginAccountBalance, +} from '@vegaprotocol/accounts'; +import { useMaxLeverage, useOpenVolume } from '@vegaprotocol/positions'; +import { activeOrdersProvider } from '@vegaprotocol/orders'; +import { usePositionEstimate } from '../../hooks/use-position-estimate'; +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; +import { getAsset, useMarket } from '@vegaprotocol/markets'; +import { NoWalletWarning } from './deal-ticket'; const defaultLeverage = 10; + +export const MarginChange = ({ + partyId, + marketId, + marginMode, + marginFactor, +}: { + partyId: string | null; + marketId: string; + marginMode: Types.MarginMode; + marginFactor: string; +}) => { + const t = useT(); + const { data: market } = useMarket(marketId); + const asset = market && getAsset(market); + const { + marginAccountBalance, + orderMarginAccountBalance, + loading: marginAccountBalanceLoading, + } = useMarginAccountBalance(marketId); + const { + accountBalance: generalAccountBalance, + loading: generalAccountBalanceLoading, + } = useAccountBalance(asset?.id); + const { openVolume, averageEntryPrice } = useOpenVolume( + partyId, + marketId + ) || { + openVolume: '0', + averageEntryPrice: '0', + }; + const { data: activeOrders } = useDataProvider({ + dataProvider: activeOrdersProvider, + variables: { partyId: partyId || '', marketId }, + }); + const orders = activeOrders + ? activeOrders.map((order) => ({ + isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET, + price: order.price, + remaining: order.remaining, + side: order.side, + })) + : []; + const skip = + (!orders?.length && openVolume === '0') || + marginAccountBalanceLoading || + generalAccountBalanceLoading; + const estimateMargin = usePositionEstimate( + { + generalAccountBalance: generalAccountBalance || '0', + marginAccountBalance: marginAccountBalance || '0', + marginFactor, + marginMode, + averageEntryPrice, + openVolume, + marketId, + orderMarginAccountBalance: orderMarginAccountBalance || '0', + includeCollateralIncreaseInAvailableCollateral: true, + orders, + }, + skip + ); + if ( + !asset || + !estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase || + estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0' + ) { + return null; + } + const collateralIncreaseEstimate = BigInt( + estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase + ); + if (!collateralIncreaseEstimate) { + return null; + } + let positionWarning = ''; + if (orders?.length && openVolume !== '0') { + positionWarning = t( + 'youHaveOpenPositionAndOrders', + 'You have an existing position and open orders on this market.', + { + count: orders.length, + } + ); + } else if (!orders?.length) { + positionWarning = t('You have an existing position on this market.'); + } else { + positionWarning = t( + 'youHaveOpenOrders', + 'You have open orders on this market.', + { + count: orders.length, + } + ); + } + let marginChangeWarning = ''; + const amount = addDecimalsFormatNumber( + collateralIncreaseEstimate.toString(), + asset?.decimals + ); + const { symbol } = asset; + const interpolation = { amount, symbol }; + if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) { + marginChangeWarning = t( + 'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.', + interpolation + ); + } else { + marginChangeWarning = t( + 'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.', + interpolation + ); + } + return ( +
+ +

{positionWarning}

+

{marginChangeWarning}

+ + } + /> +
+ ); +}; + interface MarginDialogProps { open: boolean; onClose: () => void; marketId: string; - partyId: string; create: VegaTransactionStore['create']; } @@ -33,6 +172,7 @@ const CrossMarginModeDialog = ({ marketId, create, }: MarginDialogProps) => { + const { pubKey: partyId, isReadOnly } = useVegaWallet(); const t = useT(); return (
+ +
{ - create({ - updateMarginMode: { - marketId, - mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN, - marginFactor: `${1 / leverage}`, - }, - }); + partyId && + !isReadOnly && + create({ + updateMarginMode: { + marketId, + mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN, + marginFactor: `${1 / leverage}`, + }, + }); onClose(); }} > @@ -144,7 +295,7 @@ const IsolatedMarginModeDialog = ({ setLeverage(value)} /> @@ -154,10 +305,17 @@ const IsolatedMarginModeDialog = ({ min={1} max={max} step={0.1} - value={leverage} + value={leverage || ''} onChange={(e) => setLeverage(Number(e.target.value))} /> + + @@ -169,27 +327,21 @@ const IsolatedMarginModeDialog = ({ export const MarginModeSelector = ({ marketId }: { marketId: string }) => { const t = useT(); const [dialog, setDialog] = useState<'cross' | 'isolated' | ''>(); - const { pubKey: partyId, isReadOnly } = useVegaWallet(); + const { pubKey: partyId } = useVegaWallet(); const { data: margin } = useDataProvider({ - dataProvider: marketMarginDataProvider, + dataProvider: marginModeDataProvider, variables: { partyId: partyId || '', marketId, }, skip: !partyId, }); - useEffect(() => { - if (!partyId) { - setDialog(''); - } - }, [partyId]); const create = useVegaTransactionStore((state) => state.create); const marginMode = margin?.marginMode; const marginFactor = margin?.marginFactor && margin?.marginFactor !== '0' ? margin?.marginFactor : undefined; - const disabled = isReadOnly; const onClose = () => setDialog(undefined); const enabledModeClassName = 'bg-vega-clight-500 dark:bg-vega-cdark-500'; @@ -197,8 +349,8 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => { <>
- {partyId && ( + { - )} - {partyId && ( + } + { - )} + } ); }; diff --git a/libs/deal-ticket/src/constants.ts b/libs/deal-ticket/src/constants.ts index 356ed4ebb..a8d80fa7e 100644 --- a/libs/deal-ticket/src/constants.ts +++ b/libs/deal-ticket/src/constants.ts @@ -8,7 +8,7 @@ export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT = 'To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.'; export const TOTAL_MARGIN_AVAILABLE = - 'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).'; + 'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).'; export const CONTRACTS_MARGIN_TOOLTIP_TEXT = 'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'; diff --git a/libs/deal-ticket/src/hooks/use-position-estimate.ts b/libs/deal-ticket/src/hooks/use-position-estimate.ts index 98ed0a619..ac43a5f27 100644 --- a/libs/deal-ticket/src/hooks/use-position-estimate.ts +++ b/libs/deal-ticket/src/hooks/use-position-estimate.ts @@ -5,37 +5,15 @@ import { import { useEstimatePositionQuery } from '@vegaprotocol/positions'; import { useEffect, useState } from 'react'; -interface PositionEstimateProps extends EstimatePositionQueryVariables { - skip: boolean; -} - -export const usePositionEstimate = ({ - marketId, - openVolume, - orders, - generalAccountBalance, - marginAccountBalance, - orderMarginAccountBalance, - averageEntryPrice, - marginMode, - marginFactor, - skip, -}: PositionEstimateProps) => { +export const usePositionEstimate = ( + variables: EstimatePositionQueryVariables, + skip: boolean +) => { const [estimates, setEstimates] = useState( undefined ); const { data } = useEstimatePositionQuery({ - variables: { - marketId, - openVolume, - orders, - generalAccountBalance, - marginAccountBalance, - orderMarginAccountBalance, - averageEntryPrice, - marginMode, - marginFactor, - }, + variables, skip, fetchPolicy: 'no-cache', }); diff --git a/libs/i18n/src/locales/en/deal-ticket.json b/libs/i18n/src/locales/en/deal-ticket.json index c90019b15..ebf37c430 100644 --- a/libs/i18n/src/locales/en/deal-ticket.json +++ b/libs/i18n/src/locales/en/deal-ticket.json @@ -13,6 +13,8 @@ "Any orders placed now will not trade until the auction ends": "Any orders placed now will not trade until the auction ends", "below": "below", "Cancel": "Cancel", + "Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.", + "Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.", "Closed": "Closed", "Closing on {{time}}": "Closing on {{time}}", "Confirm": "Confirm", @@ -67,6 +69,13 @@ "One cancels another": "One cancels another", "Only limit orders are permitted when market is in auction": "Only limit orders are permitted when market is in auction", "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.": "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.", + "You have an existing position on this market.": "You have an existing position on this market.", + "youHaveOpenOrders_one": "You have an open order on this market.", + "youHaveOpenOrders_other": "You have open orders on this market.", + "youHaveOpenOrders": "You have open orders on this market.", + "youHaveOpenPositionAndOrders_one": "You have an existing position and and open order on this market.", + "youHaveOpenPositionAndOrders_other": "You have an existing position and open orders on this market.", + "youHaveOpenPositionAndOrders": "You have an existing position and open orders on this market.", "Peak size": "Peak size", "Peak size cannot be greater than the size ({{size}})": "Peak size cannot be greater than the size ({{size}})", "Peak size cannot be lower than {{stepSize}}": "Peak size cannot be lower than {{stepSize}}", @@ -124,7 +133,7 @@ "Total": "Total", "Total fees": "Total fees", "Total margin available": "Total margin available", - "TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).", + "TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).", "No trading": "No trading", "Trailing percent offset cannot be higher than 99.9": "Trailing percent offset cannot be higher than 99.9", "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}": "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}", @@ -140,8 +149,10 @@ "You are setting this market to cross-margin mode.": "You are setting this market to cross-margin mode.", "You are setting this market to isolated margin mode.": "You are setting this market to isolated margin mode.", "You have only {{amount}}.": "You have only {{amount}}.", + "You have an existing position and open orders on this market": "You have an existing position and open orders on this market", "You may not have enough margin available to open this position.": "You may not have enough margin available to open this position.", "You need {{symbol}} in your wallet to trade in this market.": "You need {{symbol}} in your wallet to trade in this market.", + "You need a Vega wallet to start trading on this market": "You need a Vega wallet to start trading on this market", "You need provide a expiry time/date": "You need provide a expiry time/date", "You need provide a price": "You need provide a price", "You need provide a trailing percent offset": "You need provide a trailing percent offset", diff --git a/libs/positions/src/lib/Positions.graphql b/libs/positions/src/lib/Positions.graphql index 9d360b894..02eb53c20 100644 --- a/libs/positions/src/lib/Positions.graphql +++ b/libs/positions/src/lib/Positions.graphql @@ -41,24 +41,26 @@ subscription PositionsSubscription($partyId: ID!) { query EstimatePosition( $marketId: ID! $openVolume: String! - $orders: [OrderInfo!] $averageEntryPrice: String! + $orders: [OrderInfo!] $marginAccountBalance: String! $generalAccountBalance: String! $orderMarginAccountBalance: String! $marginMode: MarginMode! $marginFactor: String + $includeCollateralIncreaseInAvailableCollateral: Boolean ) { estimatePosition( marketId: $marketId openVolume: $openVolume - orders: $orders averageEntryPrice: $averageEntryPrice + orders: $orders marginAccountBalance: $marginAccountBalance generalAccountBalance: $generalAccountBalance orderMarginAccountBalance: $orderMarginAccountBalance marginMode: $marginMode marginFactor: $marginFactor + includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral # Everywhere in the codebase we expect price values of the underlying to have the right # number of digits for formatting with market.decimalPlaces. By default the estimatePosition # query will return a full value requiring formatting using asset.decimals. For consistency @@ -71,14 +73,24 @@ query EstimatePosition( searchLevel initialLevel collateralReleaseLevel + marginMode + marginFactor + orderMarginLevel } bestCase { maintenanceLevel searchLevel initialLevel collateralReleaseLevel + marginMode + marginFactor + orderMarginLevel } } + collateralIncreaseEstimate { + worstCase + bestCase + } liquidation { worstCase { open_volume_only diff --git a/libs/positions/src/lib/__generated__/Positions.ts b/libs/positions/src/lib/__generated__/Positions.ts index cf59d76d6..b1391a4bb 100644 --- a/libs/positions/src/lib/__generated__/Positions.ts +++ b/libs/positions/src/lib/__generated__/Positions.ts @@ -22,17 +22,18 @@ export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', p export type EstimatePositionQueryVariables = Types.Exact<{ marketId: Types.Scalars['ID']; openVolume: Types.Scalars['String']; - orders?: Types.InputMaybe | Types.OrderInfo>; averageEntryPrice: Types.Scalars['String']; + orders?: Types.InputMaybe | Types.OrderInfo>; marginAccountBalance: Types.Scalars['String']; generalAccountBalance: Types.Scalars['String']; orderMarginAccountBalance: Types.Scalars['String']; marginMode: Types.MarginMode; marginFactor?: Types.InputMaybe; + includeCollateralIncreaseInAvailableCollateral?: Types.InputMaybe; }>; -export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string } }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null }; +export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string } }, collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null }; export const PositionFieldsFragmentDoc = gql` fragment PositionFields on Position { @@ -129,17 +130,18 @@ export function usePositionsSubscriptionSubscription(baseOptions: Apollo.Subscri export type PositionsSubscriptionSubscriptionHookResult = ReturnType; export type PositionsSubscriptionSubscriptionResult = Apollo.SubscriptionResult; export const EstimatePositionDocument = gql` - query EstimatePosition($marketId: ID!, $openVolume: String!, $orders: [OrderInfo!], $averageEntryPrice: String!, $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String) { + query EstimatePosition($marketId: ID!, $openVolume: String!, $averageEntryPrice: String!, $orders: [OrderInfo!], $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String, $includeCollateralIncreaseInAvailableCollateral: Boolean) { estimatePosition( marketId: $marketId openVolume: $openVolume - orders: $orders averageEntryPrice: $averageEntryPrice + orders: $orders marginAccountBalance: $marginAccountBalance generalAccountBalance: $generalAccountBalance orderMarginAccountBalance: $orderMarginAccountBalance marginMode: $marginMode marginFactor: $marginFactor + includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral scaleLiquidationPriceToMarketDecimals: true ) { margin { @@ -148,14 +150,24 @@ export const EstimatePositionDocument = gql` searchLevel initialLevel collateralReleaseLevel + marginMode + marginFactor + orderMarginLevel } bestCase { maintenanceLevel searchLevel initialLevel collateralReleaseLevel + marginMode + marginFactor + orderMarginLevel } } + collateralIncreaseEstimate { + worstCase + bestCase + } liquidation { worstCase { open_volume_only @@ -186,13 +198,14 @@ export const EstimatePositionDocument = gql` * variables: { * marketId: // value for 'marketId' * openVolume: // value for 'openVolume' - * orders: // value for 'orders' * averageEntryPrice: // value for 'averageEntryPrice' + * orders: // value for 'orders' * marginAccountBalance: // value for 'marginAccountBalance' * generalAccountBalance: // value for 'generalAccountBalance' * orderMarginAccountBalance: // value for 'orderMarginAccountBalance' * marginMode: // value for 'marginMode' * marginFactor: // value for 'marginFactor' + * includeCollateralIncreaseInAvailableCollateral: // value for 'includeCollateralIncreaseInAvailableCollateral' * }, * }); */ diff --git a/libs/positions/src/lib/estimate-position.mock.ts b/libs/positions/src/lib/estimate-position.mock.ts index d5536737b..b1af23637 100644 --- a/libs/positions/src/lib/estimate-position.mock.ts +++ b/libs/positions/src/lib/estimate-position.mock.ts @@ -1,6 +1,7 @@ import type { PartialDeep } from 'type-fest'; import merge from 'lodash/merge'; import type { EstimatePositionQuery } from './__generated__/Positions'; +import { MarginMode } from '@vegaprotocol/types'; export const estimatePositionQuery = ( override?: PartialDeep @@ -14,14 +15,24 @@ export const estimatePositionQuery = ( initialLevel: '500000', maintenanceLevel: '200000', searchLevel: '300000', + marginFactor: '1', + orderMarginLevel: '0', + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, }, worstCase: { collateralReleaseLevel: '1100000', initialLevel: '600000', maintenanceLevel: '300000', searchLevel: '400000', + marginFactor: '1', + orderMarginLevel: '0', + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, }, }, + collateralIncreaseEstimate: { + bestCase: '0', + worstCase: '0', + }, liquidation: { bestCase: { including_buy_orders: '1', diff --git a/libs/positions/src/lib/liquidation-price.spec.tsx b/libs/positions/src/lib/liquidation-price.spec.tsx index f438e09c7..d57bfac4c 100644 --- a/libs/positions/src/lib/liquidation-price.spec.tsx +++ b/libs/positions/src/lib/liquidation-price.spec.tsx @@ -9,32 +9,23 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { MarginMode } from '@vegaprotocol/types'; describe('LiquidationPrice', () => { - const props = { + const variables = { marketId: 'market-id', openVolume: '100', - decimalPlaces: 2, - averageEntryPrice: '100', - generalAccountBalance: '100', - marginAccountBalance: '100', - orderMarginAccountBalance: '100', + averageEntryPrice: '10', + marginAccountBalance: '500', + generalAccountBalance: '500', + orderMarginAccountBalance: '0', marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, marginFactor: '1', }; + const props = { ...variables, decimalPlaces: 2 }; const worstCaseOpenVolume = '200'; const bestCaseOpenVolume = '100'; const mock: MockedResponse = { request: { query: EstimatePositionDocument, - variables: { - marketId: props.marketId, - openVolume: props.openVolume, - averageEntryPrice: props.averageEntryPrice, - generalAccountBalance: props.generalAccountBalance, - marginAccountBalance: props.marginAccountBalance, - orderMarginAccountBalance: props.orderMarginAccountBalance, - marginMode: props.marginMode, - marginFactor: props.marginFactor, - }, + variables, }, result: { data: { @@ -45,14 +36,24 @@ describe('LiquidationPrice', () => { searchLevel: '100', initialLevel: '100', collateralReleaseLevel: '100', + orderMarginLevel: '0', + marginFactor: '0', + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, }, bestCase: { maintenanceLevel: '100', searchLevel: '100', initialLevel: '100', collateralReleaseLevel: '100', + orderMarginLevel: '0', + marginFactor: '0', + marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, }, }, + collateralIncreaseEstimate: { + bestCase: '0', + worstCase: '0', + }, liquidation: { worstCase: { open_volume_only: worstCaseOpenVolume, diff --git a/libs/positions/src/lib/liquidation-price.tsx b/libs/positions/src/lib/liquidation-price.tsx index 472768258..170db778a 100644 --- a/libs/positions/src/lib/liquidation-price.tsx +++ b/libs/positions/src/lib/liquidation-price.tsx @@ -1,47 +1,35 @@ import { Tooltip } from '@vegaprotocol/ui-toolkit'; -import { useEstimatePositionQuery } from './__generated__/Positions'; +import { + type EstimatePositionQueryVariables, + useEstimatePositionQuery, +} from './__generated__/Positions'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { useT } from '../use-t'; -import { MarginMode } from '@vegaprotocol/types'; export const LiquidationPrice = ({ - marketId, - openVolume, - averageEntryPrice, - generalAccountBalance, - marginAccountBalance, - orderMarginAccountBalance, - marginMode = MarginMode.MARGIN_MODE_CROSS_MARGIN, - marginFactor, decimalPlaces, className, -}: { - marketId: string; - openVolume: string; - averageEntryPrice: string; - generalAccountBalance: string; - marginAccountBalance: string; - orderMarginAccountBalance: string; - marginMode: MarginMode; - marginFactor: string; + ...variables +}: Pick< + EstimatePositionQueryVariables, + | 'marketId' + | 'openVolume' + | 'orderMarginAccountBalance' + | 'generalAccountBalance' + | 'averageEntryPrice' + | 'marginAccountBalance' + | 'marginMode' + | 'marginFactor' +> & { decimalPlaces: number; className?: string; }) => { const t = useT(); const { data: currentData, previousData } = useEstimatePositionQuery({ - variables: { - marketId, - openVolume, - averageEntryPrice, - generalAccountBalance, - marginAccountBalance, - orderMarginAccountBalance, - marginMode, - marginFactor, - }, + variables, fetchPolicy: 'no-cache', - skip: !openVolume || openVolume === '0', + skip: !variables.openVolume || variables.openVolume === '0', }); const data = currentData || previousData; diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index 9ffacc70d..c3ac221bb 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -52,7 +52,7 @@ export interface Position { quantum: string; lossSocializationAmount: string; marginAccountBalance: string; - orderAccountBalance: string; + orderMarginAccountBalance: string; generalAccountBalance: string; marketDecimalPlaces: number; marketId: string; @@ -67,6 +67,7 @@ export interface Position { realisedPNL: string; status: PositionStatus; totalBalance: string; + totalMarginAccountBalance: string; unrealisedPNL: string; updatedAt: string | null; productType: ProductType; @@ -119,7 +120,7 @@ export const getMetrics = ( marginAccount?.balance ?? 0, asset.decimals ); - const orderAccountBalance = toBigNum( + const orderMarginAccountBalance = toBigNum( orderAccount?.balance ?? 0, asset.decimals ); @@ -137,12 +138,14 @@ export const getMetrics = ( : openVolume.multipliedBy(-1) ).multipliedBy(markPrice) : undefined; - const totalBalance = marginAccountBalance - .plus(generalAccountBalance) - .plus(orderAccountBalance); + const totalMarginAccountBalance = marginAccountBalance.plus( + orderMarginAccountBalance + ); + const totalBalance = totalMarginAccountBalance.plus(generalAccountBalance); + const marginMode = margin?.marginMode || MarginMode.MARGIN_MODE_CROSS_MARGIN; - const marginFactor = margin?.marginFactor; + const marginFactor = margin?.marginFactor || '1'; const currentLeverage = marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN ? (marginFactor && 1 / Number(marginFactor)) || undefined @@ -153,7 +156,7 @@ export const getMetrics = ( : undefined; metrics.push({ marginMode, - marginFactor: marginFactor || '0', + marginFactor, maintenanceLevel: margin?.maintenanceLevel, assetId: asset.id, assetSymbol: asset.symbol, @@ -163,7 +166,7 @@ export const getMetrics = ( quantum: asset.quantum, lossSocializationAmount: position.lossSocializationAmount || '0', marginAccountBalance: marginAccount?.balance ?? '0', - orderAccountBalance: orderAccount?.balance ?? '0', + orderMarginAccountBalance: orderAccount?.balance ?? '0', generalAccountBalance: generalAccount?.balance ?? '0', marketDecimalPlaces, marketId: market.id, @@ -180,6 +183,9 @@ export const getMetrics = ( realisedPNL: position.realisedPNL, status: position.positionStatus, totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(), + totalMarginAccountBalance: totalMarginAccountBalance + .multipliedBy(10 ** asset.decimals) + .toFixed(), unrealisedPNL: position.unrealisedPNL, updatedAt: position.updatedAt || null, productType: market?.tradableInstrument.instrument.product @@ -269,13 +275,26 @@ const positionDataProvider = makeDerivedDataProvider< } ); +export type OpenVolumeData = Pick< + PositionFieldsFragment, + 'openVolume' | 'averageEntryPrice' +>; + export const openVolumeDataProvider = makeDerivedDataProvider< - string, + OpenVolumeData, never, PositionsQueryVariables & MarketDataQueryVariables ->( - [positionDataProvider], - (data) => (data[0] as PositionFieldsFragment | null)?.openVolume || null +>([positionDataProvider], ([data], variables, previousData) => + produce(previousData, (draft) => { + if (!data) { + return data; + } + const newData = { + openVolume: (data as PositionFieldsFragment).openVolume, + averageEntryPrice: (data as PositionFieldsFragment).averageEntryPrice, + }; + return draft ? Object.assign(draft, newData) : newData; + }) ); export const rejoinPositionData = ( @@ -376,6 +395,31 @@ export const positionsMetricsProvider = makeDerivedDataProvider< }) ); +const getMaxLeverage = (market: MarketInfo | null) => { + if (!market || !market?.riskFactors) { + return 1; + } + const maxLeverage = + 1 / + (Math.max( + Number(market.riskFactors.long), + Number(market.riskFactors.short) + ) || 1); + return maxLeverage; +}; + +export const maxMarketLeverageProvider = makeDerivedDataProvider< + number, + never, + { marketId: string } +>( + [ + (callback, client, { marketId }) => + marketInfoProvider(callback, client, { marketId }), + ], + (parts) => getMaxLeverage(parts[0]) +); + export const maxLeverageProvider = makeDerivedDataProvider< number, never, @@ -392,15 +436,7 @@ export const maxLeverageProvider = makeDerivedDataProvider< const market: MarketInfo | null = parts[0]; const position: PositionFieldsFragment | null = parts[1]; const margin: MarginFieldsFragment | null = parts[2]; - if (!market || !market?.riskFactors) { - return 1; - } - const maxLeverage = - 1 / - (Math.max( - Number(market.riskFactors.long), - Number(market.riskFactors.short) - ) || 1); + const maxLeverage = getMaxLeverage(market); if ( market && @@ -432,10 +468,9 @@ export const maxLeverageProvider = makeDerivedDataProvider< } ); -export const useMaxLeverage = (marketId: string, partyId?: string) => { +export const useMaxLeverage = (marketId: string, partyId: string | null) => { return useDataProvider({ - dataProvider: maxLeverageProvider, + dataProvider: partyId ? maxLeverageProvider : maxMarketLeverageProvider, variables: { marketId, partyId: partyId || '' }, - skip: !partyId, }); }; diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index 7388cfb8e..fa5317ba6 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -137,58 +137,61 @@ const PositionMargin = ({ data }: { data: Position }) => { ? ( BigInt(data.marginAccountBalance) + BigInt(data.generalAccountBalance) ).toString() - : BigInt(data.marginAccountBalance) > BigInt(data.orderAccountBalance) + : BigInt(data.marginAccountBalance) > + BigInt(data.orderMarginAccountBalance) ? data.marginAccountBalance - : data.orderAccountBalance; + : data.orderMarginAccountBalance; const getWidth = (balance: string) => BigNumber(balance).multipliedBy(100).dividedBy(max).toNumber(); const inCrossMode = data.marginMode === MarginMode.MARGIN_MODE_CROSS_MARGIN; - const hasOrderAccountBalance = - !inCrossMode && data.orderAccountBalance !== '0'; + const hasOrderMarginAccountBalance = + !inCrossMode && data.orderMarginAccountBalance !== '0'; return ( <> - - {hasOrderAccountBalance ? ( + })} + other={ + inCrossMode + ? t('General account: {{balance}}', { + balance: addDecimalsFormatNumberQuantum( + data.generalAccountBalance, + data.assetDecimals, + data.quantum + ), + }) + : undefined + } + className={classnames({ 'mb-2': hasOrderMarginAccountBalance })} + marker={ + data.maintenanceLevel ? getWidth(data.maintenanceLevel) : undefined + } + markerLabel={ + data.maintenanceLevel && + t('Liquidation: {{maintenanceLevel}}', { + maintenanceLevel: addDecimalsFormatNumberQuantum( + data.maintenanceLevel, + data.assetDecimals, + data.quantum + ), + }) + } + /> + )} + {hasOrderMarginAccountBalance ? ( ) => { - if ( - !data || - !data.marginAccountBalance || - !data.marketDecimalPlaces - ) { + if (!data || !data.totalMarginAccountBalance) { return null; } const margin = addDecimalsFormatNumberQuantum( - data.marginAccountBalance, + data.totalMarginAccountBalance, data.assetDecimals, data.quantum ); @@ -364,7 +363,7 @@ export const PositionsTable = ({ ) } @@ -410,10 +409,10 @@ export const PositionsTable = ({ className="block text-right grow" marketId={data.marketId} openVolume={data.openVolume} + averageEntryPrice={data.averageEntryPrice} generalAccountBalance={data.generalAccountBalance} marginAccountBalance={data.marginAccountBalance} - orderMarginAccountBalance={data.orderAccountBalance} - averageEntryPrice={data.averageEntryPrice} + orderMarginAccountBalance={data.orderMarginAccountBalance} marginFactor={data.marginFactor} marginMode={data.marginMode} decimalPlaces={data.marketDecimalPlaces} diff --git a/libs/positions/src/lib/positions.mock.ts b/libs/positions/src/lib/positions.mock.ts index 971a258a6..28c085ef4 100644 --- a/libs/positions/src/lib/positions.mock.ts +++ b/libs/positions/src/lib/positions.mock.ts @@ -181,11 +181,11 @@ const marginsFields: MarginFieldsFragment[] = [ ]; export const singleRow: Position = { + marginFactor: '1', generalAccountBalance: '12345600', maintenanceLevel: '12300000', marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, - marginFactor: '1', - orderAccountBalance: '0', + orderMarginAccountBalance: '0', partyId: 'partyId', assetId: 'asset-id', assetSymbol: 'BTC', @@ -195,6 +195,7 @@ export const singleRow: Position = { quantum: '0.1', lossSocializationAmount: '0', marginAccountBalance: '12345600', + totalMarginAccountBalance: '12345600', marketDecimalPlaces: 1, marketId: 'string', marketCode: 'ETHBTC.QM21', diff --git a/libs/positions/src/lib/use-open-volume.ts b/libs/positions/src/lib/use-open-volume.ts index c9b8e774e..9e83ab0bb 100644 --- a/libs/positions/src/lib/use-open-volume.ts +++ b/libs/positions/src/lib/use-open-volume.ts @@ -1,14 +1,17 @@ import { useState, useCallback } from 'react'; -import { openVolumeDataProvider } from './positions-data-providers'; +import { + OpenVolumeData, + openVolumeDataProvider, +} from './positions-data-providers'; import { useDataProvider } from '@vegaprotocol/data-provider'; export const useOpenVolume = ( partyId: string | null | undefined, marketId: string ) => { - const [openVolume, setOpenVolume] = useState(undefined); - const update = useCallback(({ data }: { data: string | null }) => { - setOpenVolume(data ?? undefined); + const [openVolume, setOpenVolume] = useState(null); + const update = useCallback(({ data }: { data: OpenVolumeData | null }) => { + setOpenVolume(data); return true; }, []); useDataProvider({ diff --git a/libs/ui-toolkit/src/components/slider/leverage-slider.tsx b/libs/ui-toolkit/src/components/slider/leverage-slider.tsx index f41393224..43fdc4a3e 100644 --- a/libs/ui-toolkit/src/components/slider/leverage-slider.tsx +++ b/libs/ui-toolkit/src/components/slider/leverage-slider.tsx @@ -5,7 +5,9 @@ import classNames from 'classnames'; export const LeverageSlider = ( props: Omit & Required> ) => { - const step = [2, 5, 10, 20, 25].find((step) => props.max / step <= 6); + const step = [2, 5, 10, 20, 25, 50, 100].find( + (step) => props.max / step <= 6 + ); const min = 1; const value = props.value?.[0] || props.defaultValue?.[0]; return ( @@ -28,6 +30,7 @@ export const LeverageSlider = ( const higherThanValue = value && labelValue > value; return ( Date: Wed, 31 Jan 2024 09:21:29 -0500 Subject: [PATCH 5/7] feat(trading): competitions (#5621) Co-authored-by: asiaznik Co-authored-by: Ben --- apps/trading/.env | 1 + apps/trading/.env.devnet | 5 + apps/trading/.env.stagnet1 | 1 + apps/trading/.env.testnet | 1 + .../competitions/competitions-create-team.tsx | 131 +++++++++ .../competitions/competitions-home.tsx | 133 +++++++++ .../competitions/competitions-team.tsx | 238 ++++++++++++++++ .../competitions/competitions-teams.tsx | 67 +++++ .../competitions/competitions-update-team.tsx | 106 ++++++++ .../competitions/join-team.spec.tsx | 88 ++++++ .../client-pages/competitions/join-team.tsx | 225 ++++++++++++++++ .../client-pages/competitions/team-form.tsx | 254 ++++++++++++++++++ .../competitions/update-team-button.tsx | 20 ++ .../referrals/apply-code-form.tsx | 168 +++++------- .../client-pages/referrals/buttons.tsx | 35 --- .../client-pages/referrals/constants.ts | 3 - .../referrals/create-code-form.tsx | 204 ++++++++------ .../client-pages/referrals/error-boundary.tsx | 49 +--- .../referrals/hooks/use-referral.ts | 4 +- .../referrals/referral-statistics.spec.tsx | 146 +++++----- .../referrals/referral-statistics.tsx | 124 +++++---- .../client-pages/referrals/referrals.tsx | 12 +- apps/trading/client-pages/teams/index.ts | 1 - apps/trading/client-pages/teams/teams.tsx | 7 - apps/trading/components/competitions/box.tsx | 21 ++ .../competitions/competitions-cta.tsx | 41 +++ .../competitions/competitions-header.tsx | 27 ++ .../competitions/competitions-leaderboard.tsx | 71 +++++ .../competitions/games-container.tsx | 44 +++ .../competitions/graphics/dude-badge.tsx | 40 +++ .../competitions/graphics/dude-with-flag.tsx | 50 ++++ .../components/competitions/graphics/rank.tsx | 110 ++++++++ .../components/competitions/team-avatar.tsx | 41 +++ .../components/competitions/team-stats.tsx | 195 ++++++++++++++ .../trading/components/layouts-inner/index.ts | 2 + .../layouts-inner/layout-with-gradient.tsx | 14 + .../layouts-inner/layout-with-sky.tsx} | 6 +- apps/trading/components/layouts/index.ts | 3 +- apps/trading/components/navbar/navbar.tsx | 7 + .../components/rainbow-button/index.ts | 1 + .../rainbow-button/rainbow-button.tsx | 35 +++ .../rewards-container/active-rewards.spec.tsx | 2 +- .../rewards-container/active-rewards.tsx | 186 ++++++++----- apps/trading/components/table/table.tsx | 28 +- apps/trading/e2e/tests/teams/test_teams.py | 200 ++++++++++++++ .../hooks/StakeAvailable.graphql | 0 apps/trading/lib/hooks/Team.graphql | 90 +++++++ apps/trading/lib/hooks/Teams.graphql | 16 ++ .../trading/lib/hooks/TeamsStatistics.graphql | 13 + .../hooks/__generated__/StakeAvailable.ts | 0 apps/trading/lib/hooks/__generated__/Team.ts | 154 +++++++++++ apps/trading/lib/hooks/__generated__/Teams.ts | 61 +++++ .../hooks/__generated__/TeamsStatistics.ts | 58 ++++ apps/trading/lib/hooks/use-games.ts | 37 +++ apps/trading/lib/hooks/use-page-title.ts | 18 ++ .../lib/hooks/use-referral-set-transaction.ts | 33 +++ .../hooks/use-stake-available.ts | 0 apps/trading/lib/hooks/use-team.ts | 73 +++++ apps/trading/lib/hooks/use-teams.tsx | 72 +++++ apps/trading/lib/links.ts | 16 +- apps/trading/pages/client-router.tsx | 45 +++- apps/trading/public/cover.png | Bin 0 -> 684487 bytes apps/trading/public/team-avatars/01.png | Bin 0 -> 86947 bytes apps/trading/public/team-avatars/02.png | Bin 0 -> 90021 bytes apps/trading/public/team-avatars/03.png | Bin 0 -> 91680 bytes apps/trading/public/team-avatars/04.png | Bin 0 -> 101099 bytes apps/trading/public/team-avatars/05.png | Bin 0 -> 92373 bytes apps/trading/public/team-avatars/06.png | Bin 0 -> 93083 bytes apps/trading/public/team-avatars/07.png | Bin 0 -> 94331 bytes apps/trading/public/team-avatars/08.png | Bin 0 -> 89410 bytes apps/trading/public/team-avatars/09.png | Bin 0 -> 83198 bytes apps/trading/public/team-avatars/10.png | Bin 0 -> 91522 bytes apps/trading/public/team-avatars/11.png | Bin 0 -> 92644 bytes apps/trading/public/team-avatars/12.png | Bin 0 -> 99087 bytes apps/trading/public/team-avatars/13.png | Bin 0 -> 110152 bytes apps/trading/public/team-avatars/14.png | Bin 0 -> 102504 bytes apps/trading/public/team-avatars/15.png | Bin 0 -> 88118 bytes apps/trading/public/team-avatars/16.png | Bin 0 -> 103512 bytes apps/trading/public/team-avatars/17.png | Bin 0 -> 111693 bytes apps/trading/public/team-avatars/18.png | Bin 0 -> 90212 bytes apps/trading/public/team-avatars/19.png | Bin 0 -> 99664 bytes apps/trading/public/team-avatars/20.png | Bin 0 -> 100157 bytes libs/environment/src/hooks/use-links.ts | 1 + libs/i18n/src/locales/en/trading.json | 95 +++++-- libs/i18n/src/locales/en/utils.json | 1 + libs/i18n/src/locales/en/wallet.json | 3 + libs/types/src/__generated__/types.ts | 24 +- libs/types/src/global-types-mappings.ts | 15 ++ .../components/icon/vega-icons/vega-icon.tsx | 6 +- .../src/components/splash/splash.tsx | 6 +- .../trading-form-group/form-group.tsx | 2 +- libs/utils/src/lib/format/number.spec.ts | 28 ++ libs/utils/src/lib/format/number.ts | 25 +- libs/utils/src/lib/validate/common.ts | 23 +- libs/wallet/src/SimpleTransaction.graphql | 17 ++ .../src/__generated__/SimpleTransaction.ts | 57 ++++ libs/wallet/src/connectors/vega-connector.ts | 25 +- libs/wallet/src/index.ts | 6 + libs/wallet/src/use-simple-transaction.ts | 118 ++++++++ 99 files changed, 3730 insertions(+), 555 deletions(-) create mode 100644 apps/trading/client-pages/competitions/competitions-create-team.tsx create mode 100644 apps/trading/client-pages/competitions/competitions-home.tsx create mode 100644 apps/trading/client-pages/competitions/competitions-team.tsx create mode 100644 apps/trading/client-pages/competitions/competitions-teams.tsx create mode 100644 apps/trading/client-pages/competitions/competitions-update-team.tsx create mode 100644 apps/trading/client-pages/competitions/join-team.spec.tsx create mode 100644 apps/trading/client-pages/competitions/join-team.tsx create mode 100644 apps/trading/client-pages/competitions/team-form.tsx create mode 100644 apps/trading/client-pages/competitions/update-team-button.tsx delete mode 100644 apps/trading/client-pages/teams/index.ts delete mode 100644 apps/trading/client-pages/teams/teams.tsx create mode 100644 apps/trading/components/competitions/box.tsx create mode 100644 apps/trading/components/competitions/competitions-cta.tsx create mode 100644 apps/trading/components/competitions/competitions-header.tsx create mode 100644 apps/trading/components/competitions/competitions-leaderboard.tsx create mode 100644 apps/trading/components/competitions/games-container.tsx create mode 100644 apps/trading/components/competitions/graphics/dude-badge.tsx create mode 100644 apps/trading/components/competitions/graphics/dude-with-flag.tsx create mode 100644 apps/trading/components/competitions/graphics/rank.tsx create mode 100644 apps/trading/components/competitions/team-avatar.tsx create mode 100644 apps/trading/components/competitions/team-stats.tsx create mode 100644 apps/trading/components/layouts-inner/index.ts create mode 100644 apps/trading/components/layouts-inner/layout-with-gradient.tsx rename apps/trading/{client-pages/referrals/layout.tsx => components/layouts-inner/layout-with-sky.tsx} (82%) create mode 100644 apps/trading/components/rainbow-button/index.ts create mode 100644 apps/trading/components/rainbow-button/rainbow-button.tsx create mode 100644 apps/trading/e2e/tests/teams/test_teams.py rename apps/trading/{client-pages/referrals => lib}/hooks/StakeAvailable.graphql (100%) create mode 100644 apps/trading/lib/hooks/Team.graphql create mode 100644 apps/trading/lib/hooks/Teams.graphql create mode 100644 apps/trading/lib/hooks/TeamsStatistics.graphql rename apps/trading/{client-pages/referrals => lib}/hooks/__generated__/StakeAvailable.ts (100%) create mode 100644 apps/trading/lib/hooks/__generated__/Team.ts create mode 100644 apps/trading/lib/hooks/__generated__/Teams.ts create mode 100644 apps/trading/lib/hooks/__generated__/TeamsStatistics.ts create mode 100644 apps/trading/lib/hooks/use-games.ts create mode 100644 apps/trading/lib/hooks/use-page-title.ts create mode 100644 apps/trading/lib/hooks/use-referral-set-transaction.ts rename apps/trading/{client-pages/referrals => lib}/hooks/use-stake-available.ts (100%) create mode 100644 apps/trading/lib/hooks/use-team.ts create mode 100644 apps/trading/lib/hooks/use-teams.tsx create mode 100644 apps/trading/public/cover.png create mode 100644 apps/trading/public/team-avatars/01.png create mode 100644 apps/trading/public/team-avatars/02.png create mode 100644 apps/trading/public/team-avatars/03.png create mode 100644 apps/trading/public/team-avatars/04.png create mode 100644 apps/trading/public/team-avatars/05.png create mode 100644 apps/trading/public/team-avatars/06.png create mode 100644 apps/trading/public/team-avatars/07.png create mode 100644 apps/trading/public/team-avatars/08.png create mode 100644 apps/trading/public/team-avatars/09.png create mode 100644 apps/trading/public/team-avatars/10.png create mode 100644 apps/trading/public/team-avatars/11.png create mode 100644 apps/trading/public/team-avatars/12.png create mode 100644 apps/trading/public/team-avatars/13.png create mode 100644 apps/trading/public/team-avatars/14.png create mode 100644 apps/trading/public/team-avatars/15.png create mode 100644 apps/trading/public/team-avatars/16.png create mode 100644 apps/trading/public/team-avatars/17.png create mode 100644 apps/trading/public/team-avatars/18.png create mode 100644 apps/trading/public/team-avatars/19.png create mode 100644 apps/trading/public/team-avatars/20.png create mode 100644 libs/wallet/src/SimpleTransaction.graphql create mode 100644 libs/wallet/src/__generated__/SimpleTransaction.ts create mode 100644 libs/wallet/src/use-simple-transaction.ts diff --git a/apps/trading/.env b/apps/trading/.env index 565f54412..d1c361360 100644 --- a/apps/trading/.env +++ b/apps/trading/.env @@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true NX_METAMASK_SNAPS=true NX_REFERRALS=true # NX_DISABLE_CLOSE_POSITION=false +NX_TEAM_COMPETITION=true NX_TENDERMINT_URL=https://be.vega.community NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket diff --git a/apps/trading/.env.devnet b/apps/trading/.env.devnet index 364481010..12da4f51d 100644 --- a/apps/trading/.env.devnet +++ b/apps/trading/.env.devnet @@ -28,3 +28,8 @@ NX_REFERRALS=true NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/ NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket + +NX_TEAM_COMPETITION=true + +NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/ +NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg= diff --git a/apps/trading/.env.stagnet1 b/apps/trading/.env.stagnet1 index c30708ad7..8634b87af 100644 --- a/apps/trading/.env.stagnet1 +++ b/apps/trading/.env.stagnet1 @@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true # NX_PRODUCT_PERPETUALS NX_METAMASK_SNAPS=true NX_REFERRALS=true +NX_TEAM_COMPETITION=true NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/ NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg= diff --git a/apps/trading/.env.testnet b/apps/trading/.env.testnet index ad9347afe..595bfad9f 100644 --- a/apps/trading/.env.testnet +++ b/apps/trading/.env.testnet @@ -26,6 +26,7 @@ NX_ISOLATED_MARGIN=true NX_ICEBERG_ORDERS=true NX_METAMASK_SNAPS=true NX_REFERRALS=true +NX_TEAM_COMPETITION=true NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket diff --git a/apps/trading/client-pages/competitions/competitions-create-team.tsx b/apps/trading/client-pages/competitions/competitions-create-team.tsx new file mode 100644 index 000000000..e8ead8e9e --- /dev/null +++ b/apps/trading/client-pages/competitions/competitions-create-team.tsx @@ -0,0 +1,131 @@ +import { useSearchParams } from 'react-router-dom'; +import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit'; +import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; +import { useT } from '../../lib/use-t'; +import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction'; +import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment'; +import { RainbowButton } from '../../components/rainbow-button'; +import { usePageTitle } from '../../lib/hooks/use-page-title'; +import { ErrorBoundary } from '../../components/error-boundary'; +import { Box } from '../../components/competitions/box'; +import { LayoutWithGradient } from '../../components/layouts-inner'; +import { Links } from '../../lib/links'; +import { TeamForm, TransactionType } from './team-form'; + +export const CompetitionsCreateTeam = () => { + const [searchParams] = useSearchParams(); + const isSolo = Boolean(searchParams.get('solo')); + const t = useT(); + + usePageTitle(t('Create a team')); + + const { isReadOnly, pubKey } = useVegaWallet(); + const openWalletDialog = useVegaWalletDialogStore( + (store) => store.openVegaWalletDialog + ); + + return ( + + +
+ +

+ {t('Create a team')} +

+ {pubKey && !isReadOnly ? ( + + ) : ( + <> +

+ {t( + 'Create a team to participate in team based rewards as well as access the discount benefits of the current referral program.' + )} +

+ + {t('Connect wallet')} + + + )} +
+
+
+
+ ); +}; + +const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => { + const t = useT(); + const createLink = useLinks(DApp.Governance); + + const { err, status, code, isEligible, requiredStake, onSubmit } = + useReferralSetTransaction({ + onSuccess: (code) => { + // For some reason team creation takes a long time, too long even to make + // polling viable, so its not feasible to navigate to the team page + // after creation + // + // navigate(Links.COMPETITIONS_TEAM(code)); + }, + }); + + if (status === 'confirmed') { + return ( +
+

{t('Team creation transaction successful')}

+ {code && ( + <> +

+ Your team ID is:{' '} + {code} +

+ + {t('View team')} + + + )} +
+ ); + } + + if (!isEligible) { + return ( +
+ {requiredStake !== undefined && ( +

+ {t( + 'You need at least {{requiredStake}} VEGA staked to generate a referral code and participate in the referral program.', + { + requiredStake: addDecimalsFormatNumber( + requiredStake.toString(), + 18 + ), + } + )} +

+ )} + + {t('Stake some $VEGA now')} + +
+ ); + } + + return ( + + ); +}; diff --git a/apps/trading/client-pages/competitions/competitions-home.tsx b/apps/trading/client-pages/competitions/competitions-home.tsx new file mode 100644 index 000000000..bfe1ed1c7 --- /dev/null +++ b/apps/trading/client-pages/competitions/competitions-home.tsx @@ -0,0 +1,133 @@ +import { useT } from '../../lib/use-t'; +import { ErrorBoundary } from '@sentry/react'; +import { CompetitionsHeader } from '../../components/competitions/competitions-header'; +import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit'; + +import { useGames } from '../../lib/hooks/use-games'; +import { useCurrentEpochInfoQuery } from '../referrals/hooks/__generated__/Epoch'; +import { Link, useNavigate } from 'react-router-dom'; +import { Links } from '../../lib/links'; +import { + CompetitionsAction, + CompetitionsActionsContainer, +} from '../../components/competitions/competitions-cta'; +import { GamesContainer } from '../../components/competitions/games-container'; +import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard'; +import { useTeams } from '../../lib/hooks/use-teams'; +import take from 'lodash/take'; +import { usePageTitle } from '../../lib/hooks/use-page-title'; + +export const CompetitionsHome = () => { + const t = useT(); + const navigate = useNavigate(); + + usePageTitle(t('Competitions')); + + const { data: epochData } = useCurrentEpochInfoQuery(); + const currentEpoch = Number(epochData?.epoch.id); + + const { data: gamesData, loading: gamesLoading } = useGames({ + onlyActive: true, + currentEpoch, + }); + + const { data: teamsData, loading: teamsLoading } = useTeams({ + sortByField: ['totalQuantumRewards'], + order: 'desc', + }); + + return ( + + +

+ {t( + 'Be a team player! Participate in games and work together to rake in as much profit to win.' + )} +

+
+ + {/** Get started */} +

{t('Get started')}

+ + + { + e.preventDefault(); + navigate(Links.COMPETITIONS_CREATE_TEAM()); + }} + > + {t('Create a public team')} + + } + /> + { + e.preventDefault(); + navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO()); + }} + > + {t('Create a private team')} + + } + /> + { + e.preventDefault(); + navigate(Links.COMPETITIONS_TEAMS()); + }} + > + {t('Choose a team')} + + } + /> + + + {/** List of available games */} +

{t('Games')}

+ + {gamesLoading ? ( + + ) : ( + + )} + + {/** The teams ranking */} +
+

{t('Leaderboard')}

+ + {t('View all teams')} + +
+ + {teamsLoading ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/apps/trading/client-pages/competitions/competitions-team.tsx b/apps/trading/client-pages/competitions/competitions-team.tsx new file mode 100644 index 000000000..fb03b71d3 --- /dev/null +++ b/apps/trading/client-pages/competitions/competitions-team.tsx @@ -0,0 +1,238 @@ +import { useState, type ButtonHTMLAttributes } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import orderBy from 'lodash/orderBy'; +import { Splash, truncateMiddle, Loader } from '@vegaprotocol/ui-toolkit'; +import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types'; +import classNames from 'classnames'; +import { useT } from '../../lib/use-t'; +import { Table } from '../../components/table'; +import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils'; +import { + useTeam, + type TeamStats as ITeamStats, + type Team as TeamType, + type Member, + type TeamGame, +} from '../../lib/hooks/use-team'; +import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment'; +import { TeamAvatar } from '../../components/competitions/team-avatar'; +import { TeamStats } from '../../components/competitions/team-stats'; +import { usePageTitle } from '../../lib/hooks/use-page-title'; +import { ErrorBoundary } from '../../components/error-boundary'; +import { LayoutWithGradient } from '../../components/layouts-inner'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { JoinTeam } from './join-team'; +import { UpdateTeamButton } from './update-team-button'; + +export const CompetitionsTeam = () => { + const t = useT(); + const { teamId } = useParams<{ teamId: string }>(); + usePageTitle([t('Competitions'), t('Team')]); + return ( + + + + ); +}; + +const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => { + const t = useT(); + const { pubKey } = useVegaWallet(); + const { team, partyTeam, stats, members, games, loading, refetch } = useTeam( + teamId, + pubKey || undefined + ); + + if (loading) { + return ( + + + + ); + } + + if (!team) { + return ( + +

{t('Page not found')}

+
+ ); + } + + return ( + + ); +}; + +const TeamPage = ({ + team, + partyTeam, + stats, + members, + games, + refetch, +}: { + team: TeamType; + partyTeam?: TeamType; + stats?: ITeamStats; + members?: Member[]; + games?: TeamGame[]; + refetch: () => void; +}) => { + const t = useT(); + const [showGames, setShowGames] = useState(true); + + return ( + +
+ +
+

+ {team.name} +

+ + +
+
+ +
+
+ setShowGames(true)} + data-testid="games-toggle" + > + {t('Games ({{count}})', { count: games ? games.length : 0 })} + + setShowGames(false)} + data-testid="members-toggle" + > + {t('Members ({{count}})', { + count: members ? members.length : 0, + })} + +
+ {showGames ? : } +
+
+ ); +}; + +const Games = ({ games }: { games?: TeamGame[] }) => { + const t = useT(); + + if (!games?.length) { + return

{t('No games')}

; + } + + return ( + ({ + rank: game.team.rank, + epoch: game.epoch, + type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric], + amount: formatNumber(game.team.totalRewardsEarned), + participatingTeams: game.entities.length, + participatingMembers: game.numberOfParticipants, + }))} + noCollapse={true} + /> + ); +}; + +const Members = ({ members }: { members?: Member[] }) => { + const t = useT(); + + if (!members?.length) { + return

{t('No members')}

; + } + + const data = orderBy( + members.map((m) => ({ + referee: , + joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)), + joinedAtEpoch: Number(m.joinedAtEpoch), + })), + 'joinedAtEpoch', + 'desc' + ); + + return ( +
+ ); +}; + +const RefereeLink = ({ pubkey }: { pubkey: string }) => { + const linkCreator = useLinks(DApp.Explorer); + const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey)); + + return ( + + {truncateMiddle(pubkey)} + + ); +}; + +const ToggleButton = ({ + active, + ...props +}: ButtonHTMLAttributes & { active: boolean }) => { + return ( + + + ); + } + // Party is the creator of a team + else if (partyTeam && partyTeam.referrer === pubKey) { + // Party is the creator of THIS team + if (partyTeam.teamId === team.teamId) { + return ( + + ); + } else { + // Not creator of the team, but still can't switch because + // creators cannot leave their own team + return ( + + + + ); + } + } + // Party is in a team, but not this one + else if (partyTeam && partyTeam.teamId !== team.teamId) { + return ( + + ); + } + // Joined. Current party is already in this team + else if (partyTeam && partyTeam.teamId === team.teamId) { + return ( + + ); + } + + return ( + + ); +}; + +const DialogContent = ({ + type, + status, + team, + partyTeam, + onConfirm, + onCancel, +}: { + type: JoinType; + status: Status; + team: Team; + partyTeam?: Team; + onConfirm: () => void; + onCancel: () => void; +}) => { + const t = useT(); + + if (status === 'requested') { + return

{t('Confirm in wallet...')}

; + } + + if (status === 'pending') { + return

{t('Confirming transaction...')}

; + } + + if (status === 'confirmed') { + if (type === 'switch') { + return ( +

+ {t( + 'Team switch successful. You will switch team at the end of the epoch.' + )} +

+ ); + } + + return

{t('Team joined')}

; + } + + return ( +
+ {type === 'switch' && ( + <> +

{t('Switch team')}

+

+ {t( + "Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?", + { + fromTeam: partyTeam?.name, + toTeam: team.name, + } + )} +

+ + )} + {type === 'join' && ( + <> +

{t('Join team')}

+

+ {t('Are you sure you want to join team: {{team}}', { + team: team.name, + })} +

+ + )} +
+ + +
+
+ ); +}; diff --git a/apps/trading/client-pages/competitions/team-form.tsx b/apps/trading/client-pages/competitions/team-form.tsx new file mode 100644 index 000000000..93368b0bf --- /dev/null +++ b/apps/trading/client-pages/competitions/team-form.tsx @@ -0,0 +1,254 @@ +import { + TradingFormGroup, + TradingInput, + TradingInputError, + TradingCheckbox, + TextArea, + TradingButton, + Intent, +} from '@vegaprotocol/ui-toolkit'; +import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils'; + +import { type useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction'; +import { useT } from '../../lib/use-t'; +import { useForm, Controller } from 'react-hook-form'; +import type { + CreateReferralSet, + UpdateReferralSet, + Status, +} from '@vegaprotocol/wallet'; + +export type FormFields = { + id: string; + name: string; + url: string; + avatarUrl: string; + private: boolean; + allowList: string; +}; + +export enum TransactionType { + CreateReferralSet, + UpdateReferralSet, +} + +const prepareTransaction = ( + type: TransactionType, + fields: FormFields +): CreateReferralSet | UpdateReferralSet => { + switch (type) { + case TransactionType.CreateReferralSet: + return { + createReferralSet: { + isTeam: true, + team: { + name: fields.name, + teamUrl: fields.url, + avatarUrl: fields.avatarUrl, + closed: fields.private, + allowList: fields.private + ? parseAllowListText(fields.allowList) + : [], + }, + }, + }; + case TransactionType.UpdateReferralSet: + return { + updateReferralSet: { + id: fields.id, + isTeam: true, + team: { + name: fields.name, + teamUrl: fields.url, + avatarUrl: fields.avatarUrl, + closed: fields.private, + allowList: fields.private + ? parseAllowListText(fields.allowList) + : [], + }, + }, + }; + } +}; + +export const TeamForm = ({ + type, + status, + err, + isSolo, + onSubmit, + defaultValues, +}: { + type: TransactionType; + status: ReturnType['status']; + err: ReturnType['err']; + isSolo: boolean; + onSubmit: ReturnType['onSubmit']; + defaultValues?: FormFields; +}) => { + const t = useT(); + + const { + register, + handleSubmit, + control, + watch, + formState: { errors }, + } = useForm({ + defaultValues: { + private: isSolo, + ...defaultValues, + }, + }); + + const isPrivate = watch('private'); + + const sendTransaction = (fields: FormFields) => { + onSubmit(prepareTransaction(type, fields)); + }; + + return ( + + + + + {errors.name?.message && ( + + {errors.name.message} + + )} + + + + {errors.url?.message && ( + + {errors.url.message} + + )} + + + + {errors.avatarUrl?.message && ( + + {errors.avatarUrl.message} + + )} + + + { + return ( + { + field.onChange(value); + }} + disabled={isSolo} + /> + ); + }} + /> + + {isPrivate && ( + +