Compare commits

..
327 changed files with 6209 additions and 11014 deletions
+1
View File
@@ -1,2 +1,3 @@
* @vegaprotocol/frontend
* @vegaprotocol/frontend-qa
*.graphql @vegaprotocol/core
@@ -39,6 +39,7 @@ context('Market page', { tags: '@regression' }, function () {
cy.contains('Test market 1').click();
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
cy.validate_element_from_table('Name', 'Test market 1');
cy.validate_element_from_table('Market ID', this.createdMarketId);
cy.validate_element_from_table('Trading Mode', 'Opening auction');
cy.validate_element_from_table('Market Decimal Places', '5');
cy.validate_element_from_table('Position Decimal Places', '5');
+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={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
@@ -3,7 +3,6 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets';
import {
LiquidityPriceRangeInfoPanel,
LiquiditySLAParametersInfoPanel,
MarginScalingFactorsPanel,
PriceMonitoringBoundsInfoPanel,
SuccessionLineInfoPanel,
getDataSourceSpecForSettlementData,
@@ -18,6 +17,7 @@ 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('Margin scaling factors')}</h2>
<MarginScalingFactorsPanel market={market} />
<h2 className={headerClassName}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel market={market} />
<h2 className={headerClassName}>{t('Risk factors')}</h2>
<RiskFactorsInfoPanel market={market} />
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => (
@@ -12,7 +12,6 @@ import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
type Transfer = components['schemas']['commandsv1Transfer'];
@@ -64,16 +63,10 @@ export const TxDetailsTransfer = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered" data-testid="type">
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
</TableRow>
<TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell>
{txSignatureToDeterministicId(txData.signature.value)}
</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -81,7 +74,7 @@ export const TxDetailsTransfer = ({
hideTypeRow={true}
/>
{from ? (
<TableRow modifier="bordered" data-testid="from">
<TableRow modifier="bordered">
<TableCell>{t('From')}</TableCell>
<TableCell>
<PartyLink id={from} />
@@ -89,7 +82,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
{transfer.to ? (
<TableRow modifier="bordered" data-testid="to">
<TableRow modifier="bordered">
<TableCell>{t('To')}</TableCell>
<TableCell>
<PartyLink id={transfer.to} />
@@ -97,7 +90,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
{transfer.asset && transfer.amount ? (
<TableRow modifier="bordered" data-testid="amount">
<TableRow modifier="bordered">
<TableCell>{t('Amount')}</TableCell>
<TableCell>
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
@@ -1,15 +1,6 @@
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../routes/blocks/tendermint-blocks-response';
import { getTypeLabelForTransfer } from './details/tx-transfer';
import type { components } from '../../../types/explorer';
import {
TxDetailsTransfer,
getTypeLabelForTransfer,
} from './details/tx-transfer';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
type Transfer = components['schemas']['commandsv1Transfer'];
describe('TX: Transfer: getLabelForTransfer', () => {
@@ -65,70 +56,3 @@ describe('TX: Transfer: getLabelForTransfer', () => {
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
});
describe('TxDetailsTransfer', () => {
const mockBlockData = {
result: {
block: {
header: {
height: '123',
},
},
},
};
const mockTxData: Partial<BlockExplorerTransactionResult> = {
hash: 'test',
submitter:
'e1943eea46fed576cf2be42972f3c5515ad3d0ac7ac013f56677c12a53a1b3ed',
command: {
nonce: '5188810881378065222',
blockHeight: '14951513',
transfer: {
fromAccountType: 'ACCOUNT_TYPE_GENERAL',
to: '78432a2808f20b18a46ccc6a917bdc4d63c2b9e7007f777bdcab5a9f462c5ba6',
toAccountType: 'ACCOUNT_TYPE_GENERAL',
asset:
'dd20590509d30d20bdbbe64dc1090c1140c7690121a9b9940bc66f62dfa2e599',
amount: '4800000000',
reference: '',
oneOff: {
deliverOn: '0',
},
},
},
signature: {
value:
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
},
};
it('renders basic transfer details', () => {
const { getByTestId } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsTransfer
txData={mockTxData as BlockExplorerTransactionResult}
pubKey={mockTxData.command.submitter}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
const id = getByTestId('id');
expect(id.children[0].textContent).toEqual('Transfer ID');
expect(id.children[1].textContent).toEqual(
'51f3bab5eb2637651012507a64d497790a734248792c16e5cf36df8984074fbd'
);
const type = getByTestId('type');
expect(type.children[1].textContent).toEqual('Transfer');
const from = getByTestId('from');
expect(from.children[1].textContent).toEqual(mockTxData.submitter);
const to = getByTestId('to');
expect(to.children[1].textContent).toEqual(mockTxData.command.transfer.to);
});
});
+1 -1
View File
@@ -133,7 +133,7 @@ export const ErrorBoundary = () => {
);
};
export const GHOST = (
const GHOST = (
<svg
width="56"
height="85"
@@ -36,50 +36,30 @@ const PERCENTAGE_PARAMS = [
'governance.proposal.updateNetParam.requiredMajority',
'governance.proposal.updateNetParam.requiredParticipation',
'governance.proposal.updateMarket.minProposerEquityLikeShare',
'governance.proposal.VolumeDiscountProgram.requiredMajority',
'governance.proposal.VolumeDiscountProgram.requiredParticipation',
'governance.proposal.referralProgram.requiredMajority',
'governance.proposal.transfer.requiredMajority',
'governance.proposal.updateAsset.requiredMajority',
'governance.proposal.updateAsset.requiredParticipation',
'governance.proposal.transfer.requiredParticipation',
'governance.proposal.referralProgram.requiredParticipation',
'network.validators.ersatz.rewardFactor',
'network.validators.ersatz.multipleOfTendermintValidators',
'validators.vote.required',
'referralProgram.maxReferralRewardFactor',
'referralProgram.maxReferralDiscountFactor',
'referralProgram.maxReferralRewardProportion',
].map((p) => p.toLowerCase());
];
const BIG_NUMBER_PARAMS = [
'spam.protection.delegation.min.tokens',
'validators.delegation.minAmount',
'governance.proposal.transfer.maxAmount',
'reward.staking.delegation.minimumValidatorStake',
'reward.staking.delegation.maxPayoutPerParticipant',
'reward.staking.delegation.maxPayoutPerEpoch',
'spam.protection.voting.min.tokens',
'spam.protection.proposal.min.tokens',
'governance.proposal.transfer.minVoterBalance',
'governance.proposal.freeform.minProposerBalance',
'governance.proposal.updateNetParam.minVoterBalance',
'governance.proposal.updateMarket.minVoterBalance',
'governance.proposal.asset.minVoterBalance',
'governance.proposal.updateNetParam.minProposerBalance',
'governance.proposal.freeform.minVoterBalance',
'spam.protection.proposal.min.tokens',
'governance.proposal.updateMarket.minProposerBalance',
'governance.proposal.asset.minProposerBalance',
'governance.proposal.transfer.minProposerBalance',
'governance.proposal.market.minProposerBalance',
'governance.proposal.market.minVoterBalance',
'governance.proposal.updateAsset.minProposerBalance',
'governance.proposal.updateAsset.minVoterBalance',
'governance.proposal.referralProgram.minProposerBalance',
'governance.proposal.referralProgram.minVoterBalance',
'governance.proposal.VolumeDiscountProgram.minProposerBalance',
'governance.proposal.VolumeDiscountProgram.minVoterBalance',
].map((p) => p.toLowerCase());
];
export const renderGroupedParams = (
group: GroupedParams,
@@ -151,12 +131,12 @@ export const NetworkParameterRow = ({
<div className="pb-2">
<SyntaxHighlighter data={JSON.parse(value)} />
</div>
) : BIG_NUMBER_PARAMS.includes(key.toLowerCase()) ? (
addDecimalsFormatNumber(Number(value), 18)
) : PERCENTAGE_PARAMS.includes(key.toLowerCase()) ? (
`${formatNumber(Number(value) * 100, 0)}%`
) : isNaN(Number(value)) ? (
value
) : BIG_NUMBER_PARAMS.includes(key) ? (
addDecimalsFormatNumber(Number(value), 18)
) : PERCENTAGE_PARAMS.includes(key) ? (
`${formatNumber(Number(value) * 100, 0)}%`
) : (
formatNumber(Number(value), 4)
)}
@@ -1,108 +0,0 @@
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const RestrictedPage = () => {
const errorTitle = '451 Unavailable';
const errorMessage =
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
return (
<div>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
{errorTitle}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto font-mono">
{errorMessage}
</div>
</div>
</div>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 150 234"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
fill="black"
/>
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
<path
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
fill="#FF0081"
/>
<path
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
fill="white"
/>
<path
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
fill="white"
/>
<path
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
fill="white"
/>
<path
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
fill="white"
/>
<path
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
fill="white"
/>
<path
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
fill="white"
/>
<path
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
fill="white"
/>
<path
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
fill="white"
/>
<path
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
fill="white"
/>
<path
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
fill="black"
/>
</svg>
);
export default RestrictedPage;
@@ -1,6 +1,5 @@
export const Routes = {
HOME: '/',
RESTRICTED: '/restricted',
TX: 'txs',
BLOCKS: 'blocks',
PARTIES: 'parties',
@@ -30,7 +30,6 @@ import { remove0x } from '@vegaprotocol/utils';
import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer';
import { FLAGS } from '@vegaprotocol/environment';
import RestrictedPage from './restricted';
export type Navigable = {
path: string;
@@ -357,14 +356,6 @@ export const routerConfig: Route[] = [
...validators,
],
},
{
path: Routes.RESTRICTED,
element: <RestrictedPage />,
handle: {
name: t('Restricted'),
text: t('Restricted'),
},
},
];
export const router = createBrowserRouter(routerConfig);
-4
View File
@@ -22,11 +22,7 @@ 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,5 +6,3 @@ 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,5 +6,3 @@ 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,5 +6,3 @@ 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
@@ -1,15 +0,0 @@
{
"rationale": {
"title": "Governance cancel transfer proposal",
"description": "Rejected cancel transfer proposal"
},
"terms": {
"cancelTransfer": {
"changes": {
"transferId": "invalid transfer id"
}
},
"closingTimestamp": 0,
"enactmentTimestamp": 0
}
}
@@ -27,16 +27,12 @@ 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 {
createGovernanceTransferProposalTxBody,
createSuccessorMarketProposalTxBody,
} from '../../support/proposal.functions';
import { createSuccessorMarketProposalTxBody } from '../../support/proposal.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const participationNotMet = 'token-participation-not-met';
@@ -55,7 +51,6 @@ 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(
@@ -525,78 +520,5 @@ 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', 'New transfer');
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
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', 'Cancel transfer');
getProposalInformationFromTable('Error details')
.invoke('text')
.and('eq', 'Governance transfer invalid transfer id not found');
getProposalInformationFromTable('transferId')
.invoke('text')
.and('eq', 'invalid transfer id');
});
}
);
@@ -1,10 +1,9 @@
import { addDays, addSeconds, millisecondsToSeconds } from 'date-fns';
import { 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;
@@ -359,46 +358,6 @@ 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,9 +41,6 @@ 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,6 +35,4 @@ 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,5 +35,3 @@ 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,5 +27,3 @@ 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,5 +26,3 @@ 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,5 +25,3 @@ 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,6 +23,4 @@ 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,5 +28,3 @@ 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,5 +24,3 @@ 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,5 +1,4 @@
/* eslint-disable */
process.env.TZ = 'GMT';
export default {
displayName: 'governance',
preset: '../../jest.preset.js',
+3 -15
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';
@@ -106,7 +106,6 @@ const Web3Container = ({
useEthWithdrawApprovalsManager();
return null;
};
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
store.connectors,
store.initialize,
@@ -185,7 +184,7 @@ const Web3Container = ({
<TemplateSidebar sidebar={sideBar}>
<AppRouter />
</TemplateSidebar>
<footer className="p-4 break-all border-t border-neutral-700">
<footer className="p-4 border-t border-neutral-700 break-all">
<NetworkInfo />
</footer>
</AppLayout>
@@ -240,9 +239,6 @@ const AppContainer = () => {
store.setDialogOpen,
]);
// Hacky skip all the loading & web3 init for geo restricted users
const isRestricted = document?.location?.pathname?.includes('/restricted');
useEffect(() => {
if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({
@@ -308,14 +304,6 @@ const AppContainer = () => {
}
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
if (isRestricted) {
return (
<Router>
<AppRouter />
</Router>
);
}
return (
<Router>
<ScrollToTop />
@@ -324,7 +312,7 @@ const AppContainer = () => {
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<NodeFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
}
>
<AsyncRenderer<EthereumConfig | null>
+1 -64
View File
@@ -9,7 +9,6 @@
"pageTitleRedemptionTranche": "Redeem from Tranche",
"pageTitleTranches": "Vesting tranches",
"pageTitle404": "Page not found",
"pageTitle451": "451 unavailable",
"pageTitleNotPermitted": "Can not proceed!",
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
"pageTitleProposals": "Proposals",
@@ -710,8 +709,6 @@
"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",
@@ -719,8 +716,6 @@
"UpdateAssetProposal": "Update asset proposal",
"NewFreeformProposal": "New freeform proposal",
"NewRawProposal": "New proposal",
"NewTransferProposal": "New transfer proposal",
"CancelTransferProposal": "Cancel transfer proposal",
"MARKET_STATE_UPDATE_TYPE_RESUME": "Resume market",
"MARKET_STATE_UPDATE_TYPE_SUSPEND": "Suspend market",
"MARKET_STATE_UPDATE_TYPE_TERMINATE": "Terminate market",
@@ -737,12 +732,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",
"NewTransfer": "New transfer",
"CancelTransfer": "Cancel transfer",
"AssetID": "Asset ID",
"Freeform": "Freeform",
"RawProposal": "Let me choose (raw proposal)",
@@ -899,59 +890,5 @@
"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",
"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",
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
"BenefitTierMinimumQuantumBalance": "Minimum quantum balance",
"BenefitTierMinimumQuantumBalanceDescription": "The minimum amount of the vesting token to qualify",
"BenefitTierVestingMultiplier": "Vesting multiplier",
"BenefitTierVestingMultiplierDescription": "Vesting multiplier for the tier",
"BenefitTierRewardMultiplier": "Reward multiplier",
"BenefitTierRewardMultiplierDescription": "The multiplier",
"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",
"ACCOUNT_TYPE_INSURANCE": "Insurance account",
"ACCOUNT_TYPE_GLOBAL_INSURANCE": "Global insurance account",
"ACCOUNT_TYPE_SETTLEMENT": "Settlement account",
"ACCOUNT_TYPE_MARGIN": "Margin account ",
"ACCOUNT_TYPE_GENERAL": "General account",
"ACCOUNT_TYPE_FEES_INFRASTRUCTURE": "Infrastructure fees account",
"ACCOUNT_TYPE_FEES_LIQUIDITY": "Liquidity fees account",
"ACCOUNT_TYPE_FEES_MAKER": "Maker fees account",
"ACCOUNT_TYPE_BOND": "Bond account",
"ACCOUNT_TYPE_EXTERNAL": "External account",
"ACCOUNT_TYPE_GLOBAL_REWARD": "Global reward account",
"ACCOUNT_TYPE_PENDING_TRANSFERS": "Pending transfers account",
"ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES": "Maker paid fees reward account",
"ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES": "Maker received fees reward account",
"ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES": "Liquidity provider received fees reward account",
"ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS": "Market proposers reward account",
"ACCOUNT_TYPE_HOLDING": "Holding account",
"ACCOUNT_TYPE_LP_LIQUIDITY_FEES": "Liquidity provider fees account",
"ACCOUNT_TYPE_NETWORK_TREASURY": "Network treasury account",
"ACCOUNT_TYPE_VESTING_REWARDS": "Vesting rewards account",
"ACCOUNT_TYPE_VESTED_REWARDS": "Vested rewards account",
"ACCOUNT_TYPE_REWARD_AVERAGE_POSITION": "Average position reward account",
"ACCOUNT_TYPE_REWARD_RELATIVE_RETURN": "Relative return reward account",
"ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY": "Return volatility reward account",
"ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING": "Validator ranking reward account",
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account"
"proposalCancelTransferDetails": "Cancel governance transfer details"
}
@@ -188,8 +188,6 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -12,19 +12,18 @@ import {
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalHeader, NewTransferSummary } from './proposal-header';
import { ProposalHeader } from './proposal-header';
import {
lastWeek,
nextWeek,
mockWalletContext,
createUserVoteQueryMock,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { FLAGS } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
@@ -32,7 +31,6 @@ jest.mock('@vegaprotocol/proposals', () => ({
code: 'PARENT_CODE',
parentMarketId: 'PARENT_ID',
}),
useNewTransferProposalDetails: jest.fn(),
}));
const renderComponent = (
@@ -420,78 +418,3 @@ describe('Proposal header', () => {
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
});
});
jest.mock('@vegaprotocol/proposals');
describe('<NewTransferSummary />', () => {
it('renders null if no details are provided', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
const { container } = render(<NewTransferSummary proposalId="1" />);
expect(container.firstChild).toBeNull();
});
it('handles OneOffGovernanceTransfer', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: { __typename: 'OneOffGovernanceTransfer', deliverOn: null },
source: '0x123',
sourceType: 'wallet',
destination: '0x456',
destinationType: 'contract',
});
const { getByText } = render(<NewTransferSummary proposalId="1" />);
const textMatch = (content: string) => content.includes('One off transfer');
expect(getByText(textMatch)).toBeInTheDocument();
});
it('handles RecurringGovernanceTransfer', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: '0x123',
sourceType: 'wallet',
destination: '0x456',
destinationType: 'contract',
});
const { getByText } = render(<NewTransferSummary proposalId="1" />);
const textMatch = (content: string) =>
content.includes('Recurring transfer');
expect(getByText(textMatch)).toBeInTheDocument();
});
it('should fallback to translated sourceType when source is not set', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: undefined,
sourceType: 'ACCOUNT_TYPE_GENERAL',
destination: '0x456',
destinationType: 'ACCOUNT_TYPE_GENERAL',
});
render(<NewTransferSummary proposalId="1" />);
expect(screen.getByText('General account')).toBeInTheDocument();
});
it('should fallback to translated destinationType when destination is not set', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: '0x123',
sourceType: 'ACCOUNT_TYPE_GENERAL',
destination: undefined,
destinationType: 'ACCOUNT_TYPE_GLOBAL_INSURANCE',
});
render(<NewTransferSummary proposalId="1" />);
expect(screen.getByText('Global insurance account')).toBeInTheDocument();
});
});
@@ -98,16 +98,6 @@ 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');
@@ -237,11 +227,7 @@ export const ProposalHeader = ({
);
};
export const SuccessorCode = ({
proposalId,
}: {
proposalId?: string | null;
}) => {
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
const { t } = useTranslation();
const successor = useSuccessorMarketProposalDetails(proposalId);
@@ -258,11 +244,7 @@ export const SuccessorCode = ({
) : null;
};
export const NewTransferSummary = ({
proposalId,
}: {
proposalId?: string | null;
}) => {
const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
const { t } = useTranslation();
const details = useNewTransferProposalDetails(proposalId);
@@ -271,23 +253,13 @@ export const NewTransferSummary = ({
return (
<span>
{GovernanceTransferKindMapping[details.kind.__typename]}{' '}
{t('transfer from')}{' '}
<Lozenge>
{details.source
? truncateMiddle(details.source)
: t(details.sourceType)}
</Lozenge>{' '}
{t('to')}{' '}
<Lozenge>
{details.destination
? truncateMiddle(details.destination)
: t(details.destinationType)}
</Lozenge>
{t('transfer from')} <Lozenge>{truncateMiddle(details.source)}</Lozenge>{' '}
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
</span>
);
};
export const CancelTransferSummary = ({
const CancelTransferSummary = ({
proposalId,
}: {
proposalId?: string | null;
@@ -12,12 +12,12 @@ import {
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
getDataSourceSpecForSettlementSchedule,
getDataSourceSpecForSettlementData,
getDataSourceSpecForTradingTermination,
getSigners,
MarginScalingFactorsPanel,
} from '@vegaprotocol/markets';
import {
Button,
@@ -219,10 +219,8 @@ export const ProposalMarketData = ({
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>
{t('Margin scaling factors')}
</h2>
<MarginScalingFactorsPanel
<h2 className={marketDataHeaderStyles}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
@@ -1 +0,0 @@
export * from './proposal-referral-program-details';
@@ -1,156 +0,0 @@
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',
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',
},
],
endOfProgram: '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: {},
},
});
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();
});
});
@@ -1,232 +0,0 @@
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?.benefitTiers;
const stakingTiers = proposal?.terms?.change?.stakingTiers;
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
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>
);
};
@@ -1 +0,0 @@
export * from './proposal-update-benefit-tiers-details';
@@ -1,158 +0,0 @@
import { render, screen } from '@testing-library/react';
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: {
decimals: 2,
},
}),
}));
const mockVestingBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_quantum_balance: '10000',
reward_multiplier: '0.05',
},
{
minimum_quantum_balance: '500000000000',
reward_multiplier: '0.1',
},
{
minimum_quantum_balance: '10000000000000',
reward_multiplier: '10',
},
],
}),
},
},
},
});
const mockActivityStreakBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_activity_streak: '10000',
vesting_multiplier: '5',
reward_multiplier: '0.1',
},
{
minimum_activity_streak: '10000000000000',
vesting_multiplier: '100',
reward_multiplier: '10',
},
],
}),
},
},
},
});
describe('ProposalUpdateBenefitTiers', () => {
it('should not render if proposal is null', () => {
render(<ProposalUpdateBenefitTiers proposal={null} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if __typename is not UpdateNetworkParameter', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are relevant fields that are empty', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should render a valid vesting benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
expect(screen.getByText('Tier 2')).toBeInTheDocument();
expect(screen.getByText('Tier 3')).toBeInTheDocument();
expect(screen.getAllByText('Minimum quantum balance').length).toBe(3);
expect(screen.getAllByText('Reward multiplier').length).toBe(3);
expect(screen.getByText('0.00000000000001')).toBeInTheDocument();
expect(screen.getByText('0.05x')).toBeInTheDocument();
expect(screen.getByText('0.0000005')).toBeInTheDocument();
expect(screen.getByText('0.1x')).toBeInTheDocument();
expect(screen.getByText('0.00001')).toBeInTheDocument();
expect(screen.getByText('10x')).toBeInTheDocument();
});
it('should render a valid activity streak benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers
proposal={mockActivityStreakBenefitTierProposal}
/>
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
expect(screen.getByText('Tier 2')).toBeInTheDocument();
expect(screen.getAllByText('Minimum activity streak').length).toBe(2);
expect(screen.getAllByText('Vesting multiplier').length).toBe(2);
expect(screen.getAllByText('Reward multiplier').length).toBe(2);
expect(screen.getByText('10000')).toBeInTheDocument();
expect(screen.getByText('5x')).toBeInTheDocument();
expect(screen.getByText('0.1x')).toBeInTheDocument();
expect(screen.getByText('10000000000000')).toBeInTheDocument();
expect(screen.getByText('100x')).toBeInTheDocument();
expect(screen.getByText('10x')).toBeInTheDocument();
});
});
@@ -1,164 +0,0 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import {
formatMinimumStakedTokens,
formatReferralRewardMultiplier,
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
// These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = {
minimum_quantum_balance: string;
reward_multiplier: string;
};
type ActivityStreakBenefitTier = {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
};
export type BenefitTiers =
| Array<ActivityStreakBenefitTier>
| Array<VestingBenefitTier>;
export function getBenefitTiers(json: string): BenefitTiers {
try {
const parsed = JSON.parse(json);
return parsed.tiers;
} catch (e) {
return [];
}
}
export const formatVolumeDiscountFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
}
/**
* Special rendered for network proposals that change any benefit tiers,
* which is detected by:
* 1) it being a network parameter change
* 2) the name of the field ending in `.benefitTiers`
*
* It only renders known fields so that they can be formatted correctly.
*/
export const ProposalUpdateBenefitTiers = ({
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
) {
return null;
}
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
const benefitTiers = getBenefitTiers(benefitTiersString);
if (!benefitTiers) {
return null;
}
return (
<div data-testid="proposal-update-benefit-tiers">
<RoundedWrapper paddingBottom={true}>
{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.reward_multiplier) - Number(b.reward_multiplier)
)
.map((benefitTier, index) => (
<div className="mb-4" key={index}>
<h4 className="font-semibold uppercase">
Tier {index + 1}
</h4>
{'minimum_activity_streak' in benefitTier && (
<KeyValueTableRow
data-testid={`mas-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t(
'BenefitTierMinimumActivityStreakDescription'
)}
>
<span>{t('BenefitTierMinimumActivityStreak')}</span>
</Tooltip>
{benefitTier.minimum_activity_streak}
</KeyValueTableRow>
)}
{'minimum_quantum_balance' in benefitTier && (
<KeyValueTableRow
data-testid={`mqb-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t(
'BenefitTierMinimumQuantumBalanceDescription'
)}
>
<span>{t('BenefitTierMinimumQuantumBalance')}</span>
</Tooltip>
{formatMinimumStakedTokens(
benefitTier.minimum_quantum_balance,
18
)}
</KeyValueTableRow>
)}
{'vesting_multiplier' in benefitTier && (
<KeyValueTableRow
data-testid={`vm-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t('BenefitTierVestingMultiplier')}
>
<span>{t('BenefitTierVestingMultiplier')}</span>
</Tooltip>
{formatReferralRewardMultiplier(
benefitTier.vesting_multiplier
)}
</KeyValueTableRow>
)}
{'reward_multiplier' in benefitTier && (
<KeyValueTableRow
data-testid={`rm-${benefitTier.reward_multiplier}`}
>
<Tooltip description={t('BenefitTierRewardMultiplier')}>
<span>{t('BenefitTierRewardMultiplier')}</span>
</Tooltip>
{formatReferralRewardMultiplier(
benefitTier.reward_multiplier
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
</RoundedWrapper>
</div>
);
};
@@ -1 +0,0 @@
export * from './proposal-volume-discount-program-details';
@@ -1,114 +0,0 @@
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();
});
});
@@ -1,130 +0,0 @@
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,8 +6,6 @@ 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';
@@ -27,7 +25,6 @@ import {
ProposalTransferDetails,
} from '../proposal-transfer';
import { FLAGS } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
export interface ProposalProps {
proposal: ProposalQuery['proposal'];
@@ -116,15 +113,6 @@ 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;
}
}
@@ -232,26 +220,6 @@ 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>
)}
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
proposal.terms.change.networkParameter.key.slice(-13) ===
'.benefitTiers' && (
<div className="mb-4">
<ProposalUpdateBenefitTiers proposal={proposal} />
</div>
)}
{governanceTransferDetails}
<div className="mb-10">
@@ -19,8 +19,6 @@ 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,
@@ -28,10 +26,6 @@ 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,
NetworkParams.governance_proposal_transfer_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredMajority,
]);
const fallback = {
@@ -97,30 +91,6 @@ 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
),
};
case 'NewTransfer':
case 'CancelTransfer':
return {
requiredMajority: params.governance_proposal_transfer_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_transfer_requiredParticipation
),
};
default:
return fallback;
}
@@ -43,48 +43,10 @@ fragment UpdateMarketState on Proposal {
}
}
fragment UpdateReferralProgram on Proposal {
terms {
change {
... on UpdateReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: 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
@@ -102,9 +64,6 @@ 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,8 +36,6 @@ 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,
@@ -46,8 +44,6 @@ 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 {
@@ -61,8 +57,6 @@ 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,42 +43,6 @@ fragment UpdateMarketStates on Proposal {
}
}
fragment UpdateReferralPrograms on Proposal {
terms {
change {
... on UpdateReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
fragment UpdateVolumeDiscountPrograms on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
fragment ProposalFields on Proposal {
id
rationale {
@@ -163,8 +127,6 @@ fragment ProposalFields on Proposal {
query Proposals(
$includeNewMarketProductFields: Boolean!
$includeUpdateMarketStates: Boolean!
$includeUpdateReferralPrograms: Boolean!
$includeUpdateVolumeDiscountPrograms: Boolean!
) {
proposalsConnection {
edges {
@@ -172,9 +134,6 @@ query Proposals(
...ProposalFields
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms
@include(if: $includeUpdateVolumeDiscountPrograms)
}
}
}
@@ -7,21 +7,15 @@ 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', windowLength: number, endOfProgram: any, 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', windowLength: number, endOfProgram: any, 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 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 const NewMarketProductFieldsFragmentDoc = gql`
fragment NewMarketProductFields on Proposal {
@@ -70,44 +64,6 @@ export const UpdateMarketStatesFragmentDoc = gql`
}
}
`;
export const UpdateReferralProgramsFragmentDoc = gql`
fragment UpdateReferralPrograms on Proposal {
terms {
change {
... on UpdateReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: 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
@@ -191,24 +147,20 @@ export const ProposalFieldsFragmentDoc = gql`
}
`;
export const ProposalsDocument = gql`
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!, $includeUpdateReferralPrograms: Boolean!, $includeUpdateVolumeDiscountPrograms: Boolean!) {
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!) {
proposalsConnection {
edges {
node {
...ProposalFields
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms @include(if: $includeUpdateVolumeDiscountPrograms)
}
}
}
}
${ProposalFieldsFragmentDoc}
${NewMarketProductFieldsFragmentDoc}
${UpdateMarketStatesFragmentDoc}
${UpdateReferralProgramsFragmentDoc}
${UpdateVolumeDiscountProgramsFragmentDoc}`;
${UpdateMarketStatesFragmentDoc}`;
/**
* __useProposalsQuery__
@@ -224,8 +176,6 @@ ${UpdateVolumeDiscountProgramsFragmentDoc}`;
* variables: {
* includeNewMarketProductFields: // value for 'includeNewMarketProductFields'
* includeUpdateMarketStates: // value for 'includeUpdateMarketStates'
* includeUpdateReferralPrograms: // value for 'includeUpdateReferralPrograms'
* includeUpdateVolumeDiscountPrograms: // value for 'includeUpdateVolumeDiscountPrograms'
* },
* });
*/
@@ -49,8 +49,6 @@ export const ProposalsContainer = () => {
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -41,8 +41,6 @@ export const RejectedProposalsContainer = () => {
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -142,12 +142,6 @@ export const generateYesVotes = (
})
.toString(),
},
vestingBalancesSummary: {
__typename: 'PartyVestingBalancesSummary',
epoch: null,
lockedBalances: [],
vestingBalances: [],
},
},
datetime: faker.date.past().toISOString(),
};
@@ -198,12 +192,6 @@ export const generateNoVotes = (
})
.toString(),
},
vestingBalancesSummary: {
__typename: 'PartyVestingBalancesSummary',
epoch: null,
lockedBalances: [],
vestingBalances: [],
},
},
datetime: faker.date.past().toISOString(),
};
@@ -1,112 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..';
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const Restricted = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const errorMessage =
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
return (
<div>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="leading-relaxed mb-0 text-[2.7rem] font-alpha calt break-words uppercase">
{t('pageTitle451')}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto font-mono">
{errorMessage}
</div>
</div>
</div>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 150 234"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
fill="black"
/>
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
<path
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
fill="#FF0081"
/>
<path
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
fill="white"
/>
<path
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
fill="white"
/>
<path
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
fill="white"
/>
<path
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
fill="white"
/>
<path
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
fill="white"
/>
<path
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
fill="white"
/>
<path
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
fill="white"
/>
<path
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
fill="white"
/>
<path
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
fill="white"
/>
<path
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
fill="black"
/>
</svg>
);
export default Restricted;
+1 -7
View File
@@ -4,7 +4,6 @@ import Home from './token';
import NotFound from './not-found';
import NotPermitted from './not-permitted';
import Routes from './routes';
import Restricted from './restricted';
const LazyTranches = React.lazy(
() =>
@@ -361,14 +360,9 @@ const routerConfig = [
path: Routes.DISCLAIMER,
element: <LazyDisclaimer name="Disclaimer" />,
},
{
path: Routes.RESTRICTED,
// Not lazy as loaded when a user first hits the site
element: <Restricted name="451 Unavailable" />,
},
{
path: '*',
// Also not lazy as loaded when a user first hits the site
// Not lazy as loaded when a user first hits the site
element: <NotFound name="NotFound" />,
},
...redirects,
-1
View File
@@ -7,7 +7,6 @@ const Routes = {
PROPOSALS_REJECTED: '/proposals/rejected',
PROTOCOL_UPGRADES: '/protocol-upgrades',
NOT_PERMITTED: '/not-permitted',
RESTRICTED: '/restricted',
NOT_FOUND: '/not-found',
CONTRACTS: '/contracts',
TOKEN: '/token',
+147
View File
@@ -0,0 +1,147 @@
.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
@@ -0,0 +1,58 @@
.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;
}
}
}
+13 -13
View File
@@ -48,8 +48,8 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.get('@markets').then((markets) => {
cy.wrap(markets[0]).as('market');
});
cy.setOnBoardingViewed();
cy.visit('/#/portfolio');
cy.connectVegaWallet();
});
it('can deposit', function () {
@@ -70,8 +70,6 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.getByTestId('approve-default').should(
'contain.text',
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
@@ -122,7 +120,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(collateralTab).click();
cy.getByTestId('open-transfer').eq(1).click();
cy.getByTestId('open-transfer').click();
cy.getByTestId('transfer-form').should('be.visible');
cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1);
cy.get('select option')
@@ -149,8 +147,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
// 0003-WTXN-011
cy.getByTestId('Withdrawals').click();
cy.getByTestId('withdraw-dialog-button').click();
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
selectAsset(0);
cy.get(amountField).focus();
cy.get(amountField).clear().type('1');
cy.getByTestId('submit-withdrawal').click();
@@ -183,21 +180,24 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.get('@markets').then((markets) => {
cy.wrap(markets[0]).as('market');
});
cy.setOnBoardingViewed();
cy.setVegaWallet();
});
it('shows node health', function () {
// 0006-NETW-010
const regex = /^Operational\d+$/;
const market = this.market;
cy.visit(`/#/markets/${market.id}`);
cy.getByTestId('node-health-trigger').realHover();
cy.getByTestId('node-health')
.children()
.first()
.invoke('text')
.should('match', regex);
.should('contain.text', 'Operational')
.then(($el) => {
const blockHeight = parseInt($el.text());
// block height will increase over the course of the test run so best
// we can do here is check that its showing something sensible
expect(blockHeight).to.be.greaterThan(0);
});
cy.getByTestId('node-health')
.children()
.eq(1)
@@ -239,6 +239,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
.should('contain.text', order.size);
cy.getByTestId(openOrdersTab).click();
cy.getByTestId('edit', txTimeout).should('contain.text', 'Edit');
cy.getByTestId('tab-open-orders').within(() => {
cy.get('.ag-center-cols-container')
.children()
@@ -279,7 +280,8 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.visit(`/#/markets/${market.id}`);
cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId(openOrdersTab).click();
cy.getByTestId('edit').first().click();
cy.getByTestId('edit', txTimeout).should('be.visible');
cy.getByTestId('edit').first().should('be.visible').click();
cy.getByTestId('dialog-title').should('contain.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type(newPrice);
cy.getByTestId('edit-order').find('[type="submit"]').click();
@@ -348,7 +350,6 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.getByTestId('withdraw-dialog-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.get(amountField).clear().type('1');
cy.getByTestId('submit-withdrawal').click();
cy.getByTestId(toastContent, txTimeout).should(
@@ -436,7 +437,6 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.contains('Deposits of tBTC not approved').should('not.exist');
cy.contains('Use maximum').should('be.visible');
cy.get(amountField).clear().type('20000000');
@@ -0,0 +1,201 @@
import { removeDecimal } from '@vegaprotocol/cypress';
import { ethers } from 'ethers';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
const assetSelectField = 'select[name="asset"]';
const toAddressField = 'input[name="to"]';
const amountField = 'input[name="amount"]';
const formFieldError = 'input-error-text';
const ASSET_EURO = 1;
describe('deposit form validation', { tags: '@smoke' }, () => {
function openDepositForm() {
cy.mockWeb3Provider();
cy.mockSubscription();
cy.mockTradingPage();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.wait('@Assets');
}
before(() => {
openDepositForm();
});
it('handles empty fields', () => {
cy.getByTestId('deposit-submit').click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
// once Ethereum wallet is connected and key selected the only field that will
// error is the asset select
cy.getByTestId(formFieldError).should('have.length', 1);
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
'have.length',
1
);
});
it('unable to select assets not enabled', () => {
// Assets not enabled in mocks
cy.get(assetSelectField + ' option:contains(Asset 2)').should('not.exist');
cy.get(assetSelectField + ' option:contains(Asset 3)').should('not.exist');
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
});
it('invalid public key when entering address manually', () => {
cy.getByTestId('enter-pubkey-manually').click();
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
'have.text',
'Invalid Vega key'
);
});
it('invalid amount', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// Deposit amount smaller than minimum viable for selected asset
// Select an amount so that we have a known decimal places value to work with
selectAsset(ASSET_EURO);
cy.get(amountField)
.clear()
.type('0.00000000000000000000000000000000001')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Value is below minimum');
});
it('insufficient funds', () => {
// 1001-DEPO-004
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
cy.get(amountField)
.clear()
.type('850')
.next(`[data-testid="${formFieldError}"]`)
.should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
});
});
describe('deposit actions', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/markets/market-1');
});
it.skip('Deposit to trade is visible', () => {
cy.getByTestId('Collateral').click();
cy.get('[row-id="asset-id"]').contains('tEURO').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').click();
cy.getByTestId('deposit-submit').should('be.visible');
});
});
function mockWeb3DepositCalls({
allowance,
depositLifetimeLimit,
balance,
deposited,
dps,
}: {
allowance: string;
depositLifetimeLimit: string;
balance: string;
deposited: string;
dps: number;
}) {
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
const toResult = (value: string, dps: number) => {
const rawValue = removeDecimal(value, dps);
return ethers.utils.hexZeroPad(
ethers.utils.hexlify(parseInt(rawValue)),
32
);
};
cy.intercept('POST', 'http://localhost:8545', (req) => {
// Mock chainId call
if (req.body.method === 'eth_chainId') {
req.alias = 'eth_chainId';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: '0xaa36a7', // 11155111 for sepolia chain id
});
}
// Mock deposited amount
if (req.body.method === 'eth_getStorageAt') {
req.alias = 'eth_getStorageAt';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(deposited, dps),
});
}
if (req.body.method === 'eth_call') {
// Mock approved amount for asset on collateral bridge
if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
) {
req.alias = 'eth_call_allowance';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(allowance, dps),
});
}
// Mock balance of asset in Ethereum wallet
else if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
) {
req.alias = 'eth_call_balanceOf';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(balance, dps),
});
}
// Mock deposit lifetime limit
else if (
req.body.params[0].to === collateralBridgeAddress &&
req.body.params[0].data ===
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
) {
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(depositLifetimeLimit, dps),
});
}
}
});
}
@@ -69,7 +69,7 @@ describe(
cy.contains('Something went wrong').should('not.exist');
cy.contains('Application error').should('not.exist');
cy.getByTestId('tab-liquidity').within(() => {
cy.get('[col-id="partyId"]').eq(1).should('not.be.empty');
cy.get('[col-id="party.id"]').eq(1).should('not.be.empty');
});
});
}
@@ -12,7 +12,8 @@ const marketSummaryBlock = 'header-summary';
const itemValue = 'item-value';
const itemHeader = 'item-header';
const colCommitmentAmount = '[col-id="commitmentAmount"]';
const colEquityLikeShare = '[col-id="feeShare.equityLikeShare"]';
const colAverageEntryValuation = '[col-id="averageEntryValuation"]';
const colEquityLikeShare = '[col-id="equityLikeShare"]';
const colFee = '[col-id="fee"]';
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
const colBalance = '[col-id="balance"]';
@@ -23,17 +24,11 @@ const colUpdatedAt = '[col-id="updatedAt"] button';
const headers = [
'Party',
'Commitment (tDAI)',
'Obligation',
'Fee',
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Fees accrued this epoch',
'Live time on book',
'Live liquidity quality score (%)',
'Last time on the book',
'Last fee penalty',
'Last bond penalty',
'Proposed fee',
'Market valuation at entry',
'Obligation',
'Supplied',
'Status',
'Created',
'Updated',
@@ -69,8 +64,11 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
// 5002-LIQP-002
cy.get(rowSelector)
.first()
.find('[col-id="partyId"]')
.should('have.text', '69464e…dc6f');
.find('[col-id="party.id"]')
.should(
'have.text',
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
cy.get(rowSelector)
.first()
@@ -84,6 +82,12 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelector)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelector)
.first()
.find(colCommitmentAmount_1)
@@ -100,8 +104,7 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty');
cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty');
});
it('liquidity status column should be sorted properly', () => {
it.skip('liquidity status column should be sorted properly', () => {
// 5002-LIQP-003
const liquidityColDefault = ['Active', 'Pending'];
const liquidityColAsc = ['Active', 'Pending'];
@@ -217,8 +220,11 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
// 5002-LIQP-011
cy.get(rowSelectorLiquidityActive)
.first()
.find('[col-id="partyId"]')
.should('have.text', '69464e…dc6f');
.find('[col-id="party.id"]')
.should(
'have.text',
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
cy.get(rowSelectorLiquidityActive)
.first()
@@ -235,6 +241,12 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelectorLiquidityActive)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCommitmentAmount_1)
@@ -265,8 +277,11 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
.find('[col-id="partyId"]')
.should('have.text', 'cc464e…dc6f');
.find('[col-id="party.id"]')
.should(
'have.text',
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
cy.get(rowSelectorLiquidityInactive)
.first()
@@ -283,6 +298,11 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.40%');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCommitmentAmount_1)
@@ -0,0 +1,28 @@
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');
});
});
@@ -0,0 +1,70 @@
import * as Schema from '@vegaprotocol/types';
import {
TIFlist,
orderPriceField,
orderSizeField,
orderTIFDropDown,
placeOrderBtn,
toggleLimit,
toggleMarket,
} from '../support/deal-ticket';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { accountsQuery } from '@vegaprotocol/mock';
describe('suspended market validation', { tags: '@regression' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage(
Schema.MarketState.STATE_SUSPENDED,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
const accounts = accountsQuery();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.setVegaWallet();
});
it('should show warning for market order', function () {
cy.getByTestId(toggleMarket).click();
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-type').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
);
});
it('should show info for allowed TIF', function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
);
});
it('should show warning for not allowed TIF', function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
);
});
});
@@ -0,0 +1,110 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { fillsQuery } from '@vegaprotocol/mock';
const tabFills = 'tab-fills';
describe('fills', { tags: '@regression' }, () => {
// 7005-FILL-001
// 7005-FILL-002
// 7005-FILL-003
// 7005-FILL-004
// 7005-FILL-005
// 7005-FILL-006
// 7005-FILL-007
// 7005-FILL-008
beforeEach(() => {
// Ensure page loads with correct key
cy.window().then((window) => {
cy.wrap(
window.localStorage.setItem(
'vega_wallet_key',
Cypress.env('VEGA_PUBLIC_KEY')
)
);
});
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockGQL((req) => {
aliasGQLQuery(
req,
'Fills',
fillsQuery({}, Cypress.env('VEGA_PUBLIC_KEY'))
);
});
cy.mockSubscription();
});
it('renders fills on portfolio page', () => {
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Fills').click();
validateFillsDisplayed();
});
it('renders fills on trading tab', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId('Fills').click();
validateFillsDisplayed();
});
function validateFillsDisplayed() {
cy.getByTestId(tabFills).should('be.visible');
cy.getByTestId(tabFills).contains('Market');
cy.getByTestId(tabFills)
.get(
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
)
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Size');
cy.get(`[col-id='size']`).eq(1).should('contain.text', '+');
cy.get(`[col-id='size']`).eq(2).should('contain.text', '-');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="size"]')
.each(($amount) => {
cy.wrap($amount).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Price');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="price"]')
.each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Notional');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="price_1"]')
.each(($total) => {
cy.wrap($total).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Role');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="aggressor"]')
.each(($role) => {
cy.wrap($role)
.invoke('text')
.then((text) => {
const roles = ['Maker', 'Taker', '-'];
expect(roles.indexOf(text.trim())).to.be.greaterThan(-1);
});
});
cy.getByTestId(tabFills).contains('Fee');
cy.getByTestId(tabFills)
.get(
'[role="gridcell"][col-id="market.tradableInstrument.instrument.product"]'
)
.each(($fees) => {
cy.wrap($fees).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Date');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get('[col-id="createdAt"]').each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
}
});
@@ -0,0 +1,529 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderAmendment, OrderCancellation } from '@vegaprotocol/wallet';
import {
updateOrder,
getSubscriptionMocks,
} from '../support/order-update-subscription';
import {
testOrderCancellation,
testOrderAmendment,
} from '../support/order-validation';
const orderSymbol = 'instrument-code';
const orderSize = 'size';
const orderType = 'type';
const orderStatus = 'status';
const orderRemaining = 'remaining';
const orderPrice = 'price';
const orderTimeInForce = 'timeInForce';
const orderUpdatedAt = 'updatedAt';
const cancelOrderBtn = 'cancel';
const cancelAllOrdersBtn = 'cancelAll';
const editOrderBtn = 'edit';
describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.wait('@Markets');
});
it('renders orders', () => {
cy.getByTestId('tab-orders').should('be.visible');
cy.getByTestId(cancelAllOrdersBtn).should('be.visible');
cy.getByTestId(cancelOrderBtn).should('have.length.at.least', 1);
cy.getByTestId(editOrderBtn).should('have.length.at.least', 1);
cy.getByTestId('tab-orders').within(() => {
cy.get(`[role='rowgroup']`)
.first()
.within(() => {
cy.get(`[col-id='${orderSymbol}']`).each(($symbol) => {
cy.wrap($symbol).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderRemaining}']`).each(($remaining) => {
cy.wrap($remaining).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderSize}']`).each(($size) => {
cy.wrap($size).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderType}']`).each(($type) => {
cy.wrap($type).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderStatus}']`).each(($status) => {
cy.wrap($status).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderPrice}']`).each(($price) => {
cy.wrap($price).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderTimeInForce}']`).each(($timeInForce) => {
cy.wrap($timeInForce).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderUpdatedAt}']`).each(($dateTime) => {
cy.wrap($dateTime).invoke('text').should('not.be.empty');
});
});
});
});
it('partially filled orders should not show close/edit buttons', () => {
const partiallyFilledId =
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
cy.getByTestId('tab-orders').should('be.visible');
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Partially Filled').click();
cy.getByTestId('All').click();
cy.get(`[row-id="${partiallyFilledId}"]`)
.eq(0)
.within(() => {
cy.get(`[col-id='${orderStatus}']`).should(
'have.text',
'Partially Filled'
);
cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7');
cy.get(`[col-id='${orderSize}']`).should('have.text', '-10');
cy.getByTestId(cancelOrderBtn).should('not.exist');
cy.getByTestId(editOrderBtn).should('not.exist');
});
});
it('orders are sorted by most recent order', () => {
// 7003-MORD-002
const expectedOrderList = [
'BTCUSD.MF21',
'SOLUSD',
'AAPL.MF21',
'BTCUSD.MF21',
'BTCUSD.MF21',
];
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Reset').click();
cy.getByTestId('All').click();
cy.getByTestId('tab-orders')
.get(
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
)
.should('have.length.at.least', expectedOrderList.length)
.then(($symbols) => {
const symbolNames: string[] = [];
cy.wrap($symbols)
.each(($symbol) => {
cy.wrap($symbol)
.invoke('text')
.then((text) => {
symbolNames.push(text);
});
})
.then(() => {
expect(symbolNames).to.include.ordered.members(expectedOrderList);
});
});
});
});
describe('subscribe orders', { tags: '@smoke' }, () => {
let orderId = '0';
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
orderId = (parseInt(orderId, 10) + 1).toString();
});
// 7002-SORD-053
// 7002-SORD-040
// 7003-MORD-001
it('must see an active order', () => {
// 7002-SORD-041
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Active');
});
it('must see an expired order', () => {
// 7002-SORD-042
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_EXPIRED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Expired');
});
it('must see a cancelled order', () => {
// 7002-SORD-043
// NOT COVERED: see the txn that cancelled it and a link to the block explorer, if cancelled by a user transaction.
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_CANCELLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Cancelled');
});
it('must see a stopped order', () => {
// 7002-SORD-044
// NOT COVERED: see an explanation of why stopped
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_STOPPED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Stopped');
});
it('must see a partially filled order', () => {
// 7002-SORD-045
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARTIALLY_FILLED,
size: '5',
remaining: '1',
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Partially Filled'
);
cy.getByTestId(`order-status-${orderId}`)
.parentsUntil(`.ag-row`)
.siblings(`[col-id=${orderRemaining}]`)
.should('have.text', '4');
});
it('must see a filled order', () => {
// 7002-SORD-046
// 7003-MORD-020
// NOT COVERED: Must be able to see/link to all trades that were created from this order
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_FILLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
cy.get(`[col-id="${orderSymbol}"]`).contains('[title="Future"]', 'Futr');
});
it('must see a rejected order', () => {
// 7002-SORD-047
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_REJECTED,
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_INTERNAL_ERROR,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Rejected: Internal error'
);
});
it('must see a parked order', () => {
// 7002-SORD-048
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARKED,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Parked: Internal error'
);
});
it('must see the size of the order and direction/side -', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '15',
side: Schema.Side.SIDE_SELL,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '-15');
});
it('must see the size of the order and direction/side +', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '5',
side: Schema.Side.SIDE_BUY,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '+5');
});
it('for limit typy must see the Limit price that was set on the order', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '200.00');
});
it('must see a pegged order - ask', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_BUY,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
offset: '250000',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Ask - 2.50 Peg limit');
});
it('must see a pegged order - bid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
offset: '100',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Bid + 0.001 Peg limit');
});
it('must see a pegged order - mid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
offset: '0.5',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Mid + 0.00001 Peg limit');
});
it('for market typy must not see a price for active or parked orders', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_PARKED,
peggedOrder: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '-');
});
it('must see the time in force applied to the order', () => {
// 7003-MORD-006
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_ACTIVE,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id='${orderTimeInForce}']`)
.should('have.text', 'GTC');
});
it('for Active order when is part of a liquidity or peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a liquidity, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
});
describe('amend and cancel order', { tags: '@smoke' }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
cy.mockVegaWalletTransaction();
});
const orderId = '1234567890';
// this test is flakey
it('must be able to amend the price of an order', () => {
// 7003-MORD-007
// 7003-MORD-012
// 7003-MORD-014
// 7003-MORD-015
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click();
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('100');
cy.getByTestId('edit-order').find('[type="submit"]').click();
const order: OrderAmendment = {
orderId: orderId,
marketId: 'market-0',
price: '10000000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
sizeDelta: 0,
};
testOrderAmendment(order);
});
});
it('must be able to cancel an individual order', () => {
// 7003-MORD-007
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="icon-cross"]`)
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {
orderId: orderId,
marketId: 'market-0',
};
testOrderCancellation(order);
});
});
it('must be able to cancel all orders on all markets', () => {
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[data-testid="cancelAll"]`)
.should('have.text', 'Cancel all')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {};
testOrderCancellation(order);
});
});
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', () => {
// 7003-MORD-013
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('0.111111');
cy.getByTestId('edit-order').find('[type="submit"]').click();
cy.getByTestId('input-error-text').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
});
});
@@ -0,0 +1,119 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const manageVegaWallet = 'manage-vega-wallet';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_SEPOLIA_TBTC = 2;
describe.skip(
'transfer fees',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
}
);
@@ -0,0 +1,91 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const transferForm = 'transfer-form';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
@@ -0,0 +1,131 @@
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
const formFieldError = 'input-error-text';
const toAddressField = 'input[name="to"]';
const amountField = 'input[name="amount"]';
const useMaximumAmount = 'use-maximum';
const submitWithdrawBtn = 'submit-withdrawal';
const ethAddressValue = Cypress.env('ETHEREUM_WALLET_ADDRESS');
const ASSET_SEPOLIA_TBTC = 2;
const ASSET_EURO = 1;
describe('withdraw form validation', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId('Withdrawals').click();
cy.getByTestId('Withdraw').click(); // sidebar item
// It also requires connection Ethereum wallet
connectEthereumWallet('MetaMask');
cy.wait('@Accounts');
cy.wait('@Assets');
});
it('empty fields', () => {
cy.getByTestId(submitWithdrawBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
// only 2 despite 3 fields because the ethereum address will be auto populated
cy.getByTestId(formFieldError).should('have.length', 2);
// Test for Ethereum address
cy.get(toAddressField).should('have.value', ethAddressValue);
});
it('min amount', () => {
// 1002-WITH-010
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(submitWithdrawBtn).click();
cy.get('[data-testid="input-error-text"]').should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1002-WITH-005
// 1002-WITH-008
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(submitWithdrawBtn).click();
cy.get('[data-testid="input-error-text"]').should(
'contain.text',
'Insufficient amount in account'
);
});
it('can set amount using use maximum button', () => {
// 1002-WITH-004
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(useMaximumAmount).click();
cy.get(amountField).should('have.value', '1000.00001');
});
});
describe(
'withdraw actions',
{ tags: '@regression', testIsolation: true },
() => {
// this is extremely ugly hack, but setting it properly in contract is too much effort for such simple validation
// 1002-WITH-018
const withdrawalThreshold =
Cypress.env('VEGA_ENV') === 'CUSTOM' ? '0.00' : '100.00';
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.wait('@Accounts');
cy.wait('@Assets');
cy.getByTestId('Withdrawals').click();
cy.getByTestId('Withdraw').click();
// It also requires connection Ethereum wallet
connectEthereumWallet('MetaMask');
cy.mockVegaWalletTransaction();
});
it('triggers transaction when submitted', () => {
// 1002-WITH-002
// 1002-WITH-003
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId('BALANCE_AVAILABLE_label').should(
'contain.text',
'Balance available'
);
cy.getByTestId('BALANCE_AVAILABLE_value').should(
'have.text',
'1,000.00001'
);
cy.getByTestId('WITHDRAWAL_THRESHOLD_label').should(
'contain.text',
'Delayed withdrawal threshold'
);
cy.getByTestId('WITHDRAWAL_THRESHOLD_value').should(
'contain.text',
withdrawalThreshold
);
cy.getByTestId('DELAY_TIME_label').should('contain.text', 'Delay time');
cy.getByTestId('DELAY_TIME_value').should('have.text', 'None');
cy.get(amountField).clear().type('10');
cy.getByTestId(submitWithdrawBtn).click();
cy.getByTestId('toast').should('contain.text', 'Awaiting confirmation');
});
}
);
+6 -2
View File
@@ -30,11 +30,11 @@ import {
blockStatisticsQuery,
networkParamQuery,
liquidityProvisionsQuery,
liquidityProviderFeeShareQuery,
successorMarketQuery,
parentMarketIdQuery,
successorMarketIdsQuery,
successorMarketProposalDetailsQuery,
liquidityProvidersQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
@@ -162,7 +162,11 @@ const mockTradingPage = (
aliasGQLQuery(req, 'Trades', tradesQuery());
aliasGQLQuery(req, 'Chart', chartQuery());
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
aliasGQLQuery(req, 'LiquidityProviders', liquidityProvidersQuery());
aliasGQLQuery(
req,
'LiquidityProviderFeeShare',
liquidityProviderFeeShareQuery
);
aliasGQLQuery(req, 'Candles', candlesQuery());
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
+5 -1
View File
@@ -12,11 +12,12 @@ NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
@@ -25,3 +26,6 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1 -1
View File
@@ -5,7 +5,7 @@
"next",
"next/core-web-vitals"
],
"ignorePatterns": ["!**/*", "__generated__", ".next"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+32 -5
View File
@@ -2,6 +2,9 @@ import { t } from '@vegaprotocol/i18n';
import { Links } from '../../lib/links';
import classNames from 'classnames';
import { NavLink, Outlet } from 'react-router-dom';
import { useWeb3React } from '@web3-react/core';
import { truncateByChars } from '@vegaprotocol/utils';
import { useWeb3ConnectStore, useWeb3Disconnect } from '@vegaprotocol/web3';
export const Assets = () => {
const linkClasses = ({ isActive }: { isActive: boolean }) => {
@@ -11,8 +14,8 @@ export const Assets = () => {
};
return (
<div className="max-w-[500px] px-4 mx-auto my-8">
<nav className="flex mb-6 text-lg gap-4">
<div className="container px-4 mx-auto my-4">
<nav className="flex items-end my-6 text-lg gap-4">
<NavLink to={Links.DEPOSIT()} className={linkClasses}>
{t('Deposit')}
</NavLink>
@@ -22,10 +25,34 @@ export const Assets = () => {
<NavLink to={Links.TRANSFER()} className={linkClasses}>
{t('Transfer')}
</NavLink>
<div className="ml-auto">
<EthereumConnect />
</div>
</nav>
<div className="pt-4 border-t md:p-6 md:border md:rounded-xl border-default">
<Outlet />
</div>
<Outlet />
</div>
);
};
const EthereumConnect = () => {
const { account, connector } = useWeb3React();
const disconnect = useWeb3Disconnect(connector);
const openDialog = useWeb3ConnectStore((store) => store.open);
return (
<div className="text-sm">
{account ? (
<div className="flex flex-col items-end">
<p>{truncateByChars(account)}</p>
<button onClick={disconnect} className="underline underline-offset-4">
{t('Disconnect')}
</button>
</div>
) : (
<button onClick={openDialog} className="underline underline-offset-4">
{t('Connect Ethereum wallet')}
</button>
)}
</div>
);
};
@@ -0,0 +1,5 @@
import MarketPage from '../market';
export const ClosedMarketPage = () => {
return <MarketPage closed />;
};
@@ -0,0 +1 @@
export { ClosedMarketPage as default } from './closed-market';
@@ -25,20 +25,18 @@ export const DepositGetStarted = () => {
}
return (
<div className="pt-6 border-t border-default">
<div className={wrapperClasses}>
<h3 className="text-lg">{t('Get started')}</h3>
<GetStartedCheckList />
{step > OnboardingStep.ONBOARDING_DEPOSIT_STEP && (
<TradingAnchorButton
href={Links.HOME()}
onClick={() => dismiss()}
intent={Intent.Info}
>
{t('Start trading')}
</TradingAnchorButton>
)}
</div>
<div className={wrapperClasses}>
<h3 className="text-lg">{t('Get started')}</h3>
<GetStartedCheckList />
{step > OnboardingStep.ONBOARDING_DEPOSIT_STEP && (
<TradingAnchorButton
href={Links.HOME()}
onClick={() => dismiss()}
intent={Intent.Info}
>
{t('Start trading')}
</TradingAnchorButton>
)}
</div>
);
};
+670 -6
View File
@@ -1,14 +1,678 @@
import { DepositContainer } from '@vegaprotocol/deposits';
import type { ReactNode } from 'react';
import { useCallback, useEffect } from 'react';
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import * as Radio from '@radix-ui/react-radio-group';
import { DepositGetStarted } from './deposit-get-started';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
addDecimal,
isAssetTypeERC20,
truncateByChars,
} from '@vegaprotocol/utils';
import { AssetStatus } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import {
Intent,
Loader,
TradingButton,
TradingInput,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import {
EthTxStatus,
useEthereumConfig,
useEthTransactionStore,
useWeb3ConnectStore,
} from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import { Token } from '@vegaprotocol/smart-contracts';
import { MaxUint256 } from '@ethersproject/constants';
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import { getAsset } from '@vegaprotocol/markets';
import classNames from 'classnames';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { Markets } from './markets';
export const Deposit = () => {
const [searchParams] = useSearchParams();
const assetId = searchParams.get('assetId') || undefined;
return (
<div className="flex flex-col gap-6">
<DepositContainer assetId={assetId} />
<DepositGetStarted />
<div className="flex flex-col gap-4">
<div>
<DepositFlowContainer />
</div>
<div>
<DepositGetStarted />
</div>
</div>
);
};
const DepositFlowContainer = () => {
const { VEGA_ENV } = useEnvironment();
const [searchParams] = useSearchParams();
const assetId = searchParams.get('assetId') || undefined;
const { data: assets } = useAssetsDataProvider();
const { data: markets } = useTopTradedMarkets();
const { config } = useEthereumConfig();
if (!assets) return null;
if (!config) return null;
return (
<DepositFlow
assets={assets}
assetId={assetId}
bridgeAddress={config.collateral_bridge_contract.address}
markets={markets || []}
faucetEnabled={VEGA_ENV !== Networks.MAINNET}
/>
);
};
const DepositSteps = {
Asset: 'Asset',
Approve: 'Approve',
Deposit: 'Deposit',
} as const;
type Step = keyof typeof DepositSteps;
interface DepositState {
step: Step;
asset: AssetFieldsFragment | undefined;
amount: string;
allowance: BigNumber | undefined;
balance: BigNumber | undefined;
}
const DepositFlow = ({
assets,
assetId,
bridgeAddress,
markets,
faucetEnabled,
}: {
assets: AssetFieldsFragment[];
assetId?: string;
bridgeAddress: string;
markets: MarketMaybeWithDataAndCandles[];
faucetEnabled: boolean;
}) => {
const { provider, account } = useWeb3React();
const [state, setState] = useState<DepositState>(() => {
const asset = assets.find((a) => a.id === assetId);
return {
step: asset ? 'Approve' : 'Asset',
asset,
amount: '',
allowance: undefined,
balance: undefined,
};
});
const onApproved = useCallback(async () => {
if (!provider) throw new Error('no provider');
if (!account) throw new Error('no account');
if (!isAssetTypeERC20(state.asset)) throw new Error('no asset');
const signer = provider.getSigner();
const tokenContract = new Token(
state.asset.source.contractAddress,
signer || provider
);
const res = await tokenContract.allowance(account, bridgeAddress);
const allowance = new BigNumber(
addDecimal(res.toString(), state.asset.decimals)
);
setState((curr) => ({ ...curr, step: DepositSteps.Deposit, allowance }));
}, [state.asset, bridgeAddress, account, provider]);
const handleAssetChanged = async (asset: AssetFieldsFragment | undefined) => {
if (!asset) {
setState((curr) => ({
...curr,
step: DepositSteps.Asset,
asset: undefined,
allowance: undefined,
balance: undefined,
}));
return;
}
if (!isAssetTypeERC20(asset)) throw new Error('invalid asset');
let allowance = new BigNumber(0);
let balance = new BigNumber(0);
// if you have connected and therefore have a provider get
// balance and allowance
if (provider && account) {
const signer = provider.getSigner();
const tokenContract = new Token(
asset.source.contractAddress,
signer || provider
);
const allowanceRes = await tokenContract.allowance(
account,
bridgeAddress
);
allowance = new BigNumber(
addDecimal(allowanceRes.toString(), asset.decimals)
);
const balanceRes = await tokenContract.balanceOf(account);
balance = new BigNumber(
addDecimal(balanceRes.toString(), asset.decimals)
);
}
const step = allowance.isGreaterThan(0)
? DepositSteps.Deposit
: DepositSteps.Approve;
setState((curr) => ({
...curr,
step,
asset,
allowance,
balance,
}));
};
return (
<>
<div className="flex flex-col border rounded border-default">
<StepWrapper>
<AssetSelector
asset={state.asset}
assets={assets}
onSelect={handleAssetChanged}
markets={markets}
balance={state.balance}
faucetEnabled={faucetEnabled}
/>
</StepWrapper>
<StepWrapper>
<Approval
step={state.step}
asset={state.asset}
allowance={state.allowance}
bridgeAddress={bridgeAddress}
onApproved={onApproved}
/>
</StepWrapper>
<StepWrapper>
<SendDeposit
step={state.step}
asset={state.asset}
allowance={state.allowance}
balance={state.balance}
amount={state.amount}
setAmount={(amount) => setState((curr) => ({ ...curr, amount }))}
/>
</StepWrapper>
</div>
</>
);
};
const StepWrapper = ({ children }: { children: ReactNode }) => {
return (
<div className="p-4 border-b rounded gap-3 last:border-b-0 border-default">
{children}
</div>
);
};
const AssetSelector = ({
asset,
assets,
onSelect,
markets,
balance,
faucetEnabled,
}: {
asset?: AssetFieldsFragment;
assets: AssetFieldsFragment[];
onSelect: (asset?: AssetFieldsFragment) => void;
markets: MarketMaybeWithDataAndCandles[];
faucetEnabled: boolean;
balance: BigNumber | undefined;
}) => {
const [search, setSearch] = useState('');
const [id, setId] = useState<number | null>(null);
const { provider } = useWeb3React();
const send = useEthTransactionStore((store) => store.create);
const tx = useEthTransactionStore((store) => {
return store.transactions.find((t) => t?.id == id);
});
const getMarketsForAsset = (a: AssetFieldsFragment) => {
return markets
.filter((m) => {
const marketAsset = getAsset(m);
return marketAsset.id === a.id;
})
.slice(0, 4);
};
const submitFaucet = async () => {
if (!provider) throw new Error('no provider');
if (!isAssetTypeERC20(asset)) throw new Error('no asset selected');
const signer = provider.getSigner();
const contract = new Token(
asset.source.contractAddress,
signer || provider
);
const id = send(contract, 'faucet', []);
setId(id);
};
if (tx && tx.status === EthTxStatus.Pending) {
return (
<div className="flex justify-between">
<div className="flex gap-3">
<div className="mt-0.5">
<Loader size="small" />
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Waiting for faucet...')}</h3>
</div>
</div>
</div>
);
}
if (asset && isAssetTypeERC20(asset)) {
const selectedAssetMarkets = getMarketsForAsset(asset);
return (
<div className="flex gap-3">
<StepIndicator complete={true} />
<div className="flex flex-1 gap-2">
<div className="flex flex-col items-start flex-1 gap-2">
<p className="text-lg">
{asset.symbol}{' '}
<small>{truncateByChars(asset.source.contractAddress)}</small>
</p>
<div className="w-full">
<Markets markets={selectedAssetMarkets} />
</div>
<div className="flex gap-2">
<TradingButton onClick={() => onSelect(undefined)} size="small">
{t('Change asset')}
</TradingButton>
{faucetEnabled && (
<TradingButton onClick={submitFaucet} size="small">
{t('Faucet')}
</TradingButton>
)}
</div>
</div>
<div className="text-right">
{balance && (
<p className="font-mono text-lg">
{balance.toString()}
<small className="ml-1 text-muted font-alpha">
{asset.symbol}
</small>
</p>
)}
</div>
</div>
</div>
);
}
const filteredAssets = assets
.filter((a) => a.status === AssetStatus.STATUS_ENABLED)
.filter((a) => a.source.__typename === 'ERC20')
.filter((a) => a.symbol.toLowerCase().includes(search.toLowerCase()));
filteredAssets.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
return (
<div className="flex flex-col gap-4">
<div className="flex justify-between">
<p>{t('Choose an asset to deposit to the Vega network')}</p>
<div>
<TradingInput
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
</div>
</div>
<Radio.Root
className="flex flex-col gap-4"
onValueChange={(value) => {
onSelect(assets.find((a) => a.id === value));
}}
>
{filteredAssets.map((asset) => {
if (!isAssetTypeERC20(asset)) return null;
const marketsForAsset = getMarketsForAsset(asset);
return (
<div key={asset.id}>
<div className="flex gap-3">
<Radio.Item
id={asset.id}
value={asset.id}
className="w-5 h-5 border-2 rounded border-vega-clight-600"
>
<Radio.Indicator className="block w-4 h-4 border-2 border-white bg-vega-clight-300" />
</Radio.Item>
<div className="flex flex-col flex-1 gap-1">
<div>
<label
htmlFor={asset.id}
className="text-lg cursor-pointer"
>
{asset.symbol}{' '}
<small>
({truncateByChars(asset.source.contractAddress)})
</small>
</label>
</div>
<Markets markets={marketsForAsset} />
</div>
</div>
</div>
);
})}
</Radio.Root>
</div>
);
};
const Approval = ({
step,
asset,
allowance,
bridgeAddress,
onApproved,
}: {
step: Step;
asset: AssetFieldsFragment | undefined;
allowance: BigNumber | undefined;
bridgeAddress: string;
onApproved: () => void;
}) => {
const openDialog = useWeb3ConnectStore((store) => store.open);
const { account, provider } = useWeb3React();
const [id, setId] = useState<number | null>(null);
const tx = useEthTransactionStore((store) => {
return store.transactions.find((t) => t?.id == id);
});
const send = useEthTransactionStore((store) => store.create);
const handleApprove = () => {
if (!provider) throw new Error('no provider');
if (!isAssetTypeERC20(asset)) throw new Error('no provider');
const signer = provider.getSigner();
const contract = new Token(
asset.source.contractAddress,
signer || provider
);
const id = send(contract, 'approve', [
bridgeAddress,
MaxUint256.toString(),
]);
setId(id);
};
useEffect(() => {
if (!asset) return; // this can get triggered when you clear the selected asset
if (tx?.status === EthTxStatus.Confirmed) {
onApproved();
}
}, [tx?.status, asset, onApproved]);
// No asset selected, show generic approval title
if (!asset) {
return (
<div className="flex items-center justify-between w-full">
<div className="flex gap-3">
<StepIndicator complete={false} />
<h3 className="text-lg">{t('Approval')}</h3>
</div>
</div>
);
}
if (!account) {
return (
<div className="flex justify-between">
<div className="flex gap-3">
<StepIndicator complete={false} />
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Approval')}</h3>
<TradingButton onClick={openDialog} size="small">
{t('Connect Ethereum wallet')}
</TradingButton>
</div>
</div>
</div>
);
}
if (tx && tx.status === EthTxStatus.Pending) {
return (
<div className="flex justify-between">
<div className="flex gap-3">
<div className="mt-0.5">
<Loader size="small" />
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Waiting for approval...')}</h3>
<TradingButton onClick={handleApprove} size="small">
{t('Re-approve')}
</TradingButton>
</div>
</div>
</div>
);
}
// APPROVED: show muted re-approve button
if (allowance && allowance.isGreaterThan(0)) {
return (
<div className="flex justify-between">
<div className="flex gap-3">
<StepIndicator complete={true} />
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Approved')}</h3>
<TradingButton onClick={handleApprove} size="small">
{t('Re-approve')}
</TradingButton>
</div>
</div>
<div className="text-right">
{allowance && (
<div className="font-mono text-lg">
<Allowance allowance={allowance} />
<small className="ml-1 text-muted font-alpha">
{asset.symbol}
</small>
</div>
)}
</div>
</div>
);
}
// NOT APPROVED: show primary approve button
return (
<div className="flex justify-between">
<div className="flex gap-3">
<StepIndicator complete={false} />
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Approve deposits')}</h3>
<p className="text-sm text-muted">
{t(
'Before you can make a deposit of %s you need to approve its use',
asset.symbol
)}
</p>
<TradingButton
onClick={() => {
if (!provider) throw new Error('no provider');
if (!isAssetTypeERC20(asset)) throw new Error('no provider');
const signer = provider.getSigner();
const contract = new Token(
asset.source.contractAddress,
signer || provider
);
const id = send(contract, 'approve', [
bridgeAddress,
MaxUint256.toString(),
]);
setId(id);
}}
size="small"
intent={Intent.Success}
>
{t('Approve %s deposits', asset.symbol)}
</TradingButton>
</div>
</div>
</div>
);
};
const Allowance = ({ allowance }: { allowance: BigNumber }) => {
let value = '';
const format = (divisor: string) => {
const result = allowance.dividedBy(divisor);
return result.isInteger() ? result.toString() : result.toFixed(1);
};
if (allowance.isGreaterThan(new BigNumber('1e14'))) {
value = t('>100t');
} else if (allowance.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
// Trillion
value = `${format('1e12')}t`;
} else if (allowance.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
// Billion
value = `${format('1e9')}b`;
} else if (allowance.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
// Million
value = `${format('1e6')}m`;
} else {
value = allowance.toString();
}
return <span>{value}</span>;
};
const SendDeposit = ({
step,
asset,
allowance,
balance,
amount,
setAmount,
}: {
step: Step;
asset: AssetFieldsFragment | undefined;
allowance: BigNumber | undefined;
balance: BigNumber | undefined;
amount: string;
setAmount: (amount: string) => void;
}) => {
const openDialog = useWeb3ConnectStore((store) => store.open);
const { account } = useWeb3React();
const fallback = (
<div className="flex gap-3">
<StepIndicator complete={false} />
<h3 className="text-lg">{t('Deposit')}</h3>
</div>
);
if (step !== DepositSteps.Deposit) {
return fallback;
}
if (!asset) {
return fallback;
}
if (!allowance || allowance.isZero()) {
return fallback;
}
if (!account) {
return (
<div className="flex justify-between">
<div className="flex gap-3">
<StepIndicator complete={false} />
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Deposit')}</h3>
<TradingButton onClick={openDialog} size="small">
{t('Connect Ethereum wallet')}
</TradingButton>
</div>
</div>
</div>
);
}
return (
<div className="flex justify-between">
<div className="flex gap-3">
<StepIndicator complete={false} />
<div className="flex flex-col items-start gap-2">
<h3 className="text-lg">{t('Deposit')}</h3>
<form
className="flex flex-col items-start gap-2"
onSubmit={() => alert('TODO')}
>
<div className="flex gap-2">
<TradingInput
name="amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Enter amount"
className="w-[300px]"
/>
{balance && (
<TradingButton
size="small"
onClick={() => setAmount(balance.toString())}
>
{t('Use max')}
</TradingButton>
)}
</div>
<TradingButton type="submit" size="small" intent={Intent.Success}>
{t('Deposit %s', asset.symbol)}
</TradingButton>
</form>
</div>
</div>
</div>
);
};
const StepIndicator = ({ complete }: { complete: boolean }) => {
const classes = classNames(
'flex items-center mt-0.5 w-5 h-5 border-2 rounded border-vega-clight-600'
);
return (
<div className={classes}>
{complete && <VegaIcon name={VegaIconNames.TICK} />}
</div>
);
};
@@ -0,0 +1,72 @@
import { calcCandleVolume, useCandles } from '@vegaprotocol/markets';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import { Sparkline } from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
priceChangePercentage,
} from '@vegaprotocol/utils';
import { BigNumber } from 'bignumber.js';
export const Markets = ({
markets,
}: {
markets: MarketMaybeWithDataAndCandles[];
}) => {
return (
<div className="flex gap-2">
{markets.length ? (
markets.map((m) => {
return <MarketCard key={m.id} market={m} />;
})
) : (
<p className="text-xs">No markets</p>
)}
</div>
);
};
const MarketCard = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
const { oneDayCandles } = useCandles({ marketId: market.id });
const vol = oneDayCandles ? calcCandleVolume(oneDayCandles) : '0';
const volume =
vol && vol !== '0'
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
: '0.00';
const change = priceChangePercentage(
oneDayCandles?.map((c) => c.close) || []
);
return (
<div className="flex w-1/4 p-2 rounded gap-2 bg-vega-clight-700 dark:bg-vega-cdark-700">
<div className="flex-1">
<h3 className="overflow-hidden text-sm leading-4 whitespace-nowrap text-ellipsis">
{market.tradableInstrument.instrument.code}
</h3>
<p className="font-mono text-xs">{volume}</p>
</div>
<div className="flex justify-end w-1/3">
{oneDayCandles && (
<Sparkline
width={60}
height={13}
data={oneDayCandles.map((c) => Number(c.close))}
/>
)}
</div>
<div className="w-1/3 font-mono text-xs text-right">
{market.data?.markPrice && (
<>
<div>
{addDecimalsFormatNumber(
market.data.markPrice,
market.decimalPlaces
)}
</div>
<div>{formatNumberPercentage(new BigNumber(change), 2)}</div>
</>
)}
</div>
</div>
);
};
-11
View File
@@ -1,11 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { FeesContainer } from '../../components/fees-container';
export const Fees = () => {
return (
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
<FeesContainer />
</div>
);
};
-1
View File
@@ -1 +0,0 @@
export { Fees } from './fees';
@@ -1,48 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
SidebarDivider,
ViewType,
} from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LiquiditySidebar = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
);
};
@@ -43,7 +43,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const asset = getAsset(market);
return (
<>
<div className="flex gap-8">
<HeaderStat heading={t('Mark Price')} testId="market-price">
<MarketMarkPrice
marketId={market.id}
@@ -86,7 +86,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<MarketLiquiditySupplied
marketId={market.id}
assetDecimals={asset?.decimals || 0}
quantum={asset.quantum}
/>
{market.tradableInstrument.instrument.product.__typename === 'Future' && (
<HeaderStat
@@ -145,7 +144,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
</HeaderStat>
)}
<MarketProposalNotification marketId={market.id} />
</>
</div>
);
};
+19 -4
View File
@@ -9,9 +9,10 @@ import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links';
import { Links, Routes } from '../../lib/links';
import { ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { MarketState } from '@vegaprotocol/types';
const calculatePrice = (markPrice?: string, decimalPlaces?: number) => {
return markPrice && decimalPlaces
@@ -56,7 +57,7 @@ const TitleUpdater = ({
return null;
};
export const MarketPage = () => {
export const MarketPage = ({ closed }: { closed?: boolean }) => {
const { marketId } = useParams();
const navigate = useNavigate();
const currentRouteId = useGetCurrentRouteId();
@@ -69,11 +70,25 @@ export const MarketPage = () => {
const { data, loading } = useMarket(marketId);
useEffect(() => {
if (
data?.state &&
[
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(data.state) &&
currentRouteId !== Routes.CLOSED_MARKETS &&
marketId
) {
navigate(Links.CLOSED_MARKETS(marketId));
}
}, [data?.state, currentRouteId, navigate, marketId]);
useEffect(() => {
if (data?.id && data.id !== lastMarketId && !closed) {
update({ marketId: data.id });
}
}, [update, lastMarketId, data?.id]);
}, [update, lastMarketId, data?.id, closed]);
useEffect(() => {
if (largeScreen && view === undefined) {
@@ -82,7 +97,7 @@ export const MarketPage = () => {
currentRouteId
);
}
}, [setViews, view, currentRouteId, largeScreen]);
}, [setViews, view, currentRouteId, largeScreen, closed]);
const pinnedAsset = data && getAsset(data);
@@ -73,19 +73,10 @@ const MainGrid = memo(
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab id="funding-history" name={t('Funding history')}>
<Tab id="funding" name={t('Funding')}>
<TradingViews.funding.component marketId={marketId} />
</Tab>
) : null}
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab id="funding-payments" name={t('Funding payments')}>
<TradingViews.fundingPayments.component
marketId={marketId}
/>
</Tab>
) : null}
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
@@ -123,7 +114,7 @@ const MainGrid = memo(
<Tab
id="open-orders"
name={t('Open')}
menu={<TradingViews.activeOrders.menu />}
menu={<TradingViews.activeOrders.menu marketId={marketId} />}
>
<TradingViews.orders.component filter={Filter.Open} />
</Tab>
@@ -136,7 +127,7 @@ const MainGrid = memo(
<Tab
id="orders"
name={t('All')}
menu={<TradingViews.orders.menu />}
menu={<TradingViews.orders.menu marketId={marketId} />}
>
<TradingViews.orders.component />
</Tab>
@@ -148,6 +139,9 @@ const MainGrid = memo(
<Tab id="fills" name={t('Fills')}>
<TradingViews.fills.component />
</Tab>
<Tab id="funding-payments" name={t('Funding payments')}>
<TradingViews.fundingPayments.component />
</Tab>
<Tab
id="accounts"
name={t('Collateral')}
@@ -43,7 +43,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
return (
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu />
<Menu marketId={market?.id || ''} />
</div>
);
}
@@ -319,8 +319,7 @@ describe('Closed', () => {
);
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('successor marked should be visible', async () => {
it('successor marked should be visible', async () => {
const marketsWithSuccessorID = [
{
__typename: 'MarketEdge' as const,
@@ -364,12 +363,12 @@ describe('Closed', () => {
const container = within(
document.querySelector('.ag-center-cols-container') as HTMLElement
);
const cell = container.getAllByRole('gridcell', {
name: (_name, element) => element.getAttribute('col-id') === 'code',
})[0];
const cells = await container.findAllByRole('gridcell');
const cell = cells.find((el) => el.getAttribute('col-id') === 'code');
expect(
within(cell as HTMLElement).getByTestId('stack-cell-secondary')
).toHaveTextContent('PRNT');
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'PRNT'
);
});
});
+6 -9
View File
@@ -1,4 +1,3 @@
import type { CellClickedEvent } from 'ag-grid-community';
import compact from 'lodash/compact';
import { isAfter } from 'date-fns';
import type {
@@ -6,7 +5,6 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import type { Asset } from '@vegaprotocol/types';
@@ -19,11 +17,13 @@ import {
import { closedMarketsWithDataProvider, getAsset } from '@vegaprotocol/markets';
import type { DataSourceFilterFragment } from '@vegaprotocol/markets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { SettlementDateCell } from './settlement-date-cell';
import { SettlementPriceCell } from './settlement-price-cell';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { MarketCodeCell } from './market-code-cell';
import { MarketActionsDropdown } from './market-table-actions';
import type { CellClickedEvent } from 'ag-grid-community';
import { useClosedMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
type SettlementAsset = Pick<
Asset,
@@ -127,7 +127,7 @@ const ClosedMarketsDataGrid = ({
rowData: Row[];
error: Error | undefined;
}) => {
const handleOnSelect = useMarketClickHandler();
const handleOnSelect = useClosedMarketClickHandler();
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
const colDefs = useMemo(() => {
@@ -302,11 +302,8 @@ const ClosedMarketsDataGrid = ({
return;
}
handleOnSelect(
data.id,
// @ts-ignore metaKey exists
event ? event.metaKey : false
);
// @ts-ignore metaKey exists
handleOnSelect(data.id, event ? event.metaKey : false);
}}
/>
);
@@ -1,67 +0,0 @@
import { Route, Routes, useParams } from 'react-router-dom';
import { MarketState } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { useMarket } from '@vegaprotocol/markets';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
SidebarDivider,
ViewType,
} from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const MarketsSidebar = () => {
const { marketId } = useParams();
const currentRouteId = useGetCurrentRouteId();
const { data } = useMarket(marketId);
const active =
data &&
[MarketState.STATE_ACTIVE, MarketState.STATE_PENDING].includes(data.state);
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<Routes>
<Route
path=":marketId"
element={
<>
<SidebarDivider />
{active && (
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
)}
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
}
/>
</Routes>
</>
);
};
@@ -0,0 +1,39 @@
query AccountHistory(
$partyId: ID!
$assetId: ID!
$accountTypes: [AccountType!]
$dateRange: DateRange
$marketIds: [ID!]
) {
balanceChanges(
filter: {
partyIds: [$partyId]
accountTypes: $accountTypes
assetId: $assetId
marketIds: $marketIds
}
dateRange: $dateRange
) {
edges {
node {
timestamp
partyId
balance
marketId
assetId
accountType
}
}
}
}
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
balanceChanges(filter: { partyIds: [$partyId] }, dateRange: $dateRange) {
edges {
node {
assetId
accountType
}
}
}
}
@@ -0,0 +1,117 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type AccountHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
assetId: Types.Scalars['ID'];
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
dateRange?: Types.InputMaybe<Types.DateRange>;
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
export type AccountHistoryQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', timestamp: any, partyId?: string | null, balance: string, marketId?: string | null, assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
export type AccountsWithBalanceQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
dateRange?: Types.InputMaybe<Types.DateRange>;
}>;
export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
export const AccountHistoryDocument = gql`
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
balanceChanges(
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
dateRange: $dateRange
) {
edges {
node {
timestamp
partyId
balance
marketId
assetId
accountType
}
}
}
}
`;
/**
* __useAccountHistoryQuery__
*
* To run a query within a React component, call `useAccountHistoryQuery` and pass it any options that fit your needs.
* When your component renders, `useAccountHistoryQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAccountHistoryQuery({
* variables: {
* partyId: // value for 'partyId'
* assetId: // value for 'assetId'
* accountTypes: // value for 'accountTypes'
* dateRange: // value for 'dateRange'
* marketIds: // value for 'marketIds'
* },
* });
*/
export function useAccountHistoryQuery(baseOptions: Apollo.QueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
}
export function useAccountHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
}
export type AccountHistoryQueryHookResult = ReturnType<typeof useAccountHistoryQuery>;
export type AccountHistoryLazyQueryHookResult = ReturnType<typeof useAccountHistoryLazyQuery>;
export type AccountHistoryQueryResult = Apollo.QueryResult<AccountHistoryQuery, AccountHistoryQueryVariables>;
export const AccountsWithBalanceDocument = gql`
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
balanceChanges(filter: {partyIds: [$partyId]}, dateRange: $dateRange) {
edges {
node {
assetId
accountType
}
}
}
}
`;
/**
* __useAccountsWithBalanceQuery__
*
* To run a query within a React component, call `useAccountsWithBalanceQuery` and pass it any options that fit your needs.
* When your component renders, `useAccountsWithBalanceQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAccountsWithBalanceQuery({
* variables: {
* partyId: // value for 'partyId'
* dateRange: // value for 'dateRange'
* },
* });
*/
export function useAccountsWithBalanceQuery(baseOptions: Apollo.QueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
}
export function useAccountsWithBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
}
export type AccountsWithBalanceQueryHookResult = ReturnType<typeof useAccountsWithBalanceQuery>;
export type AccountsWithBalanceLazyQueryHookResult = ReturnType<typeof useAccountsWithBalanceLazyQuery>;
export type AccountsWithBalanceQueryResult = Apollo.QueryResult<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>;
@@ -0,0 +1,349 @@
import { addDecimal, fromNanoSeconds } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import uniqBy from 'lodash/uniqBy';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
AsyncRenderer,
Splash,
Toggle,
TradingButton,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
import 'pennant/dist/style.css';
import type { Account } from '@vegaprotocol/accounts';
import { accountsDataProvider } from '@vegaprotocol/accounts';
import {
useLocalStorageSnapshot,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { getAsset, type Market } from '@vegaprotocol/markets';
export const DateRange = {
RANGE_1D: '1D',
RANGE_7D: '7D',
RANGE_1M: '1M',
RANGE_3M: '3M',
RANGE_1Y: '1Y',
RANGE_YTD: 'YTD',
RANGE_ALL: 'All',
};
const dateRangeToggleItems = Object.entries(DateRange).map(([_, value]) => ({
label: t(value),
value: value,
}));
export const calculateStartDate = (range: string): string | undefined => {
const now = new Date();
switch (range) {
case DateRange.RANGE_1D:
return new Date(now.setDate(now.getDate() - 1)).toISOString();
case DateRange.RANGE_7D:
return new Date(now.setDate(now.getDate() - 7)).toISOString();
case DateRange.RANGE_1M:
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
case DateRange.RANGE_3M:
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
case DateRange.RANGE_1Y:
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
case DateRange.RANGE_YTD:
return new Date(now.setMonth(0)).toISOString();
default:
return undefined;
}
};
export const AccountHistoryContainer = () => {
const { pubKey } = useVegaWallet();
const { data: assets } = useAssetsDataProvider();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<AsyncRenderer loading={!assets} error={undefined} data={assets}>
{assets && <AccountHistoryManager pubKey={pubKey} assetData={assets} />}
</AsyncRenderer>
);
};
const AccountHistoryManager = ({
pubKey,
assetData,
}: {
pubKey: string;
assetData: AssetFieldsFragment[];
}) => {
const [accountType, setAccountType] = useState<Schema.AccountType>(
Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
const variablesForOneTimeQuery = useMemo(
() => ({
partyId: pubKey,
}),
[pubKey]
);
const { data: accounts } = useDataProvider({
dataProvider: accountsDataProvider,
variables: variablesForOneTimeQuery,
skip: !pubKey,
});
const assetIds = useMemo(
() => accounts?.map((e) => e?.asset?.id) || [],
[accounts]
);
const assets = useMemo(
() =>
assetData
.filter((a) => assetIds.includes(a.id))
.sort((a, b) => a.name.localeCompare(b.name)),
[assetData, assetIds]
);
const [assetId, setAssetId] = useLocalStorageSnapshot(
'account-history-active-asset-id'
);
const asset = useMemo(
() => assets.find((a) => a.id === assetId) || assets[0],
[assetId, assets]
);
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
DateRange.RANGE_1M
);
const [market, setMarket] = useState<Market | null>(null);
const marketFilterCb = useCallback(
(item: Market) => {
const itemAsset = getAsset(item);
return !asset?.id || itemAsset?.id === asset?.id;
},
[asset?.id]
);
const markets = useMemo<Market[] | null>(() => {
const arr =
accounts
?.filter((item: Account) => Boolean(item && item.market))
.map<Market>((item) => item.market as Market) ?? null;
return arr
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
a.tradableInstrument.instrument.code.localeCompare(
b.tradableInstrument.instrument.code
)
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
const itemAsset = getAsset(m);
const newAssetId = itemAsset?.id;
const newAsset = assets.find((item) => item.id === newAssetId);
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
setAssetId(newAsset.id);
}
},
[asset, assets, setAssetId]
);
const variables = useMemo(
() => ({
partyId: pubKey,
assetId: asset?.id || '',
accountTypes: accountType ? [accountType] : undefined,
dateRange:
range === 'All' ? undefined : { start: calculateStartDate(range) },
marketIds: market?.id ? [market.id] : undefined,
}),
[pubKey, asset, accountType, range, market?.id]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
return (
<div className="flex flex-col h-full gap-2">
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
<div className="flex items-center gap-1 shrink-0">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<TradingDropdownItem
key={type}
onClick={() => {
setAccountType(type as Schema.AccountType);
// if not a margin account clear any market selection
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
setMarket(null);
}
}}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{asset ? asset.symbol : t('Select asset')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{assets.map((a) => (
<TradingDropdownItem
key={a.id}
onClick={() => {
setAssetId(a.id);
// if the selected asset is different to the selected market clear the market
if (market && a.id !== getAsset(market).id) {
setMarket(null);
}
}}
>
{a.symbol}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{market && (
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</TradingDropdownItem>
)}
{markets?.map((m) => (
<TradingDropdownItem
key={m.id}
onClick={() => resolveMarket(m)}
>
{m.tradableInstrument.instrument.code}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</div>
<div className="justify-items-end">
<Toggle
id="account-history-date-range"
name="account-history-date-range"
toggles={dateRangeToggleItems}
checkedValue={range}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRange(e.target.value as keyof typeof DateRange)
}
size="sm"
/>
</div>
</div>
<div className="flex-1">
{asset && (
<div className="h-full">
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
</div>
)}
</div>
</div>
);
};
export const AccountHistoryChart = ({
data,
accountType,
asset,
}: {
data: AccountHistoryQuery | undefined;
accountType: Schema.AccountType;
asset: AssetFieldsFragment;
}) => {
const { theme } = useThemeSwitcher();
const values: { cols: [string, string]; rows: [Date, number][] } | null =
useMemo(() => {
if (!data?.balanceChanges.edges.length) {
return null;
}
const valuesData = compact(data.balanceChanges.edges)
.reduce((acc, edge) => {
if (edge.node.accountType === accountType) {
acc?.push({
datetime: fromNanoSeconds(edge.node.timestamp),
balance: Number(addDecimal(edge.node.balance, asset.decimals)),
});
}
return acc;
}, [] as { datetime: Date; balance: number }[])
.reverse();
return {
cols: ['Date', `${asset.symbol} account balance`],
rows: compact(valuesData).map((d) => [d.datetime, d.balance]),
};
}, [accountType, asset.decimals, asset.symbol, data?.balanceChanges.edges]);
if (!data || !values?.rows.length) {
return <Splash> {t('No account history data')}</Splash>;
}
return <PriceChart data={values} theme={theme} />;
};
@@ -1,31 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const PortfolioSidebar = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</>
);
};
@@ -15,6 +15,7 @@ import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container';
import { OrdersContainer } from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container';
import { AccountHistoryContainer } from './account-history-container';
import {
ResizableGrid,
ResizableGridPanel,
@@ -32,7 +33,7 @@ const WithdrawalsIndicator = () => {
return null;
}
return (
<span className="p-1 leading-none rounded bg-vega-clight-500 dark:bg-vega-cdark-500 text-default">
<span className="bg-vega-clight-500 dark:bg-vega-cdark-500 text-default rounded p-1 leading-none">
{ready.length}
</span>
);
@@ -65,7 +66,10 @@ export const Portfolio = () => {
<ResizableGrid vertical onChange={handleOnLayoutChange}>
<ResizableGridPanel minSize={75}>
<PortfolioGridChild>
<Tabs storageKey="console-portfolio-top-1">
<Tabs storageKey="console-portfolio-top">
<Tab id="account-history" name={t('Account history')}>
<AccountHistoryContainer />
</Tab>
<Tab
id="positions"
name={t('Positions')}
@@ -128,7 +132,7 @@ interface PortfolioGridChildProps {
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
return (
<section className="h-full p-1">
<div className="h-full border rounded-sm border-default">{children}</div>
<div className="border border-default h-full rounded-sm">{children}</div>
</section>
);
};
@@ -1,44 +1,21 @@
import {
Input,
InputError,
Loader,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { FieldValues } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { Navigate, useSearchParams } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button } from './buttons';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { t } from '@vegaprotocol/i18n';
import { Statistics } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program';
const RELOAD_DELAY = 3000;
const validateCode = (value: string) => {
const number = +`0x${value}`;
if (!value || value.length !== 64) {
return t('Code must be 64 characters in length');
} else if (Number.isNaN(number)) {
return t('Code must be be valid hex');
}
return true;
};
export const ApplyCodeForm = () => {
const program = useReferralProgram();
const navigate = useNavigate();
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const [status, setStatus] = useState<
'requested' | 'failed' | 'successful' | null
>(null);
@@ -50,17 +27,11 @@ export const ApplyCodeForm = () => {
formState: { errors },
setValue,
setError,
watch,
} = useForm();
const [params] = useSearchParams();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField) ? codeField : undefined,
});
const { data: referee } = useReferral(pubKey, 'referee');
const { data: referrer } = useReferral(pubKey, 'referrer');
useEffect(() => {
const code = params.get('code');
@@ -83,7 +54,7 @@ export const ApplyCodeForm = () => {
if (!res) {
setError('code', {
type: 'required',
message: t('The transaction could not be sent'),
message: 'The transaction could not be sent',
});
}
if (res) {
@@ -94,13 +65,9 @@ export const ApplyCodeForm = () => {
if (err.message.includes('user rejected')) {
setStatus(null);
} else {
setStatus(null);
setError('code', {
type: 'required',
message:
err instanceof Error
? err.message
: t('Your code has been rejected'),
message: 'Your code has been rejected',
});
}
});
@@ -132,21 +99,10 @@ export const ApplyCodeForm = () => {
}),
});
// go to main page when successfully applied
useEffect(() => {
if (status === 'successful') {
setTimeout(() => {
navigate(Routes.REFERRALS);
}, RELOAD_DELAY);
}
}, [navigate, status]);
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
// show "code applied" message when successfully applied
if (status === 'successful') {
return (
<div className="w-1/2 mx-auto">
@@ -154,94 +110,61 @@ export const ApplyCodeForm = () => {
<span className="text-vega-green-500">
<VegaIcon name={VegaIconNames.TICK} size={20} />
</span>{' '}
<span className="pt-1">{t('Code applied')}</span>
<span className="pt-1">Code applied</span>
</h3>
</div>
);
}
const getButtonProps = () => {
if (!pubKey) {
return {
disabled: false,
children: t('Connect wallet'),
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
onClick: ((event) => {
event.preventDefault();
openWalletDialog();
}) as MouseEventHandler,
};
}
if (isReadOnly) {
if (isReadOnly || !pubKey) {
return {
disabled: true,
children: t('Apply a code'),
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
children: 'Apply',
};
}
if (status === 'requested') {
return {
disabled: true,
children: t('Confirm in wallet...'),
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
children: 'Confirm in wallet...',
};
}
return {
disabled: false,
children: t('Apply a code'),
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
children: 'Apply',
};
};
return (
<>
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<h3 className="mb-4 text-2xl text-center calt">
{t('Apply a referral code')}
</h3>
<p className="mb-4 text-center text-base">
{t('Enter a referral code to get trading discounts.')}
</p>
<form
className={classNames('w-full flex flex-col gap-4', {
'animate-shake': Boolean(errors.code),
})}
onSubmit={handleSubmit(onSubmit)}
>
<label>
<span className="sr-only">{t('Your referral code')}</span>
<Input
hasError={Boolean(errors.code)}
{...register('code', {
required: t('You have to provide a code to apply it.'),
validate: validateCode,
})}
placeholder="Enter a code"
className="mb-2 bg-vega-clight-900 dark:bg-vega-cdark-700"
/>
</label>
<RainbowButton variant="border" {...getButtonProps()} />
</form>
{errors.code && (
<InputError className="break-words overflow-auto">
{errors.code.message?.toString()}
</InputError>
)}
</div>
{previewLoading && !previewData ? (
<div className="mt-10">
<Loader />
</div>
) : null}
{previewData ? (
<div className="mt-10">
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
<Statistics data={previewData} program={program} as="referee" />
</div>
) : null}
</>
<div className="w-1/2 mx-auto">
<h3 className="mb-5 text-xl text-center uppercase calt">
Apply a referral code
</h3>
<p className="mb-6 text-center">Enter a referral code</p>
<form
className={classNames('w-full flex flex-col gap-3', {
'animate-shake': Boolean(errors.code),
})}
onSubmit={handleSubmit(onSubmit)}
>
<label className="flex-grow">
<span className="block mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
Your referral code
</span>
<Input
hasError={Boolean(errors.code)}
{...register('code', {
required: 'You have to provide a code to apply it.',
})}
/>
</label>
<Button className="w-full" type="submit" {...getButtonProps()} />
</form>
{errors.code && (
<InputError>{errors.code.message?.toString()}</InputError>
)}
</div>
);
};
@@ -16,23 +16,20 @@ export const RainbowButton = ({
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
'bg-rainbow hover:bg-none hover:bg-rainbow enabled:hover:bg-vega-pink-500 rounded-lg overflow-hidden disabled:opacity-40',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
}
},
className
)}
{...props}
>
<div
className={classNames(
{
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
},
className
)}
className={classNames({
'bg-white dark:bg-vega-cdark-900 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
})}
>
{children}
</div>
@@ -58,20 +55,6 @@ const DISABLED_RAINBOW_TAB_STYLE = classNames(
'[&.active]:text-white'
);
const TAB_STYLE = classNames(
'inline-block',
'bg-transparent',
'text-vega-clight-200 dark:text-vega-cdark-200',
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100',
'data-[state="active"]:text-black dark:data-[state="active"]:text-white',
'data-[state="active"]:border-b-2 data-[state="active"]:border-b-black dark:data-[state="active"]:border-b-white',
'[&.active]:text-black dark:[&.active]:text-white',
'[&.active]:border-b-2 [&.active]:border-b-black dark:[&.active]:border-b-white',
'mx-4 px-0 py-3',
'uppercase'
);
const DISABLED_TAB_STYLE = classNames('pointer-events-none');
export const RainbowTabButton = forwardRef<
HTMLButtonElement,
{ disabled?: boolean } & ButtonHTMLAttributes<HTMLButtonElement>
@@ -110,26 +93,6 @@ export const RainbowTabLink = ({
</NavLink>
);
export const TabLink = ({
to,
children,
className,
disabled = false,
...props
}: { disabled?: boolean } & ComponentProps<typeof NavLink>) => (
<NavLink
to={to}
className={classNames(
TAB_STYLE,
disabled && DISABLED_TAB_STYLE,
typeof className === 'string' ? className : undefined
)}
{...props}
>
{children}
</NavLink>
);
export const Button = forwardRef<
HTMLButtonElement,
ComponentProps<typeof TradingButton>
@@ -4,8 +4,3 @@ export const GRADIENT =
export const SKY_BACKGROUND =
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
// TODO: Update the links to use the correct referral related pages
export const REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
export const ABOUT_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
@@ -19,58 +19,70 @@ import {
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import {
ABOUT_REFERRAL_DOCS_LINK,
DISCLAIMER_REFERRAL_DOCS_LINK,
} from './constants';
import { useReferral } from './hooks/use-referral';
import { t } from '@vegaprotocol/i18n';
export const CreateCodeContainer = () => {
return <CreateCodeForm />;
const { stakeAvailable, requiredStake } = useStakeAvailable();
if (stakeAvailable == null || requiredStake == null) {
return null;
}
return (
<CreateCodeForm
currentStakeAvailable={stakeAvailable}
requiredStake={requiredStake}
/>
);
};
export const CreateCodeForm = () => {
export const CreateCodeForm = ({
currentStakeAvailable,
requiredStake,
}: {
currentStakeAvailable: bigint;
requiredStake: bigint;
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { pubKey, isReadOnly } = useVegaWallet();
const { pubKey } = useVegaWallet();
return (
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')}
<div className="w-1/2 mx-auto">
<h3 className="mb-5 text-xl text-center uppercase calt">
Create a referral code
</h3>
<p className="mb-4 text-center text-base">
{t(
'Generate a referral code to share with your friends and start earning commission.'
)}
<p className="mb-6 text-center">
Generate a referral code to share with your friends and start earning
commission.
</p>
<div className="w-full flex flex-col">
<RainbowButton
variant="border"
disabled={isReadOnly}
onClick={() => {
if (pubKey) {
setDialogOpen(true);
} else {
openWalletDialog();
}
}}
>
{pubKey ? t('Create a referral code') : t('Connect wallet')}
</RainbowButton>
<div className="mb-5">
<div className="text-center">
<RainbowButton
variant="border"
onClick={() => {
if (pubKey) {
setDialogOpen(true);
} else {
openWalletDialog();
}
}}
>
{pubKey ? 'Create a referral code' : 'Connect wallet'}
</RainbowButton>
</div>
</div>
<Dialog
title={t('Create a referral code')}
title="Create a referral code"
open={dialogOpen}
onChange={() => setDialogOpen(false)}
size="small"
>
<CreateCodeDialog setDialogOpen={setDialogOpen} />
<CreateCodeDialog
currentStakeAvailable={currentStakeAvailable}
setDialogOpen={setDialogOpen}
requiredStake={requiredStake}
/>
</Dialog>
</div>
);
@@ -78,21 +90,21 @@ export const CreateCodeForm = () => {
const CreateCodeDialog = ({
setDialogOpen,
currentStakeAvailable,
requiredStake,
}: {
setDialogOpen: (open: boolean) => void;
currentStakeAvailable: bigint;
requiredStake: bigint;
}) => {
const createLink = useLinks(DApp.Governance);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' });
const [err, setErr] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
@@ -128,60 +140,45 @@ const CreateCodeDialog = ({
const getButtonProps = () => {
if (status === 'idle' || status === 'error') {
return {
children: t('Generate code'),
children: 'Generate code',
onClick: () => onSubmit(),
};
}
if (status === 'loading') {
return {
children: t('Confirm in wallet...'),
children: 'Confirm in wallet...',
disabled: true,
};
}
if (status === 'success') {
return {
children: t('Close'),
children: 'Close',
intent: Intent.Success,
onClick: () => {
refetch();
setDialogOpen(false);
},
onClick: () => setDialogOpen(false),
};
}
};
if (!pubKey || currentStakeAvailable == null || requiredStake == null) {
return (
<div className="flex flex-col gap-4">
<p>{t('You must be connected to the Vega wallet.')}</p>
<TradingButton
intent={Intent.Primary}
onClick={() => setDialogOpen(false)}
>
{t('Close')}
</TradingButton>
</div>
);
}
if (currentStakeAvailable < requiredStake) {
// TODO: Add when network parameters are updated
if (
currentStakeAvailable === BigInt(0) ||
currentStakeAvailable < requiredStake
) {
return (
<div className="flex flex-col gap-4">
<p>
{t('You need at least')}{' '}
{addDecimalsFormatNumber(requiredStake.toString(), 18)}{' '}
{t(
'VEGA staked to generate a referral code and participate in the referral program.'
)}
You need at least{' '}
{addDecimalsFormatNumber(requiredStake.toString(), 18)} VEGA staked to
generate a referral code and participate in the referral program.
</p>
<TradingAnchorButton
href={createLink(TokenStaticLinks.ASSOCIATE)}
intent={Intent.Primary}
target="_blank"
>
{t('Stake some $VEGA now')}
Stake some $VEGA now
</TradingAnchorButton>
</div>
);
@@ -191,9 +188,8 @@ const CreateCodeDialog = ({
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
<p>
{t(
'Generate a referral code to share with your friends and start earning commission.'
)}
Generate a referral code to share with your friends and start earning
commission.
</p>
)}
{status === 'success' && code && (
@@ -208,7 +204,7 @@ const CreateCodeDialog = ({
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>{t('Copy')}</span>
<span>Copy</span>
</TradingButton>
</CopyWithTooltip>
</div>
@@ -219,13 +215,10 @@ const CreateCodeDialog = ({
{...getButtonProps()}
/>
{err && <InputError>{err}</InputError>}
{/* TODO: Add links */}
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
{t('Disclaimer')}
</ExternalLink>
<ExternalLink>About the referral program</ExternalLink>
<ExternalLink>Disclaimer</ExternalLink>
</div>
</div>
);
@@ -3,7 +3,6 @@ import { RainbowButton } from './buttons';
import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';
import { t } from '@vegaprotocol/i18n';
export const ErrorBoundary = () => {
const error = useRouteError();
@@ -40,7 +39,7 @@ export const ErrorBoundary = () => {
variant="border"
className="text-xs"
>
{t('Go back and try again')}
Go back and try again
</RainbowButton>
</p>
</LayoutWithSky>
@@ -61,7 +60,7 @@ export const NotFound = () => {
<h1 className="text-6xl font-alpha calt mb-10">{'Not found'}</h1>
<p className="text-lg mb-10">
{t("The page you're looking for doesn't exists.")}
{"The page you're looking for doesn't exists."}
</p>
<p className="text-lg mb-10">
@@ -70,7 +69,7 @@ export const NotFound = () => {
variant="border"
className="text-xs"
>
{t('Go back and try again')}
Go back and try again
</RainbowButton>
</p>
</div>
@@ -1,19 +0,0 @@
query ReferralProgram {
currentReferralProgram {
id
version
endOfProgramTimestamp
windowLength
endedAt
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
@@ -1,9 +0,0 @@
query CurrentEpochInfo {
epoch {
id
timestamps {
start
end
}
}
}

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