Compare commits

...
52 changed files with 1021 additions and 677 deletions
+2
View File
@@ -24,8 +24,10 @@ NX_SUCCESSOR_MARKETS=true
NX_PRODUCT_PERPETUALS=true
NX_REFERRALS=true
NX_UPDATE_MARKET_STATE=true
NX_GOVERNANCE_TRANSFERS=true
NX_VOLUME_DISCOUNTS=true
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
@@ -0,0 +1,15 @@
{
"rationale": {
"title": "Governance cancel transfer proposal",
"description": "Rejected cancel transfer proposal"
},
"terms": {
"cancelTransfer": {
"changes": {
"transferId": "invalid transfer id"
}
},
"closingTimestamp": 0,
"enactmentTimestamp": 0
}
}
@@ -27,12 +27,16 @@ import {
} from '../../../../governance-e2e/src/support/staking.functions';
import { ethereumWalletConnect } from '../../../../governance-e2e/src/support/wallet-eth.functions';
import {
depositAsset,
switchVegaWalletPubKey,
vegaWalletSetSpecifiedApprovalAmount,
} from '../../support/wallet-functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
import { formatDateWithLocalTimezone } from '@vegaprotocol/utils';
import { createSuccessorMarketProposalTxBody } from '../../support/proposal.functions';
import {
createGovernanceTransferProposalTxBody,
createSuccessorMarketProposalTxBody,
} from '../../support/proposal.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const participationNotMet = 'token-participation-not-met';
@@ -51,6 +55,7 @@ const openProposals = 'open-proposals';
const viewProposalButton = 'view-proposal-btn';
const proposalTermsToggle = 'proposal-json-toggle';
const marketDataToggle = 'proposal-market-data-toggle';
const governanceTransferToggle = 'proposal-transfer-details';
const marketProposalType = 'proposal-type';
describe(
@@ -520,5 +525,78 @@ describe(
);
});
});
it('Able to see governance transfer proposal', function () {
const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de';
depositAsset(vegaAssetAddress, '1000', 18);
cy.getByTestId('currency-title', Cypress.env('txTimeout')).should(
'contain.text',
'Collateral'
);
cy.VegaWalletTopUpNetworkAccount('100');
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
cy.reload();
getProposalFromTitle('Governance transfer proposal').within(() => {
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
cy.getByTestId(governanceTransferToggle).click();
cy.getByTestId('proposal-transfer-details-table').within(() => {
getProposalInformationFromTable('Source Type')
.invoke('text')
.and('eq', 'Network Treasury');
getProposalInformationFromTable('Destination')
.invoke('text')
.and('eq', Cypress.env('vegaWalletPublicKey'));
getProposalInformationFromTable('Asset')
.invoke('text')
.and('eq', 'VEGA');
getProposalInformationFromTable('Fraction Of Balance')
.invoke('text')
.and('eq', '50%');
getProposalInformationFromTable('Amount')
.invoke('text')
.and('eq', '100.00');
getProposalInformationFromTable('Transfer Type')
.invoke('text')
.and('eq', 'All or nothing');
getProposalInformationFromTable('Kind')
.invoke('text')
.and('eq', 'One off');
});
});
it(' Able to see cancel transfer proposal - rejected', function () {
const proposalPath = 'src/fixtures/proposals/cancel-transfer-raw.json';
const enactmentTimestamp =
createTenDigitUnixTimeStampForSpecifiedDays(11);
const closingTimestamp = createTenDigitUnixTimeStampForSpecifiedDays(10);
submitUniqueRawProposal({
proposalBody: proposalPath,
enactmentTimestamp: enactmentTimestamp,
closingTimestamp: closingTimestamp,
submit: false,
});
cy.getByTestId('proposal-submit').should('be.visible').click();
cy.getByTestId('dialog-title').should('have.text', 'Proposal rejected');
cy.getByTestId('icon-cross').last().click();
navigateTo(navigation.proposals);
cy.get('[href="/proposals/rejected"]').click();
getProposalFromTitle('Governance cancel transfer proposal').within(() => {
cy.getByTestId(marketProposalType).should(
'have.text',
'CancelTransfer'
);
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
getProposalInformationFromTable('Error details')
.invoke('text')
.and('eq', 'Governance transfer invalid transfer id not found');
getProposalInformationFromTable('transferId')
.invoke('text')
.and('eq', 'invalid transfer id');
});
}
);
@@ -1,9 +1,10 @@
import { addSeconds, millisecondsToSeconds } from 'date-fns';
import { addDays, addSeconds, millisecondsToSeconds } from 'date-fns';
import type { ProposalSubmissionBody } from '@vegaprotocol/wallet';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { upgradeProposalsData } from '../fixtures/mocks/network-upgrade';
import { proposalsData } from '../fixtures/mocks/proposals';
import { nodeData } from '../fixtures/mocks/nodes';
import { AccountType, GovernanceTransferType } from '@vegaprotocol/types';
export function createUpdateNetworkProposalTxBody(): ProposalSubmissionBody {
const MIN_CLOSE_SEC = 5;
@@ -358,6 +359,46 @@ export function createSuccessorMarketProposalTxBody(
};
}
export function createGovernanceTransferProposalTxBody(): ProposalSubmissionBody {
const MIN_CLOSE_SEC = 5;
const MIN_ENACT_SEC = 7;
const closingDate = addDays(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addDays(closingDate, MIN_ENACT_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
const destination = Cypress.env('vegaWalletPublicKey');
return {
proposalSubmission: {
rationale: {
title: 'Governance transfer proposal',
description: 'E2E test for transfer proposal test',
},
terms: {
newTransfer: {
changes: {
fractionOfBalance: '0.5',
amount: '100' + '0'.repeat(18),
sourceType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
source: '',
transferType:
GovernanceTransferType.GOVERNANCE_TRANSFER_TYPE_ALL_OR_NOTHING,
destinationType: AccountType.ACCOUNT_TYPE_GENERAL,
destination,
asset:
'b4f2726571fbe8e33b442dc92ed2d7f0d810e21835b7371a7915a365f07ccd9b',
oneOff: {
deliverOn: '0',
},
},
},
closingTimestamp,
enactmentTimestamp,
},
},
};
}
export function mockNetworkUpgradeProposal() {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Nodes', nodeData);
@@ -41,6 +41,9 @@ export async function depositAsset(
) {
// Approve asset
const faucet = new Token(assetEthAddress, signer);
// Wait needed to allow Eth chain to catch up
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(4000);
cy.wrap(
faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)),
transactionTimeout
+1
View File
@@ -1,4 +1,5 @@
/* eslint-disable */
process.env.TZ = 'GMT';
export default {
displayName: 'governance',
preset: '../../jest.preset.js',
-147
View File
@@ -1,147 +0,0 @@
.pre-loader {
display: flex;
width: 100%;
min-height: 100vh;
justify-content: center;
align-items: center;
}
.pre-loader .loader-item {
width: 10px;
height: 10px;
background: #000;
}
.pre-loader .pre-loader-center {
align-items: center;
display: flex;
flex-direction: column;
}
.pre-loader .pre-loader-wrapper {
width: 50px;
height: 50px;
display: flex;
flex-wrap: wrap;
}
.pre-loader .loader-item:nth-child(0) {
animation-delay: 0ms;
animation-direction: reverse;
}
.pre-loader .loader-item:first-child {
animation-delay: -0.1s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(2) {
animation-delay: 0.3s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(3) {
animation-delay: -0.45s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(4) {
animation-delay: 1s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(5) {
animation-delay: -0.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(6) {
animation-delay: 0.9s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(7) {
animation-delay: -1.4s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(8) {
animation-delay: 1.6s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(9) {
animation-delay: -0.45s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(10) {
animation-delay: 1.5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(11) {
animation-delay: -2.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(12) {
animation-delay: 1.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(13) {
animation-delay: -1.95s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(14) {
animation-delay: 2.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(15) {
animation-delay: -0.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(16) {
animation-delay: 4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(17) {
animation-delay: -0.85s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(18) {
animation-delay: 1.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(19) {
animation-delay: -1.9s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(20) {
animation-delay: 5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(21) {
animation-delay: -5.25s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(22) {
animation-delay: 4.4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(23) {
animation-delay: -5.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(24) {
animation-delay: 4.8s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(25) {
animation-delay: -5s;
animation-direction: alternate;
}
.pre-loader .loader-item {
animation: flickering 0.4s linear infinite alternate;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
to {
opacity: 0;
}
}
html.dark .pre-loader .loader-item {
background: #fff;
}
-58
View File
@@ -1,58 +0,0 @@
.pre-loader {
display: flex;
width: 100%;
min-height: 100vh;
justify-content: center;
align-items: center;
.loader-item {
width: 10px;
height: 10px;
background: black;
}
.pre-loader-center {
align-items: center;
display: flex;
flex-direction: column;
}
.pre-loader-wrapper {
width: 50px;
height: 50px;
display: flex;
flex-wrap: wrap;
}
@for $i from 0 through 25 {
.loader-item:nth-child(#{$i}) {
@if $i % 2 == 0 {
animation-delay: #{$i * 50 * random(5)}ms;
animation-direction: reverse;
} @else {
animation-delay: #{$i * -50 * random(5)}ms;
animation-direction: alternate;
}
}
}
.loader-item {
animation: flickering 0.4s linear alternate infinite;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
100% {
opacity: 0;
}
}
}
html.dark {
.pre-loader {
.loader-item {
background: white;
}
}
}
@@ -1,6 +1,7 @@
import type { InMemoryCacheConfig } from '@apollo/client';
import {
AppFailure,
AppLoader,
DocsLinks,
NetworkLoader,
NodeGuard,
@@ -9,15 +10,10 @@ import {
import { t } from '@vegaprotocol/i18n';
import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import dynamic from 'next/dynamic';
import type { ReactNode } from 'react';
import { Web3Provider } from './web3-provider';
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
loading: () => <>Loading...</>,
});
export const AppLoader = ({ children }: { children: ReactNode }) => {
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
const {
error,
VEGA_URL,
@@ -47,13 +43,13 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
return (
<NetworkLoader
cache={cacheConfig}
skeleton={<DynamicLoader />}
skeleton={<AppLoader />}
failure={
<AppFailure title={t('Could not initialize app')} error={error} />
}
>
<NodeGuard
skeleton={<DynamicLoader />}
skeleton={<AppLoader />}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Web3Provider>
@@ -1,2 +1,2 @@
export * from './app-loader';
export * from './bootstrapper';
export * from './web3-provider';
@@ -1,26 +0,0 @@
import { Loader } from '@vegaprotocol/ui-toolkit';
export const Preloader = () => {
return (
<>
<style>
{`
body{
display: block;
width: 100%;
height: 100%;
margin: 0;
}
html.dark body{
background: #000;
}
`}
</style>
<div className="pre-loader">
<Loader />
</div>
</>
);
};
export default Preloader;
+5 -4
View File
@@ -36,7 +36,7 @@ import {
useSearchParams,
} from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Bootstrapper } from '../components/bootstrapper';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
@@ -52,6 +52,7 @@ import { ViewingBanner } from '../components/viewing-banner';
import { NavHeader } from '../components/navbar/nav-header';
import { Telemetry } from '../components/telemetry';
import { Routes as AppRoutes } from '../lib/links';
import { SSRLoader } from './ssr-loader';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -144,14 +145,14 @@ function VegaTradingApp(props: AppProps) {
// Prevent HashRouter from being server side rendered as it
// relies on presence of document object
if (status === 'default') {
return <DynamicLoader />;
return <SSRLoader />;
}
return (
<HashRouter>
<AppLoader>
<Bootstrapper>
<AppBody {...props} />
</AppLoader>
</Bootstrapper>
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
</HashRouter>
);
+7 -4
View File
@@ -22,14 +22,17 @@ export default function Document() {
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" content="/favicon.ico" />
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/preloader.css" media="all" />
{/* scripts */}
<script src="/theme-setter.js" type="text/javascript" async />
</Head>
<Html>
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
<body
// Nextjs will set body to display none until js runs. Because the entire app is client rendered
// and delivered via ipfs we override this to show a server side render loading animation until the
// js is downloaded and react takes over rendering
style={{ display: 'block' }}
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
>
<Main />
<NextScript />
</body>
+52
View File
@@ -0,0 +1,52 @@
export const SSRLoader = () => {
const randomDelay = () => {
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
};
return (
<div
style={{
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<style jsx>{`
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
100% {
opacity: 0;
}
}
`}</style>
<div style={{ width: 50, height: 50, display: 'flex', flexWrap: 'wrap' }}>
{new Array(25).fill(null).map((_, i) => {
return (
<div
key={i}
style={{
width: 10,
height: 10,
animation: 'flickering 0.4s linear alternate infinite',
animationDelay: `-${randomDelay()}s`,
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
background: 'black',
opacity: Math.random() > 0.5 ? 1 : 0,
}}
/>
);
})}
</div>
</div>
);
};
-116
View File
@@ -1,116 +0,0 @@
.pre-loader {
display: flex;
width: 100%;
min-height: 100vh;
justify-content: center;
align-items: center;
}
.pre-loader .loader-item {
width: 10px;
height: 10px;
background: #000;
}
.pre-loader .pre-loader-center {
align-items: center;
display: flex;
flex-direction: column;
}
.pre-loader .pre-loader-wrapper {
width: 50px;
height: 50px;
display: flex;
flex-wrap: wrap;
}
@media (prefers-color-scheme: light) {
.pre-loader .loader-item {
background: #000;
}
}
.pre-loader .loader-item:nth-child(0) {
animation-delay: 0ms;
animation-direction: reverse;
}
.pre-loader .loader-item:first-child {
animation-delay: -50ms;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(2) {
animation-delay: 0.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(3) {
animation-delay: -0.6s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(4) {
animation-delay: 0.4s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(5) {
animation-delay: -0.5s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(6) {
animation-delay: 0.3s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(7) {
animation-delay: -1.4s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(8) {
animation-delay: 2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(9) {
animation-delay: -0.9s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(10) {
animation-delay: 1.5s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(11) {
animation-delay: -0.55s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(12) {
animation-delay: 1.2s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(13) {
animation-delay: -0.65s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(14) {
animation-delay: 0.7s;
animation-direction: reverse;
}
.pre-loader .loader-item:nth-child(15) {
animation-delay: -3.75s;
animation-direction: alternate;
}
.pre-loader .loader-item:nth-child(16) {
animation-delay: 1.6s;
animation-direction: reverse;
}
.pre-loader .loader-item {
animation: flickering 0.4s linear infinite alternate;
}
@keyframes flickering {
0% {
opacity: 1;
}
25% {
opacity: 1;
}
26% {
opacity: 0;
}
to {
opacity: 0;
}
}
html.dark .pre-loader .loader-item {
background: #fff;
}
+2
View File
@@ -25,6 +25,7 @@ import { addImportNodeWallets } from './lib/commands/import-node-wallets';
import { addVegaWalletTopUpRewardsPool } from './lib/commands/vega-wallet-top-up-rewards-pool';
import { addAssociateTokensToVegaWallet } from './lib/commands/associate-tokens-to-vega-wallet';
import { addMockChainId } from './lib/commands/mock-chain-id';
import { addVegaWalletTopUpNetworkAccount } from './lib/commands/vega-wallet-fund-network-account';
addGetTestIdcommand();
addMockGQLCommand();
@@ -52,6 +53,7 @@ addImportNodeWallets();
addVegaWalletTopUpRewardsPool();
addAssociateTokensToVegaWallet();
addMockChainId();
addVegaWalletTopUpNetworkAccount();
export {
mockConnectWallet,
@@ -0,0 +1,41 @@
import { AccountType } from '@vegaprotocol/types';
import type { TransferBody } from '@vegaprotocol/wallet';
import { createWalletClient, sendVegaTx } from '../capsule/wallet-client';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
VegaWalletTopUpNetworkAccount(amount: string): void;
}
}
}
export function addVegaWalletTopUpNetworkAccount() {
Cypress.Commands.add('VegaWalletTopUpNetworkAccount', (amount: string) => {
const vegaWalletUrl = Cypress.env('VEGA_WALLET_URL');
const token = Cypress.env('VEGA_WALLET_API_TOKEN');
const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY');
const assetAddress =
'b4f2726571fbe8e33b442dc92ed2d7f0d810e21835b7371a7915a365f07ccd9b';
createWalletClient(vegaWalletUrl, token);
const transactionBody: TransferBody = {
transfer: {
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
to: '0000000000000000000000000000000000000000000000000000000000000000',
asset: assetAddress,
amount: amount + '0'.repeat(18),
oneOff: {
deliverOn: 0,
},
},
};
cy.highlight('Topping network account');
sendVegaTx(vegaPubKey, transactionBody);
});
}
@@ -29,11 +29,6 @@ export function addVegaWalletTopUpRewardsPool() {
.then((epochText) => {
transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5;
transferEndEpoch = transferStartEpoch + 100;
/* eslint-disable no-console */
console.log(transferStartEpoch);
console.log(transferEndEpoch);
/* eslint-enable */
});
})
.then(() => {
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown, getAsset, getQuoteName } from '@vegaprotocol/markets';
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -8,7 +8,11 @@ import type { Market } from '@vegaprotocol/markets';
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils';
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';
@@ -22,15 +26,23 @@ import {
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import {
sumFees,
sumFeesDiscounts,
useEstimateFees,
} from '../../hooks/use-estimate-fees';
import { KeyValue } from './key-value';
import {
Accordion,
AccordionChevron,
AccordionPanel,
Intent,
Pill,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { FeesBreakdown } from '../fees-breakdown';
const emptyValue = '-';
@@ -50,7 +62,18 @@ export const DealTicketFeeDetails = ({
const feeEstimate = useEstimateFees(order, isMarketInAuction);
const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset;
const totalFees = feeEstimate?.fees && sumFees(feeEstimate?.fees);
const feesDiscounts =
feeEstimate?.fees && sumFeesDiscounts(feeEstimate?.fees);
const totalPercentageDiscount =
feesDiscounts &&
totalFees &&
feesDiscounts.total !== '0' &&
totalFees !== '0' &&
new BigNumber(feesDiscounts.total)
.dividedBy(BigNumber.sum(totalFees, feesDiscounts.total))
.times(100);
return (
<KeyValue
label={t('Fees')}
@@ -59,8 +82,19 @@ export const DealTicketFeeDetails = ({
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
<>
{totalPercentageDiscount && (
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
-{formatNumberPercentage(totalPercentageDiscount, 2)}
</Pill>
)}
{feeEstimate?.totalFeeAmount &&
`~${formatValue(
feeEstimate?.totalFeeAmount,
assetDecimals,
quantum
)}`}
</>
}
labelDescription={
<>
@@ -9,7 +9,7 @@ export interface KeyValuePros {
symbol: string;
indent?: boolean | undefined;
labelDescription?: ReactNode;
formattedValue?: string;
formattedValue?: ReactNode;
onClick?: () => void;
}
@@ -23,7 +23,11 @@ export const KeyValue = ({
onClick,
formattedValue,
}: KeyValuePros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const displayValue = (
<>
{formattedValue || '-'} {symbol || ''}
</>
);
const valueElement = onClick ? (
<button onClick={onClick} className="font-mono ml-auto">
{displayValue}
@@ -0,0 +1,130 @@
import { sumFeesFactors } from '@vegaprotocol/markets';
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import BigNumber from 'bignumber.js';
import { sumFees, sumFeesDiscounts } from '../../hooks';
const formatValue = (
value: string | number | null | undefined,
decimals: number
): string =>
value && !isNaN(Number(value))
? addDecimalsFormatNumber(value, decimals)
: '-';
const FeesBreakdownItem = ({
label,
factor,
value,
symbol,
decimals,
}: {
label: string;
factor?: BigNumber;
value: string;
symbol?: string;
decimals: number;
}) => (
<>
<dt className="col-span-2">{label}</dt>
{factor && (
<dd className="text-right col-span-1">
{formatNumberPercentage(new BigNumber(factor).times(100), 2)}
</dd>
)}
<dd className="text-right col-span-3">
{formatValue(value, decimals)} {symbol || ''}
</dd>
</>
);
export const FeesBreakdown = ({
fees,
feeFactors,
symbol,
decimals,
}: {
fees?: TradeFee;
feeFactors?: FeeFactors;
symbol?: string;
decimals: number;
}) => {
if (!fees) return null;
const totalFees = sumFees(fees);
const {
total: totalDiscount,
referral: referralDiscount,
volume: volumeDiscount,
} = sumFeesDiscounts(fees);
if (totalFees === '0') return null;
return (
<dl className="grid grid-cols-6">
<FeesBreakdownItem
label={t('Infrastructure fee')}
factor={
feeFactors?.infrastructureFee
? new BigNumber(feeFactors?.infrastructureFee)
: undefined
}
value={fees.infrastructureFee}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Liquidity fee')}
factor={
feeFactors?.liquidityFee
? new BigNumber(feeFactors?.liquidityFee)
: undefined
}
value={fees.liquidityFee}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Maker fee')}
factor={
feeFactors?.makerFee ? new BigNumber(feeFactors?.makerFee) : undefined
}
value={fees.makerFee}
symbol={symbol}
decimals={decimals}
/>
{volumeDiscount && volumeDiscount !== '0' && (
<FeesBreakdownItem
label={t('Volume discount')}
factor={new BigNumber(volumeDiscount).dividedBy(
BigNumber.sum(totalFees, totalDiscount)
)}
value={volumeDiscount}
symbol={symbol}
decimals={decimals}
/>
)}
{referralDiscount && referralDiscount !== '0' && (
<FeesBreakdownItem
label={t('Referral discount')}
factor={new BigNumber(referralDiscount).dividedBy(
BigNumber.sum(totalFees, totalDiscount)
)}
value={referralDiscount}
symbol={symbol}
decimals={decimals}
/>
)}
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors) : undefined}
value={totalFees}
symbol={symbol}
decimals={decimals}
/>
</dl>
);
};
+1
View File
@@ -1,3 +1,4 @@
export * from './deal-ticket';
export * from './deal-ticket-validation';
export * from './fees-breakdown';
export * from './trading-mode-tooltip';
@@ -22,6 +22,12 @@ query EstimateFees(
makerFee
infrastructureFee
liquidityFee
makerFeeReferralDiscount
makerFeeVolumeDiscount
infrastructureFeeReferralDiscount
infrastructureFeeVolumeDiscount
liquidityFeeReferralDiscount
liquidityFeeVolumeDiscount
}
totalFeeAmount
}
+7 -1
View File
@@ -15,7 +15,7 @@ export type EstimateFeesQueryVariables = Types.Exact<{
}>;
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } };
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
export const EstimateFeesDocument = gql`
@@ -34,6 +34,12 @@ export const EstimateFeesDocument = gql`
makerFee
infrastructureFee
liquidityFee
makerFeeReferralDiscount
makerFeeVolumeDiscount
infrastructureFeeReferralDiscount
infrastructureFeeVolumeDiscount
liquidityFeeReferralDiscount
liquidityFeeVolumeDiscount
}
totalFeeAmount
}
@@ -6,11 +6,17 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
const data: EstimateFeesQuery = {
estimateFees: {
totalFeeAmount: '12',
totalFeeAmount: '120',
fees: {
infrastructureFee: '2',
liquidityFee: '4',
makerFee: '6',
infrastructureFee: '20',
infrastructureFeeReferralDiscount: '2',
infrastructureFeeVolumeDiscount: '4',
liquidityFee: '40',
liquidityFeeReferralDiscount: '6',
liquidityFeeVolumeDiscount: '8',
makerFee: '60',
makerFeeReferralDiscount: '10',
makerFeeVolumeDiscount: '12',
},
},
};
@@ -67,11 +73,17 @@ describe('useEstimateFees', () => {
)
);
expect(result.current).toEqual({
totalFeeAmount: '6',
totalFeeAmount: '60',
fees: {
infrastructureFee: '1',
liquidityFee: '2',
makerFee: '3',
infrastructureFee: '10',
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFee: '20',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFee: '30',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
},
});
});
@@ -5,6 +5,32 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
const divideByTwo = (n: string) => (BigInt(n) / BigInt(2)).toString();
export const sumFeesDiscounts = (
fees: EstimateFeesQuery['estimateFees']['fees']
) => {
const volume = (
BigInt(fees.makerFeeVolumeDiscount || '0') +
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
BigInt(fees.liquidityFeeVolumeDiscount || '0')
).toString();
const referral = (
BigInt(fees.makerFeeReferralDiscount || '0') +
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
BigInt(fees.liquidityFeeReferralDiscount || '0')
).toString();
return {
volume,
referral,
total: (BigInt(volume) + BigInt(referral)).toString(),
};
};
export const sumFees = (fees: EstimateFeesQuery['estimateFees']['fees']) =>
(
BigInt(fees.makerFee || '0') +
BigInt(fees.infrastructureFee || '0') +
BigInt(fees.liquidityFee || '0')
).toString();
export const useEstimateFees = (
order?: OrderSubmissionBody['orderSubmission'],
@@ -43,6 +69,26 @@ export const useEstimateFees = (
),
liquidityFee: divideByTwo(data.estimateFees.fees.liquidityFee),
makerFee: divideByTwo(data.estimateFees.fees.makerFee),
infrastructureFeeReferralDiscount:
data.estimateFees.fees.infrastructureFeeReferralDiscount &&
divideByTwo(
data.estimateFees.fees.infrastructureFeeReferralDiscount
),
infrastructureFeeVolumeDiscount:
data.estimateFees.fees.infrastructureFeeVolumeDiscount &&
divideByTwo(data.estimateFees.fees.infrastructureFeeVolumeDiscount),
liquidityFeeReferralDiscount:
data.estimateFees.fees.liquidityFeeReferralDiscount &&
divideByTwo(data.estimateFees.fees.liquidityFeeReferralDiscount),
liquidityFeeVolumeDiscount:
data.estimateFees.fees.liquidityFeeVolumeDiscount &&
divideByTwo(data.estimateFees.fees.liquidityFeeVolumeDiscount),
makerFeeReferralDiscount:
data.estimateFees.fees.makerFeeReferralDiscount &&
divideByTwo(data.estimateFees.fees.makerFeeReferralDiscount),
makerFeeVolumeDiscount:
data.estimateFees.fees.makerFeeVolumeDiscount &&
divideByTwo(data.estimateFees.fees.makerFeeVolumeDiscount),
},
}
: data?.estimateFees;
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Button } from '@vegaprotocol/ui-toolkit';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
export const AppFailure = ({
@@ -10,17 +10,15 @@ export const AppFailure = ({
error?: string | null;
}) => {
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const nonIdealWrapperClasses =
'h-full min-h-screen flex items-center justify-center';
return (
<div className={nonIdealWrapperClasses}>
<Splash>
<div className="text-center">
<p className="text-xl mb-4">{title}</p>
{error && <p className="text-sm mb-8">{error}</p>}
<p className="mb-4 text-xl">{title}</p>
{error && <p className="mb-8 text-sm">{error}</p>}
<Button onClick={() => setNodeSwitcher(true)}>
{t('Change node')}
</Button>
</div>
</div>
</Splash>
);
};
@@ -0,0 +1,9 @@
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
export const AppLoader = () => {
return (
<Splash>
<Loader />
</Splash>
);
};
@@ -0,0 +1 @@
export { AppLoader } from './app-loader';
+1
View File
@@ -1,4 +1,5 @@
export * from './app-failure';
export * from './app-loader';
export * from './network-loader';
export * from './network-switcher';
export * from './node-guard';
+16 -12
View File
@@ -1,3 +1,15 @@
fragment TradeFeeFields on TradeFee {
makerFee
infrastructureFee
liquidityFee
makerFeeReferralDiscount
makerFeeVolumeDiscount
infrastructureFeeReferralDiscount
infrastructureFeeVolumeDiscount
liquidityFeeReferralDiscount
liquidityFeeVolumeDiscount
}
fragment FillFields on Trade {
id
market {
@@ -16,14 +28,10 @@ fragment FillFields on Trade {
id
}
buyerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
}
@@ -61,14 +69,10 @@ fragment FillUpdateFields on TradeUpdate {
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
}
+26 -19
View File
@@ -3,9 +3,11 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type FillFieldsFragment = { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
export type TradeFeeFieldsFragment = { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null };
export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } };
export type FillFieldsFragment = { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } };
export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
export type FillsQueryVariables = Types.Exact<{
filter?: Types.InputMaybe<Types.TradesFilter>;
@@ -13,17 +15,30 @@ export type FillsQueryVariables = Types.Exact<{
}>;
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } };
export type FillsEventSubscriptionVariables = Types.Exact<{
filter: Types.TradesSubscriptionFilter;
}>;
export type FillsEventSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } }> | null };
export type FillsEventSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } }> | null };
export const TradeFeeFieldsFragmentDoc = gql`
fragment TradeFeeFields on TradeFee {
makerFee
infrastructureFee
liquidityFee
makerFeeReferralDiscount
makerFeeVolumeDiscount
infrastructureFeeReferralDiscount
infrastructureFeeVolumeDiscount
liquidityFeeReferralDiscount
liquidityFeeVolumeDiscount
}
`;
export const FillFieldsFragmentDoc = gql`
fragment FillFields on Trade {
id
@@ -43,17 +58,13 @@ export const FillFieldsFragmentDoc = gql`
id
}
buyerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
}
`;
${TradeFeeFieldsFragmentDoc}`;
export const FillEdgeFragmentDoc = gql`
fragment FillEdge on TradeEdge {
node {
@@ -76,17 +87,13 @@ export const FillUpdateFieldsFragmentDoc = gql`
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
...TradeFeeFields
}
}
`;
${TradeFeeFieldsFragmentDoc}`;
export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
+148 -73
View File
@@ -4,36 +4,38 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider';
import { FillsTable, getFeesBreakdown } from './fills-table';
import {
FeesDiscountBreakdownTooltip,
FillsTable,
getFeesBreakdown,
getTotalFeesDiscounts,
} from './fills-table';
import { generateFill } from './test-helpers';
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
describe('FillsTable', () => {
let defaultFill: PartialDeep<Trade>;
beforeEach(() => {
defaultFill = {
price: '100',
size: '300000',
market: {
decimalPlaces: 2,
positionDecimalPlaces: 5,
tradableInstrument: {
instrument: {
code: 'test market',
product: {
__typename: 'Future',
settlementAsset: {
decimals: 2,
symbol: 'BTC',
},
},
const partyId = 'party-id';
const defaultFill: PartialDeep<Trade> = {
price: '100',
size: '300000',
market: {
decimalPlaces: 2,
positionDecimalPlaces: 5,
tradableInstrument: {
instrument: {
code: 'test market',
product: {
__typename: 'Future',
settlementAsset: {
decimals: 2,
symbol: 'BTC',
},
},
},
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
};
});
},
},
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
};
describe('FillsTable', () => {
it('correct columns are rendered', async () => {
await act(async () => {
render(<FillsTable partyId="party-id" rowData={[generateFill()]} />);
@@ -47,6 +49,7 @@ describe('FillsTable', () => {
'Notional',
'Role',
'Fee',
'Fee Discount',
'Date',
'', // action column
];
@@ -55,7 +58,6 @@ describe('FillsTable', () => {
});
it('formats cells correctly for buyer fill', async () => {
const partyId = 'party-id';
const buyerFill = generateFill({
...defaultFill,
buyer: {
@@ -68,7 +70,9 @@ describe('FillsTable', () => {
liquidityFee: '2',
},
});
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
await act(async () => {
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.code || '',
@@ -77,6 +81,7 @@ describe('FillsTable', () => {
'3.00 BTC',
'Maker',
'2.00 BTC',
'0.27 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
'', // action column
];
@@ -89,7 +94,6 @@ describe('FillsTable', () => {
});
it('should format cells correctly for seller fill', async () => {
const partyId = 'party-id';
const buyerFill = generateFill({
...defaultFill,
seller: {
@@ -102,7 +106,9 @@ describe('FillsTable', () => {
liquidityFee: '1',
},
});
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
await act(async () => {
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
@@ -112,6 +118,7 @@ describe('FillsTable', () => {
'3.00 BTC',
'Taker',
'0.03 BTC',
'0.27 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
'', // action column
];
@@ -124,7 +131,6 @@ describe('FillsTable', () => {
});
it('should format cells correctly for side unspecified', async () => {
const partyId = 'party-id';
const buyerFill = generateFill({
...defaultFill,
seller: {
@@ -137,7 +143,9 @@ describe('FillsTable', () => {
liquidityFee: '1',
},
});
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
await act(async () => {
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
@@ -147,6 +155,7 @@ describe('FillsTable', () => {
'3.00 BTC',
'-',
'0.03 BTC',
'0.27 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
'', // action column
];
@@ -158,30 +167,31 @@ describe('FillsTable', () => {
expect(amountCell).toHaveClass('text-market-red');
});
it('should render correct maker or taker role', async () => {
const partyId = 'party-id';
it('should render correct taker role', async () => {
const takerFill = generateFill({
seller: {
id: partyId,
},
aggressor: Schema.Side.SIDE_SELL,
});
const { rerender } = render(
<FillsTable partyId={partyId} rowData={[takerFill]} />
);
await act(async () => {
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
});
expect(
screen
.getAllByRole('gridcell')
.find((c) => c.getAttribute('col-id') === 'aggressor')
).toHaveTextContent('Taker');
});
it('should render correct maker role', async () => {
const makerFill = generateFill({
seller: {
id: partyId,
},
aggressor: Schema.Side.SIDE_BUY,
});
rerender(<FillsTable partyId={partyId} rowData={[makerFill]} />);
render(<FillsTable partyId={partyId} rowData={[makerFill]} />);
expect(
screen
@@ -191,22 +201,19 @@ describe('FillsTable', () => {
});
it('should render tooltip over fees', async () => {
const partyId = 'party-id';
const takerFill = generateFill({
seller: {
id: partyId,
},
aggressor: Schema.Side.SIDE_SELL,
});
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
await act(async () => {
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
});
const feeCell = screen
.getAllByRole('gridcell')
.find(
(c) =>
c.getAttribute('col-id') ===
'market.tradableInstrument.instrument.product'
);
.find((c) => c.getAttribute('col-id') === 'fee');
expect(feeCell).toBeInTheDocument();
await userEvent.hover(feeCell as HTMLElement);
@@ -215,8 +222,29 @@ describe('FillsTable', () => {
).toBeInTheDocument();
});
it('should render tooltip over fees discounts', async () => {
const takerFill = generateFill({
seller: {
id: partyId,
},
aggressor: Schema.Side.SIDE_SELL,
});
await act(async () => {
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
});
const cell = screen
.getAllByRole('gridcell')
.find((c) => c.getAttribute('col-id') === 'fee-discount');
expect(cell).toBeInTheDocument();
await userEvent.hover(cell as HTMLElement);
expect(
await screen.findByTestId('fee-discount-breakdown-tooltip')
).toBeInTheDocument();
});
it('negative positionDecimalPoints should be properly rendered in size column', async () => {
const partyId = 'party-id';
const negativeDecimalPositionFill = generateFill({
...defaultFill,
market: {
@@ -235,36 +263,83 @@ describe('FillsTable', () => {
.find((c) => c.getAttribute('col-id') === 'size');
expect(sizeCell).toHaveTextContent('3,000,000,000');
});
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
describe('FeesDiscountBreakdownTooltip', () => {
it('shows all discounts', () => {
const data = generateFill({
...defaultFill,
buyer: {
id: partyId,
},
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '-1000',
totalFee: '4000',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
const props = {
data,
partyId,
value: data.market,
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd');
const expected = [
{ label: 'Infrastructure Fee Referral Discount', value: '0.05 BTC' },
{ label: 'Infrastructure Fee Volume Discount', value: '0.06 BTC' },
{ label: 'Liquidity Fee Referral Discount', value: '0.01 BTC' },
{ label: 'Liquidity Fee Volume Discount', value: '0.02 BTC' },
{ label: 'Maker Fee Referral Discount', value: '0.03 BTC' },
{ label: 'Maker Fee Volume Discount', value: '0.04 BTC' },
];
expected.forEach(({ label, value }, i) => {
expect(dt[i]).toHaveTextContent(label);
expect(dd[i]).toHaveTextContent(value);
});
});
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '-1000',
totalFee: '4000',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
});
});
describe('getTotalFeesDiscounts', () => {
it('should return correct total value', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
};
expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual(
(1 + 2 + 3 + 4 + 5 + 6).toString()
);
});
});
+145 -32
View File
@@ -28,7 +28,10 @@ import type {
import { forwardRef } from 'react';
import BigNumber from 'bignumber.js';
import type { Trade } from './fills-data-provider';
import type { FillFieldsFragment } from './__generated__/Fills';
import type {
FillFieldsFragment,
TradeFeeFieldsFragment,
} from './__generated__/Fills';
import { FillActionsDropdown } from './fill-actions-dropdown';
import { getAsset } from '@vegaprotocol/markets';
@@ -87,13 +90,34 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
},
{
headerName: t('Fee'),
field: 'market.tradableInstrument.instrument.product',
colId: 'fee',
field: 'market',
valueFormatter: formatFee(partyId),
type: 'rightAligned',
tooltipField: 'market.tradableInstrument.instrument.product',
tooltipField: 'market',
tooltipComponent: FeesBreakdownTooltip,
tooltipComponentParams: { partyId },
},
{
headerName: t('Fee Discount'),
colId: 'fee-discount',
field: 'market',
valueFormatter: formatFeeDiscount(partyId),
type: 'rightAligned',
// return null to disable tooltip if fee discount is 0 or empty
tooltipValueGetter: ({ valueFormatted, value }) => {
return valueFormatted && /[1-9]/.test(valueFormatted)
? valueFormatted
: null;
},
cellRenderer: ({
value,
valueFormatted,
}: VegaICellRendererParams<Trade, 'market'>) =>
`${valueFormatted} ${(value && getAsset(value))?.symbol}`,
tooltipComponent: FeesDiscountBreakdownTooltip,
tooltipComponentParams: { partyId },
},
{
headerName: t('Date'),
field: 'createdAt',
@@ -214,14 +238,11 @@ const formatRole = (partyId: string) => {
const formatFee = (partyId: string) => {
return ({
value,
value: market,
data,
}: VegaValueFormatterParams<
Trade,
'market.tradableInstrument.instrument.product'
>) => {
if (!value || !data || !data?.market) return '-';
const asset = getAsset(data.market);
}: VegaValueFormatterParams<Trade, 'market'>) => {
if (!market || !data) return '-';
const asset = getAsset(market);
const { fees: feesObj, role } = getRoleAndFees({ data, partyId });
if (!feesObj) return '-';
@@ -231,6 +252,21 @@ const formatFee = (partyId: string) => {
};
};
const formatFeeDiscount = (partyId: string) => {
return ({
value: market,
data,
}: VegaValueFormatterParams<Trade, 'market'>) => {
if (!market || !data) return '-';
const asset = getAsset(market);
const { fees } = getRoleAndFees({ data, partyId });
if (!fees) return '-';
const total = getTotalFeesDiscounts(fees);
return addDecimalsFormatNumber(total, asset.decimals);
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
@@ -251,50 +287,50 @@ export const getRoleAndFees = ({
partyId?: string;
}) => {
let role: Role;
let feesObj;
let fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
feesObj = data?.buyerFee;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
feesObj = data?.sellerFee;
fees = data?.sellerFee;
} else {
role = '-';
feesObj = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
feesObj = data?.sellerFee;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
feesObj = data?.buyerFee;
fees = data?.buyerFee;
} else {
role = '-';
feesObj = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', feesObj: '-' };
return { role: '-', fees: undefined };
}
return { role, fees: feesObj };
return { role, fees };
};
const FeesBreakdownTooltip = ({
data,
value,
value: market,
partyId,
}: ITooltipParams & { partyId?: string }) => {
if (!value?.settlementAsset || !data) {
}: ITooltipParams<Trade, Trade['market']> & { partyId?: string }) => {
if (!market || !data) {
return null;
}
const asset = value.settlementAsset;
const asset = getAsset(market);
const { role, fees: feesObj } = getRoleAndFees({ data, partyId }) ?? {};
if (!feesObj) return null;
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null;
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
getFeesBreakdown(role, feesObj);
getFeesBreakdown(role, fees);
return (
<div
@@ -344,14 +380,91 @@ const FeesBreakdownTooltip = ({
);
};
const FeesDiscountBreakdownTooltipItem = ({
value,
label,
asset,
}: {
value?: string | null;
label: string;
asset: ReturnType<typeof getAsset>;
}) =>
value ? (
<>
<dt className="col-span-1">{label}</dt>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
</dd>
</>
) : null;
export const FeesDiscountBreakdownTooltip = ({
data,
partyId,
}: ITooltipParams<Trade, Trade['market']> & { partyId?: string }) => {
if (!data || !data.market) {
return null;
}
const asset = getAsset(data.market);
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null;
return (
<div
data-testid="fee-discount-breakdown-tooltip"
className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
>
<dl className="grid grid-cols-2 gap-x-1">
<FeesDiscountBreakdownTooltipItem
value={fees.infrastructureFeeReferralDiscount}
label={t('Infrastructure Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.infrastructureFeeVolumeDiscount}
label={t('Infrastructure Fee Volume Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.liquidityFeeReferralDiscount}
label={t('Liquidity Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.liquidityFeeVolumeDiscount}
label={t('Liquidity Fee Volume Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.makerFeeReferralDiscount}
label={t('Maker Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.makerFeeVolumeDiscount}
label={t('Maker Fee Volume Discount')}
asset={asset}
/>
</dl>
</div>
);
};
export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => {
return (
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
BigInt(fees.liquidityFeeReferralDiscount || '0') +
BigInt(fees.liquidityFeeVolumeDiscount || '0') +
BigInt(fees.makerFeeReferralDiscount || '0') +
BigInt(fees.makerFeeVolumeDiscount || '0')
).toString();
};
export const getFeesBreakdown = (
role: Role,
feesObj: {
__typename?: 'TradeFee' | undefined;
makerFee: string;
infrastructureFee: string;
liquidityFee: string;
}
feesObj: TradeFeeFieldsFragment
) => {
const makerFee =
role === MAKER
+12
View File
@@ -28,12 +28,24 @@ export const generateFill = (override?: PartialDeep<Trade>) => {
makerFee: '100',
infrastructureFee: '100',
liquidityFee: '100',
liquidityFeeReferralDiscount: '1',
liquidityFeeVolumeDiscount: '2',
makerFeeReferralDiscount: '3',
makerFeeVolumeDiscount: '4',
infrastructureFeeReferralDiscount: '5',
infrastructureFeeVolumeDiscount: '6',
},
sellerFee: {
__typename: 'TradeFee',
makerFee: '200',
infrastructureFee: '200',
liquidityFee: '200',
liquidityFeeReferralDiscount: '2',
liquidityFeeVolumeDiscount: '3',
makerFeeReferralDiscount: '4',
makerFeeVolumeDiscount: '5',
infrastructureFeeReferralDiscount: '6',
infrastructureFeeVolumeDiscount: '7',
},
market: {
__typename: 'Market',
@@ -161,6 +161,50 @@ describe('LedgerExportForm', () => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('Time zone sentence should be properly displayed', () => {
let timeZoneOffset = -120;
Date.prototype.getTimezoneOffset = jest.fn(() => timeZoneOffset);
const { rerender } = render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(
screen.getByText(/^The downloaded file uses the UTC/)
).toBeInTheDocument();
expect(
screen.getByText(/Your time zone is UTC-02:00\.$/)
).toBeInTheDocument();
timeZoneOffset = 270;
rerender(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(
screen.getByText(/Your time zone is UTC\+04:30\.$/)
).toBeInTheDocument();
timeZoneOffset = 0;
rerender(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(
screen.queryByText(/^The downloaded file uses the UTC/)
).not.toBeInTheDocument();
});
});
describe('createDownloadUrl', () => {
@@ -15,6 +15,16 @@ import { subDays } from 'date-fns';
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
const toHoursAndMinutes = (totalMinutes: number) => {
const minutes = totalMinutes % 60;
const hours = Math.floor(totalMinutes / 60);
return `${hours > 0 ? '+' : '-'}${padTo2Digits(hours)}:${padTo2Digits(
minutes
)}`;
};
const padTo2Digits = (num: number) => Math.abs(num).toString().padStart(2, '0');
const getProtoHost = (vegaurl: string) => {
const loc = new URL(vegaurl);
return `${loc.protocol}//${loc.host}`;
@@ -120,6 +130,8 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
return null;
}
const offset = new Date().getTimezoneOffset();
return (
<form onSubmit={startDownload} className="p-4 w-[350px]">
<h2 className="mb-4">{t('Export ledger entries')}</h2>
@@ -166,6 +178,14 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
{t('Download')}
</TradingButton>
</div>
{offset && (
<p className="text-xs text-neutral-400 mt-1">
{t(
'The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC%s.',
[toHoursAndMinutes(offset)]
)}
</p>
)}
</form>
);
};
@@ -1,115 +0,0 @@
import { totalFeesPercentage } from '../../market-utils';
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
export const FeesCell = ({ feeFactors }: { feeFactors: FeeFactors }) => (
<Tooltip description={<FeesBreakdownPercentage feeFactors={feeFactors} />}>
<span>{totalFeesPercentage(feeFactors) ?? '-'}</span>
</Tooltip>
);
export const FeesBreakdownPercentage = ({
feeFactors,
}: {
feeFactors?: FeeFactors;
}) => {
if (!feeFactors) return null;
return (
<dl className="grid grid-cols-2 gap-x-2">
<dt>{t('Infrastructure fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.infrastructureFee).times(100)
)}
</dd>
<dt>{t('Liquidity fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.liquidityFee).times(100)
)}
</dd>
<dt>{t('Maker fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(new BigNumber(feeFactors.makerFee).times(100))}
</dd>
<dt>{t('Total fees')}</dt>
<dd className="text-right">{totalFeesPercentage(feeFactors)}</dd>
</dl>
);
};
export const FeesBreakdown = ({
fees,
feeFactors,
symbol,
decimals,
}: {
fees?: TradeFee;
feeFactors?: FeeFactors;
symbol?: string;
decimals: number;
}) => {
if (!fees) return null;
const totalFees = new BigNumber(fees.makerFee)
.plus(fees.infrastructureFee)
.plus(fees.liquidityFee)
.toString();
if (totalFees === '0') return null;
const formatValue = (value: string | number | null | undefined): string => {
return value && !isNaN(Number(value))
? addDecimalsFormatNumber(value, decimals)
: '-';
};
return (
<dl className="grid grid-cols-6">
<dt className="col-span-2">{t('Infrastructure fee')}</dt>
{feeFactors && (
<dd className="text-right col-span-1">
{formatNumberPercentage(
new BigNumber(feeFactors.infrastructureFee).times(100)
)}
</dd>
)}
<dd className="text-right col-span-3">
{formatValue(fees.infrastructureFee)} {symbol || ''}
</dd>
<dt className="col-span-2">{t('Liquidity fee')}</dt>
{feeFactors && (
<dd className="text-right col-span-1">
{formatNumberPercentage(
new BigNumber(feeFactors.liquidityFee).times(100)
)}
</dd>
)}
<dd className="text-right col-span-3">
{formatValue(fees.liquidityFee)} {symbol || ''}
</dd>
<dt className="col-span-2">{t('Maker fee')}</dt>
{feeFactors && (
<dd className="text-right col-span-1">
{formatNumberPercentage(
new BigNumber(feeFactors.makerFee).times(100)
)}
</dd>
)}
<dd className="text-right col-span-3">
{formatValue(fees.makerFee)} {symbol || ''}
</dd>
<dt className="col-span-2">{t('Total fees')}</dt>
{feeFactors && (
<dd className="text-right col-span-1">
{totalFeesPercentage(feeFactors)}
</dd>
)}
<dd className="text-right col-span-3">
{formatValue(totalFees)} {symbol || ''}
</dd>
</dl>
);
};
-1
View File
@@ -1,4 +1,3 @@
export * from './fees-breakdown';
export * from './last-24h-price-change';
export * from './last-24h-volume';
export * from './market-info';
@@ -4,7 +4,7 @@ import { Fragment, useMemo, useState } from 'react';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { marketDataProvider } from '../../market-data-provider';
import { totalFeesPercentage } from '../../market-utils';
import { totalFeesFactorsPercentage } from '../../market-utils';
import {
Accordion,
AccordionChevron,
@@ -91,7 +91,7 @@ export const CurrentFeesInfoPanel = ({ market }: MarketInfoProps) => (
makerFee: market.fees.factors.makerFee,
infrastructureFee: market.fees.factors.infrastructureFee,
liquidityFee: market.fees.factors.liquidityFee,
totalFees: totalFeesPercentage(market.fees.factors),
totalFees: totalFeesFactorsPercentage(market.fees.factors),
}}
asPercentage={true}
/>
+3 -3
View File
@@ -3,7 +3,7 @@ import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
import {
calcTradedFactor,
filterAndSortMarkets,
totalFeesPercentage,
totalFeesFactorsPercentage,
} from './market-utils';
const { MarketState, MarketTradingMode } = Schema;
@@ -63,7 +63,7 @@ describe('mapDataToMarketList', () => {
});
});
describe('totalFees', () => {
describe('totalFeesFactorsPercentage', () => {
const createFee = (...f: number[]): Market['fees']['factors'] => ({
__typename: 'FeeFactors',
infrastructureFee: f[0].toString(),
@@ -78,7 +78,7 @@ describe('totalFees', () => {
{ i: createFee(0.01, 0.056782, 0.003), o: '6.9782%' },
{ i: createFee(0.01, 0.056782, 0), o: '6.6782%' },
])('adds fees correctly', ({ i, o }) => {
expect(totalFeesPercentage(i)).toEqual(o);
expect(totalFeesFactorsPercentage(i)).toEqual(o);
});
});
+4 -5
View File
@@ -49,18 +49,17 @@ export const getQuoteName = (market: Partial<Market>) => {
throw new Error('Failed to retrieve quoteName. Invalid product type');
};
export const totalFees = (fees: Market['fees']['factors']) => {
export const sumFeesFactors = (fees: Market['fees']['factors']) => {
return fees
? new BigNumber(fees.makerFee)
.plus(fees.liquidityFee)
.plus(fees.infrastructureFee)
.times(100)
: undefined;
};
export const totalFeesPercentage = (fees: Market['fees']['factors']) => {
const total = fees && totalFees(fees);
return total ? formatNumberPercentage(total) : undefined;
export const totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => {
const total = fees && sumFeesFactors(fees);
return total ? formatNumberPercentage(total.times(100)) : undefined;
};
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
@@ -123,11 +123,9 @@ export const OrderViewDialog = ({
{t('rejection reason')}
</div>
<div data-testid={`order-rejection-reason-value`}>
{
Schema.OrderRejectionReasonMapping[
order.rejectionReason as Schema.OrderRejectionReason
]
}
{Schema.OrderRejectionReasonMapping[
order.rejectionReason as Schema.OrderRejectionReason
] || order.rejectionReason}
</div>
</KeyValueTableRow>
)}
@@ -30,11 +30,8 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
const generate = useMemo(() => pseudoRandom(1), []);
return (
<div
className="flex flex-col items-center pre-loader-center"
data-testid="loader"
>
<div className={`${wrapperClasses} flex flex-wrap pre-loader-wrapper`}>
<div className="flex flex-col items-center" data-testid="loader">
<div className={`${wrapperClasses} flex flex-wrap`}>
{new Array(items).fill(null).map((_, i) => {
return (
<div
+36 -1
View File
@@ -235,6 +235,39 @@ interface ProposalUpdateAssetTerms {
enactmentTimestamp: number;
}
interface ProposalTransferTerms {
newTransfer: {
changes: {
fractionOfBalance: string;
amount: string;
sourceType: string;
source: string;
transferType: string;
destinationType: string;
destination: string;
asset: string;
recurring?: {
startEpoch: number;
endEpoch: number;
};
oneOff?: {
deliverOn: string;
};
};
};
closingTimestamp: number;
enactmentTimestamp: number;
validationTimestamp?: number;
}
interface ProposalCancelTransferTerms {
cancelTransfer: {
changes: {
transferId: string;
};
};
}
interface DataSourceSpecBinding {
settlementDataProperty: string;
tradingTerminationProperty: string;
@@ -342,7 +375,9 @@ export interface ProposalSubmission {
| ProposalUpdateMarketTerms
| ProposalNetworkParameterTerms
| ProposalNewAssetTerms
| ProposalUpdateAssetTerms;
| ProposalUpdateAssetTerms
| ProposalTransferTerms
| ProposalCancelTransferTerms;
}
export interface ProposalSubmissionBody {
+1 -1
View File
@@ -71,7 +71,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "1.13.2",
"pennant": "1.13.4",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+1
View File
@@ -9,3 +9,4 @@ When I enter on ledger entries tab in portfolio page
- in the form **Must** see inputs for select time period, in which reports will be downloaded (<a name="7007-LEEN-003" href="#7007-LEEN-003">7007-LEEN-003</a>)
- default preselected period **Must** be the last 7 days (<a name="7007-LEEN-004" href="#7007-LEEN-004">7007-LEEN-004</a>)
- during download a loader component **Must** be visible and all interactive elements in the form **Must** be disabled (<a name="7007-LEEN-005" href="#7007-LEEN-005">7007-LEEN-005</a>)
- **Must** see a note about time in file are in UTC and timezone of the user relative to UTC (<a name="7007-LEEN-006" href="#7007-LEEN-006">7007-LEEN-006</a>)
+22
View File
@@ -0,0 +1,22 @@
# View my funding payment(s)
- **Must** be able to view my funding payment from the trading page (<a name="7008-FUND-001" href="#7008-FUND-001">7008-FUND-001</a>)
- **Must** be able to view my funding payment from the portfolio page (<a name="7008-FUND-002" href="#7008-FUND-002">7008-FUND-002</a>)
- **Must** be able to view the following columns (<a name="7008-FUND-003" href="#7008-FUND-003">7008-FUND-003</a>):
- Market
- Amount
- Date
- **Must** The columns 'Amount' must change color and have a + or - suffix depending on being positive/negative (<a name="7008-FUND-004" href="#7008-FUND-004">7008-FUND-004</a>)
- **Must** be able to sort each column by asc and dsc (<a name="7008-FUND-005" href="#7008-FUND-005">7008-FUND-005</a>)
- **Must** be able to drag and drop column names to re order (<a name="7008-FUND-006" href="#7008-FUND-006">7008-FUND-006</a>)
- **Must** be able to remove columns by dragging headers out of the table (<a name="7008-FUND-007" href="#7008-FUND-007">7008-FUND-007</a>)
- **Must** be able to resize the width of the columns (<a name="7008-FUND-008" href="#7008-FUND-008">7008-FUND-008</a>)
- **Must** retain previous column sizing on refresh (<a name="7008-FUND-009" href="#7008-FUND-009">7008-FUND-009</a>)
# Chart
- **Must** see Funding tab on trading page for perp market only (<a name="7008-FUND-010" href="#7008-FUND-010">7008-FUND-010</a>)
- **Must** see funding rate on hover (<a name="7008-FUND-011" href="#7008-FUND-011">7008-FUND-011</a>)
+7 -5
View File
@@ -69,21 +69,23 @@ The specs no longer exist in this repo and can be found [here](https://github.co
## `6000` Markets and analysis
- `6001-MARK` [Find markets](6001-MARK-find_markets.md)
- `6002-MARD` [View market specification](6002-MDET-market-details.md) `TODO`
- `6003-ORDB` [Analyze Order book](6003-ORDB-order_book.md) `TODO`
- `6002-MARD` [View market specification](6002-MDET-market-details.md)
- `6003-ORDB` [Analyze Order book](6003-ORDB-order_book.md)
- `6004-PHIS` [Analyze price history](6004-PHIS-price_history.md) `TODO`
- `6005-THIS` [Analyze trade history](6005-THIS-trade_history.md) `TODO`
- `6005-THIS` [Analyze trade history](6005-THIS-trade_history.md)
- `6006-DEPC` [View depth chart](6006-DEPC-chart.md)
- `6007-CHAR` [View chart](6007-CHAR-chart.md)
## `7000` Collateral, Orders, Positions and Fills
## `7000` Collateral, Orders, Positions, Fills and Funding
- `7001-COLL` [View my collateral / accounts](7001-COLL-collateral.md)
- `7002-SORD` [Submit an order](7002-SORD-submit_orders.md)
- `7003-MORD` [Manage my orders](7003-MORD-manage_orders.md)
- `7004-POSI` [View my positions](7004-POSI-positions.md) `TODO`
- `7004-POSI` [View my positions](7004-POSI-positions.md)
- `7005-FILL` [View my trades/fills](7005-FILL-fills.md)
- `7006-FEES` [View my trading fees](7006-FEES-fees.md) `TODO`
- `7007-LEEN` [View ledger entries](7006-FEES-fees.md)
- `7008-FUND` [View my funding payment](7006-FEES-fees.md)
## `8000` Understand transactions and blocks
+4 -4
View File
@@ -20500,10 +20500,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@1.13.2:
version "1.13.2"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.13.2.tgz#8ae0cf23ecee35a7e8baddaa9477e94006581e3b"
integrity sha512-gdzTUfuvR3Jse247+++mUdJJcDrExtoxljZ6TjVjnvOD7Y0Cx7yiA47VKzztcw5JmoEHbMoDeX3ySznAjIpi7Q==
pennant@1.13.4:
version "1.13.4"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.13.4.tgz#33a5f3413634a2341a7b91c917f023c59ecc44c7"
integrity sha512-sqwkUiYHxmS97RY8jToMfgR9ePcEr5PWVQu9BPrhdUIa1Q/NztE36SWM5tVIsPPTx3pPIHYTNEVJm02Ubl8cZQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"