Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
091b882139 | ||
|
|
61105ccfdb | ||
|
|
6421cf87c6 | ||
|
|
0ebfab64ff | ||
|
|
142f08343b | ||
|
|
61aa45a9ed | ||
|
|
4c95db5fb3 | ||
|
|
3072b7824f | ||
|
|
0580e90171 | ||
|
|
c440abc77d | ||
|
|
fbafc726a4 | ||
|
|
dd1890d8c6 | ||
|
|
c8e624eaba | ||
|
|
cc6629ad27 | ||
|
|
9838efa00e | ||
|
|
dac7142a98 | ||
|
|
1397aafd25 | ||
|
|
b192603e57 | ||
|
|
8c2c8d987c | ||
|
|
52c96794f7 | ||
|
|
58ce016d4d | ||
|
|
ac16eb06f5 | ||
|
|
3319871617 | ||
|
|
37e850304a | ||
|
|
655131c744 | ||
|
|
f1cb1b9408 | ||
|
|
891ac527a2 | ||
|
|
546087fb9f | ||
|
|
f7a82d33ba | ||
|
|
d85f413e41 | ||
|
|
c7d0025b4f | ||
|
|
6c9272cd53 | ||
|
|
82fb29d541 | ||
|
|
43cd170c77 | ||
|
|
1d39f81dfc | ||
|
|
f5868b6f1b | ||
|
|
20e1522088 | ||
|
|
ae02b153f8 | ||
|
|
fee5f862a7 | ||
|
|
c25362b084 | ||
|
|
7c105d2c6b | ||
|
|
2dfcb02d1e | ||
|
|
a98ab775c3 | ||
|
|
097bf7c08c | ||
|
|
236e35a92b | ||
|
|
abf84b9d45 | ||
|
|
6d2a2ea0a0 | ||
|
|
6ac79e9f6d | ||
|
|
835ee64243 | ||
|
|
e7c3b5054c | ||
|
|
9dc9588a14 | ||
|
|
a0844d41bf | ||
|
|
3d1aa74128 | ||
|
|
2376bfe58f | ||
|
|
72ccf96523 | ||
|
|
f416fa4a25 | ||
|
|
a24e3646cc | ||
|
|
55b092b216 | ||
|
|
ef4cf6a87e | ||
|
|
c9860a3412 | ||
|
|
127bd3faf3 | ||
|
|
1791b3ca6a | ||
|
|
6044b86ff4 | ||
|
|
341d78e761 |
@@ -1,3 +1,2 @@
|
||||
* @vegaprotocol/frontend
|
||||
* @vegaprotocol/frontend-qa
|
||||
*.graphql @vegaprotocol/core
|
||||
|
||||
@@ -39,7 +39,6 @@ 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');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
AppFailure,
|
||||
NetworkLoader,
|
||||
NodeFailure,
|
||||
NodeGuard,
|
||||
NodeSwitcherDialog,
|
||||
useEnvironment,
|
||||
@@ -31,7 +31,7 @@ function App() {
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<NodeGuard
|
||||
skeleton={<div>{t('Loading')}</div>}
|
||||
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
>
|
||||
<Suspense fallback={splashLoading}>
|
||||
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'];
|
||||
|
||||
@@ -63,10 +64,16 @@ export const TxDetailsTransfer = ({
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TableRow modifier="bordered">
|
||||
<TableRow modifier="bordered" data-testid="type">
|
||||
<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}
|
||||
@@ -74,7 +81,7 @@ export const TxDetailsTransfer = ({
|
||||
hideTypeRow={true}
|
||||
/>
|
||||
{from ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableRow modifier="bordered" data-testid="from">
|
||||
<TableCell>{t('From')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={from} />
|
||||
@@ -82,7 +89,7 @@ export const TxDetailsTransfer = ({
|
||||
</TableRow>
|
||||
) : null}
|
||||
{transfer.to ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableRow modifier="bordered" data-testid="to">
|
||||
<TableCell>{t('To')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={transfer.to} />
|
||||
@@ -90,7 +97,7 @@ export const TxDetailsTransfer = ({
|
||||
</TableRow>
|
||||
) : null}
|
||||
{transfer.asset && transfer.amount ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableRow modifier="bordered" data-testid="amount">
|
||||
<TableCell>{t('Amount')}</TableCell>
|
||||
<TableCell>
|
||||
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { getTypeLabelForTransfer } from './details/tx-transfer';
|
||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../routes/blocks/tendermint-blocks-response';
|
||||
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', () => {
|
||||
@@ -56,3 +65,70 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ export const ErrorBoundary = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
export const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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,5 +1,6 @@
|
||||
export const Routes = {
|
||||
HOME: '/',
|
||||
RESTRICTED: '/restricted',
|
||||
TX: 'txs',
|
||||
BLOCKS: 'blocks',
|
||||
PARTIES: 'parties',
|
||||
|
||||
@@ -30,6 +30,7 @@ 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;
|
||||
@@ -356,6 +357,14 @@ export const routerConfig: Route[] = [
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.RESTRICTED,
|
||||
element: <RestrictedPage />,
|
||||
handle: {
|
||||
name: t('Restricted'),
|
||||
text: t('Restricted'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createBrowserRouter(routerConfig);
|
||||
|
||||
@@ -537,10 +537,10 @@ describe(
|
||||
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
|
||||
cy.reload();
|
||||
getProposalFromTitle('Governance transfer proposal').within(() => {
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(governanceTransferToggle).click();
|
||||
cy.getByTestId('proposal-transfer-details-table').within(() => {
|
||||
getProposalInformationFromTable('Source Type')
|
||||
@@ -590,7 +590,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
|
||||
getProposalInformationFromTable('Error details')
|
||||
.invoke('text')
|
||||
.and('eq', 'Governance transfer invalid transfer id not found');
|
||||
|
||||
@@ -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,6 +106,7 @@ const Web3Container = ({
|
||||
useEthWithdrawApprovalsManager();
|
||||
return null;
|
||||
};
|
||||
|
||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||
store.connectors,
|
||||
store.initialize,
|
||||
@@ -184,7 +185,7 @@ const Web3Container = ({
|
||||
<TemplateSidebar sidebar={sideBar}>
|
||||
<AppRouter />
|
||||
</TemplateSidebar>
|
||||
<footer className="p-4 border-t border-neutral-700 break-all">
|
||||
<footer className="p-4 break-all border-t border-neutral-700">
|
||||
<NetworkInfo />
|
||||
</footer>
|
||||
</AppLayout>
|
||||
@@ -239,6 +240,9 @@ 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({
|
||||
@@ -304,6 +308,14 @@ const AppContainer = () => {
|
||||
}
|
||||
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
|
||||
|
||||
if (isRestricted) {
|
||||
return (
|
||||
<Router>
|
||||
<AppRouter />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
@@ -312,7 +324,7 @@ const AppContainer = () => {
|
||||
<NodeGuard
|
||||
skeleton={<div>{t('Loading')}</div>}
|
||||
failure={
|
||||
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
|
||||
<NodeFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
|
||||
}
|
||||
>
|
||||
<AsyncRenderer<EthereumConfig | null>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
@@ -718,6 +719,8 @@
|
||||
"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",
|
||||
@@ -738,6 +741,8 @@
|
||||
"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)",
|
||||
@@ -904,6 +909,14 @@
|
||||
"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",
|
||||
@@ -914,5 +927,31 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
+80
-3
@@ -12,18 +12,19 @@ import {
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalHeader } from './proposal-header';
|
||||
import { ProposalHeader, NewTransferSummary } 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'),
|
||||
@@ -31,6 +32,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
|
||||
code: 'PARENT_CODE',
|
||||
parentMarketId: 'PARENT_ID',
|
||||
}),
|
||||
useNewTransferProposalDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
@@ -418,3 +420,78 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
+23
-5
@@ -237,7 +237,11 @@ export const ProposalHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
export const SuccessorCode = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const successor = useSuccessorMarketProposalDetails(proposalId);
|
||||
|
||||
@@ -254,7 +258,11 @@ const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
export const NewTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const details = useNewTransferProposalDetails(proposalId);
|
||||
|
||||
@@ -263,13 +271,23 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
return (
|
||||
<span>
|
||||
{GovernanceTransferKindMapping[details.kind.__typename]}{' '}
|
||||
{t('transfer from')} <Lozenge>{truncateMiddle(details.source)}</Lozenge>{' '}
|
||||
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
|
||||
{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>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelTransferSummary = ({
|
||||
export const CancelTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
|
||||
+37
-42
@@ -63,44 +63,42 @@ const mockReferralProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
changes: {
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
referralDiscountFactor: '0.001',
|
||||
referralRewardFactor: '0.001',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 24,
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
referralDiscountFactor: '0.005',
|
||||
referralRewardFactor: '0.005',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 48,
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '2026-10-03T10:34:34Z',
|
||||
windowLength: 3,
|
||||
stakingTiers: [
|
||||
{
|
||||
minimumStakedTokens: '1',
|
||||
referralRewardMultiplier: '1',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '2',
|
||||
referralRewardMultiplier: '2',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '5',
|
||||
referralRewardMultiplier: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -130,10 +128,7 @@ describe('<ProposalReferralProgramDetails />', () => {
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
changes: {},
|
||||
},
|
||||
change: {},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+4
-5
@@ -54,11 +54,10 @@ export const ProposalReferralProgramDetails = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.changes?.benefitTiers;
|
||||
const stakingTiers = proposal?.terms?.change?.changes?.stakingTiers;
|
||||
const windowLength = proposal?.terms?.change?.changes?.windowLength;
|
||||
const endOfProgramTimestamp =
|
||||
proposal?.terms?.change?.changes?.endOfProgramTimestamp;
|
||||
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 &&
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './proposal-update-benefit-tiers-details';
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
@@ -243,6 +244,14 @@ export const 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">
|
||||
|
||||
@@ -30,6 +30,8 @@ export const useProposalNetworkParams = ({
|
||||
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 = {
|
||||
@@ -111,6 +113,14 @@ export const useProposalNetworkParams = ({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -47,19 +47,17 @@ fragment UpdateReferralProgram on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateReferralProgram {
|
||||
changes {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -47,19 +47,17 @@ fragment UpdateReferralPrograms on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateReferralProgram {
|
||||
changes {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-15
@@ -7,7 +7,7 @@ export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: {
|
||||
|
||||
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
export type 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 }> } } };
|
||||
|
||||
@@ -21,7 +21,7 @@ export type ProposalsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
|
||||
export 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 const NewMarketProductFieldsFragmentDoc = gql`
|
||||
fragment NewMarketProductFields on Proposal {
|
||||
@@ -75,19 +75,17 @@ export const UpdateReferralProgramsFragmentDoc = gql`
|
||||
terms {
|
||||
change {
|
||||
... on UpdateReferralProgram {
|
||||
changes {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,12 @@ export const generateYesVotes = (
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
vestingBalancesSummary: {
|
||||
__typename: 'PartyVestingBalancesSummary',
|
||||
epoch: null,
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
@@ -192,6 +198,12 @@ export const generateNoVotes = (
|
||||
})
|
||||
.toString(),
|
||||
},
|
||||
vestingBalancesSummary: {
|
||||
__typename: 'PartyVestingBalancesSummary',
|
||||
epoch: null,
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
},
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
@@ -4,6 +4,7 @@ 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,8 +362,13 @@ const routerConfig = [
|
||||
element: <LazyDisclaimer name="Disclaimer" />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
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
|
||||
element: <NotFound name="NotFound" />,
|
||||
},
|
||||
...redirects,
|
||||
|
||||
@@ -7,6 +7,7 @@ const Routes = {
|
||||
PROPOSALS_REJECTED: '/proposals/rejected',
|
||||
PROTOCOL_UPGRADES: '/protocol-upgrades',
|
||||
NOT_PERMITTED: '/not-permitted',
|
||||
RESTRICTED: '/restricted',
|
||||
NOT_FOUND: '/not-found',
|
||||
CONTRACTS: '/contracts',
|
||||
TOKEN: '/token',
|
||||
|
||||
@@ -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,6 +70,8 @@ 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`
|
||||
@@ -120,7 +122,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
|
||||
|
||||
cy.getByTestId(collateralTab).click();
|
||||
cy.getByTestId('open-transfer').click();
|
||||
cy.getByTestId('open-transfer').eq(1).click();
|
||||
cy.getByTestId('transfer-form').should('be.visible');
|
||||
cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1);
|
||||
cy.get('select option')
|
||||
@@ -147,7 +149,8 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
// 0003-WTXN-011
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
cy.getByTestId('withdraw-dialog-button').click();
|
||||
selectAsset(0);
|
||||
selectAsset(btcName);
|
||||
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
|
||||
cy.get(amountField).focus();
|
||||
cy.get(amountField).clear().type('1');
|
||||
cy.getByTestId('submit-withdrawal').click();
|
||||
@@ -180,24 +183,21 @@ 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()
|
||||
.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);
|
||||
});
|
||||
.invoke('text')
|
||||
.should('match', regex);
|
||||
cy.getByTestId('node-health')
|
||||
.children()
|
||||
.eq(1)
|
||||
@@ -239,7 +239,6 @@ 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()
|
||||
@@ -280,8 +279,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
|
||||
cy.visit(`/#/markets/${market.id}`);
|
||||
cy.getByTestId(toastCloseBtn, txTimeout).click();
|
||||
cy.getByTestId(openOrdersTab).click();
|
||||
cy.getByTestId('edit', txTimeout).should('be.visible');
|
||||
cy.getByTestId('edit').first().should('be.visible').click();
|
||||
cy.getByTestId('edit').first().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();
|
||||
@@ -350,6 +348,7 @@ 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(
|
||||
@@ -437,6 +436,7 @@ 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');
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
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="party.id"]').eq(1).should('not.be.empty');
|
||||
cy.get('[col-id="partyId"]').eq(1).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ const marketSummaryBlock = 'header-summary';
|
||||
const itemValue = 'item-value';
|
||||
const itemHeader = 'item-header';
|
||||
const colCommitmentAmount = '[col-id="commitmentAmount"]';
|
||||
const colAverageEntryValuation = '[col-id="averageEntryValuation"]';
|
||||
const colEquityLikeShare = '[col-id="equityLikeShare"]';
|
||||
const colEquityLikeShare = '[col-id="feeShare.equityLikeShare"]';
|
||||
const colFee = '[col-id="fee"]';
|
||||
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
|
||||
const colBalance = '[col-id="balance"]';
|
||||
@@ -24,11 +23,17 @@ const colUpdatedAt = '[col-id="updatedAt"] button';
|
||||
const headers = [
|
||||
'Party',
|
||||
'Commitment (tDAI)',
|
||||
'Share',
|
||||
'Proposed fee',
|
||||
'Market valuation at entry',
|
||||
'Obligation',
|
||||
'Supplied',
|
||||
'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',
|
||||
'Status',
|
||||
'Created',
|
||||
'Updated',
|
||||
@@ -64,11 +69,8 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
|
||||
// 5002-LIQP-002
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
.find('[col-id="partyId"]')
|
||||
.should('have.text', '69464e…dc6f');
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
@@ -82,12 +84,6 @@ 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)
|
||||
@@ -104,7 +100,8 @@ 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.skip('liquidity status column should be sorted properly', () => {
|
||||
|
||||
it('liquidity status column should be sorted properly', () => {
|
||||
// 5002-LIQP-003
|
||||
const liquidityColDefault = ['Active', 'Pending'];
|
||||
const liquidityColAsc = ['Active', 'Pending'];
|
||||
@@ -220,11 +217,8 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
// 5002-LIQP-011
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
.find('[col-id="partyId"]')
|
||||
.should('have.text', '69464e…dc6f');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
@@ -241,12 +235,6 @@ 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)
|
||||
@@ -277,11 +265,8 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Inactive').click();
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
.find('[col-id="partyId"]')
|
||||
.should('have.text', 'cc464e…dc6f');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
@@ -298,11 +283,6 @@ 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)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,529 +0,0 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,119 +0,0 @@
|
||||
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');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,91 +0,0 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,131 +0,0 @@
|
||||
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');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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,11 +162,7 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'Trades', tradesQuery());
|
||||
aliasGQLQuery(req, 'Chart', chartQuery());
|
||||
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'LiquidityProviderFeeShare',
|
||||
liquidityProviderFeeShareQuery
|
||||
);
|
||||
aliasGQLQuery(req, 'LiquidityProviders', liquidityProvidersQuery());
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
|
||||
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
|
||||
|
||||
+1
-5
@@ -12,12 +12,11 @@ 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
|
||||
@@ -26,6 +25,3 @@ 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,5 +0,0 @@
|
||||
import MarketPage from '../market';
|
||||
|
||||
export const ClosedMarketPage = () => {
|
||||
return <MarketPage closed />;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ClosedMarketPage as default } from './closed-market';
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Fees } from './fees';
|
||||
@@ -0,0 +1,48 @@
|
||||
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,6 +86,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
quantum={asset.quantum}
|
||||
/>
|
||||
{market.tradableInstrument.instrument.product.__typename === 'Future' && (
|
||||
<HeaderStat
|
||||
@@ -144,7 +145,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
</HeaderStat>
|
||||
)}
|
||||
<MarketProposalNotification marketId={market.id} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ import { useGlobalStore, usePageTitleStore } from '../../stores';
|
||||
import { TradeGrid } from './trade-grid';
|
||||
import { TradePanels } from './trade-panels';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../lib/links';
|
||||
import { Links } 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
|
||||
@@ -57,7 +56,7 @@ const TitleUpdater = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
export const MarketPage = ({ closed }: { closed?: boolean }) => {
|
||||
export const MarketPage = () => {
|
||||
const { marketId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
@@ -70,25 +69,11 @@ export const MarketPage = ({ closed }: { closed?: boolean }) => {
|
||||
|
||||
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, closed]);
|
||||
}, [update, lastMarketId, data?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (largeScreen && view === undefined) {
|
||||
@@ -97,7 +82,7 @@ export const MarketPage = ({ closed }: { closed?: boolean }) => {
|
||||
currentRouteId
|
||||
);
|
||||
}
|
||||
}, [setViews, view, currentRouteId, largeScreen, closed]);
|
||||
}, [setViews, view, currentRouteId, largeScreen]);
|
||||
|
||||
const pinnedAsset = data && getAsset(data);
|
||||
|
||||
|
||||
@@ -73,10 +73,19 @@ const MainGrid = memo(
|
||||
{market &&
|
||||
market.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding" name={t('Funding')}>
|
||||
<Tab id="funding-history" name={t('Funding history')}>
|
||||
<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>
|
||||
@@ -114,7 +123,7 @@ const MainGrid = memo(
|
||||
<Tab
|
||||
id="open-orders"
|
||||
name={t('Open')}
|
||||
menu={<TradingViews.activeOrders.menu marketId={marketId} />}
|
||||
menu={<TradingViews.activeOrders.menu />}
|
||||
>
|
||||
<TradingViews.orders.component filter={Filter.Open} />
|
||||
</Tab>
|
||||
@@ -127,7 +136,7 @@ const MainGrid = memo(
|
||||
<Tab
|
||||
id="orders"
|
||||
name={t('All')}
|
||||
menu={<TradingViews.orders.menu marketId={marketId} />}
|
||||
menu={<TradingViews.orders.menu />}
|
||||
>
|
||||
<TradingViews.orders.component />
|
||||
</Tab>
|
||||
@@ -139,9 +148,6 @@ 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 marketId={market?.id || ''} />
|
||||
<Menu />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,7 +319,8 @@ describe('Closed', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('successor marked should be visible', async () => {
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('successor marked should be visible', async () => {
|
||||
const marketsWithSuccessorID = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
@@ -363,12 +364,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];
|
||||
|
||||
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'PRNT'
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CellClickedEvent } from 'ag-grid-community';
|
||||
import compact from 'lodash/compact';
|
||||
import { isAfter } from 'date-fns';
|
||||
import type {
|
||||
@@ -5,6 +6,7 @@ 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';
|
||||
@@ -17,13 +19,11 @@ 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 = useClosedMarketClickHandler();
|
||||
const handleOnSelect = useMarketClickHandler();
|
||||
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
|
||||
|
||||
const colDefs = useMemo(() => {
|
||||
@@ -302,8 +302,11 @@ const ClosedMarketsDataGrid = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore metaKey exists
|
||||
handleOnSelect(data.id, event ? event.metaKey : false);
|
||||
handleOnSelect(
|
||||
data.id,
|
||||
// @ts-ignore metaKey exists
|
||||
event ? event.metaKey : false
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
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>;
|
||||
@@ -1,349 +0,0 @@
|
||||
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} />;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
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,7 +15,6 @@ 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,
|
||||
@@ -33,7 +32,7 @@ const WithdrawalsIndicator = () => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="bg-vega-clight-500 dark:bg-vega-cdark-500 text-default rounded p-1 leading-none">
|
||||
<span className="p-1 leading-none rounded bg-vega-clight-500 dark:bg-vega-cdark-500 text-default">
|
||||
{ready.length}
|
||||
</span>
|
||||
);
|
||||
@@ -66,10 +65,7 @@ export const Portfolio = () => {
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
<ResizableGridPanel minSize={75}>
|
||||
<PortfolioGridChild>
|
||||
<Tabs storageKey="console-portfolio-top">
|
||||
<Tab id="account-history" name={t('Account history')}>
|
||||
<AccountHistoryContainer />
|
||||
</Tab>
|
||||
<Tabs storageKey="console-portfolio-top-1">
|
||||
<Tab
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
@@ -132,7 +128,7 @@ interface PortfolioGridChildProps {
|
||||
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
|
||||
return (
|
||||
<section className="h-full p-1">
|
||||
<div className="border border-default h-full rounded-sm">{children}</div>
|
||||
<div className="h-full border rounded-sm border-default">{children}</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
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, useSearchParams } from 'react-router-dom';
|
||||
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { RainbowButton } from './buttons';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } 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);
|
||||
@@ -27,11 +50,17 @@ export const ApplyCodeForm = () => {
|
||||
formState: { errors },
|
||||
setValue,
|
||||
setError,
|
||||
watch,
|
||||
} = useForm();
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const { data: referee } = useReferral(pubKey, 'referee');
|
||||
const { data: referrer } = useReferral(pubKey, 'referrer');
|
||||
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,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const code = params.get('code');
|
||||
@@ -54,7 +83,7 @@ export const ApplyCodeForm = () => {
|
||||
if (!res) {
|
||||
setError('code', {
|
||||
type: 'required',
|
||||
message: 'The transaction could not be sent',
|
||||
message: t('The transaction could not be sent'),
|
||||
});
|
||||
}
|
||||
if (res) {
|
||||
@@ -65,9 +94,13 @@ export const ApplyCodeForm = () => {
|
||||
if (err.message.includes('user rejected')) {
|
||||
setStatus(null);
|
||||
} else {
|
||||
setStatus(null);
|
||||
setError('code', {
|
||||
type: 'required',
|
||||
message: 'Your code has been rejected',
|
||||
message:
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: t('Your code has been rejected'),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -99,10 +132,21 @@ 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">
|
||||
@@ -110,61 +154,94 @@ export const ApplyCodeForm = () => {
|
||||
<span className="text-vega-green-500">
|
||||
<VegaIcon name={VegaIconNames.TICK} size={20} />
|
||||
</span>{' '}
|
||||
<span className="pt-1">Code applied</span>
|
||||
<span className="pt-1">{t('Code applied')}</span>
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getButtonProps = () => {
|
||||
if (isReadOnly || !pubKey) {
|
||||
if (!pubKey) {
|
||||
return {
|
||||
disabled: false,
|
||||
children: t('Connect wallet'),
|
||||
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
onClick: ((event) => {
|
||||
event.preventDefault();
|
||||
openWalletDialog();
|
||||
}) as MouseEventHandler,
|
||||
};
|
||||
}
|
||||
|
||||
if (isReadOnly) {
|
||||
return {
|
||||
disabled: true,
|
||||
children: 'Apply',
|
||||
children: t('Apply a code'),
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
return {
|
||||
disabled: true,
|
||||
children: 'Confirm in wallet...',
|
||||
children: t('Confirm in wallet...'),
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
children: 'Apply',
|
||||
children: t('Apply a code'),
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<>
|
||||
<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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,20 +16,23 @@ export const RainbowButton = ({
|
||||
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button
|
||||
className={classNames(
|
||||
'bg-rainbow hover:bg-none hover:bg-rainbow enabled:hover:bg-vega-pink-500 rounded-lg overflow-hidden disabled:opacity-40',
|
||||
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
|
||||
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
|
||||
{
|
||||
'px-5 py-3 text-white': variant === 'full',
|
||||
'p-[0.125rem]': variant === 'border',
|
||||
},
|
||||
className
|
||||
}
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
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',
|
||||
})}
|
||||
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
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -55,6 +58,20 @@ 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>
|
||||
@@ -93,6 +110,26 @@ 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,3 +4,8 @@ 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,70 +19,58 @@ 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 = () => {
|
||||
const { stakeAvailable, requiredStake } = useStakeAvailable();
|
||||
if (stakeAvailable == null || requiredStake == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CreateCodeForm
|
||||
currentStakeAvailable={stakeAvailable}
|
||||
requiredStake={requiredStake}
|
||||
/>
|
||||
);
|
||||
return <CreateCodeForm />;
|
||||
};
|
||||
|
||||
export const CreateCodeForm = ({
|
||||
currentStakeAvailable,
|
||||
requiredStake,
|
||||
}: {
|
||||
currentStakeAvailable: bigint;
|
||||
requiredStake: bigint;
|
||||
}) => {
|
||||
export const CreateCodeForm = () => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
|
||||
return (
|
||||
<div className="w-1/2 mx-auto">
|
||||
<h3 className="mb-5 text-xl text-center uppercase calt">
|
||||
Create a referral code
|
||||
<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')}
|
||||
</h3>
|
||||
<p className="mb-6 text-center">
|
||||
Generate a referral code to share with your friends and start earning
|
||||
commission.
|
||||
<p className="mb-4 text-center text-base">
|
||||
{t(
|
||||
'Generate a referral code to share with your friends and start earning commission.'
|
||||
)}
|
||||
</p>
|
||||
<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 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>
|
||||
|
||||
<Dialog
|
||||
title="Create a referral code"
|
||||
title={t('Create a referral code')}
|
||||
open={dialogOpen}
|
||||
onChange={() => setDialogOpen(false)}
|
||||
size="small"
|
||||
>
|
||||
<CreateCodeDialog
|
||||
currentStakeAvailable={currentStakeAvailable}
|
||||
setDialogOpen={setDialogOpen}
|
||||
requiredStake={requiredStake}
|
||||
/>
|
||||
<CreateCodeDialog setDialogOpen={setDialogOpen} />
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
@@ -90,21 +78,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');
|
||||
@@ -140,45 +128,60 @@ const CreateCodeDialog = ({
|
||||
const getButtonProps = () => {
|
||||
if (status === 'idle' || status === 'error') {
|
||||
return {
|
||||
children: 'Generate code',
|
||||
children: t('Generate code'),
|
||||
onClick: () => onSubmit(),
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'loading') {
|
||||
return {
|
||||
children: 'Confirm in wallet...',
|
||||
children: t('Confirm in wallet...'),
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
return {
|
||||
children: 'Close',
|
||||
children: t('Close'),
|
||||
intent: Intent.Success,
|
||||
onClick: () => setDialogOpen(false),
|
||||
onClick: () => {
|
||||
refetch();
|
||||
setDialogOpen(false);
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Add when network parameters are updated
|
||||
if (
|
||||
currentStakeAvailable === BigInt(0) ||
|
||||
currentStakeAvailable < requiredStake
|
||||
) {
|
||||
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) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
You need at least{' '}
|
||||
{addDecimalsFormatNumber(requiredStake.toString(), 18)} VEGA staked to
|
||||
generate a referral code and participate in the referral program.
|
||||
{t('You need at least')}{' '}
|
||||
{addDecimalsFormatNumber(requiredStake.toString(), 18)}{' '}
|
||||
{t(
|
||||
'VEGA staked to generate a referral code and participate in the referral program.'
|
||||
)}
|
||||
</p>
|
||||
<TradingAnchorButton
|
||||
href={createLink(TokenStaticLinks.ASSOCIATE)}
|
||||
intent={Intent.Primary}
|
||||
target="_blank"
|
||||
>
|
||||
Stake some $VEGA now
|
||||
{t('Stake some $VEGA now')}
|
||||
</TradingAnchorButton>
|
||||
</div>
|
||||
);
|
||||
@@ -188,8 +191,9 @@ const CreateCodeDialog = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
||||
<p>
|
||||
Generate a referral code to share with your friends and start earning
|
||||
commission.
|
||||
{t(
|
||||
'Generate a referral code to share with your friends and start earning commission.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{status === 'success' && code && (
|
||||
@@ -204,7 +208,7 @@ const CreateCodeDialog = ({
|
||||
className="text-sm no-underline"
|
||||
icon={<VegaIcon name={VegaIconNames.COPY} />}
|
||||
>
|
||||
<span>Copy</span>
|
||||
<span>{t('Copy')}</span>
|
||||
</TradingButton>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
@@ -215,10 +219,13 @@ 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>About the referral program</ExternalLink>
|
||||
<ExternalLink>Disclaimer</ExternalLink>
|
||||
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
|
||||
{t('About the referral program')}
|
||||
</ExternalLink>
|
||||
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
|
||||
{t('Disclaimer')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ 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();
|
||||
@@ -39,7 +40,7 @@ export const ErrorBoundary = () => {
|
||||
variant="border"
|
||||
className="text-xs"
|
||||
>
|
||||
Go back and try again
|
||||
{t('Go back and try again')}
|
||||
</RainbowButton>
|
||||
</p>
|
||||
</LayoutWithSky>
|
||||
@@ -60,7 +61,7 @@ export const NotFound = () => {
|
||||
<h1 className="text-6xl font-alpha calt mb-10">{'Not found'}</h1>
|
||||
|
||||
<p className="text-lg mb-10">
|
||||
{"The page you're looking for doesn't exists."}
|
||||
{t("The page you're looking for doesn't exists.")}
|
||||
</p>
|
||||
|
||||
<p className="text-lg mb-10">
|
||||
@@ -69,7 +70,7 @@ export const NotFound = () => {
|
||||
variant="border"
|
||||
className="text-xs"
|
||||
>
|
||||
Go back and try again
|
||||
{t('Go back and try again')}
|
||||
</RainbowButton>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
query ReferralProgram {
|
||||
currentReferralProgram {
|
||||
id
|
||||
version
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
endedAt
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
query CurrentEpochInfo {
|
||||
epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
totalRefereeNotionalTakerVolume
|
||||
totalRefereeGeneratedRewards
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetStats(setId: $code, epoch: $epoch) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
partyId
|
||||
discountFactor
|
||||
rewardFactor
|
||||
epochNotionalTakerVolume
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query ReferralSets($id: ID, $referrer: ID, $referee: ID) {
|
||||
referralSets(id: $id, referrer: $referrer, referee: $referee) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ReferralProgramQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ReferralProgramQuery = { __typename?: 'Query', currentReferralProgram?: { __typename?: 'CurrentReferralProgram', id: string, version: number, endOfProgramTimestamp: any, windowLength: number, endedAt?: any | null, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | null };
|
||||
|
||||
|
||||
export const ReferralProgramDocument = gql`
|
||||
query ReferralProgram {
|
||||
currentReferralProgram {
|
||||
id
|
||||
version
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
endedAt
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useReferralProgramQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useReferralProgramQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useReferralProgramQuery` 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 } = useReferralProgramQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReferralProgramQuery(baseOptions?: Apollo.QueryHookOptions<ReferralProgramQuery, ReferralProgramQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ReferralProgramQuery, ReferralProgramQueryVariables>(ReferralProgramDocument, options);
|
||||
}
|
||||
export function useReferralProgramLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ReferralProgramQuery, ReferralProgramQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ReferralProgramQuery, ReferralProgramQueryVariables>(ReferralProgramDocument, options);
|
||||
}
|
||||
export type ReferralProgramQueryHookResult = ReturnType<typeof useReferralProgramQuery>;
|
||||
export type ReferralProgramLazyQueryHookResult = ReturnType<typeof useReferralProgramLazyQuery>;
|
||||
export type ReferralProgramQueryResult = Apollo.QueryResult<ReferralProgramQuery, ReferralProgramQueryVariables>;
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type CurrentEpochInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type CurrentEpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null } } };
|
||||
|
||||
|
||||
export const CurrentEpochInfoDocument = gql`
|
||||
query CurrentEpochInfo {
|
||||
epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useCurrentEpochInfoQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useCurrentEpochInfoQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useCurrentEpochInfoQuery` 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 } = useCurrentEpochInfoQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCurrentEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
|
||||
}
|
||||
export function useCurrentEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
|
||||
}
|
||||
export type CurrentEpochInfoQueryHookResult = ReturnType<typeof useCurrentEpochInfoQuery>;
|
||||
export type CurrentEpochInfoLazyQueryHookResult = ReturnType<typeof useCurrentEpochInfoLazyQuery>;
|
||||
export type CurrentEpochInfoQueryResult = Apollo.QueryResult<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>;
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type RefereesQueryVariables = Types.Exact<{
|
||||
code: Types.Scalars['ID'];
|
||||
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type RefereesQuery = { __typename?: 'Query', referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', referralSetId: string, refereeId: string, joinedAt: any, atEpoch: number, totalRefereeNotionalTakerVolume: string, totalRefereeGeneratedRewards: string } } | null> } };
|
||||
|
||||
|
||||
export const RefereesDocument = gql`
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
totalRefereeNotionalTakerVolume
|
||||
totalRefereeGeneratedRewards
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRefereesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRefereesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRefereesQuery` 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 } = useRefereesQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRefereesQuery(baseOptions: Apollo.QueryHookOptions<RefereesQuery, RefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RefereesQuery, RefereesQueryVariables>(RefereesDocument, options);
|
||||
}
|
||||
export function useRefereesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RefereesQuery, RefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RefereesQuery, RefereesQueryVariables>(RefereesDocument, options);
|
||||
}
|
||||
export type RefereesQueryHookResult = ReturnType<typeof useRefereesQuery>;
|
||||
export type RefereesLazyQueryHookResult = ReturnType<typeof useRefereesLazyQuery>;
|
||||
export type RefereesQueryResult = Apollo.QueryResult<RefereesQuery, RefereesQueryVariables>;
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ReferralSetStatsQueryVariables = Types.Exact<{
|
||||
code: Types.Scalars['ID'];
|
||||
epoch?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetStatsDocument = gql`
|
||||
query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetStats(setId: $code, epoch: $epoch) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
partyId
|
||||
discountFactor
|
||||
rewardFactor
|
||||
epochNotionalTakerVolume
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useReferralSetStatsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useReferralSetStatsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useReferralSetStatsQuery` 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 } = useReferralSetStatsQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* epoch: // value for 'epoch'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReferralSetStatsQuery(baseOptions: Apollo.QueryHookOptions<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>(ReferralSetStatsDocument, options);
|
||||
}
|
||||
export function useReferralSetStatsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>(ReferralSetStatsDocument, options);
|
||||
}
|
||||
export type ReferralSetStatsQueryHookResult = ReturnType<typeof useReferralSetStatsQuery>;
|
||||
export type ReferralSetStatsLazyQueryHookResult = ReturnType<typeof useReferralSetStatsLazyQuery>;
|
||||
export type ReferralSetStatsQueryResult = Apollo.QueryResult<ReferralSetStatsQuery, ReferralSetStatsQueryVariables>;
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ReferralSetsQueryVariables = Types.Exact<{
|
||||
id?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
referrer?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
referee?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetsQuery = { __typename?: 'Query', referralSets: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string, createdAt: any, updatedAt: any } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetsDocument = gql`
|
||||
query ReferralSets($id: ID, $referrer: ID, $referee: ID) {
|
||||
referralSets(id: $id, referrer: $referrer, referee: $referee) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useReferralSetsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useReferralSetsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useReferralSetsQuery` 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 } = useReferralSetsQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* referrer: // value for 'referrer'
|
||||
* referee: // value for 'referee'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useReferralSetsQuery(baseOptions?: Apollo.QueryHookOptions<ReferralSetsQuery, ReferralSetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ReferralSetsQuery, ReferralSetsQueryVariables>(ReferralSetsDocument, options);
|
||||
}
|
||||
export function useReferralSetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ReferralSetsQuery, ReferralSetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ReferralSetsQuery, ReferralSetsQueryVariables>(ReferralSetsDocument, options);
|
||||
}
|
||||
export type ReferralSetsQueryHookResult = ReturnType<typeof useReferralSetsQuery>;
|
||||
export type ReferralSetsLazyQueryHookResult = ReturnType<typeof useReferralSetsLazyQuery>;
|
||||
export type ReferralSetsQueryResult = Apollo.QueryResult<ReferralSetsQuery, ReferralSetsQueryVariables>;
|
||||
@@ -1,32 +1,8 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { addDays } from 'date-fns';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
// TODO: Generate query
|
||||
// eslint-disable-next-line
|
||||
const REFERRAL_PROGRAM_QUERY = gql`
|
||||
query ReferralProgram {
|
||||
currentReferralProgram {
|
||||
id
|
||||
version
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
endedAt
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||
|
||||
const STAKING_TIERS_MAPPING: Record<number, string> = {
|
||||
1: 'Tradestarter',
|
||||
@@ -83,11 +59,11 @@ const MOCK = {
|
||||
};
|
||||
|
||||
export const useReferralProgram = () => {
|
||||
const { data, loading, error } = useQuery(REFERRAL_PROGRAM_QUERY, {
|
||||
const { data, loading, error } = useReferralProgramQuery({
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
if (!data || !data.currentReferralProgram) {
|
||||
return {
|
||||
benefitTiers: [],
|
||||
stakingTiers: [],
|
||||
@@ -104,11 +80,15 @@ export const useReferralProgram = () => {
|
||||
.map((t, i) => {
|
||||
return {
|
||||
tier: i + 1,
|
||||
rewardFactor: Number(t.referralRewardFactor),
|
||||
commission: Number(t.referralRewardFactor) * 100 + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: Number(t.referralDiscountFactor) * 100 + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -123,10 +103,16 @@ export const useReferralProgram = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const details = omit(
|
||||
data.currentReferralProgram,
|
||||
'benefitTiers',
|
||||
'stakingTiers'
|
||||
);
|
||||
|
||||
return {
|
||||
benefitTiers,
|
||||
stakingTiers,
|
||||
details: omit(data.currentReferralProgram, 'benefitTiers', 'stakingTiers'),
|
||||
details,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
@@ -1,114 +1,116 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
|
||||
const REFERRER_QUERY = gql`
|
||||
query ReferralSets($partyId: ID!) {
|
||||
referralSets(referrer: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
const REFEREE_QUERY = gql`
|
||||
query ReferralSets($partyId: ID!) {
|
||||
referralSets(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const REFEREES_QUERY = gql`
|
||||
query ReferralSets($code: ID!) {
|
||||
referralSetReferees(id: $code) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: generate types after perps work is merged
|
||||
export type ReferralData = {
|
||||
code: string;
|
||||
referees: Array<{
|
||||
refereeId: string;
|
||||
joinedAt: string;
|
||||
atEpoch: number;
|
||||
}>;
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type UseReferralArgs = (
|
||||
| { code: string }
|
||||
| { pubKey: string | null; role: Role }
|
||||
) & {
|
||||
aggregationEpochs?: number;
|
||||
};
|
||||
|
||||
export const useReferral = (
|
||||
pubKey: string | null,
|
||||
role: 'referrer' | 'referee'
|
||||
) => {
|
||||
const query = {
|
||||
referrer: REFERRER_QUERY,
|
||||
referee: REFEREE_QUERY,
|
||||
};
|
||||
const prepareVariables = (
|
||||
args: UseReferralArgs
|
||||
): [ReferralSetsQueryVariables, boolean] => {
|
||||
const byCode = 'code' in args;
|
||||
const byRole = 'pubKey' in args && 'role' in args;
|
||||
let variables = {};
|
||||
let skip = true;
|
||||
if (byCode) {
|
||||
variables = {
|
||||
id: args.code,
|
||||
};
|
||||
skip = !args.code;
|
||||
}
|
||||
if (byRole) {
|
||||
if (args.role === 'referee') {
|
||||
variables = { referee: args.pubKey };
|
||||
}
|
||||
if (args.role === 'referrer') {
|
||||
variables = { referrer: args.pubKey };
|
||||
}
|
||||
skip = !args.pubKey;
|
||||
}
|
||||
|
||||
return [variables, skip];
|
||||
};
|
||||
|
||||
export const useReferral = (args: UseReferralArgs) => {
|
||||
const [variables, skip] = prepareVariables(args);
|
||||
|
||||
const {
|
||||
data: referralData,
|
||||
loading: referralLoading,
|
||||
error: referralError,
|
||||
} = useQuery(query[role], {
|
||||
variables: {
|
||||
partyId: pubKey,
|
||||
},
|
||||
skip: !pubKey,
|
||||
refetch: referralRefetch,
|
||||
} = useReferralSetsQuery({
|
||||
variables,
|
||||
skip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
// A user can only have 1 active referral program at a time
|
||||
const referral = referralData?.referralSets.edges.length
|
||||
? referralData.referralSets.edges[0].node
|
||||
: undefined;
|
||||
const referralSet =
|
||||
referralData?.referralSets.edges &&
|
||||
referralData.referralSets.edges.length > 0
|
||||
? referralData.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
} = useQuery(REFEREES_QUERY, {
|
||||
refetch: refereesRefetch,
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: referral?.id,
|
||||
code: referralSet?.id as string,
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs != null
|
||||
? args.aggregationEpochs
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
},
|
||||
skip: !referral?.id,
|
||||
skip: !referralSet?.id,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
|
||||
const referees = removePaginationWrapper(
|
||||
refereesData?.referralSetReferees.edges
|
||||
const referees = compact(
|
||||
removePaginationWrapper(refereesData?.referralSetReferees.edges)
|
||||
);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
referralRefetch();
|
||||
refereesRefetch();
|
||||
}, [refereesRefetch, referralRefetch]);
|
||||
|
||||
const byReferee =
|
||||
'role' in args && 'pubKey' in args && args.role === 'referee';
|
||||
const referee = byReferee
|
||||
? referees.find((r) => r.refereeId === args.pubKey) || null
|
||||
: null;
|
||||
|
||||
const data =
|
||||
referral && refereesData
|
||||
referralSet && refereesData
|
||||
? {
|
||||
code: referral.id,
|
||||
code: referralSet.id,
|
||||
role: 'role' in args ? args.role : null,
|
||||
referee: referee,
|
||||
referrerId: referralSet.referrer,
|
||||
createdAt: referralSet.createdAt,
|
||||
referees,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
data: data as ReferralData | undefined,
|
||||
data,
|
||||
loading: referralLoading || refereesLoading,
|
||||
error: referralError || refereesError,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Table } from './table';
|
||||
|
||||
export const HowItWorksTable = () => (
|
||||
@@ -13,7 +14,9 @@ export const HowItWorksTable = () => (
|
||||
1
|
||||
</span>
|
||||
),
|
||||
step: 'Referrers generate a code assigned to their key via an on chain transaction',
|
||||
step: t(
|
||||
'Referrers generate a code assigned to their key via an on chain transaction'
|
||||
),
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -21,7 +24,9 @@ export const HowItWorksTable = () => (
|
||||
2
|
||||
</span>
|
||||
),
|
||||
step: 'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction',
|
||||
step: t(
|
||||
'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction'
|
||||
),
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -29,7 +34,9 @@ export const HowItWorksTable = () => (
|
||||
3
|
||||
</span>
|
||||
),
|
||||
step: 'Discounts are applied automatically during trading based on the key(s) used',
|
||||
step: t(
|
||||
'Discounts are applied automatically during trading based on the key(s) used'
|
||||
),
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -37,7 +44,9 @@ export const HowItWorksTable = () => (
|
||||
4
|
||||
</span>
|
||||
),
|
||||
step: 'Referrers earn commission based on a percentage of the taker fees their referees pay',
|
||||
step: t(
|
||||
'Referrers earn commission based on a percentage of the taker fees their referees pay'
|
||||
),
|
||||
},
|
||||
{
|
||||
number: (
|
||||
@@ -45,7 +54,9 @@ export const HowItWorksTable = () => (
|
||||
5
|
||||
</span>
|
||||
),
|
||||
step: 'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee',
|
||||
step: t(
|
||||
'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee'
|
||||
),
|
||||
},
|
||||
]}
|
||||
></Table>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { AnimatedDudeWithWire } from './graphics/dude';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const LandingBanner = () => {
|
||||
return (
|
||||
@@ -7,22 +8,18 @@ export const LandingBanner = () => {
|
||||
<div className="">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
|
||||
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden"
|
||||
>
|
||||
<AnimatedDudeWithWire />
|
||||
</div>
|
||||
<div className="pt-32 sm:w-[50%]">
|
||||
<div className="pt-20 sm:w-[50%]">
|
||||
<h1 className="text-6xl font-alpha calt mb-10">
|
||||
Earn commission & stake rewards
|
||||
{t('Earn commission & stake rewards')}
|
||||
</h1>
|
||||
<p className="text-lg mb-10">
|
||||
Invite friends and earn commission in the form of Vega rewards from
|
||||
the trading fees they pay. Stake those rewards to earn multipliers
|
||||
on future rewards.
|
||||
</p>
|
||||
<p className="text-lg">
|
||||
Any friends that join using the code will receive discounts off
|
||||
trading fees.
|
||||
{t(
|
||||
'Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { SKY_BACKGROUND } from './constants';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Layout = ({
|
||||
className,
|
||||
@@ -28,8 +29,10 @@ export const LayoutWithSky = ({
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={classNames('h-full overflow-auto', SKY_BACKGROUND)}>
|
||||
<TinyScroll
|
||||
className={classNames('max-h-full overflow-auto', SKY_BACKGROUND)}
|
||||
>
|
||||
<Layout className={className} {...props} />
|
||||
</div>
|
||||
</TinyScroll>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,113 +1,375 @@
|
||||
import { Tile } from './tile';
|
||||
import { CodeTile, StatTile } from './tile';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
Input,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, RainbowButton } from './buttons';
|
||||
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import type { ReferralData } from './hooks/use-referral';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import { CreateCodeContainer } from './create-code-form';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const CodeTile = ({
|
||||
code,
|
||||
as,
|
||||
}: {
|
||||
code: string;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
return (
|
||||
<Tile variant="rainbow">
|
||||
<h3 className="mb-1 text-lg calt">Your referral code</h3>
|
||||
{as === 'referrer' && (
|
||||
<p className="mb-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
Share this code with friends
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Input size={1} readOnly value={code} />
|
||||
<CopyWithTooltip text={code}>
|
||||
<Button
|
||||
className="text-sm no-underline"
|
||||
icon={<VegaIcon name={VegaIconNames.COPY} />}
|
||||
>
|
||||
<span>Copy</span>
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
import { Table } from './table';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateFormat,
|
||||
getDateTimeFormat,
|
||||
getNumberFormat,
|
||||
getUserLocale,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
||||
import compact from 'lodash/compact';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data: referee } = useReferral(pubKey, 'referee');
|
||||
const { data: referrer } = useReferral(pubKey, 'referrer');
|
||||
const program = useReferralProgram();
|
||||
|
||||
const { data: referee } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<RainbowButton variant="border" onClick={() => openWalletDialog()}>
|
||||
Connect wallet
|
||||
</RainbowButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (referee?.code) {
|
||||
return <Statistics data={referee} as="referee" />;
|
||||
return <Statistics data={referee} program={program} as="referee" />;
|
||||
}
|
||||
|
||||
if (referrer?.code) {
|
||||
return <Statistics data={referrer} as="referrer" />;
|
||||
return <Statistics data={referrer} program={program} as="referrer" />;
|
||||
}
|
||||
|
||||
return <CreateCodeContainer />;
|
||||
};
|
||||
|
||||
const Statistics = ({
|
||||
export const Statistics = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data: ReferralData;
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('grid grid-cols-1 grid-rows-1 gap-5 mx-auto', {
|
||||
'md:w-1/2': as === 'referee',
|
||||
'md:w-2/3': as === 'referrer',
|
||||
})}
|
||||
const { benefitTiers, details } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data.code,
|
||||
},
|
||||
skip: !data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const stats =
|
||||
statsData?.referralSetStats.edges &&
|
||||
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
||||
const refereeInfo = data.referee;
|
||||
const refereeStats = stats?.find(
|
||||
(r) => r.partyId === data.referee?.refereeId
|
||||
);
|
||||
|
||||
const statsAvailable = stats && stats.length > 0 && stats[0];
|
||||
const baseCommissionValue = statsAvailable
|
||||
? Number(statsAvailable.rewardFactor)
|
||||
: 0;
|
||||
const runningVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||
: 0;
|
||||
const referrerVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referrerTakerVolume)
|
||||
: 0;
|
||||
const multiplier = statsAvailable
|
||||
? Number(statsAvailable.rewardsMultiplier)
|
||||
: 1;
|
||||
const finalCommissionValue = isNaN(multiplier)
|
||||
? baseCommissionValue
|
||||
: multiplier * baseCommissionValue;
|
||||
|
||||
const discountFactorValue = refereeStats?.discountFactor
|
||||
? Number(refereeStats.discountFactor)
|
||||
: 0;
|
||||
const currentBenefitTierValue = benefitTiers.find(
|
||||
(t) =>
|
||||
!isNaN(discountFactorValue) &&
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
: 0;
|
||||
const nextBenefitTierVolumeValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.minimumVolume - runningVolumeValue
|
||||
: 0;
|
||||
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.epochs - epochsValue
|
||||
: 0;
|
||||
|
||||
const baseCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Base commission rate')}
|
||||
description={t('(Combined set volume %s over last %s epochs)', [
|
||||
compactNumFormat.format(runningVolumeValue),
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString(),
|
||||
])}
|
||||
>
|
||||
<div
|
||||
className={classNames('grid grid-rows-1 gap-5', {
|
||||
'grid-cols-2': as === 'referrer',
|
||||
'grid-cols-1': as === 'referee',
|
||||
})}
|
||||
>
|
||||
{as === 'referrer' && data?.referees && (
|
||||
<Tile className="py-3 h-full">
|
||||
<div className="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%]">
|
||||
<h3 className="mb-1 text-6xl text-center">
|
||||
{data.referees.length}
|
||||
</h3>
|
||||
<p className="text-sm text-center text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{data.referees.length === 1
|
||||
? 'Trader referred'
|
||||
: 'Total traders referred'}
|
||||
</p>
|
||||
</div>
|
||||
</Tile>
|
||||
)}
|
||||
<CodeTile code={data?.code} as={as} />
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
const stakingMultiplierTile = (
|
||||
<StatTile
|
||||
title={t('Staking multiplier')}
|
||||
description={`(${addDecimalsFormatNumber(
|
||||
stakeAvailable?.toString() || 0,
|
||||
18
|
||||
)} $VEGA staked)`}
|
||||
>
|
||||
{multiplier || t('None')}
|
||||
</StatTile>
|
||||
);
|
||||
const finalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
||||
finalCommissionValue * 100
|
||||
}%)`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{finalCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
const numberOfTradersValue = data.referees.length;
|
||||
const numberOfTradersTile = (
|
||||
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = (
|
||||
<CodeTile
|
||||
code={data?.code}
|
||||
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
||||
/>
|
||||
);
|
||||
|
||||
const referrerVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'My volume (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const totalCommissionValue = data.referees
|
||||
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Total commission (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
description={t('(qUSD)')}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const referrerTiles = (
|
||||
<>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
{baseCommissionTile}
|
||||
{stakingMultiplierTile}
|
||||
{finalCommissionTile}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{codeTile}
|
||||
{referrerVolumeTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
{currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile title={t('Combined volume')}>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const epochsTile = (
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Volume to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'Epochs to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const refereeTiles = (
|
||||
<>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
{currentBenefitTierTile}
|
||||
{discountFactorTile}
|
||||
{codeTile}
|
||||
</div>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{runningVolumeTile}
|
||||
{nextTierVolumeTile}
|
||||
{epochsTile}
|
||||
{nextTierEpochsTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Stats tiles */}
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
|
||||
)}
|
||||
>
|
||||
{as === 'referrer' && referrerTiles}
|
||||
{as === 'referee' && refereeTiles}
|
||||
</div>
|
||||
|
||||
{/* Referees (only for referrer view) */}
|
||||
{as === 'referrer' && data.referees.length > 0 && (
|
||||
<div className="mt-20 mb-20">
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
'relative max-h-96 overflow-hidden',
|
||||
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
||||
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
||||
]
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
||||
{
|
||||
hidden: !collapsed,
|
||||
}
|
||||
)}
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
||||
</button>
|
||||
<Table
|
||||
ref={tableRef}
|
||||
columns={[
|
||||
{ name: 'party', displayName: t('Trader') },
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Volume (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: t(
|
||||
'Commission earned (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
data.referees.map((r) => ({
|
||||
party: (
|
||||
<span title={r.refereeId}>
|
||||
{truncateMiddle(r.refereeId)}
|
||||
</span>
|
||||
),
|
||||
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
||||
volume: Number(r.totalRefereeNotionalTakerVolume),
|
||||
commission: Number(r.totalRefereeGeneratedRewards),
|
||||
})),
|
||||
(r) => r.volume
|
||||
)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
volume: getNumberFormat(0).format(r.volume),
|
||||
commission: getNumberFormat(0).format(r.commission),
|
||||
}))
|
||||
.reverse()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Loader,
|
||||
TradingAnchorButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
@@ -6,51 +7,99 @@ import {
|
||||
import { HowItWorksTable } from './how-it-works-table';
|
||||
import { LandingBanner } from './landing-banner';
|
||||
import { TiersContainer } from './tiers';
|
||||
import { RainbowTabLink } from './buttons';
|
||||
import { TabLink } from './buttons';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { REFERRAL_DOCS_LINK } from './constants';
|
||||
import classNames from 'classnames';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
const Nav = () => (
|
||||
<div className="flex justify-center border-b border-vega-cdark-500">
|
||||
<TabLink end to={Routes.REFERRALS}>
|
||||
{t('I want a code')}
|
||||
</TabLink>
|
||||
<TabLink to={Routes.REFERRALS_APPLY_CODE}>{t('I have a code')}</TabLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Referrals = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data: referee } = useReferral(pubKey, 'referee');
|
||||
const { data: referrer } = useReferral(pubKey, 'referrer');
|
||||
|
||||
const {
|
||||
data: referee,
|
||||
loading: refereeLoading,
|
||||
error: refereeError,
|
||||
} = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
const {
|
||||
data: referrer,
|
||||
loading: referrerLoading,
|
||||
error: referrerError,
|
||||
} = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
const error = refereeError || referrerError;
|
||||
const loading = refereeLoading || referrerLoading;
|
||||
const showNav = !loading && !error && !referrer && !referee;
|
||||
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Referrals')]));
|
||||
}, [updateTitle]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LandingBanner />
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<RainbowTabLink end to={Routes.REFERRALS}>
|
||||
Your referrals
|
||||
</RainbowTabLink>
|
||||
<RainbowTabLink
|
||||
disabled={Boolean(referee || referrer)}
|
||||
to={Routes.REFERRALS_APPLY_CODE}
|
||||
>
|
||||
Apply a code
|
||||
</RainbowTabLink>
|
||||
</div>
|
||||
<div className="py-16 border-t border-b border-vega-cdark-500">
|
||||
|
||||
{showNav && <Nav />}
|
||||
<div
|
||||
className={classNames({
|
||||
'py-16': showNav,
|
||||
'h-[300px] relative': loading || error,
|
||||
})}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loader />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<p>{t('Something went wrong')}</p>
|
||||
<span className="text-xs">{error.message}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TiersContainer />
|
||||
|
||||
<div className="mt-10 mb-5 text-center">
|
||||
<h2 className="text-2xl">How it works</h2>
|
||||
<h2 className="text-2xl">{t('How it works')}</h2>
|
||||
</div>
|
||||
<div className="md:w-[60%] mx-auto">
|
||||
<HowItWorksTable />
|
||||
<div className="mt-5">
|
||||
<TradingAnchorButton
|
||||
className="mx-auto w-max"
|
||||
href="https://docs.vega.xyz/"
|
||||
href={REFERRAL_DOCS_LINK}
|
||||
target="_blank"
|
||||
>
|
||||
Read the terms <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
{t('Read the terms')}{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</TradingAnchorButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { forwardRef, type HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
type TableColumnDefinition = {
|
||||
@@ -19,98 +19,108 @@ type TableProps = {
|
||||
|
||||
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
|
||||
|
||||
export const Table = ({
|
||||
columns,
|
||||
data,
|
||||
noHeader = false,
|
||||
noCollapse = false,
|
||||
className,
|
||||
...props
|
||||
}: TableProps & HTMLAttributes<HTMLTableElement>) => {
|
||||
const header = (
|
||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||
<tr>
|
||||
{columns.map(({ displayName, name, tooltip }) => (
|
||||
<th
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100',
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<span>{displayName}</span>
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip}>
|
||||
<button className="text-vega-clight-400 dark:text-vega-cdark-400 no-underline decoration-transparent w-[12px] h-[12px] inline-flex">
|
||||
<VegaIcon size={12} name={VegaIconNames.INFO} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<table
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'border-separate border rounded-md border-spacing-0',
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!noHeader && header}
|
||||
<tbody>
|
||||
{data.map((d, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={classNames(d['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
>
|
||||
{columns.map(({ name, displayName, className }, j) => (
|
||||
<td
|
||||
className={classNames(
|
||||
'px-5 py-3 text-base',
|
||||
{
|
||||
'max-md:flex max-md:flex-col max-md:justify-between':
|
||||
!noCollapse,
|
||||
},
|
||||
INNER_BORDER_STYLE,
|
||||
{
|
||||
'border-none': i === data.length - 1 && noCollapse,
|
||||
'md:border-none': i === data.length - 1,
|
||||
'max-md:border-none':
|
||||
i === data.length - 1 && j === columns.length - 1,
|
||||
},
|
||||
className
|
||||
)}
|
||||
key={`${i}-${name}`}
|
||||
>
|
||||
{/** display column name in mobile view */}
|
||||
{!noCollapse &&
|
||||
!noHeader &&
|
||||
displayName &&
|
||||
displayName.length > 0 && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
export const Table = forwardRef<
|
||||
HTMLTableElement,
|
||||
TableProps & HTMLAttributes<HTMLTableElement>
|
||||
>(
|
||||
(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
noHeader = false,
|
||||
noCollapse = false,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const header = (
|
||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||
<tr>
|
||||
{columns.map(({ displayName, name, tooltip }) => (
|
||||
<th
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
||||
INNER_BORDER_STYLE
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row gap-2 items-center">
|
||||
<span>{displayName}</span>
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip}>
|
||||
<button className="text-vega-clight-400 dark:text-vega-cdark-400 no-underline decoration-transparent w-[12px] h-[12px] inline-flex">
|
||||
<VegaIcon size={12} name={VegaIconNames.INFO} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<table
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'border-separate border rounded-md border-spacing-0',
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!noHeader && header}
|
||||
<tbody>
|
||||
{data.map((d, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={classNames(d['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
>
|
||||
{columns.map(({ name, displayName, className }, j) => (
|
||||
<td
|
||||
className={classNames(
|
||||
'px-5 py-3 text-base',
|
||||
{
|
||||
'max-md:flex max-md:flex-col max-md:justify-between':
|
||||
!noCollapse,
|
||||
},
|
||||
INNER_BORDER_STYLE,
|
||||
{
|
||||
'border-none': i === data.length - 1 && noCollapse,
|
||||
'md:border-none': i === data.length - 1,
|
||||
'max-md:border-none':
|
||||
i === data.length - 1 && j === columns.length - 1,
|
||||
},
|
||||
className
|
||||
)}
|
||||
<span>{d[name]}</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
key={`${i}-${name}`}
|
||||
>
|
||||
{/** display column name in mobile view */}
|
||||
{!noCollapse &&
|
||||
!noHeader &&
|
||||
displayName &&
|
||||
displayName.length > 0 && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
<span>{d[name]}</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
);
|
||||
Table.displayName = 'Table';
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Tag = ({
|
||||
}: TagProps & HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'mt-3 w-max border rounded-[1rem] py-[0.125rem] px-2 text-xs',
|
||||
'w-max border rounded-[1rem] py-[0.125rem] px-2 text-xs',
|
||||
{
|
||||
'border-vega-yellow-500 text-vega-yellow-500': color === 'yellow',
|
||||
'border-vega-green-500 text-vega-green-500': color === 'green',
|
||||
|
||||
@@ -4,7 +4,10 @@ import { Table } from './table';
|
||||
import classNames from 'classnames';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
import { Tag } from './tag';
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
<div
|
||||
@@ -38,51 +41,66 @@ const StakingTier = ({
|
||||
className={classNames(
|
||||
'overflow-hidden',
|
||||
'border rounded-md w-full',
|
||||
'flex flex-row',
|
||||
GRADIENT,
|
||||
BORDER_COLOR
|
||||
)}
|
||||
>
|
||||
<div aria-hidden>
|
||||
<div aria-hidden className="max-w-[120px]">
|
||||
{tier < 4 && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`/${tier}x.png`}
|
||||
alt={`${referralRewardMultiplier}x multiplier`}
|
||||
width={768}
|
||||
height={400}
|
||||
className="w-full"
|
||||
width={240}
|
||||
height={240}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames('p-3', GRADIENT)}>
|
||||
<h3 className="mb-3 text-xl">{label}</h3>
|
||||
<p className="text-base text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
Stake a minimum of {minimumStakedTokens} $VEGA tokens
|
||||
<div className={classNames('p-3')}>
|
||||
<Tag color={color[tier]}>Multiplier {referralRewardMultiplier}x</Tag>
|
||||
<h3 className="mt-1 mb-1 text-base">{label}</h3>
|
||||
<p className="text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{t('Stake a minimum of')} {minimumStakedTokens} {t('$VEGA tokens')}
|
||||
</p>
|
||||
<Tag color={color[tier]}>
|
||||
Reward multiplier {referralRewardMultiplier}x
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TiersContainer = () => {
|
||||
const { benefitTiers, stakingTiers, details, loading } = useReferralProgram();
|
||||
const { benefitTiers, stakingTiers, details, loading, error } =
|
||||
useReferralProgram();
|
||||
|
||||
const ends = details?.endOfProgramTimestamp
|
||||
? getDateTimeFormat().format(new Date(details.endOfProgramTimestamp))
|
||||
: undefined;
|
||||
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
|
||||
if ((!loading && !details) || error) {
|
||||
return (
|
||||
<div className="text-base px-5 py-10 text-center">
|
||||
{t(
|
||||
"We're sorry but we don't have an active referral programme currently running. You can propose a new programme"
|
||||
)}{' '}
|
||||
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>
|
||||
{t('here')}
|
||||
</ExternalLink>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-baseline justify-between mt-10 mb-5">
|
||||
<h2 className="text-2xl">Referral tiers</h2>
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col items-baseline justify-between mt-10 mb-5">
|
||||
<h2 className="text-2xl">{t('Referral tiers')}</h2>
|
||||
{ends && (
|
||||
<span className="text-base">
|
||||
<span className="text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
Program ends:
|
||||
</span>{' '}
|
||||
{ends}
|
||||
<span className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
{t('Program ends:')} {ends}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -90,14 +108,25 @@ export const TiersContainer = () => {
|
||||
{loading || !benefitTiers || benefitTiers.length === 0 ? (
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable data={benefitTiers} />
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
<div className="rounded-full bg-vega-clight-900 dark:bg-vega-cdark-900 p-1 w-8 h-8 text-center">
|
||||
{bt.tier}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Staking tiers */}
|
||||
<div className="flex flex-row items-baseline justify-between mb-5">
|
||||
<h2 className="text-2xl">Staking multipliers</h2>
|
||||
<h2 className="text-2xl">{t('Staking multipliers')}</h2>
|
||||
</div>
|
||||
<div className="flex flex-col mb-20 justify-items-stretch md:flex-row gap-5">
|
||||
<div className="mb-20 flex flex-col justify-items-stretch lg:flex-row gap-5">
|
||||
{loading || !stakingTiers || stakingTiers.length === 0 ? (
|
||||
<>
|
||||
<Loading variant="large" />
|
||||
@@ -134,25 +163,37 @@ const StakingTiers = ({
|
||||
|
||||
const TiersTable = ({
|
||||
data,
|
||||
windowLength,
|
||||
}: {
|
||||
data: Array<{
|
||||
tier: number;
|
||||
tierElement: ReactNode;
|
||||
commission: string;
|
||||
discount: string;
|
||||
volume: string;
|
||||
}>;
|
||||
windowLength?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'tier', displayName: 'Tier' },
|
||||
{ name: 'tierElement', displayName: t('Tier') },
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: 'Referrer commission',
|
||||
tooltip: 'A percentage of commission earned by the referrer',
|
||||
displayName: t('Referrer commission'),
|
||||
tooltip: t('A percentage of commission earned by the referrer'),
|
||||
},
|
||||
{ name: 'discount', displayName: 'Referrer trading discount' },
|
||||
{ name: 'volume', displayName: 'Min. trading volume' },
|
||||
{ name: 'discount', displayName: t('Referrer trading discount') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Min. trading volume %s',
|
||||
windowLength
|
||||
? t('(last %s epochs)', windowLength.toString())
|
||||
: undefined
|
||||
),
|
||||
},
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
data={data.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -1,39 +1,98 @@
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
|
||||
type TileProps = {
|
||||
variant?: 'rainbow' | 'default';
|
||||
};
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const Tile = ({
|
||||
variant = 'default',
|
||||
className,
|
||||
children,
|
||||
}: TileProps & HTMLAttributes<HTMLDivElement>) => {
|
||||
}: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'bg-rainbow p-[0.125rem]': variant === 'rainbow',
|
||||
[`border-2 ${BORDER_COLOR} p-0`]: variant === 'default',
|
||||
},
|
||||
'rounded-lg overflow-hidden relative'
|
||||
'rounded-lg overflow-hidden relative',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white',
|
||||
'p-6',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'bg-white dark:bg-vega-cdark-900 text-black dark:text-white rounded-[0.35rem] overflow-hidden':
|
||||
variant === 'rainbow',
|
||||
},
|
||||
'p-6',
|
||||
GRADIENT,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
return (
|
||||
<Tile>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="text-5xl text-left">{children}</div>
|
||||
{description && (
|
||||
<div className="text-sm text-left text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
|
||||
const FADE_OUT_STYLE = classNames(
|
||||
'after:w-5 after:h-full after:absolute after:top-0 after:right-0',
|
||||
'after:bg-gradient-to-l after:from-vega-clight-800 after:dark:from-vega-cdark-800 after:to-transparent'
|
||||
);
|
||||
|
||||
export const CodeTile = ({
|
||||
code,
|
||||
createdAt,
|
||||
className,
|
||||
}: {
|
||||
code: string;
|
||||
createdAt?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Your referral code')}
|
||||
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<Tooltip
|
||||
description={
|
||||
<div className="break-all">
|
||||
<span className="text-xl bg-rainbow bg-clip-text text-transparent">
|
||||
{code}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative bg-rainbow bg-clip-text text-transparent text-5xl overflow-hidden',
|
||||
FADE_OUT_STYLE
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<CopyWithTooltip text={code}>
|
||||
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
AppLoader,
|
||||
DocsLinks,
|
||||
NetworkLoader,
|
||||
NodeFailure,
|
||||
NodeGuard,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
|
||||
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
@@ -20,15 +20,10 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
VEGA_ENV,
|
||||
VEGA_WALLET_URL,
|
||||
VEGA_EXPLORER_URL,
|
||||
MAINTENANCE_PAGE,
|
||||
MOZILLA_EXTENSION_URL,
|
||||
CHROME_EXTENSION_URL,
|
||||
} = useEnvironment();
|
||||
|
||||
if (MAINTENANCE_PAGE) {
|
||||
return <MaintenancePage />;
|
||||
}
|
||||
|
||||
if (
|
||||
!VEGA_URL ||
|
||||
!VEGA_WALLET_URL ||
|
||||
@@ -37,7 +32,7 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
!MOZILLA_EXTENSION_URL ||
|
||||
!DocsLinks
|
||||
) {
|
||||
return null;
|
||||
return <AppLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -50,9 +45,14 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
>
|
||||
<NodeGuard
|
||||
skeleton={<AppLoader />}
|
||||
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
>
|
||||
<Web3Provider>
|
||||
<Web3Provider
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t(`Could not configure web3 provider`)} />
|
||||
}
|
||||
>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
|
||||
@@ -6,14 +6,20 @@ import {
|
||||
useWeb3ConnectStore,
|
||||
createDefaultProvider,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
export const Web3Provider = ({
|
||||
children,
|
||||
skeleton,
|
||||
failure,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
skeleton: ReactNode;
|
||||
failure: ReactNode;
|
||||
}) => {
|
||||
const { config, loading, error } = useEthereumConfig();
|
||||
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
|
||||
useEnvironment();
|
||||
@@ -49,23 +55,20 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
ETH_WALLET_MNEMONIC,
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
return <>{failure}</>;
|
||||
}
|
||||
|
||||
if (loading || !connectors.length) {
|
||||
return <>{skeleton}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={connectors}
|
||||
noDataCondition={(d) => {
|
||||
if (!d) return true;
|
||||
return d.length < 1;
|
||||
}}
|
||||
noDataMessage={t('Could not fetch Ethereum configuration')}
|
||||
<Web3ProviderInternal
|
||||
connectors={connectors}
|
||||
defaultProvider={defaultProvider}
|
||||
>
|
||||
<Web3ProviderInternal
|
||||
connectors={connectors}
|
||||
defaultProvider={defaultProvider}
|
||||
>
|
||||
<>{children}</>
|
||||
</Web3ProviderInternal>
|
||||
</AsyncRenderer>
|
||||
<>{children}</>
|
||||
</Web3ProviderInternal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
query DiscountPrograms {
|
||||
currentReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
currentVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type DiscountProgramsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgram?: { __typename?: 'CurrentReferralProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string }> } | null, currentVolumeDiscountProgram?: { __typename?: 'VolumeDiscountProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } | null };
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const DiscountProgramsDocument = gql`
|
||||
query DiscountPrograms {
|
||||
currentReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
currentVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useDiscountProgramsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useDiscountProgramsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useDiscountProgramsQuery` 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 } = useDiscountProgramsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDiscountProgramsQuery(baseOptions?: Apollo.QueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
|
||||
}
|
||||
export function useDiscountProgramsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
|
||||
}
|
||||
export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgramsQuery>;
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useFeesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useFeesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useFeesQuery` 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 } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useFeesQuery(baseOptions: Apollo.QueryHookOptions<FeesQuery, FeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
|
||||
}
|
||||
export function useFeesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FeesQuery, FeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
|
||||
}
|
||||
export type FeesQueryHookResult = ReturnType<typeof useFeesQuery>;
|
||||
export type FeesLazyQueryHookResult = ReturnType<typeof useFeesLazyQuery>;
|
||||
export type FeesQueryResult = Apollo.QueryResult<FeesQuery, FeesQueryVariables>;
|
||||
@@ -0,0 +1,36 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const FeeCard = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
loading = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-4 bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full lg:col-auto',
|
||||
'rounded-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <FeeCardLoader /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeCardLoader = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-full h-5 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
<div className="w-3/4 h-6 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { CurrentVolume, TradingFees } from './fees-container';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
|
||||
describe('TradingFees', () => {
|
||||
it('renders correct fee data', () => {
|
||||
const makerFee = 0.01;
|
||||
const infraFee = 0.01;
|
||||
const minLiqFee = 0.1;
|
||||
const maxLiqFee = 0.3;
|
||||
|
||||
const referralDiscount = 0.01;
|
||||
const volumeDiscount = 0.01;
|
||||
|
||||
const makerBigNum = new BigNumber(makerFee);
|
||||
const infraBigNum = new BigNumber(infraFee);
|
||||
const minLiqBigNum = new BigNumber(minLiqFee);
|
||||
const maxLiqBigNum = new BigNumber(maxLiqFee);
|
||||
const referralBigNum = new BigNumber(referralDiscount);
|
||||
const volumeBigNum = new BigNumber(volumeDiscount);
|
||||
|
||||
const props = {
|
||||
params: {
|
||||
market_fee_factors_makerFee: makerFee.toString(),
|
||||
market_fee_factors_infrastructureFee: infraFee.toString(),
|
||||
},
|
||||
markets: [
|
||||
{ fees: { factors: { liquidityFee: minLiqFee.toString() } } },
|
||||
{ fees: { factors: { liquidityFee: '0.2' } } },
|
||||
{ fees: { factors: { liquidityFee: maxLiqFee.toString() } } },
|
||||
],
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
};
|
||||
render(<TradingFees {...props} />);
|
||||
|
||||
const minFee = formatPercentage(
|
||||
makerBigNum.plus(infraFee).plus(minLiqFee).toNumber()
|
||||
);
|
||||
const maxFee = formatPercentage(
|
||||
makerBigNum.plus(infraFee).plus(maxLiqFee).toNumber()
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Total fee before discount').nextElementSibling
|
||||
).toHaveTextContent(`${minFee}%-${maxFee}%`);
|
||||
expect(
|
||||
screen.getByText('Infrastructure').nextElementSibling
|
||||
).toHaveTextContent(formatPercentage(infraFee) + '%');
|
||||
expect(screen.getByText('Maker').nextElementSibling).toHaveTextContent(
|
||||
formatPercentage(makerFee) + '%'
|
||||
);
|
||||
|
||||
const minAdjustedFees = formatPercentage(
|
||||
getAdjustedFee(
|
||||
[makerBigNum, infraBigNum, minLiqBigNum],
|
||||
[referralBigNum, volumeBigNum]
|
||||
)
|
||||
);
|
||||
|
||||
const maxAdjustedFees = formatPercentage(
|
||||
getAdjustedFee(
|
||||
[makerBigNum, infraBigNum, maxLiqBigNum],
|
||||
[referralBigNum, volumeBigNum]
|
||||
)
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('adjusted-fees')).toHaveTextContent(
|
||||
`${minAdjustedFees}%-${maxAdjustedFees}%`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CurerntVolume', () => {
|
||||
it('renders the required amount for the next tier', () => {
|
||||
const windowLengthVolume = 1500;
|
||||
const nextTierVolume = 2000;
|
||||
|
||||
const props = {
|
||||
tiers: [
|
||||
{ minimumRunningNotionalTakerVolume: '1000' },
|
||||
{ minimumRunningNotionalTakerVolume: nextTierVolume.toString() },
|
||||
{ minimumRunningNotionalTakerVolume: '3000' },
|
||||
],
|
||||
tierIndex: 0,
|
||||
windowLengthVolume,
|
||||
epochs: 5,
|
||||
};
|
||||
|
||||
render(<CurrentVolume {...props} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(formatNumber(windowLengthVolume)).nextElementSibling
|
||||
).toHaveTextContent(`Past ${props.epochs} epochs`);
|
||||
|
||||
expect(
|
||||
screen.getByText(formatNumber(nextTierVolume - windowLengthVolume))
|
||||
.nextElementSibling
|
||||
).toHaveTextContent('Required for next tier');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,490 @@
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import minBy from 'lodash/minBy';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useNetworkParams,
|
||||
NetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||
import { FeeCard } from './fees-card';
|
||||
import { MarketFees } from './market-fees';
|
||||
import { Stat } from './stat';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { Table, Td, Th, THead, Tr } from './table';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const FeesContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { params, loading: paramsLoading } = useNetworkParams([
|
||||
NetworkParams.market_fee_factors_makerFee,
|
||||
NetworkParams.market_fee_factors_infrastructureFee,
|
||||
]);
|
||||
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
|
||||
const volumeDiscountEpochs =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountEpochs =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs,
|
||||
referralDiscountEpochs,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
});
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
feesData?.volumeDiscountStats,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
const {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
const isConnected = Boolean(pubKey);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
{isConnected && (
|
||||
<>
|
||||
<FeeCard
|
||||
title={t('My trading fees')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<TradingFees
|
||||
params={params}
|
||||
markets={markets}
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Total discount')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<TotalDiscount
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('My current volume')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<CurrentVolume
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
epochs={volumeDiscountEpochs}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountEpochs}
|
||||
/>
|
||||
</FeeCard>
|
||||
</>
|
||||
)}
|
||||
<FeeCard
|
||||
title={t('Volume discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<VolumeTiers
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
lastEpochVolume={volumeInWindow}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Referral discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<ReferralTiers
|
||||
tiers={referralTiers}
|
||||
tierIndex={referralTierIndex}
|
||||
epochsInSet={epochsInSet}
|
||||
referralVolumeInWindow={referralVolumeInWindow}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Liquidity fees')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
>
|
||||
<MarketFees
|
||||
markets={markets}
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TradingFees = ({
|
||||
params,
|
||||
markets,
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
}: {
|
||||
params: {
|
||||
market_fee_factors_infrastructureFee: string;
|
||||
market_fee_factors_makerFee: string;
|
||||
};
|
||||
markets: Array<{ fees: { factors: { liquidityFee: string } } }> | null;
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
}) => {
|
||||
const referralDiscountBigNum = new BigNumber(referralDiscount);
|
||||
const volumeDiscountBigNum = new BigNumber(volumeDiscount);
|
||||
|
||||
// Show min and max liquidity fees from all markets
|
||||
const minLiq = minBy(markets, (m) => Number(m.fees.factors.liquidityFee));
|
||||
const maxLiq = maxBy(markets, (m) => Number(m.fees.factors.liquidityFee));
|
||||
|
||||
const total = new BigNumber(params.market_fee_factors_makerFee).plus(
|
||||
new BigNumber(params.market_fee_factors_infrastructureFee)
|
||||
);
|
||||
|
||||
const adjustedTotal = getAdjustedFee(
|
||||
[total],
|
||||
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||
);
|
||||
|
||||
let minTotal;
|
||||
let maxTotal;
|
||||
|
||||
let minAdjustedTotal;
|
||||
let maxAdjustedTotal;
|
||||
|
||||
if (minLiq && maxLiq) {
|
||||
const minLiqFee = new BigNumber(minLiq.fees.factors.liquidityFee);
|
||||
const maxLiqFee = new BigNumber(maxLiq.fees.factors.liquidityFee);
|
||||
|
||||
minTotal = total.plus(minLiqFee);
|
||||
maxTotal = total.plus(maxLiqFee);
|
||||
|
||||
minAdjustedTotal = getAdjustedFee(
|
||||
[total, minLiqFee],
|
||||
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||
);
|
||||
|
||||
maxAdjustedTotal = getAdjustedFee(
|
||||
[total, maxLiqFee],
|
||||
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pt-6 leading-none">
|
||||
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
|
||||
maxAdjustedTotal
|
||||
)}%`
|
||||
: `${formatPercentage(adjustedTotal)}%`}
|
||||
</p>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left text-default">
|
||||
{t('Total fee before discount')}
|
||||
</th>
|
||||
<td className="text-right text-default">
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
? `${formatPercentage(
|
||||
minTotal.toNumber()
|
||||
)}%-${formatPercentage(maxTotal.toNumber())}%`
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Infrastructure')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(
|
||||
Number(params.market_fee_factors_infrastructureFee)
|
||||
)}
|
||||
%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Maker')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Liquidity')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
{'-'}
|
||||
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CurrentVolume = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
windowLengthVolume,
|
||||
epochs,
|
||||
}: {
|
||||
tiers: Array<{ minimumRunningNotionalTakerVolume: string }>;
|
||||
tierIndex: number;
|
||||
windowLengthVolume: number;
|
||||
epochs: number;
|
||||
}) => {
|
||||
const nextTier = tiers[tierIndex + 1];
|
||||
const requiredForNextTier = nextTier
|
||||
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past %s epochs', epochs.toString())}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
<Stat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferralBenefits = ({
|
||||
epochsInSet,
|
||||
setRunningNotionalTakerVolume,
|
||||
epochs,
|
||||
}: {
|
||||
epochsInSet: number;
|
||||
setRunningNotionalTakerVolume: number;
|
||||
epochs: number;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
text={t(
|
||||
'Combined running notional over the %s epochs',
|
||||
epochs.toString()
|
||||
)}
|
||||
/>
|
||||
<Stat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TotalDiscount = ({
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
}: {
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
|
||||
highlight={true}
|
||||
/>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Volume discount')}</th>
|
||||
<td className="text-right">{formatPercentage(volumeDiscount)}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Referral discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(referralDiscount)}%
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VolumeTiers = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
lastEpochVolume,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
volumeDiscountFactor: string;
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>;
|
||||
tierIndex: number;
|
||||
lastEpochVolume: number;
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('My volume (last epoch)')}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((tier, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>
|
||||
{formatPercentage(Number(tier.volumeDiscountFactor))}%
|
||||
</Td>
|
||||
<Td>
|
||||
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||
</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferralTiers = ({
|
||||
tiers,
|
||||
tierIndex,
|
||||
epochsInSet,
|
||||
referralVolumeInWindow,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
referralDiscountFactor: string;
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>;
|
||||
tierIndex: number;
|
||||
epochsInSet: number;
|
||||
referralVolumeInWindow: number;
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">{t('No referral program active')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('Required epochs')}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(
|
||||
t.minimumRunningNotionalTakerVolume
|
||||
);
|
||||
let unlocksIn = null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const YourTier = () => {
|
||||
return (
|
||||
<span className="px-4 py-1.5 rounded-xl bg-rainbow whitespace-nowrap text-white">
|
||||
{t('Your tier')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { FeesContainer } from './fees-container';
|
||||
@@ -0,0 +1,104 @@
|
||||
import compact from 'lodash/compact';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||
import { AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { MarketCodeCell } from '../../client-pages/markets/market-code-cell';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
const feesTableColumnDefs = [
|
||||
{ field: 'code', cellRenderer: 'MarketCodeCell' },
|
||||
{
|
||||
field: 'feeAfterDiscount',
|
||||
headerName: t('Total fee after discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'infraFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'makerFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'liquidityFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'totalFee',
|
||||
headerName: t('Total fee before discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
];
|
||||
|
||||
const feesTableDefaultColDef = {
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
const components = {
|
||||
MarketCodeCell,
|
||||
};
|
||||
|
||||
export const MarketFees = ({
|
||||
markets,
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
}: {
|
||||
markets: MarketMaybeWithDataAndCandles[] | null;
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
}) => {
|
||||
const navigateWithMeta = useNavigateWithMeta();
|
||||
|
||||
const rows = compact(markets || []).map((m) => {
|
||||
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
|
||||
const makerFee = new BigNumber(m.fees.factors.makerFee);
|
||||
const liquidityFee = new BigNumber(m.fees.factors.liquidityFee);
|
||||
const totalFee = infraFee.plus(makerFee).plus(liquidityFee);
|
||||
|
||||
const feeAfterDiscount = getAdjustedFee(
|
||||
[infraFee, makerFee, liquidityFee],
|
||||
[new BigNumber(referralDiscount), new BigNumber(volumeDiscount)]
|
||||
);
|
||||
|
||||
return {
|
||||
id: m.id,
|
||||
code: m.tradableInstrument.instrument.code,
|
||||
productType: m.tradableInstrument.instrument.product.__typename,
|
||||
infraFee: formatPercentage(infraFee.toNumber()),
|
||||
makerFee: formatPercentage(makerFee.toNumber()),
|
||||
liquidityFee: formatPercentage(liquidityFee.toNumber()),
|
||||
totalFee: formatPercentage(totalFee.toNumber()),
|
||||
feeAfterDiscount: formatPercentage(feeAfterDiscount),
|
||||
parentMarketID: m.parentMarketID,
|
||||
successorMarketID: m.successorMarketID,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="border rounded-sm border-default">
|
||||
<AgGrid
|
||||
columnDefs={feesTableColumnDefs}
|
||||
rowData={rows}
|
||||
getRowId={({ data }) => data.id}
|
||||
defaultColDef={feesTableDefaultColDef}
|
||||
domLayout="autoHeight"
|
||||
components={components}
|
||||
rowHeight={45}
|
||||
rowClass="cursor-pointer"
|
||||
onRowClicked={({ data, event }) => {
|
||||
navigateWithMeta(
|
||||
Links.MARKET(data.id),
|
||||
// @ts-ignore metaKey and ctrlKey exist
|
||||
event.metaKey || event.ctrlKey
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Stat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
}: {
|
||||
value: string | number;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<p className="pt-3 leading-none first:pt-6">
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{text && (
|
||||
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const cellClass = 'px-4 py-2 text-xs font-normal text-left last:text-right';
|
||||
|
||||
export const Th = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<th className={classNames(cellClass, 'text-secondary leading-none py-3')}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
export const Td = ({ children }: { children?: ReactNode }) => {
|
||||
return <th className={cellClass}>{children}</th>;
|
||||
};
|
||||
|
||||
export const Tr = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<tr className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700">
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export const Table = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<table className="w-full border border-separate rounded-sm border-spacing-0 border-vega-clight-600 dark:border-vega-cdark-600">
|
||||
{children}
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export const THead = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<thead className="border-b bg-vega-clight-700 dark:bg-vega-cdark-700 border-vega-clight-600 dark:border-vega-cdark-600">
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const setStats = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
};
|
||||
|
||||
const program = {
|
||||
windowLength: 5,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 4,
|
||||
minimumRunningNotionalTakerVolume: '100',
|
||||
referralDiscountFactor: '0.01',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '200',
|
||||
referralDiscountFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 8,
|
||||
minimumRunningNotionalTakerVolume: '300',
|
||||
referralDiscountFactor: '0.1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useReferralStats());
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: [],
|
||||
epochsInSet: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ joinedAt: 2, index: -1 },
|
||||
{ joinedAt: 3, index: -1 },
|
||||
{ joinedAt: 4, index: 0 },
|
||||
{ joinedAt: 5, index: 0 },
|
||||
{ joinedAt: 6, index: 1 },
|
||||
{ joinedAt: 7, index: 1 },
|
||||
{ joinedAt: 8, index: 2 },
|
||||
{ joinedAt: 9, index: 2 },
|
||||
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '100000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: Number(epoch.id) - obj.joinedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '50', index: -1 },
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '999', index: 2 },
|
||||
])('volume: $volume should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers,
|
||||
epochsInSet: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
|
||||
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
|
||||
|
||||
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
|
||||
|
||||
const referralDiscount = Number(referralStats?.discountFactor || 0);
|
||||
const referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const statsList = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const program = {
|
||||
windowLength: 5,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '100',
|
||||
volumeDiscountFactor: '0.01',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '200',
|
||||
volumeDiscountFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '300',
|
||||
volumeDiscountFactor: '0.1',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
volumeInWindow: Number(stats.runningVolume),
|
||||
volumeTierIndex: 1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '350', index: 2 },
|
||||
])('returns index: $index for the running volume: $volume', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVolumeStats(statsA, program));
|
||||
expect(result.current.volumeTierIndex).toBe(obj.index);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getVolumeTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useVolumeStats = (
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!stats || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeInWindow: 0,
|
||||
volumeTiers,
|
||||
};
|
||||
}
|
||||
|
||||
const volumeStats = compact(stats.edges).map((e) => e.node);
|
||||
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
|
||||
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
|
||||
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
volumeTierIndex,
|
||||
volumeInWindow,
|
||||
volumeTiers,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getAdjustedFee } from './utils';
|
||||
|
||||
describe('getAdjustedFee', () => {
|
||||
it('simple', () => {
|
||||
const volumeDiscount = 0.5;
|
||||
const referralDiscount = 0.5;
|
||||
|
||||
const infraFee = 0.1;
|
||||
const makerFee = 0.1;
|
||||
const liqFee = 0.1;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
|
||||
// 0.5 * 0.5 = 0.25
|
||||
// 1 - 0.25 = 0.75
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
const infraFee = 0.0005;
|
||||
const makerFee = 0.0002;
|
||||
const liqFee = 0.01;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// formula for calculating adjusted fees
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user