Compare commits

...
Author SHA1 Message Date
Art 43cd170c77 feat(trading): referrals (Mk2), referrals stats, apply preview (#5021) 2023-10-23 16:57:18 +02:00
Edd 1d39f81dfc fix(governance): remove restricted route from navigation (#5106) 2023-10-23 14:16:07 +00:00
Bartłomiej Głownia f5868b6f1b feat(trading): show when a market may be closed through governance (#5057) 2023-10-23 13:42:37 +02:00
m.ray 20e1522088 chore(trading): use quantum for liquidity supplied (#5099) 2023-10-23 10:24:26 +01:00
Radosław Szpiech ae02b153f8 chore(trading): fix cancel all button and move trading orders tests (#5100) 2023-10-23 08:34:50 +02:00
Maciek fee5f862a7 chore(trading): remove account history tab (#5098) 2023-10-20 15:08:19 +02:00
Edd c25362b084 feat(governance): add restricted page (#5097) 2023-10-20 11:27:14 +00:00
Edd 7c105d2c6b feat(explorer): add restricted route (#5094) 2023-10-19 18:28:33 +00:00
Maciek 2dfcb02d1e chore(orders): 4970 add no rows and error info (#5076) 2023-10-19 14:45:55 +01:00
Sam Keen a98ab775c3 feat(governance): transfers destination field can fallback to type (#5092) 2023-10-19 11:16:00 +01:00
Maciek 097bf7c08c chore(trading): add button for copying asset id (#5075) 2023-10-18 17:02:34 +02:00
Bartłomiej Głowniaandbwallacee 236e35a92b feat(trading): show successor banner if market is settled or successor market exists (#5056)
Co-authored-by: bwallacee <ben@vega.xyz>
2023-10-18 14:48:52 +02:00
Maciek abf84b9d45 chore(ui-toolkit): 4970 async renderer - improve error handling (#5063) 2023-10-18 11:11:05 +02:00
m.ray 6d2a2ea0a0 chore(trading): update snap version to 0.3.1 (#5059) 2023-10-18 09:56:50 +01:00
Matthew Russell 6ac79e9f6d chore(trading,explorer,governance): update codeowners so dev team only is required for reviews 2023-10-17 20:14:48 -07:00
Matthew Russell 835ee64243 fix(trading): hydration error due to loader (#5043) 2023-10-17 20:05:53 -07:00
Matthew Russell e7c3b5054c fix(trading): market selector dropdown not updating (#5027) 2023-10-17 09:56:48 -07:00
Maciek 9dc9588a14 chore(ledger): 4895 refactor ledger csv extract (#5053) 2023-10-17 17:15:38 +02:00
Ben a0844d41bf chore(trading): move transfer test to jest (#5058) 2023-10-17 10:56:21 +01:00
m.ray 3d1aa74128 fix(trading): liquidation price tooltip link (#5050) 2023-10-17 10:44:49 +01:00
107 changed files with 3201 additions and 1960 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
* @vegaprotocol/frontend @vegaprotocol/frontend-qa
* @vegaprotocol/frontend
*.graphql @vegaprotocol/core
+1 -1
View File
@@ -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);
+12
View File
@@ -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",
@@ -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();
});
});
@@ -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;
+7 -1
View File
@@ -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,
+1
View File
@@ -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',
@@ -1,529 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderAmendment, OrderCancellation } from '@vegaprotocol/wallet';
import {
updateOrder,
getSubscriptionMocks,
} from '../support/order-update-subscription';
import {
testOrderCancellation,
testOrderAmendment,
} from '../support/order-validation';
const orderSymbol = 'instrument-code';
const orderSize = 'size';
const orderType = 'type';
const orderStatus = 'status';
const orderRemaining = 'remaining';
const orderPrice = 'price';
const orderTimeInForce = 'timeInForce';
const orderUpdatedAt = 'updatedAt';
const cancelOrderBtn = 'cancel';
const cancelAllOrdersBtn = 'cancelAll';
const editOrderBtn = 'edit';
describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.wait('@Markets');
});
it('renders orders', () => {
cy.getByTestId('tab-orders').should('be.visible');
cy.getByTestId(cancelAllOrdersBtn).should('be.visible');
cy.getByTestId(cancelOrderBtn).should('have.length.at.least', 1);
cy.getByTestId(editOrderBtn).should('have.length.at.least', 1);
cy.getByTestId('tab-orders').within(() => {
cy.get(`[role='rowgroup']`)
.first()
.within(() => {
cy.get(`[col-id='${orderSymbol}']`).each(($symbol) => {
cy.wrap($symbol).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderRemaining}']`).each(($remaining) => {
cy.wrap($remaining).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderSize}']`).each(($size) => {
cy.wrap($size).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderType}']`).each(($type) => {
cy.wrap($type).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderStatus}']`).each(($status) => {
cy.wrap($status).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderPrice}']`).each(($price) => {
cy.wrap($price).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderTimeInForce}']`).each(($timeInForce) => {
cy.wrap($timeInForce).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderUpdatedAt}']`).each(($dateTime) => {
cy.wrap($dateTime).invoke('text').should('not.be.empty');
});
});
});
});
it('partially filled orders should not show close/edit buttons', () => {
const partiallyFilledId =
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
cy.getByTestId('tab-orders').should('be.visible');
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Partially Filled').click();
cy.getByTestId('All').click();
cy.get(`[row-id="${partiallyFilledId}"]`)
.eq(0)
.within(() => {
cy.get(`[col-id='${orderStatus}']`).should(
'have.text',
'Partially Filled'
);
cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7');
cy.get(`[col-id='${orderSize}']`).should('have.text', '-10');
cy.getByTestId(cancelOrderBtn).should('not.exist');
cy.getByTestId(editOrderBtn).should('not.exist');
});
});
it('orders are sorted by most recent order', () => {
// 7003-MORD-002
const expectedOrderList = [
'BTCUSD.MF21',
'SOLUSD',
'AAPL.MF21',
'BTCUSD.MF21',
'BTCUSD.MF21',
];
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Reset').click();
cy.getByTestId('All').click();
cy.getByTestId('tab-orders')
.get(
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
)
.should('have.length.at.least', expectedOrderList.length)
.then(($symbols) => {
const symbolNames: string[] = [];
cy.wrap($symbols)
.each(($symbol) => {
cy.wrap($symbol)
.invoke('text')
.then((text) => {
symbolNames.push(text);
});
})
.then(() => {
expect(symbolNames).to.include.ordered.members(expectedOrderList);
});
});
});
});
describe('subscribe orders', { tags: '@smoke' }, () => {
let orderId = '0';
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
orderId = (parseInt(orderId, 10) + 1).toString();
});
// 7002-SORD-053
// 7002-SORD-040
// 7003-MORD-001
it('must see an active order', () => {
// 7002-SORD-041
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Active');
});
it('must see an expired order', () => {
// 7002-SORD-042
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_EXPIRED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Expired');
});
it('must see a cancelled order', () => {
// 7002-SORD-043
// NOT COVERED: see the txn that cancelled it and a link to the block explorer, if cancelled by a user transaction.
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_CANCELLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Cancelled');
});
it('must see a stopped order', () => {
// 7002-SORD-044
// NOT COVERED: see an explanation of why stopped
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_STOPPED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Stopped');
});
it('must see a partially filled order', () => {
// 7002-SORD-045
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARTIALLY_FILLED,
size: '5',
remaining: '1',
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Partially Filled'
);
cy.getByTestId(`order-status-${orderId}`)
.parentsUntil(`.ag-row`)
.siblings(`[col-id=${orderRemaining}]`)
.should('have.text', '4');
});
it('must see a filled order', () => {
// 7002-SORD-046
// 7003-MORD-020
// NOT COVERED: Must be able to see/link to all trades that were created from this order
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_FILLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
cy.get(`[col-id="${orderSymbol}"]`).contains('[title="Future"]', 'Futr');
});
it('must see a rejected order', () => {
// 7002-SORD-047
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_REJECTED,
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_INTERNAL_ERROR,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Rejected: Internal error'
);
});
it('must see a parked order', () => {
// 7002-SORD-048
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARKED,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Parked: Internal error'
);
});
it('must see the size of the order and direction/side -', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '15',
side: Schema.Side.SIDE_SELL,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '-15');
});
it('must see the size of the order and direction/side +', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '5',
side: Schema.Side.SIDE_BUY,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '+5');
});
it('for limit typy must see the Limit price that was set on the order', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '200.00');
});
it('must see a pegged order - ask', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_BUY,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
offset: '250000',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Ask - 2.50 Peg limit');
});
it('must see a pegged order - bid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
offset: '100',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Bid + 0.001 Peg limit');
});
it('must see a pegged order - mid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
offset: '0.5',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Mid + 0.00001 Peg limit');
});
it('for market typy must not see a price for active or parked orders', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_PARKED,
peggedOrder: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '-');
});
it('must see the time in force applied to the order', () => {
// 7003-MORD-006
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_ACTIVE,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id='${orderTimeInForce}']`)
.should('have.text', 'GTC');
});
it('for Active order when is part of a liquidity or peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a liquidity, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
});
describe('amend and cancel order', { tags: '@smoke' }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
cy.mockVegaWalletTransaction();
});
const orderId = '1234567890';
// this test is flakey
it('must be able to amend the price of an order', () => {
// 7003-MORD-007
// 7003-MORD-012
// 7003-MORD-014
// 7003-MORD-015
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click();
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('100');
cy.getByTestId('edit-order').find('[type="submit"]').click();
const order: OrderAmendment = {
orderId: orderId,
marketId: 'market-0',
price: '10000000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
sizeDelta: 0,
};
testOrderAmendment(order);
});
});
it('must be able to cancel an individual order', () => {
// 7003-MORD-007
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="icon-cross"]`)
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {
orderId: orderId,
marketId: 'market-0',
};
testOrderCancellation(order);
});
});
it('must be able to cancel all orders on all markets', () => {
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[data-testid="cancelAll"]`)
.should('have.text', 'Cancel all')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {};
testOrderCancellation(order);
});
});
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', () => {
// 7003-MORD-013
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('0.111111');
cy.getByTestId('edit-order').find('[type="submit"]').click();
cy.getByTestId('input-error-text').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
});
});
@@ -1,91 +0,0 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const transferForm = 'transfer-form';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
@@ -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
}
}
}
}
@@ -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} />
+105 -95
View File
@@ -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';
+1 -1
View File
@@ -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',
+47 -21
View File
@@ -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&apos;re sorry but we don&apos;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,
+79 -26
View File
@@ -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 (
+4 -2
View File
@@ -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}
/>
);
};
@@ -6,7 +6,7 @@ import { TelemetryApproval } from './telemetry-approval';
import { t } from '@vegaprotocol/i18n';
import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step';
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_toast_id';
export const Telemetry = () => {
const onboardingDissmissed = useOnboardingStore((store) => store.dismissed);
+5 -6
View File
@@ -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,
}}
/>
);
@@ -7,6 +7,7 @@ import {
useEthTransactionUpdater,
useEthWithdrawApprovalsManager,
} from '@vegaprotocol/web3';
import { useLedgerDownloadManager } from '@vegaprotocol/ledger';
export const TransactionHandlers = () => {
useVegaTransactionManager();
@@ -14,5 +15,6 @@ export const TransactionHandlers = () => {
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
useLedgerDownloadManager();
return null;
};
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

+136 -2
View File
@@ -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',
},
},
},
@@ -88,6 +88,9 @@ describe('TransferForm', () => {
});
it('validates a manually entered address', async () => {
// 1003-TRAN-012
// 1003-TRAN-013
// 1003-TRAN-004
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
@@ -116,6 +119,11 @@ describe('TransferForm', () => {
});
it('validates fields and submits', async () => {
// 1003-TRAN-002
// 1003-TRAN-003
// 1002-WITH-010
// 1003-TRAN-011
// 1003-TRAN-014
render(<TransferForm {...props} />);
// check current pubkey not shown
+10 -1
View File
@@ -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,
+3 -1
View File
@@ -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>
);
};
@@ -37,6 +37,7 @@ import {
AccordionChevron,
AccordionPanel,
Intent,
ExternalLink,
Pill,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
@@ -385,7 +386,21 @@ export const DealTicketMarginDetails = ({
value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
labelDescription={
<>
<span>{LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}</span>{' '}
<span>
{t('For full details please see ')}
<ExternalLink
href={
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
}
>
{t('liquidation price estimate documentation.')}
</ExternalLink>
</span>
</>
}
/>
{partyId && (
<AccountBreakdownDialog
@@ -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) {
+1 -1
View File
@@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
);
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
'This is an approximation (or a range) for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
'This is an approximation for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
);
export const EST_SLIPPAGE = t(
+1
View File
@@ -1 +1,2 @@
export * from './lib/ledger-export-form';
export * from './lib/ledger-download-store';
@@ -0,0 +1,142 @@
import { create } from 'zustand';
import type { ReactNode } from 'react';
import { useCallback, useEffect } from 'react';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { useToasts, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { subscribeWithSelector } from 'zustand/middleware';
type DownloadSettings = {
title: string;
link: string;
filename?: string;
isDownloaded?: boolean;
isChanged?: boolean;
isError?: boolean;
errorMessage?: string;
isDelayed?: boolean;
intent?: Intent;
blob?: Blob;
};
export type LedgerDownloadFileStore = {
queue: DownloadSettings[];
hasItem: (link: string) => boolean;
removeItem: (link: string) => void;
updateQueue: (item: DownloadSettings) => void;
};
export const useLedgerDownloadFile = create<LedgerDownloadFileStore>()(
subscribeWithSelector((set, get) => ({
queue: [],
hasItem: (link: string) =>
get().queue.findIndex((item) => item.link === link) > -1,
removeItem: (link: string) => {
const queue = get().queue;
const index = queue.findIndex((item) => item.link === link);
if (index > -1) {
queue.splice(index, 1);
set({ queue: [...queue] });
}
},
updateQueue: (newitem: DownloadSettings) => {
const queue = get().queue;
const index = queue.findIndex((item) => item.link === newitem.link);
if (index > -1) {
queue[index] = { ...queue[index], ...newitem };
set({ queue: [...queue] });
} else {
set({ queue: [newitem, ...queue] });
}
},
}))
);
const ErrorContent = ({ message }: { message?: string }) => (
<>
<h4 className="mb-1 text-sm">{t('Something went wrong')}</h4>
<p>{message || t('Try again later')}</p>
</>
);
const InfoContent = ({ progress = false }) => (
<>
<p>{t('Please note this can take several minutes.')}</p>
<p>{t('You will be notified here when your file is ready.')}</p>
<h4 className="my-2">
{progress ? t('Still in progress') : t('Download has been started')}
</h4>
</>
);
export const useLedgerDownloadManager = () => {
const queue = useLedgerDownloadFile((store) => store.queue);
const updateQueue = useLedgerDownloadFile((store) => store.updateQueue);
const removeItem = useLedgerDownloadFile((store) => store.removeItem);
const [setToast, updateToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.update,
store.hasToast,
store.remove,
]);
const onDownloadClose = useCallback(
(id: string) => {
removeToast(id);
removeItem(id);
},
[removeToast, removeItem]
);
const createToast = (item: DownloadSettings) => {
let content: ReactNode;
switch (true) {
case item.isError:
content = <ErrorContent message={item.errorMessage} />;
break;
case Boolean(item.blob):
content = (
<>
<h4 className="mb-1 text-sm">{t('Your file is ready')}</h4>
<a
onClick={() => onDownloadClose(item.link)}
href={URL.createObjectURL(item.blob as Blob)}
download={item.filename}
className="underline"
>
{t('Get file here')}
</a>
</>
);
break;
default:
content = <InfoContent progress={item.isDelayed} />;
}
const toast: Toast = {
id: item.link,
intent: item.intent || Intent.Primary,
content: (
<>
<h3 className="mb-1 text-md uppercase">{item.title}</h3>
{content}
</>
),
onClose: () => onDownloadClose(item.link),
loader: !item.isDownloaded && !item.isError,
};
if (hasToast(toast.id)) {
updateToast(toast.id, toast);
} else {
setToast(toast);
}
};
useEffect(() => {
queue.forEach((item) => {
if (item.isChanged) {
createToast(item);
updateQueue({ ...item, isChanged: false });
}
});
}, [queue, createToast, updateQueue]);
};
+56 -20
View File
@@ -1,10 +1,21 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createDownloadUrl, LedgerExportForm } from './ledger-export-form';
import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils';
import {
useLedgerDownloadManager,
useLedgerDownloadFile,
} from './ledger-download-store';
import { Intent } from '@vegaprotocol/ui-toolkit';
const mockSetToast = jest.fn();
jest.mock('@vegaprotocol/ui-toolkit', () => ({
...jest.requireActual('@vegaprotocol/ui-toolkit'),
useToasts: jest.fn(() => [mockSetToast, jest.fn(), jest.fn(() => false)]),
}));
const vegaUrl = 'https://vega-url.co.uk/querystuff';
const mockResponse = {
ok: true,
headers: { get: jest.fn() },
blob: () => '',
};
@@ -28,6 +39,7 @@ describe('LedgerExportForm', () => {
afterAll(() => {
jest.useRealTimers();
jest.clearAllMocks();
});
it('should be properly rendered', async () => {
@@ -43,8 +55,6 @@ describe('LedgerExportForm', () => {
// userEvent does not work with faked timers
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -52,9 +62,6 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=1691057410000000000`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('assetID should be properly change request url', async () => {
@@ -75,8 +82,6 @@ describe('LedgerExportForm', () => {
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -84,9 +89,6 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=1691057410000000000`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('date-from should properly change request url', async () => {
@@ -110,8 +112,6 @@ describe('LedgerExportForm', () => {
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -119,10 +119,6 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=${toNanoSeconds(newDate)}`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('date-to should properly change request url', async () => {
@@ -156,10 +152,6 @@ describe('LedgerExportForm', () => {
)}`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('Time zone sentence should be properly displayed', () => {
@@ -205,6 +197,50 @@ describe('LedgerExportForm', () => {
screen.queryByText(/^The downloaded file uses the UTC/)
).not.toBeInTheDocument();
});
it('A toast notification should be displayed', async () => {
useLedgerDownloadFile.setState({ queue: [] });
const TestWrapper = () => {
useLedgerDownloadManager();
return (
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
};
render(<TestWrapper />);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ledger-download-button'));
const link = `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000`;
await waitFor(() => {
expect(mockSetToast).toHaveBeenCalledWith({
id: link,
content: expect.any(Object),
onClose: expect.any(Function),
intent: Intent.Primary,
loader: true,
});
expect(global.fetch).toHaveBeenCalledWith(link);
});
mockSetToast.mockClear();
(global.fetch as jest.Mock).mockClear();
fireEvent.click(screen.getByTestId('ledger-download-button')); // click again
await waitFor(() => {
expect(mockSetToast).toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
});
});
});
describe('createDownloadUrl', () => {
+75 -31
View File
@@ -1,17 +1,22 @@
import { useRef, useState } from 'react';
import { z } from 'zod';
import { format, subDays } from 'date-fns';
import {
TradingButton,
Intent,
Loader,
TradingButton,
TradingFormGroup,
TradingInput,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { toNanoSeconds, VEGA_ID_REGEX } from '@vegaprotocol/utils';
import { z } from 'zod';
import {
formatForInput,
toNanoSeconds,
VEGA_ID_REGEX,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/logger';
import { formatForInput } from '@vegaprotocol/utils';
import { subDays } from 'date-fns';
import { useLedgerDownloadFile } from './ledger-download-store';
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
@@ -73,10 +78,14 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current);
const [isDownloading, setIsDownloading] = useState(false);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId || isDownloading);
const disabled = Boolean(!assetId);
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
const updateDownloadQueue = useLedgerDownloadFile(
(store) => store.updateQueue
);
const assetDropDown = (
<TradingSelect
@@ -87,7 +96,6 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
}}
className="w-full"
data-testid="select-ledger-asset"
disabled={isDownloading}
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
@@ -97,32 +105,78 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
</TradingSelect>
);
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
const title = t('Downloading for %s from %s till %s', [
assets[assetId],
format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
]);
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
});
setIsDownloading(true);
}, 1000 * 30);
try {
updateDownloadQueue(downloadStoreItem);
const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
}
throw new Error('Download of ledger entries failed');
}
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob();
if (blob) {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
updateDownloadQueue({
...downloadStoreItem,
blob,
isDownloaded: true,
isChanged: true,
intent: Intent.Success,
});
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally {
setIsDownloading(false);
clearTimeout(ts);
}
};
@@ -145,7 +199,6 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
id="date-from"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
disabled={disabled}
max={maxFromDate}
/>
</TradingFormGroup>
@@ -156,19 +209,10 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
id="date-to"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
disabled={disabled}
max={maxToDate}
/>
</TradingFormGroup>
<div className="relative text-sm" title={t('Download all to .csv file')}>
{isDownloading && (
<div
className="absolute flex items-center justify-center w-full h-full"
data-testid="download-spinner"
>
<Loader size="small" />
</div>
)}
<TradingButton
fill
disabled={disabled}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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;

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