Compare commits

...
Author SHA1 Message Date
Matthew Russell 79ce55f160 chore: removed unused function 2023-10-13 07:42:55 -07:00
Matthew Russell 5ffc67aff3 chore: remove maintenance page from environment 2023-10-13 07:09:41 -07:00
Matthew Russell 9880aea753 chore: delete maintenance page 2023-10-13 07:09:41 -07:00
Matthew Russell c2901bb27d chore: split out files for _app.page.tsx 2023-10-13 07:09:41 -07:00
Matthew Russell b3b10c83fb chore: add comments 2023-10-13 07:09:41 -07:00
Matthew Russell 38904b6442 fix: use NodeFailure in governance and explorer 2023-10-13 07:09:41 -07:00
Matthew Russell 98ed5778a3 fix: glitchy waterfall loading 2023-10-13 07:09:41 -07:00
Matthew Russell 341d78e761 chore(trading): fix hydration errors and improve loading experience (#5038) 2023-10-13 16:02:33 +02:00
Joe Tsanganddaro-maj 58cc074538 test(governance): governance transfer proposal tests (#5011)
Co-authored-by: daro-maj <119658839+daro-maj@users.noreply.github.com>
2023-10-12 18:48:39 +02:00
Bartłomiej Głownia 852c9bb8f4 feat(trading): display fees discounts (#4800) 2023-10-12 16:58:57 +02:00
Maciek efe9b7a0e7 chore(trading): upgrade pennant version (#5023) 2023-10-12 13:18:17 +00:00
daro-maj 695a059dde test(trading): add funding spec (#5028) 2023-10-12 11:58:32 +01:00
Maciek 5d7e4d273a chore(trading): add timezone note to the ledger entries form (#5018) 2023-10-12 09:12:54 +00:00
Sam Keen b137e024c6 feat(governance): volume discount proposals (#5022) 2023-10-11 18:51:52 +00:00
Ben 6651ee52b4 chore(trading): oracle banner test (#5020) 2023-10-11 15:54:26 +01:00
m.rayandRado 0d14722c5d feat(trading): add risk factors leverage and update accordion panel (#5000)
Co-authored-by: Rado <szpiechrados@gmail.com>
2023-10-11 10:19:43 +01:00
Sam Keen 4968b537b8 feat(governance): referral proposals (#5013) 2023-10-11 09:11:35 +00:00
Bartłomiej Głownia 5f5c45f3b9 feat(trading): display empty values for funding and countdown if perp market is in auction (#4987) 2023-10-10 16:59:31 +01:00
111 changed files with 2461 additions and 1178 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import {
AppFailure,
NetworkLoader,
NodeFailure,
NodeGuard,
NodeSwitcherDialog,
useEnvironment,
@@ -31,7 +31,7 @@ function App() {
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
@@ -3,6 +3,7 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets';
import {
LiquidityPriceRangeInfoPanel,
LiquiditySLAParametersInfoPanel,
MarginScalingFactorsPanel,
PriceMonitoringBoundsInfoPanel,
SuccessionLineInfoPanel,
getDataSourceSpecForSettlementData,
@@ -17,7 +18,6 @@ import {
OracleInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import { MarketInfoTable } from '@vegaprotocol/markets';
@@ -69,8 +69,8 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
<MetadataInfoPanel market={market} />
<h2 className={headerClassName}>{t('Risk model')}</h2>
<RiskModelInfoPanel market={market} />
<h2 className={headerClassName}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel market={market} />
<h2 className={headerClassName}>{t('Margin scaling factors')}</h2>
<MarginScalingFactorsPanel market={market} />
<h2 className={headerClassName}>{t('Risk factors')}</h2>
<RiskFactorsInfoPanel market={market} />
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => (
+4
View File
@@ -22,7 +22,11 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
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
+2
View File
@@ -6,3 +6,5 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+2
View File
@@ -6,3 +6,5 @@ NX_ETHERSCAN_URL=https://etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
+2
View File
@@ -6,3 +6,5 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
@@ -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
+2
View File
@@ -35,4 +35,6 @@ NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_GOVERNANCE_TRANSFERS=false
NX_VOLUME_DISCOUNTS=false
+2
View File
@@ -35,3 +35,5 @@ NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+2
View File
@@ -27,3 +27,5 @@ NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+2
View File
@@ -26,3 +26,5 @@ NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
+2
View File
@@ -25,3 +25,5 @@ NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
+2
View File
@@ -23,4 +23,6 @@ NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=true
NX_VOLUME_DISCOUNTS=true
+2
View File
@@ -28,3 +28,5 @@ NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+2
View File
@@ -24,3 +24,5 @@ NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
+1
View File
@@ -1,4 +1,5 @@
/* eslint-disable */
process.env.TZ = 'GMT';
export default {
displayName: 'governance',
preset: '../../jest.preset.js',
+3 -3
View File
@@ -38,10 +38,10 @@ import {
NetworkLoader,
useInitializeEnv,
NodeGuard,
AppFailure,
NodeSwitcherDialog,
useNodeSwitcherStore,
DocsLinks,
NodeFailure,
} from '@vegaprotocol/environment';
import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client';
@@ -184,7 +184,7 @@ const Web3Container = ({
<TemplateSidebar sidebar={sideBar}>
<AppRouter />
</TemplateSidebar>
<footer className="p-4 border-t border-neutral-700 break-all">
<footer className="p-4 break-all border-t border-neutral-700">
<NetworkInfo />
</footer>
</AppLayout>
@@ -312,7 +312,7 @@ const AppContainer = () => {
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
<NodeFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
}
>
<AsyncRenderer<EthereumConfig | null>
+25 -1
View File
@@ -709,6 +709,8 @@
"NewMarketProposal": "New market proposal",
"UpdateMarketProposal": "Update market proposal",
"UpdateMarketStateProposal": "Update market state proposal",
"UpdateReferralProgramProposal": "Update referral program proposal",
"UpdateVolumeDiscountProgramProposal": "Update volume discount program proposal",
"MarketChange": "Market change",
"MarketStateChange": "Market state change",
"MarketDetails": "Market details",
@@ -732,6 +734,8 @@
"NewMarketSpotProduct": "New market - spot",
"UpdateMarket": "Update market",
"UpdateMarketState": "Update market state",
"UpdateReferralProgram": "Update referral program",
"UpdateVolumeDiscountProgram": "Update volume discount program",
"NewAsset": "New asset",
"UpdateAsset": "Update asset",
"AssetID": "Asset ID",
@@ -890,5 +894,25 @@
"HowToProposeRawStep2": "2. Use the appropriate proposal template in the docs:",
"HowToProposeRawStep3": "3. Submit on-chain below",
"proposalTransferDetails": "New governance transfer details",
"proposalCancelTransferDetails": "Cancel governance transfer details"
"proposalCancelTransferDetails": "Cancel governance transfer details",
"BenefitTiers": "Benefit tiers",
"BenefitTierMinimumEpochs": "Minimum epochs",
"BenefitTierMinimumEpochsDescription": "The minimum number of epochs the party needs to be in the referral set to be eligible for the benefit",
"BenefitTierMinimumRunningNotionalTakerVolume": "Minimum running notional taker volume",
"BenefitTierMinimumRunningNotionalTakerVolumeDescription": "The minimum running notional for the given benefit tier",
"BenefitTierReferralDiscountFactor": "Referral discount factor",
"BenefitTierReferralDiscountFactorDescription": "The proportion of the referee's taker fees to be discounted",
"BenefitTierReferralRewardFactor": "Referral reward factor",
"BenefitTierReferralRewardFactorDescription": "The proportion of the referee's taker fees to be rewarded to the referrer",
"StakingTiers": "Staking tiers",
"StakingTierMinimumStakedTokens": "Minimum staked tokens",
"StakingTierMinimumStakedTokensDescription": "Required number of governance tokens ($VEGA) a referrer must have staked to receive the multiplier",
"StakingTierReferralRewardMultiplier": "Referral reward multiplier",
"StakingTierReferralRewardMultiplierDescription": "Multiplier applied to the referral reward factor when calculating referral rewards due to the referrer",
"WindowLength": "Window length",
"WindowLengthDescription": "Number of epochs over which to evaluate a referral set's running volume",
"EndOfProgramTimestamp": "End of program",
"EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled.",
"BenefitTierVolumeDiscountFactor": "Volume discount factor",
"BenefitTierVolumeDiscountFactorDescription": "Discount given to those in this benefit tier"
}
@@ -188,6 +188,8 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -98,6 +98,16 @@ export const ProposalHeader = ({
);
break;
}
case 'UpdateReferralProgram': {
proposalType = 'UpdateReferralProgram';
fallbackTitle = t('UpdateReferralProgramProposal');
break;
}
case 'UpdateVolumeDiscountProgram': {
proposalType = 'UpdateVolumeDiscountProgram';
fallbackTitle = t('UpdateVolumeDiscountProgramProposal');
break;
}
case 'NewAsset': {
proposalType = 'NewAsset';
fallbackTitle = t('NewAssetProposal');
@@ -12,12 +12,12 @@ import {
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
getDataSourceSpecForSettlementSchedule,
getDataSourceSpecForSettlementData,
getDataSourceSpecForTradingTermination,
getSigners,
MarginScalingFactorsPanel,
} from '@vegaprotocol/markets';
import {
Button,
@@ -219,8 +219,10 @@ export const ProposalMarketData = ({
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel
<h2 className={marketDataHeaderStyles}>
{t('Margin scaling factors')}
</h2>
<MarginScalingFactorsPanel
market={marketData}
parentMarket={parentMarketData}
/>
@@ -0,0 +1 @@
export * from './proposal-referral-program-details';
@@ -0,0 +1,161 @@
import { render, screen } from '@testing-library/react';
import {
formatMinimumRunningNotionalTakerVolume,
formatReferralDiscountFactor,
formatReferralRewardFactor,
formatMinimumStakedTokens,
formatReferralRewardMultiplier,
ProposalReferralProgramDetails,
} from './proposal-referral-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: {
decimals: 2,
},
}),
}));
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterEach(() => {
jest.useRealTimers();
});
describe('ProposalReferralProgramDetails helper functions', () => {
it('should format minimum running notional taker volume correctly', () => {
const input = '1000';
const formatted = formatMinimumRunningNotionalTakerVolume(input);
expect(formatted).toBe('1,000');
});
it('should format referral discount factor correctly', () => {
const input = '0.05';
const formatted = formatReferralDiscountFactor(input);
expect(formatted).toBe('5.00%');
});
it('should format referral reward factor correctly', () => {
const input = '0.1';
const formatted = formatReferralRewardFactor(input);
expect(formatted).toBe('10.00%');
});
it('should format minimum staked tokens correctly', () => {
const input = '15';
const decimals = 18;
const formatted = formatMinimumStakedTokens(input, decimals);
expect(formatted).toBe('0.000000000000000015');
});
it('should format referral reward multiplier correctly', () => {
const input = '3';
const formatted = formatReferralRewardMultiplier(input);
expect(formatted).toBe('3x');
});
});
const mockReferralProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateReferralProgram',
changes: {
benefitTiers: [
{
minimumEpochs: 6,
minimumRunningNotionalTakerVolume: '10000',
referralDiscountFactor: '0.001',
referralRewardFactor: '0.001',
},
{
minimumEpochs: 24,
minimumRunningNotionalTakerVolume: '500000',
referralDiscountFactor: '0.005',
referralRewardFactor: '0.005',
},
{
minimumEpochs: 48,
minimumRunningNotionalTakerVolume: '1000000',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
],
endOfProgramTimestamp: '2026-10-03T10:34:34Z',
windowLength: 3,
stakingTiers: [
{
minimumStakedTokens: '1',
referralRewardMultiplier: '1',
},
{
minimumStakedTokens: '2',
referralRewardMultiplier: '2',
},
{
minimumStakedTokens: '5',
referralRewardMultiplier: '3',
},
],
},
},
},
});
describe('<ProposalReferralProgramDetails />', () => {
it('should not render if proposal is null', () => {
render(<ProposalReferralProgramDetails proposal={null} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should not render if __typename is not UpdateReferralProgram', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(<ProposalReferralProgramDetails proposal={updateMarketProposal} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateReferralProgram',
changes: {},
},
},
});
render(<ProposalReferralProgramDetails proposal={incompleteProposal} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should render relevant fields if present', () => {
render(<ProposalReferralProgramDetails proposal={mockReferralProposal} />);
expect(
screen.getByTestId('proposal-referral-program-window-length')
).toBeInTheDocument();
expect(
screen.getByTestId('proposal-referral-program-end-of-program-timestamp')
).toBeInTheDocument();
expect(
screen.getByTestId('proposal-referral-program-benefit-tiers')
).toBeInTheDocument();
expect(
screen.getByTestId('proposal-referral-program-benefit-tiers')
).toBeInTheDocument();
});
});
@@ -0,0 +1,233 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../../lib/format-number';
import {
formatDateWithLocalTimezone,
formatNumberPercentage,
toBigNum,
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
}
export const formatEndOfProgramTimestamp = (value: string) => {
return formatDateWithLocalTimezone(new Date(value));
};
export const formatMinimumRunningNotionalTakerVolume = (value: string) => {
return formatNumber(toBigNum(value, 0), 0);
};
export const formatReferralDiscountFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};
export const formatReferralRewardFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};
export const formatMinimumStakedTokens = (value: string, decimals: number) => {
return formatNumber(toBigNum(value, decimals));
};
export const formatReferralRewardMultiplier = (value: string) => {
return `${value}x`;
};
export const ProposalReferralProgramDetails = ({
proposal,
}: ProposalReferralProgramDetailsProps) => {
const {
appState: { decimals },
} = useAppState();
const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateReferralProgram') {
return null;
}
const benefitTiers = proposal?.terms?.change?.changes?.benefitTiers;
const stakingTiers = proposal?.terms?.change?.changes?.stakingTiers;
const windowLength = proposal?.terms?.change?.changes?.windowLength;
const endOfProgramTimestamp =
proposal?.terms?.change?.changes?.endOfProgramTimestamp;
if (
!benefitTiers &&
!stakingTiers &&
!windowLength &&
!endOfProgramTimestamp
) {
return null;
}
return (
<div data-testid="proposal-referral-program-details">
<RoundedWrapper paddingBottom={true}>
{windowLength && (
<div data-testid="proposal-referral-program-window-length">
<KeyValueTable>
<KeyValueTableRow>
<Tooltip description={t('WindowLengthDescription')}>
<span>{t('WindowLength')}</span>
</Tooltip>
{windowLength}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}
{endOfProgramTimestamp && (
<div
className="mb-6"
data-testid="proposal-referral-program-end-of-program-timestamp"
>
<KeyValueTable>
<KeyValueTableRow>
<Tooltip description={t('EndOfProgramTimestampDescription')}>
<span>{t('EndOfProgramTimestamp')}</span>
</Tooltip>
{formatEndOfProgramTimestamp(endOfProgramTimestamp)}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}
{benefitTiers && (
<div
className="mb-6"
data-testid="proposal-referral-program-benefit-tiers"
>
<h3 className="mb-3 uppercase font-semibold text-lg">
{t('BenefitTiers')}
</h3>
<KeyValueTable>
{benefitTiers
.sort((a, b) => a.minimumEpochs - b.minimumEpochs)
.map((benefitTier, index) => (
<div className="mb-4" key={index}>
<h4 className="font-semibold uppercase">
Tier {index + 1}
</h4>
{benefitTier.minimumEpochs && (
<KeyValueTableRow>
<Tooltip
description={t('BenefitTierMinimumEpochsDescription')}
>
<span>{t('BenefitTierMinimumEpochs')}</span>
</Tooltip>
{benefitTier.minimumEpochs}
</KeyValueTableRow>
)}
{benefitTier.minimumRunningNotionalTakerVolume && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierMinimumRunningNotionalTakerVolumeDescription'
)}
>
<span>
{t('BenefitTierMinimumRunningNotionalTakerVolume')}
</span>
</Tooltip>
{formatMinimumRunningNotionalTakerVolume(
benefitTier.minimumRunningNotionalTakerVolume
)}
</KeyValueTableRow>
)}
{benefitTier.referralDiscountFactor && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierReferralDiscountFactorDescription'
)}
>
<span>{t('BenefitTierReferralDiscountFactor')}</span>
</Tooltip>
{formatReferralDiscountFactor(
benefitTier.referralDiscountFactor
)}
</KeyValueTableRow>
)}
{benefitTier.referralRewardFactor && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierReferralRewardFactorDescription'
)}
>
<span>{t('BenefitTierReferralRewardFactor')}</span>
</Tooltip>
{formatReferralRewardFactor(
benefitTier.referralRewardFactor
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
{stakingTiers && (
<div data-testid="proposal-referral-program-staking-tiers">
<h3 className="mb-3 uppercase font-semibold text-lg">
{t('StakingTiers')}
</h3>
<KeyValueTable>
{stakingTiers
.sort(
(a, b) =>
Number(a.minimumStakedTokens) -
Number(b.minimumStakedTokens)
)
.map((stakingTier, index) => (
<div className="mb-4" key={index}>
{stakingTier.referralRewardMultiplier && (
<KeyValueTableRow>
<Tooltip
description={t(
'StakingTierReferralRewardMultiplierDescription'
)}
>
<span>
{t('StakingTierReferralRewardMultiplier')}
</span>
</Tooltip>
{formatReferralRewardMultiplier(
stakingTier.referralRewardMultiplier
)}
</KeyValueTableRow>
)}
{stakingTier.minimumStakedTokens && (
<KeyValueTableRow>
<Tooltip
description={t(
'StakingTierMinimumStakedTokensFactorDescription'
)}
>
<span>{t('StakingTierMinimumStakedTokens')}</span>
</Tooltip>
{formatMinimumStakedTokens(
stakingTier.minimumStakedTokens,
decimals
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
</RoundedWrapper>
</div>
);
};
@@ -0,0 +1 @@
export * from './proposal-volume-discount-program-details';
@@ -0,0 +1,114 @@
import { render, screen } from '@testing-library/react';
import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: {
decimals: 2,
},
}),
}));
const mockReferralProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateVolumeDiscountProgram',
benefitTiers: [
{
minimumRunningNotionalTakerVolume: '10000',
volumeDiscountFactor: '0.05',
},
{
minimumRunningNotionalTakerVolume: '50000',
volumeDiscountFactor: '0.1',
},
{
minimumRunningNotionalTakerVolume: '100000',
volumeDiscountFactor: '0.15',
},
{
minimumRunningNotionalTakerVolume: '250000',
volumeDiscountFactor: '0.2',
},
{
minimumRunningNotionalTakerVolume: '500000',
volumeDiscountFactor: '0.25',
},
{
minimumRunningNotionalTakerVolume: '1000000',
volumeDiscountFactor: '0.3',
},
{
minimumRunningNotionalTakerVolume: '1500000',
volumeDiscountFactor: '0.35',
},
{
minimumRunningNotionalTakerVolume: '2000000',
volumeDiscountFactor: '0.4',
},
],
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
windowLength: 7,
},
},
});
describe('ProposalVolumeDiscountProgramDetails', () => {
it('should not render if proposal is null', () => {
render(<ProposalVolumeDiscountProgramDetails proposal={null} />);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should not render if __typename is not UpdateVolumeDiscountProgram', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(
<ProposalVolumeDiscountProgramDetails proposal={updateMarketProposal} />
);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateVolumeDiscountProgram',
},
},
});
render(
<ProposalVolumeDiscountProgramDetails proposal={incompleteProposal} />
);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should render relevant fields if present', () => {
render(
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
);
expect(
screen.getByTestId('proposal-volume-discount-program-window-length')
).toBeInTheDocument();
expect(
screen.getByTestId(
'proposal-volume-discount-program-end-of-program-timestamp'
)
).toBeInTheDocument();
expect(
screen.getByTestId('proposal-volume-discount-program-benefit-tiers')
).toBeInTheDocument();
});
});
@@ -0,0 +1,130 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import {
formatEndOfProgramTimestamp,
formatMinimumRunningNotionalTakerVolume,
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
}
export const formatVolumeDiscountFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};
export const ProposalVolumeDiscountProgramDetails = ({
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
return null;
}
const benefitTiers = proposal?.terms?.change?.benefitTiers;
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;
if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
return null;
}
return (
<div data-testid="proposal-volume-discount-program-details">
<RoundedWrapper paddingBottom={true}>
{windowLength && (
<div data-testid="proposal-volume-discount-program-window-length">
<KeyValueTable>
<KeyValueTableRow>
<Tooltip description={t('WindowLengthDescription')}>
<span>{t('WindowLength')}</span>
</Tooltip>
{windowLength}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}
{endOfProgramTimestamp && (
<div
className="mb-6"
data-testid="proposal-volume-discount-program-end-of-program-timestamp"
>
<KeyValueTable>
<KeyValueTableRow>
<Tooltip description={t('EndOfProgramTimestampDescription')}>
<span>{t('EndOfProgramTimestamp')}</span>
</Tooltip>
{formatEndOfProgramTimestamp(endOfProgramTimestamp)}
</KeyValueTableRow>
</KeyValueTable>
</div>
)}
{benefitTiers && (
<div
className="mb-6"
data-testid="proposal-volume-discount-program-benefit-tiers"
>
<h3 className="mb-3 uppercase font-semibold text-lg">
{t('BenefitTiers')}
</h3>
<KeyValueTable>
{benefitTiers
.sort(
(a, b) =>
Number(a.minimumRunningNotionalTakerVolume) -
Number(b.minimumRunningNotionalTakerVolume)
)
.map((benefitTier, index) => (
<div className="mb-4" key={index}>
<h4 className="font-semibold uppercase">
Tier {index + 1}
</h4>
{benefitTier.minimumRunningNotionalTakerVolume && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierMinimumRunningNotionalTakerVolumeDescription'
)}
>
<span>
{t('BenefitTierMinimumRunningNotionalTakerVolume')}
</span>
</Tooltip>
{formatMinimumRunningNotionalTakerVolume(
benefitTier.minimumRunningNotionalTakerVolume
)}
</KeyValueTableRow>
)}
{benefitTier.volumeDiscountFactor && (
<KeyValueTableRow>
<Tooltip
description={t(
'BenefitTierVolumeDiscountFactorDescription'
)}
>
<span>{t('BenefitTierVolumeDiscountFactor')}</span>
</Tooltip>
{formatVolumeDiscountFactor(
benefitTier.volumeDiscountFactor
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
</RoundedWrapper>
</div>
);
};
@@ -6,6 +6,8 @@ import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalAssetDetails } from '../proposal-asset-details';
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
import { UserVote } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
@@ -113,6 +115,15 @@ export const Proposal = ({
// TODO: check minVoterBalance for 'CancelTransfer'
minVoterBalance =
networkParams.governance_proposal_freeform_minVoterBalance;
break;
case 'UpdateReferralProgram':
minVoterBalance =
networkParams.governance_proposal_referralProgram_minVoterBalance;
break;
case 'UpdateVolumeDiscountProgram':
minVoterBalance =
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
break;
}
}
@@ -220,6 +231,18 @@ export const Proposal = ({
</div>
)}
{proposal.terms.change.__typename === 'UpdateReferralProgram' && (
<div className="mb-4">
<ProposalReferralProgramDetails proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateVolumeDiscountProgram' && (
<div className="mb-4">
<ProposalVolumeDiscountProgramDetails proposal={proposal} />
</div>
)}
{governanceTransferDetails}
<div className="mb-10">
@@ -19,6 +19,8 @@ export const useProposalNetworkParams = ({
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_market_requiredParticipation,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_asset_requiredParticipation,
@@ -26,6 +28,8 @@ export const useProposalNetworkParams = ({
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredParticipation,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
]);
const fallback = {
@@ -91,6 +95,22 @@ export const useProposalNetworkParams = ({
params.governance_proposal_freeform_requiredParticipation
),
};
case 'UpdateReferralProgram':
return {
requiredMajority:
params.governance_proposal_referralProgram_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_referralProgram_requiredParticipation
),
};
case 'UpdateVolumeDiscountProgram':
return {
requiredMajority:
params.governance_proposal_VolumeDiscountProgram_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_VolumeDiscountProgram_requiredParticipation
),
};
default:
return fallback;
}
@@ -43,10 +43,50 @@ fragment UpdateMarketState on Proposal {
}
}
fragment UpdateReferralProgram on Proposal {
terms {
change {
... on UpdateReferralProgram {
changes {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
}
fragment UpdateVolumeDiscountProgram on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
query Proposal(
$proposalId: ID!
$includeNewMarketProductField: Boolean!
$includeUpdateMarketState: Boolean!
$includeUpdateReferralProgram: Boolean!
$includeUpdateVolumeDiscountProgram: Boolean!
) {
proposal(id: $proposalId) {
id
@@ -64,6 +104,9 @@ query Proposal(
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
@include(if: $includeUpdateVolumeDiscountProgram)
terms {
closingDatetime
enactmentDatetime
File diff suppressed because one or more lines are too long
@@ -36,6 +36,8 @@ export const ProposalContainer = () => {
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
NetworkParams.governance_proposal_freeform_minVoterBalance,
NetworkParams.governance_proposal_referralProgram_minVoterBalance,
NetworkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance,
NetworkParams.spam_protection_voting_min_tokens,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
@@ -44,6 +46,8 @@ export const ProposalContainer = () => {
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
]);
const {
@@ -57,6 +61,8 @@ export const ProposalContainer = () => {
proposalId: params.proposalId || '',
includeNewMarketProductField: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketState: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralProgram: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountProgram: !!FLAGS.VOLUME_DISCOUNTS,
},
skip: !params.proposalId,
});
@@ -43,6 +43,44 @@ fragment UpdateMarketStates on Proposal {
}
}
fragment UpdateReferralPrograms on Proposal {
terms {
change {
... on UpdateReferralProgram {
changes {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
}
fragment UpdateVolumeDiscountPrograms on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
fragment ProposalFields on Proposal {
id
rationale {
@@ -127,6 +165,8 @@ fragment ProposalFields on Proposal {
query Proposals(
$includeNewMarketProductFields: Boolean!
$includeUpdateMarketStates: Boolean!
$includeUpdateReferralPrograms: Boolean!
$includeUpdateVolumeDiscountPrograms: Boolean!
) {
proposalsConnection {
edges {
@@ -134,6 +174,9 @@ query Proposals(
...ProposalFields
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms
@include(if: $includeUpdateVolumeDiscountPrograms)
}
}
}
@@ -7,15 +7,21 @@ export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: {
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
export type ProposalsQueryVariables = Types.Exact<{
includeNewMarketProductFields: Types.Scalars['Boolean'];
includeUpdateMarketStates: Types.Scalars['Boolean'];
includeUpdateReferralPrograms: Types.Scalars['Boolean'];
includeUpdateVolumeDiscountPrograms: Types.Scalars['Boolean'];
}>;
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export const NewMarketProductFieldsFragmentDoc = gql`
fragment NewMarketProductFields on Proposal {
@@ -64,6 +70,46 @@ export const UpdateMarketStatesFragmentDoc = gql`
}
}
`;
export const UpdateReferralProgramsFragmentDoc = gql`
fragment UpdateReferralPrograms on Proposal {
terms {
change {
... on UpdateReferralProgram {
changes {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
}
`;
export const UpdateVolumeDiscountProgramsFragmentDoc = gql`
fragment UpdateVolumeDiscountPrograms on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
`;
export const ProposalFieldsFragmentDoc = gql`
fragment ProposalFields on Proposal {
id
@@ -147,20 +193,24 @@ export const ProposalFieldsFragmentDoc = gql`
}
`;
export const ProposalsDocument = gql`
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!) {
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!, $includeUpdateReferralPrograms: Boolean!, $includeUpdateVolumeDiscountPrograms: Boolean!) {
proposalsConnection {
edges {
node {
...ProposalFields
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms @include(if: $includeUpdateVolumeDiscountPrograms)
}
}
}
}
${ProposalFieldsFragmentDoc}
${NewMarketProductFieldsFragmentDoc}
${UpdateMarketStatesFragmentDoc}`;
${UpdateMarketStatesFragmentDoc}
${UpdateReferralProgramsFragmentDoc}
${UpdateVolumeDiscountProgramsFragmentDoc}`;
/**
* __useProposalsQuery__
@@ -176,6 +226,8 @@ ${UpdateMarketStatesFragmentDoc}`;
* variables: {
* includeNewMarketProductFields: // value for 'includeNewMarketProductFields'
* includeUpdateMarketStates: // value for 'includeUpdateMarketStates'
* includeUpdateReferralPrograms: // value for 'includeUpdateReferralPrograms'
* includeUpdateVolumeDiscountPrograms: // value for 'includeUpdateVolumeDiscountPrograms'
* },
* });
*/
@@ -49,6 +49,8 @@ export const ProposalsContainer = () => {
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -41,6 +41,8 @@ export const RejectedProposalsContainer = () => {
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
-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,28 +0,0 @@
import { MarketState } from '@vegaprotocol/types';
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
const oracleBannerStatus = 'oracle-banner-status';
const oracleFullProfile = 'oracle-full-profile';
describe('oracle information', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
undefined,
undefined,
'COMPROMISED'
);
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('show oracle banner', () => {
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerDialogTrigger)
.should('contain.text', 'Show more')
.click();
cy.getByTestId(oracleFullProfile).should('exist');
});
});
@@ -15,9 +15,11 @@ import {
Last24hVolume,
getAsset,
getDataSourceSpecForSettlementSchedule,
isMarketInAuction,
marketInfoProvider,
useFundingPeriodsQuery,
useFundingRate,
useMarketTradingMode,
useExternalTwap,
} from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types';
@@ -187,9 +189,11 @@ const useNow = () => {
};
const useEvery = (marketId: string) => {
const { data: marketTradingMode } = useMarketTradingMode(marketId);
const { data: marketInfo } = useDataProvider({
dataProvider: marketInfoProvider,
variables: { marketId },
skip: !marketTradingMode || isMarketInAuction(marketTradingMode),
});
let every: number | undefined = undefined;
const sourceType =
@@ -1,38 +1,29 @@
import type { InMemoryCacheConfig } from '@apollo/client';
import {
AppFailure,
AppLoader,
DocsLinks,
NetworkLoader,
NodeFailure,
NodeGuard,
useEnvironment,
} from '@vegaprotocol/environment';
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,
VEGA_ENV,
VEGA_WALLET_URL,
VEGA_EXPLORER_URL,
MAINTENANCE_PAGE,
MOZILLA_EXTENSION_URL,
CHROME_EXTENSION_URL,
} = useEnvironment();
if (MAINTENANCE_PAGE) {
return <MaintenancePage />;
}
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
@@ -41,22 +32,27 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
!MOZILLA_EXTENSION_URL ||
!DocsLinks
) {
return null;
return <AppLoader />;
}
return (
<NetworkLoader
cache={cacheConfig}
skeleton={<DynamicLoader />}
skeleton={<AppLoader />}
failure={
<AppFailure title={t('Could not initialize app')} error={error} />
}
>
<NodeGuard
skeleton={<DynamicLoader />}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
skeleton={<AppLoader />}
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Web3Provider>
<Web3Provider
skeleton={<AppLoader />}
failure={
<AppFailure title={t(`Could not configure web3 provider`)} />
}
>
<VegaWalletProvider
config={{
network: VEGA_ENV,
@@ -1,2 +1,2 @@
export * from './app-loader';
export * from './bootstrapper';
export * from './web3-provider';
@@ -6,14 +6,20 @@ import {
useWeb3ConnectStore,
createDefaultProvider,
} from '@vegaprotocol/web3';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
export const Web3Provider = ({ children }: { children: ReactNode }) => {
export const Web3Provider = ({
children,
skeleton,
failure,
}: {
children: ReactNode;
skeleton: ReactNode;
failure: ReactNode;
}) => {
const { config, loading, error } = useEthereumConfig();
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
useEnvironment();
@@ -49,23 +55,20 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
ETH_WALLET_MNEMONIC,
]);
if (error) {
return <>{failure}</>;
}
if (loading || !connectors.length) {
return <>{skeleton}</>;
}
return (
<AsyncRenderer
loading={loading}
error={error}
data={connectors}
noDataCondition={(d) => {
if (!d) return true;
return d.length < 1;
}}
noDataMessage={t('Could not fetch Ethereum configuration')}
<Web3ProviderInternal
connectors={connectors}
defaultProvider={defaultProvider}
>
<Web3ProviderInternal
connectors={connectors}
defaultProvider={defaultProvider}
>
<>{children}</>
</Web3ProviderInternal>
</AsyncRenderer>
<>{children}</>
</Web3ProviderInternal>
);
};
@@ -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;
+17 -75
View File
@@ -1,21 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import Head from 'next/head';
import type { AppProps } from 'next/app';
import { t } from '@vegaprotocol/i18n';
import {
useEagerConnect as useVegaEagerConnect,
useVegaWallet,
} from '@vegaprotocol/wallet';
import {
useVegaTransactionManager,
useVegaTransactionUpdater,
} from '@vegaprotocol/web3';
import {
useEagerConnect as useEthereumEagerConnect,
useEthTransactionManager,
useEthTransactionUpdater,
useEthWithdrawApprovalsManager,
} from '@vegaprotocol/web3';
import {
envTriggerMapping,
Networks,
@@ -25,21 +11,11 @@ import {
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import './styles.css';
import { useGlobalStore, usePageTitleStore } from '../stores';
import { usePageTitleStore } from '../stores';
import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager';
import {
HashRouter,
useLocation,
Route,
Routes,
useSearchParams,
} from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
import { HashRouter, useLocation, Route, Routes } from 'react-router-dom';
import { Bootstrapper } from '../components/bootstrapper';
import { AnnouncementBanner } from '../components/banner';
import { Navbar } from '../components/navbar';
import classNames from 'classnames';
@@ -52,6 +28,10 @@ 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';
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
import { TransactionHandlers } from './transaction-handlers';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -76,15 +56,6 @@ const Title = () => {
);
};
const InitializeHandlers = () => {
useVegaTransactionManager();
useVegaTransactionUpdater();
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
return null;
};
function AppBody({ Component }: AppProps) {
const location = useLocation();
const { VEGA_ENV } = useEnvironment();
@@ -124,9 +95,9 @@ function AppBody({ Component }: AppProps) {
</div>
<DialogsContainer />
<ToastsManager />
<InitializeHandlers />
<TransactionHandlers />
<MaybeConnectEagerly />
<PartyData />
<PartyActiveOrdersHandler />
<Telemetry />
</div>
);
@@ -139,55 +110,26 @@ function VegaTradingApp(props: AppProps) {
store.setDialogOpen,
]);
// Start validation of env vars and determine VEGA_URL
useInitializeEnv();
// Prevent HashRouter from being server side rendered as it
// relies on presence of document object
//
// This is the last point at which we get pregenerated
// HTML so render a ssr friendly loader component
if (status === 'default') {
return <DynamicLoader />;
return <SSRLoader />;
}
return (
<HashRouter>
<AppLoader>
<Bootstrapper>
<AppBody {...props} />
</AppLoader>
</Bootstrapper>
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
</HashRouter>
);
}
export default VegaTradingApp;
const PartyData = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
const MaybeConnectEagerly = () => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const eagerConnecting = useVegaEagerConnect(Connectors);
const [isTelemetryApproved] = useTelemetryApproval();
useEthereumEagerConnect(
isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {}
);
const { pubKey, connect } = useVegaWallet();
const [searchParams] = useSearchParams();
const [query] = useState(searchParams.get('address'));
if (query && !pubKey) {
connect(Connectors['view']);
}
useEffect(() => {
update({ eagerConnecting });
}, [update, eagerConnecting]);
return null;
};
+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>
@@ -0,0 +1,32 @@
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEagerConnect as useEthereumEagerConnect } from '@vegaprotocol/web3';
import {
useEagerConnect as useVegaEagerConnect,
useVegaWallet,
} from '@vegaprotocol/wallet';
import { useGlobalStore } from '../stores';
import { Connectors } from '../lib/vega-connectors';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
export const MaybeConnectEagerly = () => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const eagerConnecting = useVegaEagerConnect(Connectors);
const [isTelemetryApproved] = useTelemetryApproval();
useEthereumEagerConnect(
isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {}
);
const { pubKey, connect } = useVegaWallet();
const [searchParams] = useSearchParams();
const [query] = useState(searchParams.get('address'));
if (query && !pubKey) {
connect(Connectors['view']);
}
useEffect(() => {
update({ eagerConnecting });
}, [update, eagerConnecting]);
return null;
};
@@ -0,0 +1,15 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const PartyActiveOrdersHandler = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
+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>
);
};
@@ -0,0 +1,18 @@
import {
useVegaTransactionManager,
useVegaTransactionUpdater,
} from '@vegaprotocol/web3';
import {
useEthTransactionManager,
useEthTransactionUpdater,
useEthWithdrawApprovalsManager,
} from '@vegaprotocol/web3';
export const TransactionHandlers = () => {
useVegaTransactionManager();
useVegaTransactionUpdater();
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
return null;
};
-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,6 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
import { Splash } from '@vegaprotocol/ui-toolkit';
export const AppFailure = ({
title,
@@ -9,18 +7,10 @@ export const AppFailure = ({
title: string;
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}>
<div className="text-center">
<p className="text-xl mb-4">{title}</p>
{error && <p className="text-sm mb-8">{error}</p>}
<Button onClick={() => setNodeSwitcher(true)}>
{t('Change node')}
</Button>
</div>
</div>
<Splash>
<p className="mb-4 text-xl">{title}</p>
{error && <p className="mb-8 text-sm">{error}</p>}
</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';
+2
View File
@@ -1,6 +1,8 @@
export * from './app-failure';
export * from './app-loader';
export * from './network-loader';
export * from './network-switcher';
export * from './node-guard';
export * from './node-switcher';
export * from './node-failure';
export * from './etherscan-link';
@@ -33,15 +33,14 @@ export function NetworkLoader({
return undefined;
}, [VEGA_URL, status, cache]);
const nonIdealWrapperClasses =
'h-full min-h-screen flex items-center justify-center';
if (status === 'failed') {
return <div className={nonIdealWrapperClasses}>{failure}</div>;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{failure}</>;
}
if (status === 'default' || status === 'pending' || !client) {
return <div className={nonIdealWrapperClasses}>{skeleton}</div>;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{skeleton}</>;
}
return <ApolloProvider client={client}>{children}</ApolloProvider>;
@@ -0,0 +1 @@
export { NodeFailure } from './node-failure';
@@ -0,0 +1,24 @@
import { t } from '@vegaprotocol/i18n';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
export const NodeFailure = ({
title,
error,
}: {
title: string;
error?: string | null;
}) => {
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
return (
<Splash>
<div className="text-center">
<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>
</Splash>
);
};
@@ -11,11 +11,10 @@ export const NodeGuard = ({
skeleton: ReactNode;
}) => {
const { data, error, loading } = useNodeGuardQuery();
const wrapperClasses =
'h-full min-h-screen flex items-center justify-center text-black dark:text-white';
if (loading) {
return <div className={wrapperClasses}>{skeleton}</div>;
// eslint-disable-next-line
return <>{skeleton}</>;
}
// It is possible for nodes to have a functioning datanode, but not return
@@ -23,9 +22,10 @@ export const NodeGuard = ({
const netParamEdges = data?.networkParametersConnection.edges;
if (error || !netParamEdges || !netParamEdges.length) {
return <div className={wrapperClasses}>{failure}</div>;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{failure}</>;
}
// eslint-disable-next-line
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
};
@@ -338,9 +338,6 @@ function compileEnvVars() {
'GITHUB_FEEDBACK_URL',
process.env['NX_GITHUB_FEEDBACK_URL']
),
MAINTENANCE_PAGE: parseBoolean(
windowOrDefault('MAINTENANCE_PAGE', process.env['NX_MAINTENANCE_PAGE'])
),
GIT_BRANCH: windowOrDefault(
'GIT_COMMIT_BRANCH',
process.env['GIT_COMMIT_BRANCH']
@@ -429,6 +426,12 @@ function compileFeatureFlags(): FeatureFlags {
process.env['NX_GOVERNANCE_TRANSFERS']
) as string
),
VOLUME_DISCOUNTS: TRUTHY.includes(
windowOrDefault(
'NX_VOLUME_DISCOUNTS',
process.env['NX_VOLUME_DISCOUNTS']
) as string
),
};
const EXPLORER_FLAGS = {
EXPLORER_ASSETS: TRUTHY.includes(
@@ -518,10 +521,6 @@ function parseNetworks(value?: string) {
return {};
}
function parseBoolean(value?: string) {
return ['true', '1', 'yes'].includes(value?.toLowerCase() || '');
}
/**
* Provides a fallback ethereum provider url for test purposes in some apps
*/
+1
View File
@@ -26,6 +26,7 @@ export type CosmicElevatorFlags = Pick<
| 'REFERRALS'
| 'UPDATE_MARKET_STATE'
| 'GOVERNANCE_TRANSFERS'
| 'VOLUME_DISCOUNTS'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;
@@ -50,7 +50,6 @@ export const envSchema = z
message: 'The NX_ETHERSCAN_URL environment variable must be a valid url',
}),
HOSTED_WALLET_URL: z.optional(z.string()),
MAINTENANCE_PAGE: z.optional(z.boolean()),
ETH_LOCAL_PROVIDER_URL: z.optional(z.string()),
ETH_WALLET_MNEMONIC: z.optional(z.string()),
ANNOUNCEMENTS_CONFIG_URL: z.optional(z.string()),
@@ -81,6 +80,7 @@ const COSMIC_ELEVATOR_FLAGS = {
REFERRALS: z.optional(z.boolean()),
UPDATE_MARKET_STATE: z.optional(z.boolean()),
GOVERNANCE_TRANSFERS: z.optional(z.boolean()),
VOLUME_DISCOUNTS: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {
+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';
@@ -138,6 +138,8 @@ query MarketInfo($marketId: ID!) {
positionDecimalPlaces
state
tradingMode
linearSlippageFactor
quadraticSlippageFactor
proposal {
id
rationale {
File diff suppressed because one or more lines are too long
@@ -44,8 +44,6 @@ export const Row = ({
// a specific parentValue is missing. These values are only used when we
// have successor market parent data.
const className = 'text-sm';
const getFormattedValue = (value: ReactNode) => {
if (typeof value !== 'string' && typeof value !== 'number') return value;
if (unformatted || isNaN(Number(value))) {
@@ -75,8 +73,8 @@ export const Row = ({
key={field}
inline={true}
noBorder={noBorder}
dtClassName={className}
ddClassName={className}
dtClassName="text-xs"
ddClassName="text-xs"
>
<div className="flex items-center gap-3">
<Tooltip description={tooltipMapping[field]} align="start">
@@ -32,6 +32,7 @@ import {
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
LiquiditySLAParametersInfoPanel,
MarginScalingFactorsPanel,
MarketPriceInfoPanel,
MarketVolumeInfoPanel,
MetadataInfoPanel,
@@ -39,7 +40,6 @@ import {
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
SuccessionLineInfoPanel,
} from './market-info-panels';
@@ -236,9 +236,9 @@ export const MarketInfoAccordion = ({
content={<RiskModelInfoPanel market={market} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={market} />}
itemId="margin-scaling-factors"
title={t('Margin scaling factors')}
content={<MarginScalingFactorsPanel market={market} />}
/>
<AccordionItem
itemId="risk-factors"
@@ -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,
@@ -45,6 +45,7 @@ import type {
} from '@vegaprotocol/types';
import {
ConditionOperatorMapping,
MarketStateMapping,
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import {
@@ -90,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}
/>
@@ -220,58 +221,78 @@ export const KeyDetailsInfoPanel = ({
const assetDecimals = getAsset(market).decimals;
return (
<MarketInfoTable
data={
FLAGS.SUCCESSOR_MARKETS
? {
name: market.tradableInstrument.instrument.name,
marketID: market.id,
parentMarketID: parentMarketIdData?.market?.parentMarketID || '-',
insurancePoolFraction:
(successorProposalDetails?.proposal?.terms.change.__typename ===
'NewMarket' &&
successorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction) ||
'-',
tradingMode:
market.tradingMode &&
MarketTradingModeMapping[market.tradingMode],
marketDecimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}
: {
name: market.tradableInstrument.instrument.name,
marketID: market.id,
tradingMode:
market.tradingMode &&
MarketTradingModeMapping[market.tradingMode],
marketDecimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}
}
parentData={
parentMarket && {
name: parentMarket?.tradableInstrument?.instrument?.name,
marketID: parentMarket?.id,
parentMarketID: grandparentMarketIdData?.market?.parentMarketID,
insurancePoolFraction:
parentSuccessorProposalDetails?.proposal?.terms.change
.__typename === 'NewMarket' &&
parentSuccessorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction,
tradingMode:
parentMarket?.tradingMode &&
MarketTradingModeMapping[
parentMarket.tradingMode as MarketTradingMode
],
marketDecimalPlaces: parentMarket?.decimalPlaces,
positionDecimalPlaces: parentMarket?.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
<>
<KeyValueTable>
<KeyValueTableRow noBorder>
<div>{t('Market ID')}</div>
<CopyWithTooltip text={market.id}>
<button
data-testid="copy-eth-oracle-address"
className="uppercase text-right"
>
<span className="flex gap-1">
{truncateMiddle(market.id)}
<VegaIcon name={VegaIconNames.COPY} size={16} />
</span>
</button>
</CopyWithTooltip>
</KeyValueTableRow>
</KeyValueTable>
<MarketInfoTable
data={
FLAGS.SUCCESSOR_MARKETS
? {
name: market.tradableInstrument.instrument.name,
parentMarketID:
parentMarketIdData?.market?.parentMarketID || '-',
insurancePoolFraction:
(successorProposalDetails?.proposal?.terms.change
.__typename === 'NewMarket' &&
successorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction) ||
'-',
status: market.state && MarketStateMapping[market.state],
tradingMode:
market.tradingMode &&
MarketTradingModeMapping[market.tradingMode],
marketDecimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}
: {
name: market.tradableInstrument.instrument.name,
status: market.state && MarketStateMapping[market.state],
tradingMode:
market.tradingMode &&
MarketTradingModeMapping[market.tradingMode],
marketDecimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}
}
}
/>
parentData={
parentMarket && {
name: parentMarket?.tradableInstrument?.instrument?.name,
parentMarketID: grandparentMarketIdData?.market?.parentMarketID,
insurancePoolFraction:
parentSuccessorProposalDetails?.proposal?.terms.change
.__typename === 'NewMarket' &&
parentSuccessorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction,
status:
parentMarket?.state && MarketStateMapping[parentMarket.state],
tradingMode:
parentMarket?.tradingMode &&
MarketTradingModeMapping[
parentMarket.tradingMode as MarketTradingMode
],
marketDecimalPlaces: parentMarket?.decimalPlaces,
positionDecimalPlaces: parentMarket?.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}
}
/>
</>
);
};
@@ -432,8 +453,8 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
asset={asset}
inline={true}
noBorder={true}
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
dtClassName="text-black dark:text-white text-ui !px-0 text-xs"
ddClassName="text-black dark:text-white text-ui !px-0 max-w-full text-xs"
/>
<p className="mt-4 text-xs">
{t(
@@ -505,18 +526,53 @@ export const RiskModelInfoPanel = ({
}
return (
<MarketInfoTable
data={{ tau, riskAversionParameter }}
parentData={parentData}
unformatted
/>
<>
<MarketInfoTable
data={{ tau, riskAversionParameter }}
parentData={parentData}
unformatted
/>
<RiskParametersInfoPanel market={market} parentMarket={parentMarket} />
</>
);
};
export const RiskParametersInfoPanel = ({
export const MarginScalingFactorsPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const data = {
linearSlippageFactor: market.linearSlippageFactor,
quadraticSlippageFactor: market.quadraticSlippageFactor,
searchLevel:
market.tradableInstrument.marginCalculator?.scalingFactors.searchLevel,
initialMargin:
market.tradableInstrument.marginCalculator?.scalingFactors.initialMargin,
collateralRelease:
market.tradableInstrument.marginCalculator?.scalingFactors
.collateralRelease,
};
const parentData = parentMarket
? {
linearSlippageFactor: parentMarket?.linearSlippageFactor,
quadraticSlippageFactor: parentMarket?.quadraticSlippageFactor,
searchLevel:
parentMarket?.tradableInstrument.marginCalculator?.scalingFactors
.searchLevel,
initialMargin:
parentMarket?.tradableInstrument.marginCalculator?.scalingFactors
.initialMargin,
collateralRelease:
parentMarket?.tradableInstrument.marginCalculator?.scalingFactors
.collateralRelease,
}
: undefined;
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
};
const RiskParametersInfoPanel = ({ market, parentMarket }: MarketInfoProps) => {
const marketType = market.tradableInstrument.riskModel.__typename;
let data, parentData;
@@ -562,27 +618,66 @@ export const RiskFactorsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
if (!market.riskFactors) {
return null;
}
const getLeverageFactors = (market: MarketInfo) => {
if (!market.riskFactors) {
return undefined;
}
const { short, long } = market.riskFactors;
const { short, long } = market.riskFactors;
let parentData;
const maxLeverageLong = new BigNumber(1).dividedBy(
new BigNumber(market.linearSlippageFactor).plus(long)
);
if (parentMarket?.riskFactors) {
const parentShort = parentMarket.riskFactors.short;
const parentLong = parentMarket.riskFactors.long;
parentData = { short: parentShort, long: parentLong };
}
const maxLeverageShort = new BigNumber(1).dividedBy(
new BigNumber(market.linearSlippageFactor).plus(short)
);
return (
<MarketInfoTable
data={{ short, long }}
parentData={parentData}
unformatted
/>
);
const maxInitialLeverageLong = !market.tradableInstrument.marginCalculator
? undefined
: new BigNumber(1)
.dividedBy(
market.tradableInstrument.marginCalculator.scalingFactors
.initialMargin
)
.times(maxLeverageLong);
const maxInitialLeverageShort = !market.tradableInstrument.marginCalculator
? undefined
: new BigNumber(1)
.dividedBy(
market.tradableInstrument.marginCalculator.scalingFactors
.initialMargin
)
.times(maxLeverageShort);
const formatValue = (number: BigNumber | string | undefined) => {
if (!number) return undefined;
const value = new BigNumber(number);
if (value.gte(10)) {
return value.toFixed(3);
} else {
return value.toFixed(5);
}
};
const data = {
long: formatValue(long),
short: formatValue(short),
maxLeverageLong: formatValue(maxLeverageLong),
maxLeverageShort: formatValue(maxLeverageShort),
maxInitialLeverageLong: formatValue(maxInitialLeverageLong),
maxInitialLeverageShort: formatValue(maxInitialLeverageShort),
};
return data;
};
const data = getLeverageFactors(market);
const parentData = parentMarket
? getLeverageFactors(parentMarket)
: undefined;
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
};
export const PriceMonitoringBoundsInfoPanel = ({
@@ -23,6 +23,8 @@ export const marketInfoQuery = (
description: '',
},
},
linearSlippageFactor: '0.01',
quadraticSlippageFactor: '0.0001',
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '2022-11-15T02:15:24.543614154Z',
@@ -0,0 +1,68 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { MockedProvider } from '@apollo/client/testing';
import { OracleBanner } from './oracle-banner';
const mockOracleData = {
data: {
dataSourceSpecId: 'someId',
provider: {
name: 'Test oracle',
url: 'https://zombo.com',
description_markdown:
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
oracle: {
status: 'COMPROMISED',
status_reason: 'Reason here',
first_verified: '2021-01-01T00:00:00Z',
last_verified: '2022-01-01T00:00:00Z',
type: 'public_key',
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
proofs: [
{
format: 'url',
available: true,
type: 'web',
url: 'https://proofweb.com',
},
{
format: 'signed_message',
available: true,
type: 'public_key',
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
message: 'SOMEHEX',
},
],
github_link:
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
},
},
};
jest.mock('../../hooks', () => ({
...jest.requireActual('../../hooks'),
useMarketOracle: jest.fn((...args) => {
return mockOracleData;
}),
}));
describe('OracleBanner', () => {
it('should render successfully', async () => {
render(
<MockedProvider mocks={[]} addTypename={false}>
<OracleBanner marketId="someMarketId" />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('oracle-banner-status')).toHaveTextContent(
'COMPROMISED'
);
expect(
screen.getByTestId('oracle-banner-dialog-trigger')
).toHaveTextContent('Show more');
fireEvent.click(screen.getByTestId('oracle-banner-dialog-trigger'));
expect(screen.getByTestId('oracle-full-profile')).toBeInTheDocument();
});
});
});
+23 -3
View File
@@ -16,6 +16,7 @@ import type {
MarketDataQueryVariables,
} from './__generated__/market-data';
import { getMarketPrice } from './get-price';
import { isMarketInAuction } from './is-market-in-auction';
export type MarketData = MarketDataFieldsFragment;
@@ -115,14 +116,25 @@ export const staticMarketDataProvider = makeDerivedDataProvider<
});
});
export const marketTradingModeProvider = makeDerivedDataProvider<
MarketDataFieldsFragment['marketTradingMode'] | undefined,
never,
MarketDataQueryVariables
>(
[marketDataProvider],
(parts, variables, prevData) =>
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode
);
export const fundingRateProvider = makeDerivedDataProvider<
string,
never,
MarketDataQueryVariables
>([marketDataProvider], (parts) => {
return (
(parts[0] as ReturnType<typeof getData>)?.productData?.fundingRate || null
);
const marketData = parts[0] as ReturnType<typeof getData>;
return marketData && !isMarketInAuction(marketData.marketTradingMode)
? marketData?.productData?.fundingRate || null
: null;
});
export const useFundingRate = (marketId?: string, skip?: boolean) =>
@@ -156,3 +168,11 @@ export const useStaticMarketData = (marketId?: string, skip?: boolean) => {
skip: skip || !marketId,
});
};
export const useMarketTradingMode = (marketId?: string, skip?: boolean) => {
return useDataProvider({
dataProvider: marketTradingModeProvider,
variables: { marketId: marketId || '' },
skip: skip || !marketId,
});
};
+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[]) => {
@@ -100,6 +100,34 @@ export const NetworkParams = {
'governance_proposal_freeform_requiredMajority',
governance_proposal_freeform_minProposerBalance:
'governance_proposal_freeform_minProposerBalance',
governance_proposal_referralProgram_minClose:
'governance_proposal_referralProgram_minClose',
governance_proposal_referralProgram_minEnact:
'governance_proposal_referralProgram_minEnact',
governance_proposal_referralProgram_minProposerBalance:
'governance_proposal_referralProgram_minProposerBalance',
governance_proposal_referralProgram_minVoterBalance:
'governance_proposal_referralProgram_minVoterBalance',
governance_proposal_referralProgram_requiredMajority:
'governance_proposal_referralProgram_requiredMajority',
governance_proposal_referralProgram_requiredParticipation:
'governance_proposal_referralProgram_requiredParticipation',
governance_proposal_VolumeDiscountProgram_maxClose:
'governance_proposal_VolumeDiscountProgram_maxClose',
governance_proposal_VolumeDiscountProgram_maxEnact:
'governance_proposal_VolumeDiscountProgram_maxEnact',
governance_proposal_VolumeDiscountProgram_minClose:
'governance_proposal_VolumeDiscountProgram_minClose',
governance_proposal_VolumeDiscountProgram_minEnact:
'governance_proposal_VolumeDiscountProgram_minEnact',
governance_proposal_VolumeDiscountProgram_minProposerBalance:
'governance_proposal_VolumeDiscountProgram_minProposerBalance',
governance_proposal_VolumeDiscountProgram_minVoterBalance:
'governance_proposal_VolumeDiscountProgram_minVoterBalance',
governance_proposal_VolumeDiscountProgram_requiredMajority:
'governance_proposal_VolumeDiscountProgram_requiredMajority',
governance_proposal_VolumeDiscountProgram_requiredParticipation:
'governance_proposal_VolumeDiscountProgram_requiredParticipation',
validators_delegation_minAmount: 'validators_delegation_minAmount',
spam_protection_minimumWithdrawalQuantumMultiple:
'spam_protection_minimumWithdrawalQuantumMultiple',
@@ -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>
)}
-1
View File
@@ -22,7 +22,6 @@ export * from './key-value-table';
export * from './link';
export * from './loader';
export * from './lozenge';
export * from './maintenance-page';
export * from './nav-dropdown';
export * from './nav';
export * from './navigation';
@@ -74,9 +74,13 @@ export const KeyValueTableRow = ({
{ 'flex-row items-center': inline },
className
);
const dtClassNames = `break-words ${dtClassName}`;
const dtClassNames = classNames(
'break-words',
dtClassName,
'text-neutral-500 dark:text-neutral-300'
);
const ddClassNames = classNames(
'break-words text-neutral-500 dark:text-neutral-300',
'break-words',
{
'font-mono': numerical,
},

Some files were not shown because too many files have changed in this diff Show More