Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53f9baff8 | ||
|
|
371fcb1f77 | ||
|
|
6333169bf1 | ||
|
|
ae630d53db | ||
|
|
c1ea5631ea | ||
|
|
f350bafb56 | ||
|
|
db4afadffa | ||
|
|
9c58e194c8 | ||
|
|
2b4736c585 | ||
|
|
6047178547 | ||
|
|
031c687107 | ||
|
|
1dc50bd658 | ||
|
|
f5868b6f1b | ||
|
|
20e1522088 | ||
|
|
ae02b153f8 | ||
|
|
fee5f862a7 | ||
|
|
c25362b084 | ||
|
|
7c105d2c6b | ||
|
|
2dfcb02d1e | ||
|
|
a98ab775c3 | ||
|
|
097bf7c08c | ||
|
|
236e35a92b | ||
|
|
abf84b9d45 | ||
|
|
6d2a2ea0a0 | ||
|
|
6ac79e9f6d | ||
|
|
835ee64243 | ||
|
|
e7c3b5054c |
+1
-1
@@ -1,2 +1,2 @@
|
||||
* @vegaprotocol/frontend @vegaprotocol/frontend-qa
|
||||
* @vegaprotocol/frontend
|
||||
*.graphql @vegaprotocol/core
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -106,6 +106,7 @@ const Web3Container = ({
|
||||
useEthWithdrawApprovalsManager();
|
||||
return null;
|
||||
};
|
||||
|
||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||
store.connectors,
|
||||
store.initialize,
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
|
||||
+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();
|
||||
});
|
||||
});
|
||||
|
||||
+20
-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);
|
||||
|
||||
@@ -265,14 +273,21 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
{GovernanceTransferKindMapping[details.kind.__typename]}{' '}
|
||||
{t('transfer from')}{' '}
|
||||
<Lozenge>
|
||||
{truncateMiddle(details.source) || t(details.sourceType)}
|
||||
{details.source
|
||||
? truncateMiddle(details.source)
|
||||
: t(details.sourceType)}
|
||||
</Lozenge>{' '}
|
||||
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
|
||||
{t('to')}{' '}
|
||||
<Lozenge>
|
||||
{details.destination
|
||||
? truncateMiddle(details.destination)
|
||||
: t(details.destinationType)}
|
||||
</Lozenge>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelTransferSummary = ({
|
||||
export const CancelTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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', windowLength: number, endOfProgram: string, 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', windowLength: number, endOfProgram: string, 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 {
|
||||
|
||||
@@ -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',
|
||||
@@ -33,6 +34,10 @@ export const TOP_LEVEL_ROUTES = [
|
||||
name: 'Rewards',
|
||||
path: Routes.REWARDS,
|
||||
},
|
||||
{
|
||||
name: 'Restricted',
|
||||
path: Routes.RESTRICTED,
|
||||
},
|
||||
];
|
||||
|
||||
export const TOKEN_DROPDOWN_ROUTES = [
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -123,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>
|
||||
@@ -136,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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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')}
|
||||
|
||||
@@ -1,21 +1,42 @@
|
||||
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';
|
||||
|
||||
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 navigate = useNavigate();
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
|
||||
const [status, setStatus] = useState<
|
||||
'requested' | 'failed' | 'successful' | null
|
||||
>(null);
|
||||
@@ -27,11 +48,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');
|
||||
@@ -65,9 +92,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
|
||||
: 'Your code has been rejected',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -99,10 +130,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">
|
||||
@@ -117,10 +159,23 @@ export const ApplyCodeForm = () => {
|
||||
}
|
||||
|
||||
const getButtonProps = () => {
|
||||
if (isReadOnly || !pubKey) {
|
||||
if (!pubKey) {
|
||||
return {
|
||||
disabled: false,
|
||||
children: 'Connect wallet',
|
||||
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
onClick: ((event) => {
|
||||
event.preventDefault();
|
||||
openWalletDialog();
|
||||
}) as MouseEventHandler,
|
||||
};
|
||||
}
|
||||
|
||||
if (isReadOnly) {
|
||||
return {
|
||||
disabled: true,
|
||||
children: 'Apply',
|
||||
children: 'Apply a code',
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,43 +183,63 @@ export const ApplyCodeForm = () => {
|
||||
return {
|
||||
disabled: true,
|
||||
children: 'Confirm in wallet...',
|
||||
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
children: 'Apply',
|
||||
children: '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">
|
||||
Apply a referral code
|
||||
</h3>
|
||||
<p className="mb-4 text-center text-base">
|
||||
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">Your referral code</span>
|
||||
<Input
|
||||
hasError={Boolean(errors.code)}
|
||||
{...register('code', {
|
||||
required: '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">You are joining</h2>
|
||||
<Statistics data={previewData} 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,54 @@ 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';
|
||||
|
||||
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
|
||||
</h3>
|
||||
<p className="mb-6 text-center">
|
||||
<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">Create a referral code</h3>
|
||||
<p className="mb-4 text-center text-base">
|
||||
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 ? 'Create a referral code' : 'Connect wallet'}
|
||||
</RainbowButton>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
title="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 +74,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');
|
||||
@@ -156,16 +140,29 @@ const CreateCodeDialog = ({
|
||||
return {
|
||||
children: '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>You must be connected to the Vega wallet.</p>
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={() => setDialogOpen(false)}
|
||||
>
|
||||
Close
|
||||
</TradingButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStakeAvailable < requiredStake) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
@@ -215,10 +212,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}>
|
||||
About the referral program
|
||||
</ExternalLink>
|
||||
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
|
||||
Disclaimer
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</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!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
refereeId
|
||||
joinedAt
|
||||
atEpoch
|
||||
totalRefereeNotionalTakerVolume
|
||||
totalRefereeGeneratedRewards
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetStats(setId: $code, epoch: $epoch) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
partyId
|
||||
discountFactor
|
||||
rewardFactor
|
||||
epochNotionalTakerVolume
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
aggregationDays?: 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!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
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'
|
||||
* aggregationDays: // value for 'aggregationDays'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
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,61 @@
|
||||
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 } } | 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
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 }
|
||||
) & {
|
||||
aggregationDays?: 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,
|
||||
aggregationDays:
|
||||
args.aggregationDays != null
|
||||
? args.aggregationDays
|
||||
: 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,66 +1,44 @@
|
||||
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 { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { 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';
|
||||
|
||||
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 { data: referee } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
const { data: referrer } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
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" />;
|
||||
}
|
||||
@@ -72,42 +50,260 @@ export const ReferralStatistics = () => {
|
||||
return <CreateCodeContainer />;
|
||||
};
|
||||
|
||||
const Statistics = ({
|
||||
export const Statistics = ({
|
||||
data,
|
||||
as,
|
||||
}: {
|
||||
data: ReferralData;
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
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 { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data.code,
|
||||
},
|
||||
skip: !data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
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 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);
|
||||
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="Base commission rate">
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
const stakingMultiplierTile = (
|
||||
<StatTile
|
||||
title="Staking multiplier"
|
||||
description={`(${addDecimalsFormatNumber(
|
||||
stakeAvailable?.toString() || 0,
|
||||
18
|
||||
)} $VEGA staked)`}
|
||||
>
|
||||
<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} />
|
||||
{multiplier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const finalCommissionTile = (
|
||||
<StatTile title="Final commission rate">
|
||||
{finalCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
const numberOfTradersValue = data.referees.length;
|
||||
const numberOfTradersTile = (
|
||||
<StatTile title="Number of traders">{numberOfTradersValue}</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = <CodeTile code={data?.code} />;
|
||||
const createdAtTile = (
|
||||
<StatTile title="Created at">
|
||||
<span className="text-3xl">
|
||||
{getDateFormat().format(new Date(data.createdAt))}
|
||||
</span>
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const totalCommissionValue = data.referees
|
||||
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile title="Total commission (last 30 days)" description="(Quantum)">
|
||||
{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}
|
||||
{createdAtTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title="Current tier">
|
||||
{currentBenefitTierValue?.tier || '-'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<StatTile title="Discount">{discountFactorValue * 100}%</StatTile>
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile title="Combined volume">
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const epochsTile = <StatTile title="Epochs in set">{epochsValue}</StatTile>;
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile title="Volume to next tier">
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '-'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile title="Epochs to next tier">
|
||||
{nextBenefitTierEpochsValue <= 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="text-2xl mb-5">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: 'Trader' },
|
||||
{ name: 'joined', displayName: 'Date Joined' },
|
||||
{ name: 'volume', displayName: 'Volume (last 30 days)' },
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: 'Commission earned (last 30 days)',
|
||||
},
|
||||
]}
|
||||
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,35 +7,82 @@ 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}>
|
||||
I want a code
|
||||
</TabLink>
|
||||
<TabLink to={Routes.REFERRALS_APPLY_CODE}>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>Something went wrong</p>
|
||||
<span className="text-xs">{error.message}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TiersContainer />
|
||||
@@ -47,7 +95,7 @@ export const Referrals = () => {
|
||||
<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} />
|
||||
|
||||
@@ -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,9 @@ 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';
|
||||
|
||||
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
<div
|
||||
@@ -38,51 +40,63 @@ 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">
|
||||
<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">
|
||||
Stake a minimum of {minimumStakedTokens} $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">
|
||||
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)}>here</ExternalLink>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-baseline justify-between mt-10 mb-5">
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col items-baseline justify-between mt-10 mb-5">
|
||||
<h2 className="text-2xl">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">
|
||||
Program ends: {ends}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -90,14 +104,24 @@ export const TiersContainer = () => {
|
||||
{loading || !benefitTiers || benefitTiers.length === 0 ? (
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable data={benefitTiers} />
|
||||
<TiersTable
|
||||
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>
|
||||
</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" />
|
||||
@@ -137,6 +161,7 @@ const TiersTable = ({
|
||||
}: {
|
||||
data: Array<{
|
||||
tier: number;
|
||||
tierElement: ReactNode;
|
||||
commission: string;
|
||||
discount: string;
|
||||
volume: string;
|
||||
@@ -145,7 +170,7 @@ const TiersTable = ({
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'tier', displayName: 'Tier' },
|
||||
{ name: 'tierElement', displayName: 'Tier' },
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: 'Referrer commission',
|
||||
@@ -153,6 +178,7 @@ const TiersTable = ({
|
||||
},
|
||||
{ name: 'discount', displayName: 'Referrer trading discount' },
|
||||
{ name: 'volume', displayName: 'Min. trading volume' },
|
||||
{ name: 'epochs', displayName: 'Min. epochs' },
|
||||
]}
|
||||
data={data.map((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -1,39 +1,92 @@
|
||||
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';
|
||||
|
||||
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,
|
||||
className,
|
||||
}: {
|
||||
code: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<StatTile title="Your referral code">
|
||||
<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">Copy</span>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,10 +9,36 @@ import { useMemo } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DateRange,
|
||||
calculateStartDate,
|
||||
} from '../../client-pages/portfolio/account-history-container';
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const DateRange = {
|
||||
RANGE_1D: '1D',
|
||||
RANGE_7D: '7D',
|
||||
RANGE_1M: '1M',
|
||||
RANGE_3M: '3M',
|
||||
RANGE_1Y: '1Y',
|
||||
RANGE_YTD: 'YTD',
|
||||
RANGE_ALL: 'All',
|
||||
};
|
||||
|
||||
export const FundingContainer = ({ marketId }: { marketId: string }) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
|
||||
@@ -18,7 +18,7 @@ import BigNumber from 'bignumber.js';
|
||||
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -30,12 +30,14 @@ interface Props {
|
||||
marketId?: string;
|
||||
noUpdate?: boolean;
|
||||
assetDecimals: number;
|
||||
quantum: string;
|
||||
}
|
||||
|
||||
export const MarketLiquiditySupplied = ({
|
||||
marketId,
|
||||
assetDecimals,
|
||||
noUpdate = false,
|
||||
quantum,
|
||||
}: Props) => {
|
||||
const [market, setMarket] = useState<MarketData>();
|
||||
const { params } = useNetworkParams([
|
||||
@@ -79,11 +81,12 @@ export const MarketLiquiditySupplied = ({
|
||||
});
|
||||
|
||||
const supplied = market?.suppliedStake
|
||||
? addDecimalsFormatNumber(
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
new BigNumber(market?.suppliedStake)
|
||||
.multipliedBy(stakeToCcyVolume || 1)
|
||||
.toString(),
|
||||
assetDecimals
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-';
|
||||
|
||||
@@ -108,9 +111,10 @@ export const MarketLiquiditySupplied = ({
|
||||
<span>{t('Supplied stake')}</span>
|
||||
<span>
|
||||
{market?.suppliedStake
|
||||
? addDecimalsFormatNumber(
|
||||
new BigNumber(market?.suppliedStake).toString(),
|
||||
assetDecimals
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
market?.suppliedStake,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
@@ -119,9 +123,10 @@ export const MarketLiquiditySupplied = ({
|
||||
<span>{t('Target stake')}</span>
|
||||
<span>
|
||||
{market?.targetStake
|
||||
? addDecimalsFormatNumber(
|
||||
new BigNumber(market?.targetStake).toString(),
|
||||
assetDecimals
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
market?.targetStake,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
|
||||
@@ -21,6 +21,7 @@ const market = {
|
||||
} as unknown as Market;
|
||||
|
||||
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
|
||||
let mockDataMarketState: Market['state'] | null = null;
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn().mockImplementation((args) => {
|
||||
@@ -43,6 +44,12 @@ jest.mock('@vegaprotocol/utils', () => ({
|
||||
let mockCandles = {};
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketState: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataMarketState,
|
||||
}
|
||||
: { data: undefined },
|
||||
useSuccessorMarket: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
@@ -81,30 +88,6 @@ describe('MarketSuccessorBanner', () => {
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('successor market not in continuous mode', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
tradingMode: Types.MarketTradingMode.TRADING_MODE_NO_TRADING,
|
||||
};
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('successor market is not active', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
state: Types.MarketState.STATE_PENDING,
|
||||
};
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should be displayed', () => {
|
||||
@@ -120,6 +103,17 @@ describe('MarketSuccessorBanner', () => {
|
||||
).toHaveAttribute('href', '/#/markets/successorMarketID');
|
||||
});
|
||||
|
||||
it('no successor market data, market settled', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
mockDataMarketState = Types.MarketState.STATE_SETTLED;
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally successor volume', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
@@ -137,7 +131,9 @@ describe('MarketSuccessorBanner', () => {
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(screen.getByText('has 101.367 24h vol.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('has a 24h trading volume of 101.367')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally duration', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useMarketState,
|
||||
useSuccessorMarket,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
@@ -29,7 +30,9 @@ export const MarketSuccessorBanner = ({
|
||||
}: {
|
||||
market: Market | null;
|
||||
}) => {
|
||||
const { data: successorData } = useSuccessorMarket(market?.id);
|
||||
const { data: marketState } = useMarketState(market?.id);
|
||||
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
|
||||
const { data: successorData, loading } = useSuccessorMarket(market?.id);
|
||||
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
@@ -45,11 +48,6 @@ export const MarketSuccessorBanner = ({
|
||||
? intervalToDuration({ start: new Date(), end: expiry })
|
||||
: null;
|
||||
|
||||
const isInContinuesMode =
|
||||
successorData?.state === Types.MarketState.STATE_ACTIVE &&
|
||||
successorData?.tradingMode ===
|
||||
Types.MarketTradingMode.TRADING_MODE_CONTINUOUS;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorData?.id,
|
||||
});
|
||||
@@ -66,7 +64,7 @@ export const MarketSuccessorBanner = ({
|
||||
)
|
||||
: null;
|
||||
|
||||
if (isInContinuesMode && visible) {
|
||||
if (!loading && (isSettled || successorData) && visible) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
@@ -74,34 +72,47 @@ export const MarketSuccessorBanner = ({
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase mb-1">
|
||||
{t('This market has been succeeded')}
|
||||
</div>
|
||||
<div>
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in %s.', [
|
||||
formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
])}
|
||||
</span>
|
||||
)}{' '}
|
||||
{t('The successor market')}{' '}
|
||||
<ExternalLink href={`/#/markets/${successorData?.id}`}>
|
||||
{successorData?.tradableInstrument.instrument.name}
|
||||
</ExternalLink>
|
||||
{successorVolume && (
|
||||
<span> {t('has %s 24h vol.', [successorVolume])}</span>
|
||||
)}
|
||||
<div className="uppercase">
|
||||
{successorData
|
||||
? t('This market has been succeeded')
|
||||
: t('This market has been settled')}
|
||||
</div>
|
||||
{(duration || successorData) && (
|
||||
<div className="mt-1">
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in %s.', [
|
||||
formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
])}
|
||||
</span>
|
||||
)}
|
||||
{successorData && (
|
||||
<>
|
||||
{' '}
|
||||
{t('The successor market')}
|
||||
{!successorVolume ? ' is ' : ' '}
|
||||
<ExternalLink href={`/#/markets/${successorData?.id}`}>
|
||||
{successorData?.tradableInstrument.instrument.name}
|
||||
</ExternalLink>
|
||||
{successorVolume && (
|
||||
<span>
|
||||
{' '}
|
||||
{t('has a 24h trading volume of %s', [successorVolume])}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import type { NewMarketSuccessorFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useMarketViewProposals } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
marketViewProposalsDataProvider,
|
||||
type NewMarketSuccessorFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
@@ -9,17 +12,20 @@ import {
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
export const MarketSuccessorProposalBanner = ({
|
||||
marketId,
|
||||
}: {
|
||||
marketId?: string;
|
||||
}) => {
|
||||
const proposals = useMarketViewProposals({
|
||||
const { data: proposals } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip: !marketId,
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
typename: 'NewMarket',
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
},
|
||||
});
|
||||
|
||||
const successors =
|
||||
|
||||
@@ -20,10 +20,13 @@ const marketMock = {
|
||||
},
|
||||
} as Market;
|
||||
|
||||
const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: { inState: Types.ProposalState.STATE_PASSED },
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -31,7 +34,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'first-id',
|
||||
id: '1',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
@@ -56,7 +59,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'second-id',
|
||||
id: '2',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
@@ -84,7 +87,99 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
},
|
||||
},
|
||||
};
|
||||
const mocks: MockedResponse[] = [proposalMock];
|
||||
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '3',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market three name',
|
||||
code: 'Market three',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '4',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-11T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market three name',
|
||||
code: 'Market three',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '5',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-4',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market four name',
|
||||
code: 'Market four',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
|
||||
|
||||
describe('MarketTerminationBanner', () => {
|
||||
beforeAll(() => {
|
||||
@@ -108,4 +203,28 @@ describe('MarketTerminationBanner', () => {
|
||||
screen.getByTestId('termination-warning-banner-market-1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render link to proposals', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(screen.getByText('View proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render link to proposal', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(screen.getByText('View proposal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,67 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMarketViewProposals } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { getQuoteName } from '@vegaprotocol/markets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
DApp,
|
||||
TOKEN_PROPOSAL,
|
||||
TOKEN_PROPOSALS,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
const filterProposals = (
|
||||
data: MarketViewProposalFieldsFragment[] | null,
|
||||
marketId: string,
|
||||
now: number
|
||||
) =>
|
||||
sortBy(
|
||||
(data || []).filter(
|
||||
(item) =>
|
||||
item.terms.change.__typename === 'UpdateMarketState' &&
|
||||
item.terms.change.market.id === marketId &&
|
||||
item.terms.change.updateType ===
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
||||
item.terms.enactmentDatetime &&
|
||||
new Date(item.terms.enactmentDatetime).getTime() > now
|
||||
),
|
||||
(item) => item.terms.enactmentDatetime
|
||||
);
|
||||
|
||||
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
||||
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
||||
const date = format(enactmentDatetime, 'dd MMMM');
|
||||
const duration = formatDuration(
|
||||
intervalToDuration({
|
||||
start: new Date(),
|
||||
end: enactmentDatetime,
|
||||
}),
|
||||
{
|
||||
format: ['days', 'hours'],
|
||||
}
|
||||
);
|
||||
const price =
|
||||
proposal.terms.change.__typename === 'UpdateMarketState'
|
||||
? proposal.terms.change.price
|
||||
: '';
|
||||
return {
|
||||
date,
|
||||
duration,
|
||||
price,
|
||||
};
|
||||
};
|
||||
|
||||
export const MarketTerminationBanner = ({
|
||||
market,
|
||||
@@ -15,57 +70,55 @@ export const MarketTerminationBanner = ({
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const skip = !market || !visible;
|
||||
const proposalsData = useMarketViewProposals({
|
||||
const { data: passedProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
typename: 'UpdateMarketState',
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: openProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
|
||||
if (!market) return null;
|
||||
const marketFound = (proposalsData || []).find(
|
||||
(item) =>
|
||||
item.terms.change.__typename === 'UpdateMarketState' &&
|
||||
item.terms.change.market.id === market.id &&
|
||||
item.terms.change.updateType ===
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
||||
item.state === Types.ProposalState.STATE_PASSED // subscription doesn't have state parameter
|
||||
);
|
||||
const now = Date.now();
|
||||
const passedProposals = filterProposals(passedProposalsData, market.id, now);
|
||||
const openProposals = filterProposals(openProposalsData, market.id, now);
|
||||
|
||||
const enactmentDatetime = new Date(marketFound?.terms.enactmentDatetime);
|
||||
const name =
|
||||
marketFound?.terms.change.__typename === 'UpdateMarketState'
|
||||
? marketFound.terms.change.market.tradableInstrument.instrument.code
|
||||
: '';
|
||||
const name = market.tradableInstrument.instrument.code;
|
||||
if (!passedProposals.length && !openProposals.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name && enactmentDatetime.getTime() > Date.now()) {
|
||||
const dayMonthDate = format(enactmentDatetime, 'dd MMMM');
|
||||
const duration = intervalToDuration({
|
||||
start: new Date(),
|
||||
end: enactmentDatetime,
|
||||
});
|
||||
const formattedDuration = formatDuration(duration, {
|
||||
format: ['days', 'hours'],
|
||||
});
|
||||
const price =
|
||||
marketFound?.terms.change.__typename === 'UpdateMarketState'
|
||||
? marketFound.terms.change.price
|
||||
: '';
|
||||
const assetSymbol = getQuoteName(market);
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Warning}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
data-testid={`termination-warning-banner-${market.id}`}
|
||||
>
|
||||
const assetSymbol = getQuoteName(market);
|
||||
const proposalLink =
|
||||
!passedProposals.length && openProposals[0]?.id
|
||||
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
|
||||
: undefined;
|
||||
const proposalsLink =
|
||||
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
|
||||
let content: ReactNode;
|
||||
if (passedProposals.length) {
|
||||
const { date, duration, price } = getMessageVariables(passedProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t('Trading on Market %s will stop on %s', [name, dayMonthDate])}
|
||||
{t('Trading on Market %s will stop on %s', [name, date])}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'You will no longer be able to hold a position on this market when it closes in %s.',
|
||||
[formattedDuration]
|
||||
[duration]
|
||||
)}{' '}
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
@@ -74,8 +127,57 @@ export const MarketTerminationBanner = ({
|
||||
assetSymbol,
|
||||
])}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
</>
|
||||
);
|
||||
} else if (openProposals.length > 1) {
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on Market %s may stop. There are open proposals to close this market',
|
||||
[name]
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<ExternalLink href={proposalsLink}>
|
||||
{t('View proposals')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const { date, price } = getMessageVariables(openProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on Market %s may stop on %s. There is open proposal to close this market.',
|
||||
[name, date]
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
t('Proposed final price is %s %s.', [
|
||||
addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||
assetSymbol,
|
||||
])}
|
||||
</div>
|
||||
<div>
|
||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={openProposals.length ? Intent.Warning : Intent.Info}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
data-testid={`termination-warning-banner-${market.id}`}
|
||||
>
|
||||
{content}
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ const MarketData = ({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const marketData = data?.marketsData[0];
|
||||
@@ -70,6 +71,8 @@ const MarketData = ({
|
||||
|
||||
const marketTradingMode = marketData
|
||||
? marketData.marketTradingMode
|
||||
: market.data
|
||||
? market.data.marketTradingMode
|
||||
: market.tradingMode;
|
||||
|
||||
const mode = [
|
||||
@@ -95,7 +98,7 @@ const MarketData = ({
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<h3 className="flex items-baseline">
|
||||
<span className="text-sm lg:text-base text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</span>
|
||||
{allProducts && productType && (
|
||||
|
||||
@@ -2,7 +2,10 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MarketSelector } from './market-selector';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import {
|
||||
createMarketFragment,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -36,6 +39,10 @@ describe('MarketSelector', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'a',
|
||||
@@ -56,7 +63,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'b',
|
||||
@@ -77,7 +87,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -91,7 +104,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'c',
|
||||
@@ -112,6 +128,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'cd',
|
||||
@@ -132,7 +152,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const activeMarkets = markets.filter((m) => isMarketActive(m.state));
|
||||
const activeMarkets = markets.filter((m) =>
|
||||
// @ts-ignore candles get joined outside this type
|
||||
isMarketActive(m.data.marketState)
|
||||
);
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useMarketSelectorList } from './use-market-selector-list';
|
||||
import { isMarketActive } from '../../lib/utils';
|
||||
import { Product } from './product-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import {
|
||||
createMarketFragment,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import type { Filter } from './market-selector';
|
||||
@@ -31,22 +34,40 @@ describe('useMarketSelectorList', () => {
|
||||
|
||||
it('returns all markets active and suspended markets', () => {
|
||||
const markets = [
|
||||
createMarketFragment({ id: 'market-0' }),
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
state: MarketState.STATE_PENDING,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_PENDING,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
mockUseMarketList.mockReturnValue({
|
||||
@@ -56,7 +77,8 @@ describe('useMarketSelectorList', () => {
|
||||
});
|
||||
const { result } = setup();
|
||||
const expectedFilteredMarkets = markets.filter((m) =>
|
||||
isMarketActive(m.state)
|
||||
// @ts-ignore candles get joined outside this type
|
||||
isMarketActive(m.data.marketState)
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
data: markets,
|
||||
@@ -70,6 +92,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -90,6 +116,10 @@ describe('useMarketSelectorList', () => {
|
||||
// }),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -135,6 +165,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -148,6 +182,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -161,6 +199,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -174,6 +216,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -238,6 +284,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'abc',
|
||||
@@ -247,6 +297,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'def',
|
||||
@@ -256,6 +310,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'defg',
|
||||
@@ -265,6 +323,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'ggg',
|
||||
@@ -333,11 +395,11 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -349,9 +411,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -363,9 +426,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -377,9 +441,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -414,6 +479,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -426,6 +495,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -438,6 +511,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -482,18 +559,30 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 3).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 1).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 2).toISOString(),
|
||||
},
|
||||
|
||||
@@ -22,8 +22,12 @@ export const useMarketSelectorList = ({
|
||||
const markets = useMemo(() => {
|
||||
if (!data?.length) return [];
|
||||
const markets = data
|
||||
// only active
|
||||
.filter((m) => isMarketActive(m.state))
|
||||
// show only active markets, using m.data.marketState as this will be
|
||||
// data that will get refreshed when calling reload
|
||||
.filter((m) => {
|
||||
if (!m.data) return false;
|
||||
return isMarketActive(m.data.marketState);
|
||||
})
|
||||
// only selected product type
|
||||
.filter((m) => {
|
||||
if (
|
||||
|
||||
@@ -182,7 +182,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
</NavbarItem>
|
||||
{FLAGS.REFERRALS && (
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.REFERRALS()} onClick={onClick}>
|
||||
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
|
||||
{t('Referrals')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
@@ -255,16 +255,18 @@ const NavbarLink = ({
|
||||
children,
|
||||
to,
|
||||
onClick,
|
||||
end = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
to: string;
|
||||
onClick?: () => void;
|
||||
end?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<N.Link asChild={true}>
|
||||
<NavLink
|
||||
to={to}
|
||||
end={true}
|
||||
end={end}
|
||||
className={classNames(
|
||||
'block lg:flex lg:h-full flex-col justify-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
|
||||
@@ -10,6 +10,19 @@ import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
const resolveNoRowsMessage = (filter?: Filter) => {
|
||||
switch (filter) {
|
||||
case Filter.Open:
|
||||
return t('No open orders');
|
||||
case Filter.Closed:
|
||||
return t('No closed orders');
|
||||
case Filter.Rejected:
|
||||
return t('No rejected orders');
|
||||
default:
|
||||
return t('No orders');
|
||||
}
|
||||
};
|
||||
|
||||
export const FilterStatusValue = {
|
||||
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
|
||||
[Filter.Closed]: [
|
||||
@@ -43,6 +56,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
if (!pubKey) {
|
||||
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||
}
|
||||
const noRowsMessage = resolveNoRowsMessage(filter);
|
||||
|
||||
return (
|
||||
<OrderListManager
|
||||
@@ -56,6 +70,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
}
|
||||
isReadOnly={isReadOnly}
|
||||
gridProps={gridStoreCallbacks}
|
||||
noRowsMessage={noRowsMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const SSRLoader = () => {
|
||||
const randomDelay = () => {
|
||||
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
|
||||
};
|
||||
import { pseudoRandom } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const generate = pseudoRandom(1);
|
||||
|
||||
export const SSRLoader = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -38,10 +38,9 @@ export const SSRLoader = () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
animation: 'flickering 0.4s linear alternate infinite',
|
||||
animationDelay: `-${randomDelay()}s`,
|
||||
animationDelay: `-${generate()}s`,
|
||||
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
|
||||
background: 'black',
|
||||
opacity: Math.random() > 0.5 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 572 KiB After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 120 KiB |
@@ -24,8 +24,8 @@ module.exports = {
|
||||
...theme.backgroundImage,
|
||||
rainbow:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
'rainbow-shifted':
|
||||
'linear-gradient(103.47deg, #0075FF 1.68%, #8028FF 47.49%, #FF077F 100%)',
|
||||
'rainbow-180':
|
||||
'linear-gradient(283.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
highlight:
|
||||
'linear-gradient(170deg, var(--tw-gradient-from), transparent var(--tw-gradient-to-position))',
|
||||
},
|
||||
@@ -38,10 +38,144 @@ module.exports = {
|
||||
'75%': { transform: 'translateX(5px)' },
|
||||
'100%': { transform: 'translateX(0)' },
|
||||
},
|
||||
'spin-rainbow-180': {
|
||||
'0%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'10%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(121.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'20%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(139.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'30%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(157.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'40%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(175.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'50%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(193.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'60%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(211.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'70%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(229.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'80%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(247.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'90%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(265.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'100%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(283.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
},
|
||||
'spin-rainbow-360': {
|
||||
'0%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'5%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(121.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'10%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(139.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'15%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(157.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'20%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(175.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'25%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(193.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'30%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(211.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'35%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(229.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'40%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(247.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'45%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(265.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'50%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(283.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'55%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(301.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'60%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(319.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'65%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(337.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'70%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(355.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'75%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(13.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'80%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(31.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'85%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(49.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'90%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(67.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'95%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(85.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
'100%': {
|
||||
backgroundImage:
|
||||
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
...theme.animation,
|
||||
shake: 'shake 200ms linear',
|
||||
'spin-rainbow': 'spin-rainbow-180 500ms linear',
|
||||
'rotate-rainbow': 'spin-rainbow-360 1000ms linear',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,7 +57,16 @@ export const rows: Rows = [
|
||||
key: AssetDetail.ID,
|
||||
label: t('ID'),
|
||||
tooltip: '',
|
||||
value: (asset) => truncateMiddle(asset.id),
|
||||
value: (asset) => (
|
||||
<>
|
||||
{truncateMiddle(asset.id)}{' '}
|
||||
<CopyWithTooltip text={asset.id}>
|
||||
<button title={t('Copy id to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.TYPE,
|
||||
|
||||
@@ -63,7 +63,9 @@ export const CandlesChartContainer = ({
|
||||
overlays,
|
||||
studies,
|
||||
notEnoughDataText: (
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
<span className="text-xs text-center">
|
||||
{t('No open orders')}
|
||||
</span>
|
||||
),
|
||||
initialNumCandlesToDisplay: candlesCount,
|
||||
studySize: STUDY_SIZE,
|
||||
|
||||
@@ -14,16 +14,17 @@ export const MarketProductPill = ({
|
||||
}: {
|
||||
productType?: ProductType;
|
||||
}) => {
|
||||
if (!productType) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
productType && (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</Pill>
|
||||
)
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</Pill>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
REDUCE_ONLY_TOOLTIP,
|
||||
stopSubmit,
|
||||
getNotionalSize,
|
||||
getAssetUnit,
|
||||
getBaseQuoteUnit,
|
||||
} from './deal-ticket';
|
||||
import { TypeToggle } from './type-selector';
|
||||
import {
|
||||
@@ -854,7 +854,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
}, [watch, market.id, updateStoredFormValues]);
|
||||
|
||||
const quoteName = getQuoteName(market);
|
||||
const assetUnit = getAssetUnit(
|
||||
const assetUnit = getBaseQuoteUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const getDefaultValues = (
|
||||
...storedValues,
|
||||
});
|
||||
|
||||
export const getAssetUnit = (tags?: string[] | null) =>
|
||||
export const getBaseQuoteUnit = (tags?: string[] | null) =>
|
||||
tags
|
||||
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
|
||||
?.replace(/^[^:]*:/, '');
|
||||
@@ -273,9 +273,7 @@ export const DealTicket = ({
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
const assetUnit = getAssetUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
const assetUnit = getQuoteName(market);
|
||||
|
||||
const summaryError = useMemo(() => {
|
||||
if (!pubKey) {
|
||||
|
||||
@@ -238,7 +238,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
volumeFormat={volumeFormat}
|
||||
priceFormat={priceFormat}
|
||||
notEnoughDataText={
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
<span className="text-xs text-center">{t('No open orders')}</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -11,14 +11,8 @@ import type {
|
||||
MarketDepthQuery,
|
||||
MarketDepthQueryVariables,
|
||||
MarketDepthUpdateSubscription,
|
||||
PriceLevelFieldsFragment,
|
||||
} from './__generated__/MarketDepth';
|
||||
|
||||
export type OrderbookData = {
|
||||
asks: PriceLevelFieldsFragment[];
|
||||
bids: PriceLevelFieldsFragment[];
|
||||
};
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
onClick: (args: { price?: string; size?: string }) => void;
|
||||
|
||||
@@ -238,7 +238,7 @@ export const Orderbook = ({
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
<Splash>{t('No open orders')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -3,12 +3,12 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
|
||||
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type MarketsDataQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
|
||||
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
|
||||
|
||||
export const MarketsDataFieldsFragmentDoc = gql`
|
||||
fragment MarketsDataFields on MarketData {
|
||||
@@ -20,6 +20,7 @@ export const MarketsDataFieldsFragmentDoc = gql`
|
||||
markPrice
|
||||
trigger
|
||||
staticMidPrice
|
||||
marketState
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
|
||||
@@ -126,6 +126,16 @@ export const marketTradingModeProvider = makeDerivedDataProvider<
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
||||
);
|
||||
|
||||
export const marketStateProvider = makeDerivedDataProvider<
|
||||
MarketDataFieldsFragment['marketState'] | undefined,
|
||||
never,
|
||||
MarketDataQueryVariables
|
||||
>(
|
||||
[marketDataProvider],
|
||||
(parts, variables, prevData) =>
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketState
|
||||
);
|
||||
|
||||
export const fundingRateProvider = makeDerivedDataProvider<
|
||||
string,
|
||||
never,
|
||||
@@ -176,3 +186,11 @@ export const useMarketTradingMode = (marketId?: string, skip?: boolean) => {
|
||||
skip: skip || !marketId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMarketState = (marketId?: string, skip?: boolean) => {
|
||||
return useDataProvider({
|
||||
dataProvider: marketStateProvider,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: skip || !marketId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ fragment MarketsDataFields on MarketData {
|
||||
markPrice
|
||||
trigger
|
||||
staticMidPrice
|
||||
marketState
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
|
||||
@@ -34,6 +34,7 @@ export const createMarketsDataFragment = (
|
||||
__typename: 'Market',
|
||||
},
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
staticMidPrice: '0',
|
||||
indicativePrice: '0',
|
||||
bestStaticBidPrice: '0',
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
import { useHasAmendableOrder } from '../../order-hooks';
|
||||
|
||||
export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => {
|
||||
export const OpenOrdersMenu = () => {
|
||||
const { isReadOnly } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const hasAmendableOrder = useHasAmendableOrder(marketId);
|
||||
const hasAmendableOrder = useHasAmendableOrder();
|
||||
|
||||
if (isReadOnly) {
|
||||
return null;
|
||||
|
||||
@@ -239,11 +239,11 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
|
||||
export const hasAmendableOrderProvider = makeDerivedDataProvider<
|
||||
boolean,
|
||||
never,
|
||||
{ partyId: string; marketId?: string }
|
||||
{ partyId: string }
|
||||
>([activeOrdersProvider], (parts) => {
|
||||
const activeOrders = parts[0] as ReturnType<typeof getData>;
|
||||
const hasAmendableOrder = activeOrders.some(
|
||||
(order) => !(order.liquidityProvision || order.peggedOrder)
|
||||
(order) => !order.liquidityProvision
|
||||
);
|
||||
return hasAmendableOrder;
|
||||
});
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import { OrderListManager } from './order-list-manager';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { OrderListManagerProps } from './order-list-manager';
|
||||
import { OrderListManager, Filter } from './order-list-manager';
|
||||
import * as useDataProviderHook from '@vegaprotocol/data-provider';
|
||||
import type { OrderFieldsFragment } from '../';
|
||||
import * as orderListMock from '../order-list/order-list';
|
||||
import { forwardRef } from 'react';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
|
||||
// avoid warnings about padding refs
|
||||
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
||||
|
||||
const generateJsx = () => {
|
||||
const generateJsx = (props: Partial<OrderListManagerProps> | null = null) => {
|
||||
const pubKey = '0x123';
|
||||
return (
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
|
||||
<OrderListManager partyId={pubKey} isReadOnly={false} />
|
||||
<OrderListManager partyId={pubKey} isReadOnly={false} {...props} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
@@ -33,9 +28,46 @@ describe('OrderListManager', () => {
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
await act(async () => {
|
||||
render(generateJsx());
|
||||
render(generateJsx());
|
||||
|
||||
expect(
|
||||
await screen.getByRole('treegrid', {
|
||||
name: (_, element) => element.classList.contains('ag-root'),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display order info', async () => {
|
||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||
data: [{ id: '1' } as OrderFieldsFragment],
|
||||
loading: false,
|
||||
error: new Error('Query error'),
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
expect(await screen.findByText('OrderList')).toBeInTheDocument();
|
||||
|
||||
render(generateJsx());
|
||||
|
||||
expect(
|
||||
await screen.getByText('Something went wrong: Query error')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display no rows overlay', async () => {
|
||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||
data: [],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
generateJsx({ filter: Filter.Closed, noRowsMessage: 'no rows message' })
|
||||
);
|
||||
|
||||
expect(screen.getByText('no rows message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { OrderListTable } from '../order-list/order-list';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
|
||||
@@ -11,6 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -25,6 +27,7 @@ export interface OrderListManagerProps {
|
||||
isReadOnly: boolean;
|
||||
filter?: Filter;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
noRowsMessage?: string;
|
||||
}
|
||||
|
||||
export const OrderListManager = ({
|
||||
@@ -34,6 +37,7 @@ export const OrderListManager = ({
|
||||
isReadOnly,
|
||||
filter,
|
||||
gridProps,
|
||||
noRowsMessage,
|
||||
}: OrderListManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
@@ -56,6 +60,29 @@ export const OrderListManager = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
gridProps?.onFilterChanged?.(event);
|
||||
if (event.api) {
|
||||
const isEmpty = event.api.getDisplayedRowCount() === 0;
|
||||
if (isEmpty) {
|
||||
event.api.showNoRowsOverlay();
|
||||
} else {
|
||||
event.api.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !data.length) {
|
||||
gridRef.current?.api?.showNoRowsOverlay();
|
||||
} else {
|
||||
gridRef.current?.api?.hideOverlay();
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const cancel = useCallback(
|
||||
(order: Order) => {
|
||||
if (!order.market) return;
|
||||
@@ -69,6 +96,10 @@ export const OrderListManager = ({
|
||||
[create]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
@@ -82,8 +113,9 @@ export const OrderListManager = ({
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No orders')}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...gridProps}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
</div>
|
||||
{editOrder && (
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { OrderEditDialog } from './order-edit-dialog';
|
||||
import { limitOrder } from '../mocks';
|
||||
|
||||
describe('OrderEditDialog', () => {
|
||||
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', async () => {
|
||||
// 7003-MORD-013
|
||||
await act(async () => {
|
||||
render(
|
||||
<OrderEditDialog
|
||||
order={limitOrder}
|
||||
onChange={jest.fn()}
|
||||
isOpen={true}
|
||||
onSubmit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const editOrder = await screen.findByTestId('edit-order');
|
||||
const limitPrice = within(editOrder).getByLabelText('Price');
|
||||
await userEvent.type(limitPrice, '0.111111');
|
||||
const submitButton = within(editOrder).getByRole('button', {
|
||||
name: 'Update',
|
||||
});
|
||||
await userEvent.click(submitButton);
|
||||
const inputErrorText = within(editOrder).getByTestId('input-error-text');
|
||||
expect(inputErrorText).toHaveTextContent(
|
||||
'Price accepts up to 1 decimal places'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,12 @@ const generateJsx = (
|
||||
|
||||
describe('OrderListTable', () => {
|
||||
it('should render correct columns', async () => {
|
||||
// 7003-MORD-001
|
||||
// 7003-MORD-002
|
||||
// 7003-MORD-003
|
||||
// 7003-MORD-004
|
||||
// 7003-MORD-005
|
||||
// 7003-MORD-006
|
||||
await act(async () => {
|
||||
render(generateJsx({ rowData: [marketOrder, limitOrder] }));
|
||||
});
|
||||
@@ -184,6 +190,7 @@ describe('OrderListTable', () => {
|
||||
|
||||
describe('amend cell', () => {
|
||||
it('allows cancelling and editing for permitted orders', async () => {
|
||||
// 7003-MORD-007
|
||||
const mockEdit = jest.fn();
|
||||
const mockCancel = jest.fn();
|
||||
const order = generateOrder({
|
||||
@@ -209,6 +216,7 @@ describe('OrderListTable', () => {
|
||||
});
|
||||
|
||||
it('does not allow cancelling and editing for permitted orders if read only', async () => {
|
||||
// 7003-MORD-008
|
||||
const mockEdit = jest.fn();
|
||||
const mockCancel = jest.fn();
|
||||
const order = generateOrder({
|
||||
@@ -233,6 +241,7 @@ describe('OrderListTable', () => {
|
||||
});
|
||||
|
||||
it('shows if an order is a liquidity provision order and does not show order actions', async () => {
|
||||
// 7003-MORD-008
|
||||
const order = generateOrder({
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
@@ -251,6 +260,7 @@ describe('OrderListTable', () => {
|
||||
});
|
||||
|
||||
it('shows if an order is a pegged order and does not show order actions', async () => {
|
||||
// 7003-MORD-008
|
||||
const order = generateOrder({
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
|
||||
@@ -34,7 +34,7 @@ import type {
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { Filter } from '../order-list-manager';
|
||||
import { Filter } from '../order-list-manager/order-list-manager';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
|
||||
const defaultColDef = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { hasAmendableOrderProvider } from '../components/order-data-provider';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
export const useHasAmendableOrder = (marketId?: string) => {
|
||||
export const useHasAmendableOrder = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [hasAmendableOrder, setHasAmendableOrder] = useState(false);
|
||||
const update = useCallback(({ data }: { data: boolean | null }) => {
|
||||
@@ -15,7 +15,6 @@ export const useHasAmendableOrder = (marketId?: string) => {
|
||||
update,
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
marketId,
|
||||
},
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './proposals-data-provider';
|
||||
export {
|
||||
marketViewProposalsDataProvider,
|
||||
proposalsDataProvider,
|
||||
} from './proposals-data-provider';
|
||||
export * from './__generated__/Proposals';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { update } from './proposals-data-provider';
|
||||
import type {
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables,
|
||||
} from './__generated__/Proposals';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
describe('proposals data provider', () => {
|
||||
const proposal = {
|
||||
id: '1',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
},
|
||||
},
|
||||
} as MarketViewProposalFieldsFragment;
|
||||
const data = [proposal];
|
||||
const delta = { ...proposal };
|
||||
const reload = jest.fn();
|
||||
|
||||
const variables: MarketViewProposalsQueryVariables = {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
};
|
||||
|
||||
it('update existing data', () => {
|
||||
expect(update(data, delta, reload, variables)?.[0]).toBe(delta);
|
||||
});
|
||||
|
||||
it('removes existing data if delta do not match inState', () => {
|
||||
expect(
|
||||
update(
|
||||
data,
|
||||
{ ...delta, state: Types.ProposalState.STATE_PASSED },
|
||||
reload,
|
||||
variables
|
||||
)?.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('do not add delta if it do not match inState', () => {
|
||||
expect(
|
||||
update(
|
||||
data,
|
||||
{ ...delta, id: '2', state: Types.ProposalState.STATE_PASSED },
|
||||
reload,
|
||||
variables
|
||||
)?.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('do not add delta if it do not match proposalType', () => {
|
||||
expect(
|
||||
update(
|
||||
data,
|
||||
{
|
||||
...delta,
|
||||
id: '2',
|
||||
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
|
||||
},
|
||||
reload,
|
||||
variables
|
||||
)?.length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Update } from '@vegaprotocol/data-provider';
|
||||
import { makeDataProvider } from '@vegaprotocol/data-provider';
|
||||
import produce from 'immer';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type {
|
||||
ProposalsListQuery,
|
||||
ProposalsListQueryVariables,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQuery,
|
||||
MarketViewProposalsQueryVariables,
|
||||
MarketViewLiveProposalsSubscriptionVariables,
|
||||
} from './__generated__/Proposals';
|
||||
import {
|
||||
MarketViewLiveProposalsDocument,
|
||||
@@ -42,21 +45,61 @@ export const proposalsDataProvider = makeDataProvider<
|
||||
errors.every((e) => e.message.match(/failed to get asset for ID/)),
|
||||
});
|
||||
|
||||
const update = (
|
||||
const ProposalTypeMap: Record<
|
||||
Types.ProposalType,
|
||||
Types.ProposalChange['__typename']
|
||||
> = {
|
||||
[Types.ProposalType.TYPE_CANCEL_TRANSFER]: 'CancelTransfer',
|
||||
[Types.ProposalType.TYPE_NETWORK_PARAMETERS]: 'UpdateNetworkParameter',
|
||||
[Types.ProposalType.TYPE_NEW_ASSET]: 'NewAsset',
|
||||
[Types.ProposalType.TYPE_NEW_FREE_FORM]: 'NewFreeform',
|
||||
[Types.ProposalType.TYPE_NEW_MARKET]: 'NewMarket',
|
||||
[Types.ProposalType.TYPE_NEW_SPOT_MARKET]: 'NewSpotMarket',
|
||||
[Types.ProposalType.TYPE_NEW_TRANSFER]: 'NewTransfer',
|
||||
[Types.ProposalType.TYPE_UPDATE_ASSET]: 'UpdateAsset',
|
||||
[Types.ProposalType.TYPE_UPDATE_MARKET]: 'UpdateMarket',
|
||||
[Types.ProposalType.TYPE_UPDATE_MARKET_STATE]: 'UpdateMarketState',
|
||||
[Types.ProposalType.TYPE_UPDATE_REFERRAL_PROGRAM]: 'UpdateReferralProgram',
|
||||
[Types.ProposalType.TYPE_UPDATE_SPOT_MARKET]: 'UpdateSpotMarket',
|
||||
[Types.ProposalType.TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM]:
|
||||
'UpdateVolumeDiscountProgram',
|
||||
};
|
||||
|
||||
const matchFilter = (
|
||||
data: MarketViewProposalFieldsFragment,
|
||||
variables: MarketViewProposalsQueryVariables
|
||||
) => {
|
||||
return (
|
||||
(!variables.inState || data.state === variables.inState) &&
|
||||
(!variables.proposalType ||
|
||||
data.terms.change.__typename === ProposalTypeMap[variables.proposalType])
|
||||
);
|
||||
};
|
||||
|
||||
export const update: Update<
|
||||
MarketViewProposalFieldsFragment[] | null,
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables
|
||||
> = (
|
||||
data: MarketViewProposalFieldsFragment[] | null,
|
||||
delta: MarketViewProposalFieldsFragment
|
||||
delta: MarketViewProposalFieldsFragment,
|
||||
reload,
|
||||
variables
|
||||
) => {
|
||||
const updateData = produce(data || [], (draft) => {
|
||||
const { id } = delta;
|
||||
const index = draft.findIndex((item) => item.id === id);
|
||||
const match = matchFilter(delta, variables);
|
||||
if (index === -1) {
|
||||
draft.unshift(delta);
|
||||
if (match) {
|
||||
draft.unshift(delta);
|
||||
}
|
||||
} else {
|
||||
const currNode = draft[index];
|
||||
draft[index] = {
|
||||
...currNode,
|
||||
...delta,
|
||||
};
|
||||
if (match) {
|
||||
draft[index] = delta;
|
||||
} else {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
return updateData;
|
||||
@@ -66,12 +109,15 @@ const getMarketProposalsData = (
|
||||
responseData: MarketViewProposalsQuery | null
|
||||
) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || [];
|
||||
|
||||
const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {};
|
||||
|
||||
export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
MarketViewProposalsQuery,
|
||||
MarketViewProposalFieldsFragment[],
|
||||
MarketViewLiveProposalsSubscription,
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables
|
||||
MarketViewProposalsQueryVariables,
|
||||
MarketViewLiveProposalsSubscriptionVariables
|
||||
>({
|
||||
query: MarketViewProposalsDocument,
|
||||
subscriptionQuery: MarketViewLiveProposalsDocument,
|
||||
@@ -79,4 +125,5 @@ export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
getDelta: (subscriptionData: MarketViewLiveProposalsSubscription) =>
|
||||
subscriptionData.proposals,
|
||||
getData: getMarketProposalsData,
|
||||
getSubscriptionVariables: () => subscriptionVariables,
|
||||
});
|
||||
|
||||
@@ -7,4 +7,3 @@ export * from './use-update-network-paramaters-toasts';
|
||||
export * from './use-successor-market-proposal-details';
|
||||
export * from './use-new-transfer-proposal-details';
|
||||
export * from './use-cancel-transfer-proposal-details';
|
||||
export * from './use-market-view-proposals';
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketViewProposalsDataProvider } from '../proposals-data-provider';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
|
||||
export const useMarketViewProposals = ({
|
||||
skip = false,
|
||||
typename,
|
||||
proposalType,
|
||||
inState,
|
||||
}: {
|
||||
typename: Types.ProposalChange['__typename'];
|
||||
skip?: boolean;
|
||||
inState: Types.ProposalState;
|
||||
proposalType?: Types.ProposalType;
|
||||
}) => {
|
||||
const variables = {
|
||||
inState,
|
||||
...(proposalType ? { proposalType } : null),
|
||||
};
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
variables,
|
||||
});
|
||||
return (data || []).filter(
|
||||
(item) => item.terms.change.__typename === typename
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { proposalsDataProvider } from '..';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useMemo } from 'react';
|
||||
import first from 'lodash/first';
|
||||
import type { ProposalListFieldsFragment } from '..';
|
||||
import { proposalsDataProvider } from '../proposals-data-provider';
|
||||
import type { ProposalListFieldsFragment } from '../proposals-data-provider';
|
||||
|
||||
type UseUpdateProposalProps = {
|
||||
id?: string;
|
||||
|
||||
@@ -58,7 +58,7 @@ type ThemeStore = {
|
||||
setTheme: (theme?: Theme) => void;
|
||||
};
|
||||
|
||||
const useThemeStore = create<ThemeStore>((set) => ({
|
||||
const useThemeStore = create<ThemeStore>()((set) => ({
|
||||
theme: getCurrentTheme(),
|
||||
setTheme: (newTheme) => {
|
||||
set((state) => {
|
||||
|
||||
Generated
+243
-48
@@ -68,9 +68,13 @@ export type AccountEvent = {
|
||||
|
||||
/** Filter input for historical balance queries */
|
||||
export type AccountFilter = {
|
||||
/** Restrict accounts to those connected to any of the types in this list. Pass an empty list for no filter. */
|
||||
accountTypes?: InputMaybe<Array<AccountType>>;
|
||||
/** Restrict accounts to those holding balances in this asset ID. */
|
||||
assetId?: InputMaybe<Scalars['ID']>;
|
||||
/** Restrict accounts to those connected to the markets in this list. Pass an empty list for no filter. */
|
||||
marketIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
/** Restrict accounts to those owned by the parties in this list. Pass an empty list for no filter. */
|
||||
partyIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
};
|
||||
|
||||
@@ -510,7 +514,7 @@ export type CurrentReferralProgram = {
|
||||
__typename?: 'CurrentReferralProgram';
|
||||
/** Defined tiers in increasing order. First element will give Tier 1, second element will give Tier 2, etc. */
|
||||
benefitTiers: Array<BenefitTier>;
|
||||
/** Timestamp as RFC3339Nano, after which when the current epoch ends, the program will end and benefits will be disabled. */
|
||||
/** Timestamp as Unix time in nanoseconds, after which when the current epoch ends, the program will end and benefits will be disabled. */
|
||||
endOfProgramTimestamp: Scalars['Timestamp'];
|
||||
/** Timestamp as RFC3339Nano when the program ended. If present, the current program has ended and no program is currently running. */
|
||||
endedAt?: Maybe<Scalars['Timestamp']>;
|
||||
@@ -1272,6 +1276,44 @@ export type Fees = {
|
||||
factors: FeeFactors;
|
||||
};
|
||||
|
||||
/** Fees that have been applied on a specific market/asset up to the given epoch. */
|
||||
export type FeesStats = {
|
||||
__typename?: 'FeesStats';
|
||||
/** The settlement asset of the market. */
|
||||
assetId: Scalars['String'];
|
||||
/** The epoch for which these stats were valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** The total maker fees generated by all parties. */
|
||||
makerFeesGenerated: Array<MakerFeesGenerated>;
|
||||
/** The market the fees were paid in */
|
||||
marketId: Scalars['String'];
|
||||
/** The total referral discounts applied to all referee taker fees */
|
||||
refereesDiscountApplied: Array<PartyAmount>;
|
||||
/** The total referral rewards generated by all referee taker fees. */
|
||||
referrerRewardsGenerated: Array<ReferrerRewardsGenerated>;
|
||||
/** The total maker fees received by the maker side. */
|
||||
totalMakerFeesReceived: Array<PartyAmount>;
|
||||
/** The total referral rewards received by referrer of the referral set. */
|
||||
totalRewardsReceived: Array<PartyAmount>;
|
||||
/** The total volume discounts applied to all referee taker fees */
|
||||
volumeDiscountApplied: Array<PartyAmount>;
|
||||
};
|
||||
|
||||
/** Fees that have been applied on a specific asset for a given party. */
|
||||
export type FeesStatsForParty = {
|
||||
__typename?: 'FeesStatsForParty';
|
||||
/** The settlement asset of the market. */
|
||||
assetId: Scalars['String'];
|
||||
/** The total referral discounts applied to all referee taker fees */
|
||||
refereesDiscountApplied: Scalars['String'];
|
||||
/** The total maker fees received by the maker side. */
|
||||
totalMakerFeesReceived: Scalars['String'];
|
||||
/** The total referral rewards received by referrer of the referral set. */
|
||||
totalRewardsReceived: Scalars['String'];
|
||||
/** The total volume discounts applied to all referee taker fees */
|
||||
volumeDiscountApplied: Scalars['String'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter describes the conditions under which oracle data is considered of
|
||||
* interest or not.
|
||||
@@ -1596,11 +1638,19 @@ export enum LedgerEntryField {
|
||||
TransferType = 'TransferType'
|
||||
}
|
||||
|
||||
/** Filter for historical entry ledger queries */
|
||||
/** Filter for historical entry ledger queries, you must provide at least one party in FromAccountFilter, or ToAccountFilter */
|
||||
export type LedgerEntryFilter = {
|
||||
/**
|
||||
* Determines whether an entry must have accounts matching both the account_from_filter
|
||||
* and the account_to_filter. If set to 'true', entries must have matches in both filters.
|
||||
* If set to `false`, entries matching only the account_from_filter or the account_to_filter will also be included.
|
||||
*/
|
||||
CloseOnAccountFilters?: InputMaybe<Scalars['Boolean']>;
|
||||
/** Used to set values for filtering sender accounts. Party must be provided in this filter or to_account_filter, or both. */
|
||||
FromAccountFilter?: InputMaybe<AccountFilter>;
|
||||
/** Used to set values for filtering receiver accounts. Party must be provided in this filter or from_account_filter, or both. */
|
||||
ToAccountFilter?: InputMaybe<AccountFilter>;
|
||||
/** List of transfer types that is used for filtering sender and receiver accounts. */
|
||||
TransferTypes?: InputMaybe<Array<InputMaybe<TransferType>>>;
|
||||
};
|
||||
|
||||
@@ -1728,31 +1778,31 @@ export type LiquidityProvision = {
|
||||
__typename?: 'LiquidityProvision';
|
||||
/** A set of liquidity buy orders to meet the liquidity provision obligation. */
|
||||
buys: Array<LiquidityOrderReference>;
|
||||
/** Specified as a unit-less number that represents the amount of settlement asset of the market. */
|
||||
/** Specified as a unitless number that represents the amount of the market's settlement asset for the commitment. */
|
||||
commitmentAmount: Scalars['String'];
|
||||
/** RFC3339Nano time when the liquidity provision was initially created */
|
||||
createdAt: Scalars['Timestamp'];
|
||||
/** Nominated liquidity fee factor, which is an input to the calculation of liquidity fees on the market, as per setting fees and rewarding liquidity providers. */
|
||||
/** Provider's nominated liquidity fee factor, which is an input to the calculation of liquidity fees on the market, as per setting fees and rewarding liquidity providers. */
|
||||
fee: Scalars['String'];
|
||||
/** Unique identifier for the order (set by the system after consensus) */
|
||||
/** Unique identifier for the provision (set by the system after consensus) */
|
||||
id: Scalars['ID'];
|
||||
/** Market for the order */
|
||||
/** Market ID for the liquidity provision */
|
||||
market: Market;
|
||||
/** The party making this commitment */
|
||||
party: Party;
|
||||
/** A reference for the orders created out of this liquidity provision */
|
||||
/** A reference for the orders created to support this liquidity provision */
|
||||
reference?: Maybe<Scalars['String']>;
|
||||
/** A set of liquidity sell orders to meet the liquidity provision obligation. */
|
||||
sells: Array<LiquidityOrderReference>;
|
||||
/** The current status of this liquidity provision */
|
||||
status: LiquidityProvisionStatus;
|
||||
/** RFC3339Nano time of when the liquidity provision was updated */
|
||||
/** RFC3339Nano time when the liquidity provision was updated */
|
||||
updatedAt?: Maybe<Scalars['Timestamp']>;
|
||||
/** The version of this liquidity provision */
|
||||
version: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Status of a liquidity provision order */
|
||||
/** Status of a liquidity provision */
|
||||
export enum LiquidityProvisionStatus {
|
||||
/** An active liquidity provision */
|
||||
STATUS_ACTIVE = 'STATUS_ACTIVE',
|
||||
@@ -1801,6 +1851,20 @@ export type LiquidityProvisionUpdate = {
|
||||
version: Scalars['String'];
|
||||
};
|
||||
|
||||
export type LiquidityProvisionWithPending = {
|
||||
__typename?: 'LiquidityProvisionWithPending';
|
||||
current: LiquidityProvision;
|
||||
/** Liquidity provision that has been updated by the liquidity provider, and has been accepted by the network, but will not be active until the next epoch. */
|
||||
pending?: Maybe<LiquidityProvision>;
|
||||
};
|
||||
|
||||
/** Edge type containing the liquidity provision and cursor information returned by a LiquidityProvisionsWithPendingConnection */
|
||||
export type LiquidityProvisionWithPendingEdge = {
|
||||
__typename?: 'LiquidityProvisionWithPendingEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: LiquidityProvisionWithPending;
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated liquidity provision information */
|
||||
export type LiquidityProvisionsConnection = {
|
||||
__typename?: 'LiquidityProvisionsConnection';
|
||||
@@ -1815,6 +1879,13 @@ export type LiquidityProvisionsEdge = {
|
||||
node: LiquidityProvision;
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated liquidity provision information */
|
||||
export type LiquidityProvisionsWithPendingConnection = {
|
||||
__typename?: 'LiquidityProvisionsWithPendingConnection';
|
||||
edges?: Maybe<Array<Maybe<LiquidityProvisionWithPendingEdge>>>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type LiquiditySLAParameters = {
|
||||
__typename?: 'LiquiditySLAParameters';
|
||||
/** Specifies the minimum fraction of time LPs must spend 'on the book' providing their committed liquidity */
|
||||
@@ -1861,6 +1932,15 @@ export type LossSocialization = {
|
||||
partyId: Scalars['ID'];
|
||||
};
|
||||
|
||||
/** Maker fees generated by the trade aggressor */
|
||||
export type MakerFeesGenerated = {
|
||||
__typename?: 'MakerFeesGenerated';
|
||||
/** Amount of maker fees paid by the taker to the maker */
|
||||
makerFeesPaid: Array<PartyAmount>;
|
||||
/** Party that paid the fees */
|
||||
taker: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MarginCalculator = {
|
||||
__typename?: 'MarginCalculator';
|
||||
/** The scaling factors that will be used for margin calculation */
|
||||
@@ -1979,6 +2059,11 @@ export type Market = {
|
||||
/** Liquidity monitoring parameters for the market */
|
||||
liquidityMonitoringParameters: LiquidityMonitoringParameters;
|
||||
/** The list of the liquidity provision commitments for this market */
|
||||
liquidityProvisions?: Maybe<LiquidityProvisionsWithPendingConnection>;
|
||||
/**
|
||||
* The list of the liquidity provision commitments for this market
|
||||
* @deprecated Use liquidityProvisions instead
|
||||
*/
|
||||
liquidityProvisionsConnection?: Maybe<LiquidityProvisionsConnection>;
|
||||
/** Optional: Liquidity SLA parameters for the market */
|
||||
liquiditySLAParameters?: Maybe<LiquiditySLAParameters>;
|
||||
@@ -2046,6 +2131,14 @@ export type MarketdepthArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
|
||||
export type MarketliquidityProvisionsArgs = {
|
||||
live?: InputMaybe<Scalars['Boolean']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
partyId?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
|
||||
export type MarketliquidityProvisionsConnectionArgs = {
|
||||
live?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -3219,6 +3312,39 @@ export type Pagination = {
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
/** Liquidity fees that have been paid to a party in a specific market/asset up to the given epoch. */
|
||||
export type PaidLiquidityFees = {
|
||||
__typename?: 'PaidLiquidityFees';
|
||||
/** The settlement asset of the market. */
|
||||
assetId: Scalars['String'];
|
||||
/** The epoch for which these stats were valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** Fees paid per party */
|
||||
feesPaidPerParty: Array<PartyAmount>;
|
||||
/** The market the fees were paid in */
|
||||
marketId: Scalars['String'];
|
||||
/** Total fees paid across all parties */
|
||||
totalFeesPaid: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated paid liquidity fees statistics */
|
||||
export type PaidLiquidityFeesConnection = {
|
||||
__typename?: 'PaidLiquidityFeesConnection';
|
||||
/** The volume discount statistics in this connection */
|
||||
edges: Array<Maybe<PaidLiquidityFeesEdge>>;
|
||||
/** The pagination information */
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
/** Edge type containing the volume discount statistics and cursor information returned by a PaidLiquidityFeesConnection */
|
||||
export type PaidLiquidityFeesEdge = {
|
||||
__typename?: 'PaidLiquidityFeesEdge';
|
||||
/** The cursor for this volume discount statistics */
|
||||
cursor: Scalars['String'];
|
||||
/** The volume discount statistics */
|
||||
node: PaidLiquidityFees;
|
||||
};
|
||||
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type Party = {
|
||||
__typename?: 'Party';
|
||||
@@ -3231,7 +3357,12 @@ export type Party = {
|
||||
depositsConnection?: Maybe<DepositsConnection>;
|
||||
/** Party identifier */
|
||||
id: Scalars['ID'];
|
||||
/** The list of the liquidity provision commitment for this party */
|
||||
/** The list of the liquidity provision commitments for this party */
|
||||
liquidityProvisions?: Maybe<LiquidityProvisionsWithPendingConnection>;
|
||||
/**
|
||||
* The list of the liquidity provision commitment for this party
|
||||
* @deprecated Use liquidityProvisions instead
|
||||
*/
|
||||
liquidityProvisionsConnection?: Maybe<LiquidityProvisionsConnection>;
|
||||
/** Margin levels for a market */
|
||||
marginsConnection?: Maybe<MarginConnection>;
|
||||
@@ -3254,6 +3385,8 @@ export type Party = {
|
||||
tradesConnection?: Maybe<TradeConnection>;
|
||||
/** All transfers for a public key */
|
||||
transfersConnection?: Maybe<TransferConnection>;
|
||||
/** The current reward vesting summary of the party for the last epoch */
|
||||
vestingBalancesSummary: PartyVestingBalancesSummary;
|
||||
/** All votes on proposals in the Vega network by the given party */
|
||||
votesConnection?: Maybe<ProposalVoteConnection>;
|
||||
/** The list of all withdrawals initiated by the party */
|
||||
@@ -3290,6 +3423,15 @@ export type PartydepositsConnectionArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type PartyliquidityProvisionsArgs = {
|
||||
live?: InputMaybe<Scalars['Boolean']>;
|
||||
marketId?: InputMaybe<Scalars['ID']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
reference?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type PartyliquidityProvisionsConnectionArgs = {
|
||||
live?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -3364,6 +3506,12 @@ export type PartytransfersConnectionArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type PartyvestingBalancesSummaryArgs = {
|
||||
assetId?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type PartyvotesConnectionArgs = {
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
@@ -3424,6 +3572,17 @@ export type PartyEdge = {
|
||||
node: Party;
|
||||
};
|
||||
|
||||
/** A party reward locked balance. */
|
||||
export type PartyLockedBalance = {
|
||||
__typename?: 'PartyLockedBalance';
|
||||
/** The asset locked */
|
||||
asset: Asset;
|
||||
/** The amount locked */
|
||||
balance: Scalars['String'];
|
||||
/** Epoch in which the funds will be moved to the vesting balance */
|
||||
untilEpoch: Scalars['Int'];
|
||||
};
|
||||
|
||||
/**
|
||||
* All staking information related to a Party.
|
||||
* Contains the current recognised balance by the network and
|
||||
@@ -3437,6 +3596,26 @@ export type PartyStake = {
|
||||
linkings?: Maybe<Array<StakeLinking>>;
|
||||
};
|
||||
|
||||
/** A party's reward vesting balance. */
|
||||
export type PartyVestingBalance = {
|
||||
__typename?: 'PartyVestingBalance';
|
||||
/** The asset being vested */
|
||||
asset: Asset;
|
||||
/** The amount locked */
|
||||
balance: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Summary of a party's reward vesting balances. */
|
||||
export type PartyVestingBalancesSummary = {
|
||||
__typename?: 'PartyVestingBalancesSummary';
|
||||
/** The epoch for which this summary is valid */
|
||||
epoch?: Maybe<Scalars['Int']>;
|
||||
/** The party's vesting balances */
|
||||
lockedBalances?: Maybe<Array<PartyLockedBalance>>;
|
||||
/** The party vesting balances */
|
||||
vestingBalances?: Maybe<Array<PartyVestingBalance>>;
|
||||
};
|
||||
|
||||
/** Create an order linked to an index rather than a price */
|
||||
export type PeggedOrder = {
|
||||
__typename?: 'PeggedOrder';
|
||||
@@ -4163,6 +4342,10 @@ export type Query = {
|
||||
estimatePosition?: Maybe<PositionEstimate>;
|
||||
/** Query for historic ethereum key rotations */
|
||||
ethereumKeyRotations: EthereumKeyRotationsConnection;
|
||||
/** Get fees statistics */
|
||||
feesStats?: Maybe<FeesStats>;
|
||||
/** Get fees statistics for a given party */
|
||||
feesStatsForParty?: Maybe<Array<Maybe<FeesStatsForParty>>>;
|
||||
/** Funding payment for perpetual markets. */
|
||||
fundingPayments: FundingPaymentConnection;
|
||||
/**
|
||||
@@ -4178,7 +4361,14 @@ export type Query = {
|
||||
keyRotationsConnection: KeyRotationConnection;
|
||||
/** The last block process by the blockchain */
|
||||
lastBlockHeight: Scalars['String'];
|
||||
/** Get ledger entries by asset, market, party, account type, transfer type within the given date range. */
|
||||
/**
|
||||
* Get ledger entries by asset, market, party, account type, transfer type within the given date range.
|
||||
* Note: The date range is restricted to any 5 days.
|
||||
* If no start or end date is provided, only ledger entries from the last 5 days will be returned.
|
||||
* If a start and end date are provided, but the end date is more than 5 days after the start date, only data up to 5 days after the start date will be returned.
|
||||
* If a start date is provided but no end date, the end date will be set to 5 days after the start date.
|
||||
* If no start date is provided, but the end date is, the start date will be set to 5 days before the end date.
|
||||
*/
|
||||
ledgerEntries: AggregatedLedgerEntriesConnection;
|
||||
/** List all active liquidity providers for a specific market */
|
||||
liquidityProviders?: Maybe<LiquidityProviderConnection>;
|
||||
@@ -4216,6 +4406,8 @@ export type Query = {
|
||||
orderByReference: Order;
|
||||
/** Order versions (created via amendments if any) found by orderID */
|
||||
orderVersionsConnection?: Maybe<OrderConnection>;
|
||||
/** List paid liquidity fees statistics */
|
||||
paidLiquidityFees?: Maybe<PaidLiquidityFeesConnection>;
|
||||
/** One or more entities that are trading on the Vega network */
|
||||
partiesConnection?: Maybe<PartyConnection>;
|
||||
/** An entity that is trading on the Vega network */
|
||||
@@ -4230,8 +4422,6 @@ export type Query = {
|
||||
protocolUpgradeProposals?: Maybe<ProtocolUpgradeProposalConnection>;
|
||||
/** Flag indicating whether the data-node is ready to begin the protocol upgrade */
|
||||
protocolUpgradeStatus?: Maybe<ProtocolUpgradeStatus>;
|
||||
/** Get referrer fee and discount stats */
|
||||
referralFeeStats?: Maybe<ReferralSetFeeStats>;
|
||||
referralSetReferees: ReferralSetRefereeConnection;
|
||||
/** Get referral set statistics */
|
||||
referralSetStats: ReferralSetStatsConnection;
|
||||
@@ -4410,6 +4600,24 @@ export type QueryethereumKeyRotationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryfeesStatsArgs = {
|
||||
assetId?: InputMaybe<Scalars['ID']>;
|
||||
epoch?: InputMaybe<Scalars['Int']>;
|
||||
marketId?: InputMaybe<Scalars['ID']>;
|
||||
partyId?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryfeesStatsForPartyArgs = {
|
||||
assetId?: InputMaybe<Scalars['ID']>;
|
||||
fromEpoch?: InputMaybe<Scalars['Int']>;
|
||||
partyId: Scalars['ID'];
|
||||
toEpoch?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryfundingPaymentsArgs = {
|
||||
marketId?: InputMaybe<Scalars['ID']>;
|
||||
@@ -4557,6 +4765,15 @@ export type QueryorderVersionsConnectionArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QuerypaidLiquidityFeesArgs = {
|
||||
assetId?: InputMaybe<Scalars['ID']>;
|
||||
epoch?: InputMaybe<Scalars['Int']>;
|
||||
marketId?: InputMaybe<Scalars['ID']>;
|
||||
partyIDs?: InputMaybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QuerypartiesConnectionArgs = {
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
@@ -4600,18 +4817,9 @@ export type QueryprotocolUpgradeProposalsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryreferralFeeStatsArgs = {
|
||||
assetId?: InputMaybe<Scalars['ID']>;
|
||||
epoch?: InputMaybe<Scalars['Int']>;
|
||||
marketId?: InputMaybe<Scalars['ID']>;
|
||||
referee?: InputMaybe<Scalars['ID']>;
|
||||
referrer?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryreferralSetRefereesArgs = {
|
||||
aggregationDays?: InputMaybe<Scalars['Int']>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
referee?: InputMaybe<Scalars['ID']>;
|
||||
@@ -4773,8 +4981,8 @@ export type ReferralProgram = {
|
||||
__typename?: 'ReferralProgram';
|
||||
/** Defined tiers in increasing order. First element will give Tier 1, second element will give Tier 2, etc. */
|
||||
benefitTiers: Array<BenefitTier>;
|
||||
/** Timestamp as RFC3339, after which when the current epoch ends, the programs will end and benefits will be disabled. */
|
||||
endOfProgramTimestamp: Scalars['String'];
|
||||
/** Timestamp as Unix time in nanoseconds, after which when the current epoch ends, the program will end and benefits will be disabled. */
|
||||
endOfProgramTimestamp: Scalars['Timestamp'];
|
||||
/** Unique ID generated from the proposal that created this program. */
|
||||
id: Scalars['ID'];
|
||||
/**
|
||||
@@ -4820,25 +5028,6 @@ export type ReferralSetEdge = {
|
||||
node: ReferralSet;
|
||||
};
|
||||
|
||||
/** Referral rewards and discounts that have been applied on a specific market/asset up to the given epoch. */
|
||||
export type ReferralSetFeeStats = {
|
||||
__typename?: 'ReferralSetFeeStats';
|
||||
/** The settlement asset of the market. */
|
||||
assetId: Scalars['String'];
|
||||
/** The epoch for which these stats were valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** The market the fees were paid in */
|
||||
marketId: Scalars['String'];
|
||||
/** The total referral discounts applied to all referee taker fees */
|
||||
refereesDiscountApplied: Array<PartyAmount>;
|
||||
/** The total referral rewards generated by all referee taker fees. */
|
||||
referrerRewardsGenerated: Array<ReferrerRewardsGenerated>;
|
||||
/** The total referral rewards paid to the referrer of the referral set. */
|
||||
totalRewardsPaid: Array<PartyAmount>;
|
||||
/** The total volume discounts applied to all referee taker fees */
|
||||
volumeDiscountApplied: Array<PartyAmount>;
|
||||
};
|
||||
|
||||
/** Data relating to referees that have joined a referral set */
|
||||
export type ReferralSetReferee = {
|
||||
__typename?: 'ReferralSetReferee';
|
||||
@@ -4850,6 +5039,10 @@ export type ReferralSetReferee = {
|
||||
refereeId: Scalars['ID'];
|
||||
/** Unique ID of the referral set the referee joined. */
|
||||
referralSetId: Scalars['ID'];
|
||||
/** Total rewards generated from the referee over the aggregation period, default is 30 days. */
|
||||
totalRefereeGeneratedRewards: Scalars['String'];
|
||||
/** Total notional volume of the referee's aggressive trades over the aggregation period, default is 30 days. */
|
||||
totalRefereeNotionalTakerVolume: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated information about the referral set referees */
|
||||
@@ -4926,6 +5119,8 @@ export type Reward = {
|
||||
asset: Asset;
|
||||
/** Epoch for which this reward was distributed */
|
||||
epoch: Epoch;
|
||||
/** The epoch when the reward is released */
|
||||
lockedUntilEpoch: Epoch;
|
||||
/** The market ID for which this reward is paid if any */
|
||||
marketId: Scalars['ID'];
|
||||
/** Party receiving the reward */
|
||||
@@ -6151,8 +6346,8 @@ export type UpdateReferralProgram = {
|
||||
__typename?: 'UpdateReferralProgram';
|
||||
/** Defined tiers in increasing order. First element will give Tier 1, second element will give Tier 2, etc. */
|
||||
benefitTiers: Array<BenefitTier>;
|
||||
/** Timestamp as RFC3339, after which when the current epoch ends, the programs will end and benefits will be disabled. */
|
||||
endOfProgramTimestamp: Scalars['String'];
|
||||
/** Timestamp as Unix time in nanoseconds, after which when the current epoch ends, the program will end and benefits will be disabled. */
|
||||
endOfProgramTimestamp: Scalars['Timestamp'];
|
||||
/**
|
||||
* Defined staking tiers in increasing order. First element will give Tier 1,
|
||||
* second element will give Tier 2, and so on. Determines the level of
|
||||
@@ -6190,7 +6385,7 @@ export type UpdateVolumeDiscountProgram = {
|
||||
__typename?: 'UpdateVolumeDiscountProgram';
|
||||
/** The benefit tiers for the program */
|
||||
benefitTiers: Array<VolumeBenefitTier>;
|
||||
/** The end time of the program */
|
||||
/** Timestamp as Unix time in nanoseconds, after which program ends. */
|
||||
endOfProgramTimestamp: Scalars['Timestamp'];
|
||||
/** The window length to consider for the volume discount program */
|
||||
windowLength: Scalars['Int'];
|
||||
@@ -6219,7 +6414,7 @@ export type VolumeDiscountProgram = {
|
||||
__typename?: 'VolumeDiscountProgram';
|
||||
/** Defined tiers in increasing order. First element will give Tier 1, second element will give Tier 2, etc. */
|
||||
benefitTiers: Array<VolumeBenefitTier>;
|
||||
/** Timestamp as Unix time in nanoseconds, after which when the current epoch ends, the programs will end and benefits will be disabled. */
|
||||
/** Timestamp as Unix time in nanoseconds, after which when the current epoch ends, the program will end and benefits will be disabled. */
|
||||
endOfProgramTimestamp: Scalars['Timestamp'];
|
||||
/** Timestamp as RFC3339Nano when the program ended. If present, the current program has ended and no program is currently running. */
|
||||
endedAt?: Maybe<Scalars['Timestamp']>;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { render, act, screen } from '@testing-library/react';
|
||||
import { AsyncRenderer } from './async-renderer';
|
||||
|
||||
describe('AsyncRenderer', () => {
|
||||
const reload = jest.fn();
|
||||
it('timeout error should render button', async () => {
|
||||
const reload = jest.fn();
|
||||
await act(() => {
|
||||
render(
|
||||
<AsyncRenderer
|
||||
@@ -20,4 +20,22 @@ describe('AsyncRenderer', () => {
|
||||
});
|
||||
expect(reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors should be handled properly', async () => {
|
||||
const message = 'Node has been collapsed';
|
||||
await act(() => {
|
||||
render(
|
||||
<AsyncRenderer
|
||||
reload={reload}
|
||||
error={new Error(message)}
|
||||
loading={false}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(`Something went wrong: ${message}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AsyncRenderer<T = object>({
|
||||
reload,
|
||||
}: AsyncRendererProps<T>) {
|
||||
if (error) {
|
||||
if (!data) {
|
||||
if (!data || (Array.isArray(data) && !data.length)) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="h-12 flex flex-col items-center">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import styles from './loader.module.scss';
|
||||
|
||||
const pseudoRandom = (seed: number) => {
|
||||
export const pseudoRandom = (seed: number) => {
|
||||
let value = seed;
|
||||
return () => {
|
||||
value = (value * 16807) % 2147483647;
|
||||
@@ -10,6 +9,8 @@ const pseudoRandom = (seed: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const generate = pseudoRandom(1);
|
||||
|
||||
export interface LoaderProps {
|
||||
size?: 'small' | 'large';
|
||||
forceTheme?: 'dark' | 'light';
|
||||
@@ -27,8 +28,6 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
|
||||
size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]';
|
||||
const items = size === 'small' ? 9 : 25;
|
||||
|
||||
const generate = useMemo(() => pseudoRandom(1), []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center" data-testid="loader">
|
||||
<div className={`${wrapperClasses} flex flex-wrap`}>
|
||||
|
||||
@@ -41,7 +41,7 @@ const ethereumRequest = <T>(args: RequestArguments): Promise<T> => {
|
||||
|
||||
export const LOCAL_SNAP_ID = 'local:http://localhost:8080';
|
||||
export const DEFAULT_SNAP_ID = 'npm:@vegaprotocol/snap';
|
||||
export const DEFAULT_SNAP_VERSION = '0.2.0';
|
||||
export const DEFAULT_SNAP_VERSION = '0.3.1';
|
||||
|
||||
type GetSnapsResponse = Record<string, Snap>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user