Merge branch 'develop' into 5554-enhancements-to-transfer-fee
This commit is contained in:
commit
80e30e7678
@ -1,4 +1,4 @@
|
||||
export type HashProps = {
|
||||
export type HashProps = React.HTMLProps<HTMLSpanElement> & {
|
||||
text: string;
|
||||
truncate?: boolean;
|
||||
};
|
||||
|
@ -2,4 +2,5 @@ export { default as BlockLink } from './block-link/block-link';
|
||||
export { default as PartyLink } from './party-link/party-link';
|
||||
export { default as NodeLink } from './node-link/node-link';
|
||||
export { default as MarketLink } from './market-link/market-link';
|
||||
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
|
||||
export * from './asset-link/asset-link';
|
||||
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
parameter: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Links a given network parameter to the relevant page and anchor on the page
|
||||
*/
|
||||
const NetworkParameterLink = ({
|
||||
parameter,
|
||||
...props
|
||||
}: NetworkParameterLinkProps) => {
|
||||
return (
|
||||
<Link
|
||||
className="underline"
|
||||
{...props}
|
||||
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
|
||||
>
|
||||
<Hash text={parameter} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkParameterLink;
|
@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
||||
>;
|
||||
|
||||
const base = ENV.dataSources.governanceUrl;
|
||||
const label = proposal?.rationale.title || id;
|
||||
const label = proposal?.rationale?.title || id;
|
||||
|
||||
return (
|
||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||
|
@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
... on BatchProposal {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||
|
||||
|
||||
export const ExplorerProposalStatusDocument = gql`
|
||||
@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
... on BatchProposal {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -0,0 +1,257 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BatchItem } from './batch-item';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
type Item = components['schemas']['vegaBatchProposalTermsChange'];
|
||||
|
||||
describe('BatchItem', () => {
|
||||
it('Renders "Unknown proposal type" by default', () => {
|
||||
const item = {};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Unknown proposal type" for unknown items', () => {
|
||||
const item = {
|
||||
newLochNessMonster: {
|
||||
location: 'Loch Ness',
|
||||
},
|
||||
} as unknown as Item;
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "New spot market"', () => {
|
||||
const item = {
|
||||
newSpotMarket: {},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('New spot market')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Cancel transfer"', () => {
|
||||
const item = {
|
||||
cancelTransfer: {
|
||||
changes: {
|
||||
transferId: 'transfer123',
|
||||
},
|
||||
},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
|
||||
expect(screen.getByText('transf')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Cancel transfer" without an id', () => {
|
||||
const item = {
|
||||
cancelTransfer: {
|
||||
changes: {},
|
||||
},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "New freeform"', () => {
|
||||
const item = {
|
||||
newFreeform: {},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('New freeform proposal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "New market"', () => {
|
||||
const item = {
|
||||
newMarket: {},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('New market')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "New transfer"', () => {
|
||||
const item = {
|
||||
newTransfer: {},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('New transfer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update asset" with assetId', () => {
|
||||
const item = {
|
||||
updateAsset: {
|
||||
assetId: 'asset123',
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update asset')).toBeInTheDocument();
|
||||
expect(screen.getByText('asset123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update asset" even if assetId is not set', () => {
|
||||
const item = {
|
||||
updateAsset: {
|
||||
assetId: undefined,
|
||||
},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Update asset')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update market state" with marketId', () => {
|
||||
const item = {
|
||||
updateMarketState: {
|
||||
changes: {
|
||||
marketId: 'market123',
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update market state')).toBeInTheDocument();
|
||||
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update market state" even if marketId is not set', () => {
|
||||
const item = {
|
||||
updateMarketState: {
|
||||
changes: {
|
||||
marketId: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update market state')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update network parameter" with parameter', () => {
|
||||
const item = {
|
||||
updateNetworkParameter: {
|
||||
changes: {
|
||||
key: 'parameter123',
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<BatchItem item={item} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
|
||||
expect(screen.getByText('parameter123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update network parameter" even if parameter is not set', () => {
|
||||
const item = {
|
||||
updateNetworkParameter: {
|
||||
changes: {
|
||||
key: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update referral program"', () => {
|
||||
const item = {
|
||||
updateReferralProgram: {},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(screen.getByText('Update referral program')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update spot market" with marketId', () => {
|
||||
const item = {
|
||||
updateSpotMarket: {
|
||||
marketId: 'market123',
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update spot market')).toBeInTheDocument();
|
||||
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update spot market" even if marketId is not set', () => {
|
||||
const item = {
|
||||
updateSpotMarket: {
|
||||
marketId: undefined,
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update spot market')).toBeInTheDocument();
|
||||
});
|
||||
it('Renders "Update market" with marketId', () => {
|
||||
const item = {
|
||||
updateMarket: {
|
||||
marketId: 'market123',
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update market')).toBeInTheDocument();
|
||||
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update market" even if marketId is not set', () => {
|
||||
const item = {
|
||||
updateMarket: {
|
||||
marketId: undefined,
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BatchItem item={item} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getByText('Update market')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders "Update volume discount program"', () => {
|
||||
const item = {
|
||||
updateVolumeDiscountProgram: {},
|
||||
};
|
||||
render(<BatchItem item={item} />);
|
||||
expect(
|
||||
screen.getByText('Update volume discount program')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,87 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import Hash from '../../../links/hash';
|
||||
|
||||
type Item = components['schemas']['vegaBatchProposalTermsChange'];
|
||||
|
||||
export interface BatchItemProps {
|
||||
item: Item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a one line summary for an item in a batch proposal. Could
|
||||
* easily be adapted to summarise individual proposals, but there is no
|
||||
* place for that yet.
|
||||
*
|
||||
* Details (like IDs) should be shown and linked if available, but handled
|
||||
* if not available. This is adequate as the ProposalSummary component contains
|
||||
* a JSON viewer for the full proposal.
|
||||
*/
|
||||
export const BatchItem = ({ item }: BatchItemProps) => {
|
||||
if (item.cancelTransfer) {
|
||||
const transferId = item?.cancelTransfer?.changes?.transferId || false;
|
||||
return (
|
||||
<span>
|
||||
{t('Cancel transfer')}
|
||||
{transferId && (
|
||||
<Hash className="ml-1" truncate={true} text={transferId} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
} else if (item.newFreeform) {
|
||||
return <span>{t('New freeform proposal')}</span>;
|
||||
} else if (item.newMarket) {
|
||||
return <span>{t('New market')}</span>;
|
||||
} else if (item.newSpotMarket) {
|
||||
return <span>{t('New spot market')}</span>;
|
||||
} else if (item.newTransfer) {
|
||||
return <span>{t('New transfer')}</span>;
|
||||
} else if (item.updateAsset) {
|
||||
const assetId = item?.updateAsset?.assetId || false;
|
||||
return (
|
||||
<span>
|
||||
{t('Update asset')}
|
||||
{assetId && <AssetLink className="ml-1" assetId={assetId} />}
|
||||
</span>
|
||||
);
|
||||
} else if (item.updateMarket) {
|
||||
const marketId = item?.updateMarket?.marketId || false;
|
||||
return (
|
||||
<span>
|
||||
{t('Update market')}{' '}
|
||||
{marketId && <MarketLink className="ml-1" id={marketId} />}
|
||||
</span>
|
||||
);
|
||||
} else if (item.updateMarketState) {
|
||||
const marketId = item?.updateMarketState?.changes?.marketId || false;
|
||||
return (
|
||||
<span>
|
||||
{t('Update market state')}
|
||||
{marketId && <MarketLink className="ml-1" id={marketId} />}
|
||||
</span>
|
||||
);
|
||||
} else if (item.updateNetworkParameter) {
|
||||
const param = item?.updateNetworkParameter?.changes?.key || false;
|
||||
return (
|
||||
<span>
|
||||
{t('Update network parameter')}
|
||||
{param && <NetworkParameterLink className="ml-1" parameter={param} />}
|
||||
</span>
|
||||
);
|
||||
} else if (item.updateReferralProgram) {
|
||||
return <span>{t('Update referral program')}</span>;
|
||||
} else if (item.updateSpotMarket) {
|
||||
const marketId = item?.updateSpotMarket?.marketId || '';
|
||||
return (
|
||||
<span>
|
||||
{t('Update spot market')}
|
||||
<MarketLink className="ml-1" id={marketId} />
|
||||
</span>
|
||||
);
|
||||
} else if (item.updateVolumeDiscountProgram) {
|
||||
return <span>{t('Update volume discount program')}</span>;
|
||||
}
|
||||
|
||||
return <span>{t('Unknown proposal type')}</span>;
|
||||
};
|
@ -1,6 +1,4 @@
|
||||
import type { ProposalTerms } from '../tx-proposal';
|
||||
import { useState } from 'react';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
||||
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
||||
import truncate from 'lodash/truncate';
|
||||
@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
|
||||
import { ProposalDate } from './proposal-date';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
import type { ProposalTerms } from '../tx-proposal';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { BatchItem } from './batch-item';
|
||||
|
||||
type Rationale = components['schemas']['vegaProposalRationale'];
|
||||
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
|
||||
|
||||
type ProposalTermsDialog = {
|
||||
open: boolean;
|
||||
@ -21,6 +24,7 @@ interface ProposalSummaryProps {
|
||||
id: string;
|
||||
rationale?: Rationale;
|
||||
terms?: ProposalTerms;
|
||||
batch?: Batch;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,6 +35,7 @@ export const ProposalSummary = ({
|
||||
id,
|
||||
rationale,
|
||||
terms,
|
||||
batch,
|
||||
}: ProposalSummaryProps) => {
|
||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||
open: false,
|
||||
@ -72,6 +77,18 @@ export const ProposalSummary = ({
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{batch && (
|
||||
<section className="pt-2 text-sm leading-tight my-3">
|
||||
<h2 className="text-lg pb-1">{t('Changes')}</h2>
|
||||
<ol>
|
||||
{batch.map((change, index) => (
|
||||
<li className="ml-4 list-decimal" key={`batch-${index}`}>
|
||||
<BatchItem item={change} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
<div className="pt-5">
|
||||
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
||||
{t('View terms')}
|
||||
|
@ -0,0 +1,75 @@
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||
import { ProposalSummary } from './proposal/summary';
|
||||
import Hash from '../../links/hash';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export type Proposal = components['schemas']['v1BatchProposalSubmission'];
|
||||
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
|
||||
|
||||
interface TxBatchProposalProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const TxBatchProposal = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxBatchProposalProps) => {
|
||||
if (!txData || !txData.command.batchProposalSubmission) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
let deterministicId = '';
|
||||
|
||||
const proposal: Proposal = txData.command.batchProposalSubmission;
|
||||
const sig = txData?.signature?.value;
|
||||
if (sig) {
|
||||
deterministicId = txSignatureToDeterministicId(sig);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{t('Batch proposal')}</TableCell>
|
||||
</TableRow>
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
blockData={blockData}
|
||||
hideTypeRow={true}
|
||||
/>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Batch size')}</TableCell>
|
||||
<TableCell>
|
||||
{proposal.terms?.changes?.length || t('No changes')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Proposal ID')}</TableCell>
|
||||
<TableCell>
|
||||
<Hash text={deterministicId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
{proposal && (
|
||||
<ProposalSummary
|
||||
id={deterministicId}
|
||||
rationale={proposal?.rationale}
|
||||
terms={proposal.terms}
|
||||
batch={proposal.terms?.changes}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -33,6 +33,7 @@ import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
||||
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||
import { TxBatchProposal } from './tx-batch-proposal';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@ -136,6 +137,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsJoinTeam;
|
||||
case 'Update Margin Mode':
|
||||
return TxDetailsUpdateMarginMode;
|
||||
case 'Batch Proposal':
|
||||
return TxBatchProposal;
|
||||
default:
|
||||
return TxDetailsGeneric;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export type FilterOption =
|
||||
| 'Amend Order'
|
||||
| 'Apply Referral Code'
|
||||
| 'Batch Market Instructions'
|
||||
| 'Batch Proposal'
|
||||
| 'Cancel LiquidityProvision Order'
|
||||
| 'Cancel Order'
|
||||
| 'Cancel Transfer Funds'
|
||||
@ -67,7 +68,13 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
||||
'Cancel Transfer Funds',
|
||||
'Withdraw',
|
||||
],
|
||||
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
|
||||
Governance: [
|
||||
'Batch Proposal',
|
||||
'Delegate',
|
||||
'Undelegate',
|
||||
'Vote on Proposal',
|
||||
'Proposal',
|
||||
],
|
||||
Referrals: [
|
||||
'Apply Referral Code',
|
||||
'Create Referral Set',
|
||||
|
@ -71,18 +71,27 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||
|
||||
if (status === 'confirmed') {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div
|
||||
className="flex flex-col items-start gap-2"
|
||||
data-testid="team-creation-success-message"
|
||||
>
|
||||
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||
{code && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
Your team ID is:{' '}
|
||||
<span className="font-mono break-all">{code}</span>
|
||||
<span
|
||||
className="font-mono break-all"
|
||||
data-testid="team-id-display"
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
</p>
|
||||
<TradingAnchorButton
|
||||
href={Links.COMPETITIONS_TEAM(code)}
|
||||
intent={Intent.Info}
|
||||
size="small"
|
||||
data-testid="view-team-button"
|
||||
>
|
||||
{t('View team')}
|
||||
</TradingAnchorButton>
|
||||
|
@ -16,6 +16,8 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
|
||||
import { useTeams } from '../../lib/hooks/use-teams';
|
||||
import take from 'lodash/take';
|
||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||
import { TeamCard } from '../../components/competitions/team-card';
|
||||
import { useMyTeam } from '../../lib/hooks/use-my-team';
|
||||
|
||||
export const CompetitionsHome = () => {
|
||||
const t = useT();
|
||||
@ -33,6 +35,13 @@ export const CompetitionsHome = () => {
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
|
||||
const {
|
||||
team: myTeam,
|
||||
stats: myTeamStats,
|
||||
games: myTeamGames,
|
||||
rank: myTeamRank,
|
||||
} = useMyTeam();
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CompetitionsHeader title={t('Competitions')}>
|
||||
@ -43,65 +52,83 @@ export const CompetitionsHome = () => {
|
||||
</p>
|
||||
</CompetitionsHeader>
|
||||
|
||||
{/** Get started */}
|
||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||
{/** Team card */}
|
||||
{myTeam ? (
|
||||
<>
|
||||
<h2 className="text-2xl mb-6">{t('My team')}</h2>
|
||||
<div className="mb-12">
|
||||
<TeamCard
|
||||
team={myTeam}
|
||||
rank={myTeamRank}
|
||||
stats={myTeamStats}
|
||||
games={myTeamGames}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/** Get started */}
|
||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||
|
||||
<CompetitionsActionsContainer>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
<CompetitionsActionsContainer>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
data-testId="create-public-team-button"
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/** List of available games */}
|
||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
||||
|
@ -105,7 +105,11 @@ export const JoinButton = ({
|
||||
// Party is in a team, but not this one
|
||||
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
||||
return (
|
||||
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
|
||||
<Button
|
||||
onClick={() => onJoin('switch')}
|
||||
intent={Intent.Primary}
|
||||
data-testid="switch-team-button"
|
||||
>
|
||||
{t('Switch team')}{' '}
|
||||
</Button>
|
||||
);
|
||||
@ -215,7 +219,11 @@ const DialogContent = ({
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button onClick={joinTeam} intent={Intent.Success}>
|
||||
<Button
|
||||
onClick={joinTeam}
|
||||
intent={Intent.Success}
|
||||
data-testid="confirm-switch-button"
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
TextArea,
|
||||
TradingButton,
|
||||
Intent,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||
|
||||
@ -17,6 +19,8 @@ import type {
|
||||
UpdateReferralSet,
|
||||
Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import classNames from 'classnames';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export type FormFields = {
|
||||
id: string;
|
||||
@ -111,9 +115,12 @@ export const TeamForm = ({
|
||||
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||
<input type="hidden" {...register('id')} />
|
||||
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||
<TradingInput {...register('name', { required: t('Required') })} />
|
||||
<TradingInput
|
||||
{...register('name', { required: t('Required') })}
|
||||
data-testid="team-name-input"
|
||||
/>
|
||||
{errors.name?.message && (
|
||||
<TradingInputError forInput="name">
|
||||
<TradingInputError forInput="name" data-testid="team-name-error">
|
||||
{errors.name.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@ -129,9 +136,10 @@ export const TeamForm = ({
|
||||
{...register('url', {
|
||||
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
||||
})}
|
||||
data-testid="team-url-input"
|
||||
/>
|
||||
{errors.url?.message && (
|
||||
<TradingInputError forInput="url">
|
||||
<TradingInputError forInput="url" data-testid="team-url-error">
|
||||
{errors.url.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@ -148,9 +156,13 @@ export const TeamForm = ({
|
||||
message: t('Invalid image URL'),
|
||||
},
|
||||
})}
|
||||
data-testid="avatar-url-input"
|
||||
/>
|
||||
{errors.avatarUrl?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
<TradingInputError
|
||||
forInput="avatarUrl"
|
||||
data-testid="avatar-url-error"
|
||||
>
|
||||
{errors.avatarUrl.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@ -175,6 +187,7 @@ export const TeamForm = ({
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
data-testid="team-private-checkbox"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@ -203,9 +216,13 @@ export const TeamForm = ({
|
||||
},
|
||||
},
|
||||
})}
|
||||
data-testid="team-allow-list-textarea"
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
<TradingInputError
|
||||
forInput="avatarUrl"
|
||||
data-testid="team-allow-list-error"
|
||||
>
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@ -239,16 +256,57 @@ const SubmitButton = ({
|
||||
text = t('Update');
|
||||
}
|
||||
|
||||
let confirmedText = t('Created');
|
||||
if (type === TransactionType.UpdateReferralSet) {
|
||||
confirmedText = t('Updated');
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
text = t('Confirm in wallet...');
|
||||
} else if (status === 'pending') {
|
||||
text = t('Confirming transaction...');
|
||||
}
|
||||
|
||||
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
|
||||
useLayoutEffect(() => {
|
||||
let to: ReturnType<typeof setTimeout>;
|
||||
if (status === 'confirmed' && !showConfirmed) {
|
||||
to = setTimeout(() => {
|
||||
setShowConfirmed(true);
|
||||
}, 100);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(to);
|
||||
};
|
||||
}, [showConfirmed, status]);
|
||||
|
||||
const confirmed = (
|
||||
<span
|
||||
className={classNames('text-sm transition-opacity opacity-0', {
|
||||
'opacity-100': showConfirmed,
|
||||
})}
|
||||
>
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
size={18}
|
||||
className="text-vega-green-500"
|
||||
/>{' '}
|
||||
{confirmedText}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
||||
{text}
|
||||
</TradingButton>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<TradingButton
|
||||
type="submit"
|
||||
intent={Intent.Info}
|
||||
disabled={disabled}
|
||||
data-testid="team-form-submit-button"
|
||||
>
|
||||
{text}
|
||||
</TradingButton>
|
||||
{status === 'confirmed' && confirmed}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,16 +1,24 @@
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { type ComponentProps } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
||||
export const UpdateTeamButton = ({
|
||||
team,
|
||||
size = 'medium',
|
||||
}: {
|
||||
team: Pick<Team, 'teamId' | 'referrer'>;
|
||||
size?: ComponentProps<typeof TradingAnchorButton>['size'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
|
||||
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||
return (
|
||||
<TradingAnchorButton
|
||||
size={size}
|
||||
data-testid="update-team-button"
|
||||
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||
intent={Intent.Info}
|
||||
|
@ -5,7 +5,11 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
export const Box = ({
|
||||
children,
|
||||
backgroundImage,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@ -13,9 +17,22 @@ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
'border rounded-lg',
|
||||
'p-6',
|
||||
'relative p-6 overflow-hidden',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
>
|
||||
{Boolean(backgroundImage?.length) && (
|
||||
<div
|
||||
className={classNames(
|
||||
'pointer-events-none',
|
||||
'bg-no-repeat bg-center bg-[length:500px_500px]',
|
||||
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
|
||||
)}
|
||||
style={{ backgroundImage: `url("${backgroundImage}")` }}
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
||||
const NUM_AVATARS = 20;
|
||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||
|
||||
const getFallbackAvatar = (teamId: string) => {
|
||||
export const getFallbackAvatar = (teamId: string) => {
|
||||
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||
.toString()
|
||||
.padStart(2, '0'); // between 01 - 20
|
||||
|
154
apps/trading/components/competitions/team-card.tsx
Normal file
154
apps/trading/components/competitions/team-card.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import { type TeamGame, type TeamStats } from '../../lib/hooks/use-team';
|
||||
import { type TeamsFieldsFragment } from '../../lib/hooks/__generated__/Teams';
|
||||
import { TeamAvatar, getFallbackAvatar } from './team-avatar';
|
||||
import { FavoriteGame, Stat } from './team-stats';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Box } from './box';
|
||||
import { Intent, Tooltip, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { Links } from '../../lib/links';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { take } from 'lodash';
|
||||
import { DispatchMetricLabels } from '@vegaprotocol/types';
|
||||
import classNames from 'classnames';
|
||||
import { UpdateTeamButton } from '../../client-pages/competitions/update-team-button';
|
||||
|
||||
export const TeamCard = ({
|
||||
rank,
|
||||
team,
|
||||
stats,
|
||||
games,
|
||||
}: {
|
||||
rank: number;
|
||||
team: TeamsFieldsFragment;
|
||||
stats?: TeamStats;
|
||||
games?: TeamGame[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const lastGames = take(
|
||||
orderBy(
|
||||
games?.map((g) => ({
|
||||
rank: g.team.rank,
|
||||
metric: g.team.rewardMetric,
|
||||
epoch: g.epoch,
|
||||
})),
|
||||
(i) => i.epoch,
|
||||
'desc'
|
||||
),
|
||||
5
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'gap-6 grid grid-cols-1 grid-rows-1',
|
||||
'md:grid-cols-3'
|
||||
)}
|
||||
>
|
||||
{/** Card */}
|
||||
<Box
|
||||
backgroundImage={team.avatarUrl || getFallbackAvatar(team.teamId)}
|
||||
className="flex flex-col items-center gap-3 min-w-[80px] lg:min-w-[112px]"
|
||||
>
|
||||
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||
<h1 className="calt lg:text-2xl" data-testid="team-name">
|
||||
{team.name}
|
||||
</h1>
|
||||
{games && <FavoriteGame games={games} noLabel />}
|
||||
<TradingAnchorButton
|
||||
size="extra-small"
|
||||
intent={Intent.Primary}
|
||||
href={Links.COMPETITIONS_TEAM(team.teamId)}
|
||||
>
|
||||
{t('Profile')}
|
||||
</TradingAnchorButton>
|
||||
<UpdateTeamButton team={team} size="extra-small" />
|
||||
</Box>
|
||||
|
||||
{/** Tiles */}
|
||||
<Box className="w-full md:col-span-2">
|
||||
<div
|
||||
className={classNames(
|
||||
'grid gap-3 w-full mb-4',
|
||||
'md:grid-cols-3 md:grid-rows-2',
|
||||
'grid-cols-2 grid-rows-3'
|
||||
)}
|
||||
>
|
||||
<Stat
|
||||
className="flex flex-col-reverse"
|
||||
value={rank}
|
||||
label={t('Rank')}
|
||||
valueTestId="team-rank"
|
||||
/>
|
||||
<Stat
|
||||
className="flex flex-col-reverse"
|
||||
value={team.totalMembers || 0}
|
||||
label={t('Members')}
|
||||
valueTestId="members-count-stat"
|
||||
/>
|
||||
<Stat
|
||||
className="flex flex-col-reverse"
|
||||
value={stats?.totalGamesPlayed || 0}
|
||||
label={t('Total games')}
|
||||
valueTestId="total-games-stat"
|
||||
/>
|
||||
<Stat
|
||||
className="flex flex-col-reverse"
|
||||
value={
|
||||
stats?.totalQuantumVolume
|
||||
? formatNumberRounded(
|
||||
new BigNumber(stats.totalQuantumVolume || 0),
|
||||
'1e3'
|
||||
)
|
||||
: 0
|
||||
}
|
||||
label={t('Total volume')}
|
||||
valueTestId="total-volume-stat"
|
||||
/>
|
||||
<Stat
|
||||
className="flex flex-col-reverse"
|
||||
value={
|
||||
stats?.totalQuantumRewards
|
||||
? formatNumberRounded(
|
||||
new BigNumber(stats.totalQuantumRewards || 0),
|
||||
'1e3'
|
||||
)
|
||||
: 0
|
||||
}
|
||||
label={t('Rewards paid out')}
|
||||
valueTestId="rewards-paid-stat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<dl className="w-full pt-4 border-t border-vega-clight-700 dark:border-vega-cdark-700">
|
||||
<dt className="mb-1 text-sm text-muted">
|
||||
{t('Last {{games}} games result', {
|
||||
replace: { games: lastGames.length || '' },
|
||||
})}
|
||||
</dt>
|
||||
<dd className="flex flex-row flex-wrap gap-2">
|
||||
{lastGames.length === 0 && t('None available')}
|
||||
{lastGames.map((game, i) => (
|
||||
<Tooltip key={i} description={DispatchMetricLabels[game.metric]}>
|
||||
<button className="cursor-help text-sm bg-vega-clight-700 dark:bg-vega-cdark-700 px-2 py-1 rounded-full">
|
||||
<RankLabel rank={game.rank} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</dd>
|
||||
</dl>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the english ordinal for given rank only if the current language is set
|
||||
* to english.
|
||||
*/
|
||||
const RankLabel = ({ rank }: { rank: number }) => {
|
||||
const t = useT();
|
||||
return t('place', { count: rank, ordinal: true });
|
||||
};
|
@ -15,6 +15,7 @@ import {
|
||||
} from '../../lib/hooks/use-team';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const TeamStats = ({
|
||||
stats,
|
||||
@ -102,7 +103,13 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
export const FavoriteGame = ({
|
||||
games,
|
||||
noLabel = false,
|
||||
}: {
|
||||
games: TeamGame[];
|
||||
noLabel?: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const rewardMetrics = games.map(
|
||||
@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
|
||||
return (
|
||||
<dl className="flex flex-col gap-1">
|
||||
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
||||
<dt
|
||||
className={classNames('text-muted text-sm', {
|
||||
hidden: noLabel,
|
||||
})}
|
||||
>
|
||||
{t('Favorite game')}
|
||||
</dt>
|
||||
<dd>
|
||||
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||
<VegaIcon
|
||||
@ -142,7 +155,7 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||
{children}
|
||||
@ -150,11 +163,11 @@ const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const StatSectionSeparator = () => {
|
||||
export const StatSectionSeparator = () => {
|
||||
return <div className="hidden md:block border-r border-default" />;
|
||||
};
|
||||
|
||||
const StatList = ({ children }: { children: ReactNode }) => {
|
||||
export const StatList = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||
{children}
|
||||
@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Stat = ({
|
||||
export const Stat = ({
|
||||
value,
|
||||
label,
|
||||
tooltip,
|
||||
valueTestId,
|
||||
className,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
valueTestId?: string;
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={classNames(className)}>
|
||||
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||
{value}
|
||||
</dd>
|
||||
|
@ -152,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||
|
||||
return (
|
||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
||||
<Card
|
||||
title={t('Active rewards')}
|
||||
className="lg:col-span-full"
|
||||
data-testid="active-rewards-card"
|
||||
>
|
||||
{enrichedTransfers.length > 1 && (
|
||||
<TradingInput
|
||||
onChange={(e) =>
|
||||
@ -312,49 +316,30 @@ export const ActiveRewardCard = ({
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
|
||||
if (marketSettled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetInSettledMarket =
|
||||
const assetInActiveMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return (
|
||||
m?.state &&
|
||||
[
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Gray out the cards that are related to suspended markets
|
||||
const suspended = transferNode.markets?.some(
|
||||
const marketSuspended = transferNode.markets?.some(
|
||||
(m) =>
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
);
|
||||
|
||||
const assetInSuspendedMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return (
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Gray out the cards that are related to suspended markets
|
||||
// Or settlement assets in markets that are not active and eligible for rewards
|
||||
const { gradientClassName, mainClassName } =
|
||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
||||
marketSuspended || !assetInActiveMarket
|
||||
? {
|
||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||
@ -371,6 +356,7 @@ export const ActiveRewardCard = ({
|
||||
'rounded-lg',
|
||||
gradientClassName
|
||||
)}
|
||||
data-testid="active-rewards-card"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
@ -382,7 +368,7 @@ export const ActiveRewardCard = ({
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<EntityIcon transfer={transfer} />
|
||||
{entityScope && (
|
||||
<span className="text-muted text-xs">
|
||||
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||
</span>
|
||||
)}
|
||||
@ -390,7 +376,7 @@ export const ActiveRewardCard = ({
|
||||
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||
<span className="font-glitch">
|
||||
<span className="font-glitch" data-testid="reward-value">
|
||||
{addDecimalsFormatNumber(
|
||||
transferNode.transfer.amount,
|
||||
transferNode.transfer.asset?.decimals || 0,
|
||||
@ -411,7 +397,7 @@ export const ActiveRewardCard = ({
|
||||
)}
|
||||
underline={true}
|
||||
>
|
||||
<span className="text-xs">
|
||||
<span className="text-xs" data-testid="distribution-strategy">
|
||||
{
|
||||
DistributionStrategyMapping[
|
||||
dispatchStrategy.distributionStrategy
|
||||
@ -429,7 +415,10 @@ export const ActiveRewardCard = ({
|
||||
'Number of epochs after distribution to delay vesting of rewards by'
|
||||
)}
|
||||
/>
|
||||
<span className="text-muted text-xs whitespace-nowrap">
|
||||
<span
|
||||
className="text-muted text-xs whitespace-nowrap"
|
||||
data-testid="locked-for"
|
||||
>
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.dispatchStrategy?.lockPeriod,
|
||||
})}
|
||||
@ -438,15 +427,15 @@ export const ActiveRewardCard = ({
|
||||
</div>
|
||||
|
||||
<span className="border-[0.5px] border-gray-700" />
|
||||
<span>
|
||||
<span data-testid="dispatch-metric-info">
|
||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||
<Tooltip
|
||||
underline={suspended}
|
||||
underline={marketSuspended}
|
||||
description={
|
||||
(suspended || assetInSuspendedMarket) &&
|
||||
(marketSuspended || !assetInActiveMarket) &&
|
||||
(specificMarkets
|
||||
? t('Eligible market(s) currently suspended')
|
||||
: assetInSuspendedMarket
|
||||
: !assetInActiveMarket
|
||||
? t('Currently no markets eligible for reward')
|
||||
: '')
|
||||
}
|
||||
@ -458,8 +447,8 @@ export const ActiveRewardCard = ({
|
||||
<div className="flex items-center gap-8 flex-wrap">
|
||||
{kind.endEpoch && (
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
||||
<span>
|
||||
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||
<span data-testid="ends-in">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.endEpoch - currentEpoch,
|
||||
})}
|
||||
@ -470,7 +459,7 @@ export const ActiveRewardCard = ({
|
||||
{
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||
<span>
|
||||
<span data-testid="assessed-over">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: dispatchStrategy.windowLength,
|
||||
})}
|
||||
@ -513,7 +502,7 @@ const RewardRequirements = ({
|
||||
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||
})}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<dd className="flex items-center gap-1" data-testid="scope">
|
||||
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||
</dd>
|
||||
</div>
|
||||
@ -522,7 +511,10 @@ const RewardRequirements = ({
|
||||
<dt className="flex items-center gap-1 text-muted">
|
||||
{t('Staked VEGA')}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<dd
|
||||
className="flex items-center gap-1"
|
||||
data-testid="staking-requirement"
|
||||
>
|
||||
{addDecimalsFormatNumber(
|
||||
dispatchStrategy?.stakingRequirement || 0,
|
||||
assetDecimalPlaces
|
||||
@ -534,7 +526,7 @@ const RewardRequirements = ({
|
||||
<dt className="flex items-center gap-1 text-muted">
|
||||
{t('Average position')}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<dd className="flex items-center gap-1" data-testid="average-position">
|
||||
{addDecimalsFormatNumber(
|
||||
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||
0,
|
||||
|
@ -33,7 +33,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
|
||||
"You may not have enough margin available to open this position.")
|
||||
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
||||
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
||||
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
|
||||
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
|
||||
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
||||
expect(page.get_by_test_id("sidebar-content")
|
||||
).to_contain_text("DepositFrom")
|
||||
|
@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
next_epoch(vega=vega)
|
||||
|
||||
page.reload()
|
||||
expect(page.locator(".from-vega-cdark-400")).to_be_visible(timeout=15000)
|
||||
expect(page.get_by_test_id("active-rewards-card")).to_be_visible(timeout=15000)
|
||||
governance.submit_oracle_data(
|
||||
wallet=vega.wallet,
|
||||
payload={"trading.terminated": "true"},
|
||||
@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
)
|
||||
next_epoch(vega=vega)
|
||||
page.reload()
|
||||
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
|
||||
expect(page.get_by_test_id("active-rewards-card")).not_to_be_in_viewport()
|
||||
|
@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
|
||||
import vega_sim.proto.vega as vega_protos
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from actions.utils import next_epoch
|
||||
from actions.utils import next_epoch, change_keys
|
||||
from fixtures.market import setup_continuous_market
|
||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
||||
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
|
||||
@ -14,6 +14,7 @@ def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def team_page(vega, browser, request, setup_teams_and_games):
|
||||
with init_page(vega, browser, request) as page:
|
||||
@ -23,9 +24,20 @@ def team_page(vega, browser, request, setup_teams_and_games):
|
||||
page.goto(f"/#/competitions/teams/{team_id}")
|
||||
yield page
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def competitions_page(vega, browser, request, setup_teams_and_games):
|
||||
with init_page(vega, browser, request) as page:
|
||||
risk_accepted_setup(page)
|
||||
auth_setup(vega, page)
|
||||
team_id = setup_teams_and_games["team_id"]
|
||||
page.goto(f"/#/competitions/")
|
||||
yield page
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setup_teams_and_games(vega: VegaServiceNull):
|
||||
tDAI_market = setup_continuous_market(vega)
|
||||
tDAI_market = setup_continuous_market(vega, custom_quantum=100000)
|
||||
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
|
||||
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
|
||||
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
|
||||
@ -46,6 +58,18 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
|
||||
# list_teams actually returns a dictionary {"team_id": Team}
|
||||
team_id = list(teams.keys())[0]
|
||||
vega.create_referral_set(
|
||||
key_name="market_maker",
|
||||
name="test",
|
||||
team_url="https://vega.xyz",
|
||||
avatar_url="http://placekitten.com/200/200",
|
||||
closed=False,
|
||||
)
|
||||
next_epoch(vega)
|
||||
teams = vega.list_teams()
|
||||
|
||||
team_id_2 = list(teams.keys())[0]
|
||||
vega.apply_referral_code("Key 1", team_id_2)
|
||||
|
||||
vega.apply_referral_code(PARTY_B.name, team_id)
|
||||
|
||||
@ -63,7 +87,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
|
||||
current_epoch = vega.statistics().epoch_seq
|
||||
game_start = current_epoch + 1
|
||||
game_end = current_epoch + 11
|
||||
game_end = current_epoch + 14
|
||||
|
||||
current_epoch = vega.statistics().epoch_seq
|
||||
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
||||
@ -84,9 +108,42 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
factor=1.0,
|
||||
start_epoch=game_start,
|
||||
end_epoch=game_end,
|
||||
window_length=10
|
||||
window_length=15,
|
||||
)
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
vega.recurring_transfer(
|
||||
from_key_name=PARTY_B.name,
|
||||
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
|
||||
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
asset=tDAI_asset_id,
|
||||
reference="reward",
|
||||
asset_for_metric=tDAI_asset_id,
|
||||
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
|
||||
entity_scope=vega_protos.vega.ENTITY_SCOPE_INDIVIDUALS,
|
||||
individual_scope=vega_protos.vega.INDIVIDUAL_SCOPE_IN_TEAM,
|
||||
n_top_performers=1,
|
||||
amount=100,
|
||||
factor=1.0,
|
||||
window_length=15
|
||||
)
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
vega.recurring_transfer(
|
||||
from_key_name=PARTY_C.name,
|
||||
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
|
||||
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
asset=tDAI_asset_id,
|
||||
reference="reward",
|
||||
asset_for_metric=tDAI_asset_id,
|
||||
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
|
||||
entity_scope=vega_protos.vega.ENTITY_SCOPE_INDIVIDUALS,
|
||||
individual_scope=vega_protos.vega.INDIVIDUAL_SCOPE_NOT_IN_TEAM,
|
||||
n_top_performers=1,
|
||||
amount=100,
|
||||
factor=1.0,
|
||||
window_length=15
|
||||
)
|
||||
|
||||
next_epoch(vega)
|
||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
||||
|
||||
@ -113,6 +170,22 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
side="SIDE_BUY",
|
||||
volume=1,
|
||||
)
|
||||
vega.submit_order(
|
||||
trading_key="Key 1",
|
||||
market_id=tDAI_market,
|
||||
order_type="TYPE_MARKET",
|
||||
time_in_force="TIME_IN_FORCE_IOC",
|
||||
side="SIDE_BUY",
|
||||
volume=1,
|
||||
)
|
||||
vega.submit_order(
|
||||
trading_key="market_maker",
|
||||
market_id=tDAI_market,
|
||||
order_type="TYPE_MARKET",
|
||||
time_in_force="TIME_IN_FORCE_IOC",
|
||||
side="SIDE_BUY",
|
||||
volume=1,
|
||||
)
|
||||
next_epoch(vega)
|
||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
||||
|
||||
@ -120,6 +193,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
"market_id": tDAI_market,
|
||||
"asset_id": tDAI_asset_id,
|
||||
"team_id": team_id,
|
||||
"team_id_2": team_id_2,
|
||||
"team_name": team_name,
|
||||
}
|
||||
|
||||
@ -136,66 +210,109 @@ def create_team(vega: VegaServiceNull):
|
||||
|
||||
return team_name
|
||||
|
||||
|
||||
def test_team_page_games_table(team_page: Page):
|
||||
team_page.pause()
|
||||
team_page.get_by_test_id("games-toggle").click()
|
||||
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
|
||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
|
||||
expect(team_page.get_by_test_id("type-0")).to_have_text("Price maker fees paid")
|
||||
expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
|
||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
|
||||
"1"
|
||||
)
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
|
||||
"2"
|
||||
)
|
||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19")
|
||||
expect(team_page.get_by_test_id("type-0")
|
||||
).to_have_text("Price maker fees paid")
|
||||
expect(team_page.get_by_test_id("amount-0")).to_have_text("74")
|
||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("4")
|
||||
|
||||
|
||||
def test_team_page_members_table(team_page: Page):
|
||||
team_page.get_by_test_id("members-toggle").click()
|
||||
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (4)")
|
||||
expect(team_page.get_by_test_id("members-toggle")
|
||||
).to_have_text("Members (4)")
|
||||
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
|
||||
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
|
||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("8")
|
||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9")
|
||||
|
||||
def test_team_page_headline(team_page: Page, setup_teams_and_games
|
||||
):
|
||||
|
||||
def test_team_page_headline(team_page: Page, setup_teams_and_games):
|
||||
team_name = setup_teams_and_games["team_name"]
|
||||
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
|
||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("4")
|
||||
|
||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
|
||||
"1"
|
||||
)
|
||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text("2")
|
||||
|
||||
# TODO this still seems wrong as its always 0
|
||||
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
|
||||
"0"
|
||||
)
|
||||
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text("0")
|
||||
|
||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
|
||||
"100m"
|
||||
)
|
||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("214")
|
||||
|
||||
|
||||
def test_switch_teams(team_page: Page, vega: VegaServiceNull):
|
||||
team_page.get_by_test_id("switch-team-button").click()
|
||||
team_page.get_by_test_id("confirm-switch-button").click()
|
||||
expect(team_page.get_by_test_id("dialog-content").first).to_be_visible()
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
next_epoch(vega=vega)
|
||||
team_page.reload()
|
||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("5")
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def competitions_page(vega, browser, request):
|
||||
with init_page(vega, browser, request) as page:
|
||||
risk_accepted_setup(page)
|
||||
auth_setup(vega, page)
|
||||
yield page
|
||||
|
||||
def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||
team_name = setup_teams_and_games["team_name"]
|
||||
competitions_page.goto(f"/#/competitions/")
|
||||
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
||||
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
|
||||
competitions_page.reload()
|
||||
expect(
|
||||
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
|
||||
).to_have_count(1)
|
||||
expect(
|
||||
competitions_page.get_by_test_id(
|
||||
"rank-1").locator(".text-vega-clight-500")
|
||||
).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("team-1")).to_have_text(team_name)
|
||||
expect(competitions_page.get_by_test_id("status-1")).to_have_text("Open")
|
||||
|
||||
expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
|
||||
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
|
||||
# FIXME: the numbers are different we need to clarify this with the backend
|
||||
# expect(competitions_page.get_by_test_id("earned-1")).to_have_text("160")
|
||||
expect(competitions_page.get_by_test_id("games-1")).to_have_text("2")
|
||||
|
||||
# TODO still odd that this is 0
|
||||
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
||||
|
||||
#TODO def test_games(competitions_page: Page):
|
||||
#TODO currently no games appear which i think is a bug
|
||||
|
||||
def test_game_card(competitions_page: Page):
|
||||
expect(competitions_page.get_by_test_id(
|
||||
"active-rewards-card")).to_have_count(2)
|
||||
game_1 = competitions_page.get_by_test_id("active-rewards-card").first
|
||||
expect(game_1).to_be_visible()
|
||||
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Individual")
|
||||
expect(game_1.get_by_test_id("locked-for")).to_have_text("1 epoch")
|
||||
expect(game_1.get_by_test_id("reward-value")).to_have_text("100.00")
|
||||
expect(game_1.get_by_test_id("distribution-strategy")
|
||||
).to_have_text("Pro rata")
|
||||
expect(game_1.get_by_test_id("dispatch-metric-info")
|
||||
).to_have_text("Price maker fees paid • ")
|
||||
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
|
||||
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
|
||||
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
|
||||
expect(game_1.get_by_test_id("average-position")).to_have_text("0.00")
|
||||
|
||||
|
||||
def test_create_team(competitions_page: Page, vega: VegaServiceNull):
|
||||
change_keys(competitions_page, vega, "market_maker_2")
|
||||
competitions_page.get_by_test_id("create-public-team-button").click()
|
||||
competitions_page.get_by_test_id("team-name-input").fill("e2e")
|
||||
competitions_page.get_by_test_id("team-url-input").fill("https://vega.xyz")
|
||||
competitions_page.get_by_test_id("avatar-url-input").fill(
|
||||
"http://placekitten.com/200/200"
|
||||
)
|
||||
competitions_page.get_by_test_id("team-form-submit-button").click()
|
||||
expect(competitions_page.get_by_test_id("team-form-submit-button")).to_have_text(
|
||||
"Confirming transaction..."
|
||||
)
|
||||
vega.wait_fn(2)
|
||||
vega.wait_for_total_catchup()
|
||||
expect(
|
||||
competitions_page.get_by_test_id("team-creation-success-message")
|
||||
).to_be_visible()
|
||||
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
|
||||
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
|
||||
competitions_page.get_by_test_id("view-team-button").click()
|
||||
expect(competitions_page.get_by_test_id("team-name")).to_have_text("e2e")
|
||||
|
@ -1,15 +1,20 @@
|
||||
fragment TeamsFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
totalMembers
|
||||
}
|
||||
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
...TeamsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
apps/trading/lib/hooks/__generated__/Teams.ts
generated
29
apps/trading/lib/hooks/__generated__/Teams.ts
generated
@ -3,33 +3,40 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamsFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number };
|
||||
|
||||
export type TeamsQueryVariables = Types.Exact<{
|
||||
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null };
|
||||
|
||||
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number } }> } | null };
|
||||
|
||||
export const TeamsFieldsFragmentDoc = gql`
|
||||
fragment TeamsFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
totalMembers
|
||||
}
|
||||
`;
|
||||
export const TeamsDocument = gql`
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
...TeamsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
${TeamsFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamsQuery__
|
||||
|
@ -1,12 +1,23 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
||||
import { isActiveReward } from '../../components/rewards-container/active-rewards';
|
||||
import { EntityScope, type TransferNode } from '@vegaprotocol/types';
|
||||
import {
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
type TransferNode,
|
||||
} from '@vegaprotocol/types';
|
||||
|
||||
const isScopedToTeams = (node: TransferNode) =>
|
||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS;
|
||||
// scoped to teams
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS ||
|
||||
// or to individuals
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||
// but they have to be in a team
|
||||
node.transfer.kind.dispatchStrategy.individualScope ===
|
||||
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM));
|
||||
|
||||
export const useGames = ({
|
||||
currentEpoch,
|
||||
|
25
apps/trading/lib/hooks/use-my-team.ts
Normal file
25
apps/trading/lib/hooks/use-my-team.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import compact from 'lodash/compact';
|
||||
import first from 'lodash/first';
|
||||
import { useTeamsQuery } from './__generated__/Teams';
|
||||
import { useTeam } from './use-team';
|
||||
import { useTeams } from './use-teams';
|
||||
|
||||
export const useMyTeam = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data: teams } = useTeams();
|
||||
|
||||
const { data: maybeMyTeam } = useTeamsQuery({
|
||||
variables: {
|
||||
partyId: pubKey,
|
||||
},
|
||||
skip: !pubKey,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const team = first(compact(maybeMyTeam?.teams?.edges.map((n) => n.node)));
|
||||
const rank = teams.findIndex((t) => t.teamId === team?.teamId) + 1;
|
||||
const { games, stats } = useTeam(team?.teamId);
|
||||
|
||||
return { team, stats, games, rank };
|
||||
};
|
@ -4,7 +4,8 @@ import { useTeamsQuery } from './__generated__/Teams';
|
||||
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
export const DEFAULT_AGGREGATION_EPOCHS = 10;
|
||||
// 192
|
||||
export const DEFAULT_AGGREGATION_EPOCHS = 192;
|
||||
|
||||
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||
const {
|
||||
|
@ -3,27 +3,13 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT, ns } from '../../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
@ -31,9 +17,9 @@ import { emptyValue } from './deal-ticket-fee-details';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
|
||||
export interface DealTicketMarginDetailsProps {
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
orderMarginAccountBalance?: string;
|
||||
generalAccountBalance: string;
|
||||
marginAccountBalance: string;
|
||||
orderMarginAccountBalance: string;
|
||||
market: Market;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
assetSymbol: string;
|
||||
@ -54,25 +40,13 @@ export const DealTicketMarginDetails = ({
|
||||
const t = useT();
|
||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId: market.id, partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
});
|
||||
const isInIsolatedMode =
|
||||
positionEstimate?.margin.bestCase.marginMode ===
|
||||
Schema.MarginMode.MARGIN_MODE_ISOLATED_MARGIN;
|
||||
const liquidationEstimate = positionEstimate?.liquidation;
|
||||
const marginEstimate = positionEstimate?.margin;
|
||||
const totalMarginAccountBalance =
|
||||
BigInt(marginAccountBalance || '0') +
|
||||
BigInt(orderMarginAccountBalance || '0');
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
|
||||
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
let marginRequiredBestCase: string | undefined = undefined;
|
||||
let marginRequiredWorstCase: string | undefined = undefined;
|
||||
|
||||
const collateralIncreaseEstimateBestCase = BigInt(
|
||||
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
|
||||
@ -80,102 +54,6 @@ export const DealTicketMarginDetails = ({
|
||||
const collateralIncreaseEstimateWorstCase = BigInt(
|
||||
positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0'
|
||||
);
|
||||
const marginEstimateBestCase = isInIsolatedMode
|
||||
? totalMarginAccountBalance + collateralIncreaseEstimateBestCase
|
||||
: BigInt(marginEstimate?.bestCase.initialLevel ?? 0);
|
||||
const marginEstimateWorstCase = isInIsolatedMode
|
||||
? totalMarginAccountBalance + collateralIncreaseEstimateWorstCase
|
||||
: BigInt(marginEstimate?.worstCase.initialLevel ?? 0);
|
||||
if (isInIsolatedMode) {
|
||||
marginRequiredBestCase = collateralIncreaseEstimateBestCase.toString();
|
||||
marginRequiredWorstCase = collateralIncreaseEstimateWorstCase.toString();
|
||||
} else if (marginEstimate) {
|
||||
if (currentMargins) {
|
||||
const currentMargin = BigInt(currentMargins.initialLevel);
|
||||
marginRequiredBestCase = (
|
||||
marginEstimateBestCase - currentMargin
|
||||
).toString();
|
||||
if (marginRequiredBestCase.startsWith('-')) {
|
||||
marginRequiredBestCase = '0';
|
||||
}
|
||||
|
||||
marginRequiredWorstCase = (
|
||||
marginEstimateWorstCase - currentMargin
|
||||
).toString();
|
||||
|
||||
if (marginRequiredWorstCase.startsWith('-')) {
|
||||
marginRequiredWorstCase = '0';
|
||||
}
|
||||
} else {
|
||||
marginRequiredBestCase = marginEstimateBestCase.toString();
|
||||
marginRequiredWorstCase = marginEstimateWorstCase.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const totalMarginAvailable = (
|
||||
currentMargins
|
||||
? totalBalance - BigInt(currentMargins.maintenanceLevel)
|
||||
: totalBalance
|
||||
).toString();
|
||||
|
||||
let deductionFromCollateral = null;
|
||||
let projectedMargin = null;
|
||||
if (totalMarginAccountBalance) {
|
||||
const deductionFromCollateralBestCase =
|
||||
marginEstimateBestCase - totalMarginAccountBalance;
|
||||
|
||||
const deductionFromCollateralWorstCase =
|
||||
marginEstimateWorstCase - totalMarginAccountBalance;
|
||||
|
||||
deductionFromCollateral = (
|
||||
<KeyValue
|
||||
indent
|
||||
label={t('Deduction from collateral')}
|
||||
value={formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
projectedMargin = (
|
||||
<KeyValue
|
||||
label={t('Projected margin')}
|
||||
value={formatRange(
|
||||
marginEstimateBestCase.toString(),
|
||||
marginEstimateWorstCase.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
marginEstimateWorstCase.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateRange = emptyValue;
|
||||
@ -232,128 +110,50 @@ export const DealTicketMarginDetails = ({
|
||||
const quoteName = getQuoteName(market);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full pt-2',
|
||||
'flex items-center gap-2 text-xs',
|
||||
'group'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-testid={`deal-ticket-fee-margin-required`}
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip
|
||||
description={t(
|
||||
'MARGIN_DIFF_TOOLTIP_TEXT',
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
>
|
||||
<span className="text-muted">{t('Margin required')}</span>
|
||||
</Tooltip>
|
||||
|
||||
<AccordionChevron size={10} />
|
||||
</div>
|
||||
<Tooltip
|
||||
description={
|
||||
formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
) ?? '-'
|
||||
}
|
||||
>
|
||||
<div className="font-mono text-right">
|
||||
{formatValue(
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}{' '}
|
||||
{assetSymbol || ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionPrimitive.Trigger>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<KeyValue
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'TOTAL_MARGIN_AVAILABLE',
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
{
|
||||
generalAccountBalance: formatValue(
|
||||
generalAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginAccountBalance: formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
orderMarginAccountBalance: formatValue(
|
||||
orderMarginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginMaintenance: formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
assetSymbol,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<KeyValue
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
{projectedMargin}
|
||||
<div className="flex flex-col w-full gap-2 mt-2">
|
||||
<KeyValue
|
||||
label={t('Liquidation')}
|
||||
label={t('Current margin')}
|
||||
onClick={
|
||||
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
|
||||
}
|
||||
value={formatValue(totalMarginAccountBalance.toString(), assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
<KeyValue
|
||||
label={t('Available collateral')}
|
||||
value={formatValue(generalAccountBalance, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
generalAccountBalance.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
<KeyValue
|
||||
label={t('Additional margin required')}
|
||||
value={formatRange(
|
||||
collateralIncreaseEstimateBestCase.toString(),
|
||||
collateralIncreaseEstimateWorstCase.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
collateralIncreaseEstimateBestCase.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
<KeyValue
|
||||
label={t('Liquidation estimate')}
|
||||
value={liquidationPriceEstimateRange}
|
||||
formattedValue={liquidationPriceEstimate}
|
||||
symbol={quoteName}
|
||||
|
@ -73,7 +73,7 @@ import {
|
||||
} from '../../hooks';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
@ -177,12 +177,6 @@ export const DealTicket = ({
|
||||
loading: loadingGeneralAccountBalance,
|
||||
} = useAccountBalance(asset.id);
|
||||
|
||||
const balance = (
|
||||
BigInt(marginAccountBalance) +
|
||||
BigInt(generalAccountBalance) +
|
||||
BigInt(orderMarginAccountBalance)
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
const timeInForce = watch('timeInForce');
|
||||
|
||||
@ -729,17 +723,11 @@ export const DealTicket = ({
|
||||
error={summaryError}
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
margin={(
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
||||
'0'
|
||||
) +
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase
|
||||
.orderMarginLevel || '0'
|
||||
)
|
||||
).toString()}
|
||||
balance={generalAccountBalance}
|
||||
margin={
|
||||
positionEstimate?.estimatePosition?.collateralIncreaseEstimate
|
||||
.bestCase || '0'
|
||||
}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onDeposit={onDeposit}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface KeyValuePros {
|
||||
@ -19,7 +18,6 @@ export const KeyValue = ({
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
indent,
|
||||
onClick,
|
||||
formattedValue,
|
||||
}: KeyValuePros) => {
|
||||
@ -43,10 +41,7 @@ export const KeyValue = ({
|
||||
: id
|
||||
}`}
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className={classnames(
|
||||
'text-xs flex justify-between items-center gap-4 flex-wrap text-right',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
className="text-xs flex justify-between items-center gap-4 flex-wrap text-right"
|
||||
>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div className="text-muted text-left">{label}</div>
|
||||
|
@ -29,6 +29,7 @@ import { usePositionEstimate } from '../../hooks/use-position-estimate';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
||||
import { NoWalletWarning } from './deal-ticket';
|
||||
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
|
||||
|
||||
const defaultLeverage = 10;
|
||||
|
||||
@ -93,66 +94,78 @@ export const MarginChange = ({
|
||||
},
|
||||
skip
|
||||
);
|
||||
if (
|
||||
!asset ||
|
||||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
|
||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
|
||||
) {
|
||||
if (!asset || !estimateMargin?.estimatePosition) {
|
||||
return null;
|
||||
}
|
||||
const collateralIncreaseEstimate = BigInt(
|
||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
||||
);
|
||||
if (!collateralIncreaseEstimate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let positionWarning = '';
|
||||
if (orders?.length && openVolume !== '0') {
|
||||
positionWarning = t(
|
||||
'youHaveOpenPositionAndOrders',
|
||||
'You have an existing position and open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
} else if (!orders?.length) {
|
||||
positionWarning = t('You have an existing position on this market.');
|
||||
} else {
|
||||
positionWarning = t(
|
||||
'youHaveOpenOrders',
|
||||
'You have open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
let marginChangeWarning = '';
|
||||
const amount = addDecimalsFormatNumber(
|
||||
collateralIncreaseEstimate.toString(),
|
||||
asset?.decimals
|
||||
);
|
||||
const { symbol } = asset;
|
||||
const interpolation = { amount, symbol };
|
||||
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
|
||||
marginChangeWarning = t(
|
||||
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||
interpolation
|
||||
);
|
||||
} else {
|
||||
marginChangeWarning = t(
|
||||
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||
interpolation
|
||||
if (collateralIncreaseEstimate) {
|
||||
if (orders?.length && openVolume !== '0') {
|
||||
positionWarning = t(
|
||||
'youHaveOpenPositionAndOrders',
|
||||
'You have an existing position and open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
} else if (!orders?.length) {
|
||||
positionWarning = t('You have an existing position on this market.');
|
||||
} else {
|
||||
positionWarning = t(
|
||||
'youHaveOpenOrders',
|
||||
'You have open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const amount = addDecimalsFormatNumber(
|
||||
collateralIncreaseEstimate.toString(),
|
||||
asset?.decimals
|
||||
);
|
||||
const { symbol } = asset;
|
||||
const interpolation = { amount, symbol };
|
||||
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
|
||||
marginChangeWarning = t(
|
||||
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||
interpolation
|
||||
);
|
||||
} else {
|
||||
marginChangeWarning = t(
|
||||
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||
interpolation
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<>
|
||||
<p>{positionWarning}</p>
|
||||
<p>{marginChangeWarning}</p>
|
||||
</>
|
||||
{positionWarning && marginChangeWarning && (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<>
|
||||
<p>{positionWarning}</p>
|
||||
<p>{marginChangeWarning}</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<DealTicketMarginDetails
|
||||
marginAccountBalance={marginAccountBalance}
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
orderMarginAccountBalance={orderMarginAccountBalance}
|
||||
assetSymbol={asset.symbol}
|
||||
market={market}
|
||||
positionEstimate={estimateMargin.estimatePosition}
|
||||
side={
|
||||
openVolume.startsWith('-')
|
||||
? Schema.Side.SIDE_SELL
|
||||
: Schema.Side.SIDE_BUY
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import type {
|
||||
} from '../hooks/use-form-values';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { isPersistentOrder } from './time-in-force-persistance';
|
||||
import { isPersistentOrder } from './time-in-force-persistence';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
|
@ -2,9 +2,9 @@ import { OrderTimeInForce } from '@vegaprotocol/types';
|
||||
import {
|
||||
isNonPersistentOrder,
|
||||
isPersistentOrder,
|
||||
} from './time-in-force-persistance';
|
||||
} from './time-in-force-persistence';
|
||||
|
||||
it('isNonPeristentOrder', () => {
|
||||
it('isNonPersistentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
||||
@ -13,7 +13,7 @@ it('isNonPeristentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
||||
});
|
||||
|
||||
it('isPeristentOrder', () => {
|
||||
it('isPersistentOrder', () => {
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
@ -23,7 +23,6 @@ import {
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@ -162,19 +161,6 @@ describe('useNodeBasicStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RowData', () => {
|
||||
const props = {
|
||||
id: '0',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@ -8,6 +7,7 @@ import {
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { useT } from '../../use-t';
|
||||
import { useResponseTime } from '../../utils/time';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
@ -108,20 +108,6 @@ export const useNodeBasicStatus = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } =
|
||||
(requests.length && requests[requests.length - 1]) || {};
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const RowData = ({
|
||||
id,
|
||||
url,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
getUserEnabledFeatureFlags,
|
||||
setUserEnabledFeatureFlag,
|
||||
} from './use-environment';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
const noop = () => {
|
||||
/* no op*/
|
||||
@ -17,6 +18,10 @@ const noop = () => {
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client');
|
||||
jest.mock('zustand');
|
||||
jest.mock('../utils/time');
|
||||
|
||||
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
|
||||
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
|
||||
|
||||
const mockCreateClient = createClient as jest.Mock;
|
||||
const createDefaultMockClient = () => {
|
||||
@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||
const fastWait = 1000;
|
||||
const nodes = [slowNode, fastNode];
|
||||
|
||||
mockCanMeasureResponseTime.mockImplementation(() => true);
|
||||
mockMeasureResponseTime.mockImplementation((url: string) => {
|
||||
if (url === slowNode) return slowWait;
|
||||
if (url === fastNode) return fastWait;
|
||||
return Infinity;
|
||||
});
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||
|
||||
@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
||||
statistics: {
|
||||
chainId: 'chain-id',
|
||||
blockHeight: '100',
|
||||
vegaTime: new Date().toISOString(),
|
||||
vegaTime: new Date(1).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
||||
expect(result.current.nodes).toEqual(nodes);
|
||||
});
|
||||
|
||||
jest.runAllTimers();
|
||||
jest.advanceTimersByTime(2000);
|
||||
// jest.runAllTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toEqual('success');
|
||||
|
@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
||||
import { envSchema } from '../utils/validate-environment';
|
||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||
import uniq from 'lodash/uniq';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import first from 'lodash/first';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
type Client = ReturnType<typeof createClient>;
|
||||
type ClientCollection = {
|
||||
@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
||||
|
||||
const VERSION = 1;
|
||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||
|
||||
const QUERY_TIMEOUT = 3000;
|
||||
const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
|
||||
const raceAgainst = (timeout: number): Promise<false> =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch and validate a vega node configuration
|
||||
*/
|
||||
@ -64,53 +76,88 @@ const fetchConfig = async (url?: string) => {
|
||||
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||
try {
|
||||
const url = await Promise.any(tests);
|
||||
return url;
|
||||
} catch {
|
||||
const nodes = await Promise.all(tests);
|
||||
const responsiveNodes = nodes
|
||||
.filter(([, q, s]) => q && s)
|
||||
.map(([url, q]) => {
|
||||
return {
|
||||
url,
|
||||
...q,
|
||||
};
|
||||
});
|
||||
|
||||
// more recent and faster at the top
|
||||
const ordered = orderBy(
|
||||
responsiveNodes,
|
||||
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
|
||||
['desc', 'desc', 'asc']
|
||||
);
|
||||
|
||||
const best = first(ordered);
|
||||
return best ? best.url : null;
|
||||
} catch (err) {
|
||||
// All tests rejected, no suitable node found
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type Maybe<T> = T | false;
|
||||
type QueryTestResult = {
|
||||
blockHeight: number;
|
||||
vegaTime: Date;
|
||||
responseTime: number;
|
||||
};
|
||||
type SubscriptionTestResult = true;
|
||||
type NodeTestResult = [
|
||||
/** url */
|
||||
string,
|
||||
Maybe<QueryTestResult>,
|
||||
Maybe<SubscriptionTestResult>
|
||||
];
|
||||
/**
|
||||
* Test a node for suitability for connection
|
||||
*/
|
||||
const testNode = async (
|
||||
url: string,
|
||||
client: Client
|
||||
): Promise<string | null> => {
|
||||
): Promise<NodeTestResult> => {
|
||||
const results = await Promise.all([
|
||||
// these promises will only resolve with true/false
|
||||
testQuery(client),
|
||||
testQuery(client, url),
|
||||
testSubscription(client),
|
||||
]);
|
||||
if (results[0] && results[1]) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const message = `Tests failed for node: ${url}`;
|
||||
console.warn(message);
|
||||
|
||||
// throwing here will mean this tests is ignored and a different
|
||||
// node that hopefully does resolve will fulfill the Promise.any
|
||||
throw new Error(message);
|
||||
return [url, ...results];
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a test query on a client
|
||||
*/
|
||||
const testQuery = async (client: Client) => {
|
||||
try {
|
||||
const result = await client.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
});
|
||||
if (!result || result.error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
const testQuery = (
|
||||
client: Client,
|
||||
url: string
|
||||
): Promise<Maybe<QueryTestResult>> => {
|
||||
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||
client
|
||||
.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result && !result.error) {
|
||||
const res = {
|
||||
blockHeight: Number(result.data.statistics.blockHeight),
|
||||
vegaTime: new Date(result.data.statistics.vegaTime),
|
||||
// only after a request has been sent we can retrieve the response time
|
||||
responseTime: canMeasureResponseTime(url)
|
||||
? measureResponseTime(url) || Infinity
|
||||
: Infinity,
|
||||
} as QueryTestResult;
|
||||
resolve(res);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => resolve(false))
|
||||
);
|
||||
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -118,7 +165,9 @@ const testQuery = async (client: Client) => {
|
||||
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||
* is deemed a failure
|
||||
*/
|
||||
const testSubscription = (client: Client) => {
|
||||
const testSubscription = (
|
||||
client: Client
|
||||
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||
return new Promise((resolve) => {
|
||||
const sub = client
|
||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
||||
|
22
libs/environment/src/utils/time.spec.ts
Normal file
22
libs/environment/src/utils/time.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useResponseTime } from './time';
|
||||
|
||||
const mockResponseTime = 50;
|
||||
global.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||
{
|
||||
duration: mockResponseTime,
|
||||
},
|
||||
]);
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
25
libs/environment/src/utils/time.ts
Normal file
25
libs/environment/src/utils/time.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!canMeasureResponseTime(url)) return;
|
||||
const duration = measureResponseTime(url);
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const canMeasureResponseTime = (url: string) => {
|
||||
if (!isValidUrl(url)) return false;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const measureResponseTime = (url: string) => {
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||
return duration;
|
||||
};
|
@ -417,6 +417,7 @@
|
||||
"myVolume_other": "My volume (last {{count}} epochs)",
|
||||
"numberEpochs": "{{count}} epochs",
|
||||
"numberEpochs_one": "{{count}} epoch",
|
||||
"Rewards earned": "Rewards earned",
|
||||
"Rewards paid out": "Rewards paid out",
|
||||
"{{reward}}x": "{{reward}}x",
|
||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||
@ -431,5 +432,19 @@
|
||||
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
||||
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
||||
"{{distance}} ago": "{{distance}} ago",
|
||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision"
|
||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision",
|
||||
"My team": "My team",
|
||||
"Profile": "Profile",
|
||||
"Last {{games}} games result_one": "Last game result",
|
||||
"Last {{games}} games result_other": "Last {{games}} games result",
|
||||
"Leaderboard": "Leaderboard",
|
||||
"View all teams": "View all teams",
|
||||
"Competitions": "Competitions",
|
||||
"Be a team player! Participate in games and work together to rake in as much profit to win.": "Be a team player! Participate in games and work together to rake in as much profit to win.",
|
||||
"Create a public team": "Create a public team",
|
||||
"Create a private team": "Create a private team",
|
||||
"Choose a team": "Choose a team",
|
||||
"Join a team": "Join a team",
|
||||
"Solo team / lone wolf": "Solo team / lone wolf",
|
||||
"Choose a team to get involved": "Choose a team to get involved"
|
||||
}
|
||||
|
@ -67,26 +67,6 @@ query EstimatePosition(
|
||||
# we can set this variable to true so that we can format with market.decimalPlaces
|
||||
scaleLiquidationPriceToMarketDecimals: true
|
||||
) {
|
||||
margin {
|
||||
worstCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
bestCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
}
|
||||
collateralIncreaseEstimate {
|
||||
worstCase
|
||||
bestCase
|
||||
|
22
libs/positions/src/lib/__generated__/Positions.ts
generated
22
libs/positions/src/lib/__generated__/Positions.ts
generated
@ -33,7 +33,7 @@ export type EstimatePositionQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string } }, collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
|
||||
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
|
||||
|
||||
export const PositionFieldsFragmentDoc = gql`
|
||||
fragment PositionFields on Position {
|
||||
@ -144,26 +144,6 @@ export const EstimatePositionDocument = gql`
|
||||
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
||||
scaleLiquidationPriceToMarketDecimals: true
|
||||
) {
|
||||
margin {
|
||||
worstCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
bestCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
}
|
||||
collateralIncreaseEstimate {
|
||||
worstCase
|
||||
bestCase
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import merge from 'lodash/merge';
|
||||
import type { EstimatePositionQuery } from './__generated__/Positions';
|
||||
import { MarginMode } from '@vegaprotocol/types';
|
||||
|
||||
export const estimatePositionQuery = (
|
||||
override?: PartialDeep<EstimatePositionQuery>
|
||||
@ -9,26 +8,6 @@ export const estimatePositionQuery = (
|
||||
const defaultResult: EstimatePositionQuery = {
|
||||
estimatePosition: {
|
||||
__typename: 'PositionEstimate',
|
||||
margin: {
|
||||
bestCase: {
|
||||
collateralReleaseLevel: '1000000',
|
||||
initialLevel: '500000',
|
||||
maintenanceLevel: '200000',
|
||||
searchLevel: '300000',
|
||||
marginFactor: '1',
|
||||
orderMarginLevel: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
worstCase: {
|
||||
collateralReleaseLevel: '1100000',
|
||||
initialLevel: '600000',
|
||||
maintenanceLevel: '300000',
|
||||
searchLevel: '400000',
|
||||
marginFactor: '1',
|
||||
orderMarginLevel: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
},
|
||||
collateralIncreaseEstimate: {
|
||||
bestCase: '0',
|
||||
worstCase: '0',
|
||||
|
@ -30,26 +30,6 @@ describe('LiquidationPrice', () => {
|
||||
result: {
|
||||
data: {
|
||||
estimatePosition: {
|
||||
margin: {
|
||||
worstCase: {
|
||||
maintenanceLevel: '100',
|
||||
searchLevel: '100',
|
||||
initialLevel: '100',
|
||||
collateralReleaseLevel: '100',
|
||||
orderMarginLevel: '0',
|
||||
marginFactor: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
bestCase: {
|
||||
maintenanceLevel: '100',
|
||||
searchLevel: '100',
|
||||
initialLevel: '100',
|
||||
collateralReleaseLevel: '100',
|
||||
orderMarginLevel: '0',
|
||||
marginFactor: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
},
|
||||
collateralIncreaseEstimate: {
|
||||
bestCase: '0',
|
||||
worstCase: '0',
|
||||
|
@ -41,16 +41,17 @@ export const TradingView = ({
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||
const widgetRef = useRef<IChartingLibraryWidget>();
|
||||
|
||||
const datafeed = useDatafeed();
|
||||
|
||||
const prevMarketId = usePrevious(marketId);
|
||||
const prevTheme = usePrevious(theme);
|
||||
|
||||
const datafeed = useDatafeed(marketId);
|
||||
|
||||
useEffect(() => {
|
||||
// Widget already created
|
||||
if (widgetRef.current !== undefined) {
|
||||
// Update the symbol if changed
|
||||
if (marketId !== prevMarketId) {
|
||||
datafeed.setSymbol(marketId);
|
||||
widgetRef.current.setSymbol(
|
||||
marketId,
|
||||
(interval ? interval : '15') as TVResolutionString,
|
||||
|
@ -44,14 +44,22 @@ const configurationData: DatafeedConfiguration = {
|
||||
supported_resolutions: supportedResolutions as ResolutionString[],
|
||||
} as const;
|
||||
|
||||
export const useDatafeed = () => {
|
||||
// HACK: local handle for market id
|
||||
let requestedSymbol: string | undefined = undefined;
|
||||
|
||||
export const useDatafeed = (marketId: string) => {
|
||||
const hasHistory = useRef(false);
|
||||
const subRef = useRef<Subscription>();
|
||||
const client = useApolloClient();
|
||||
|
||||
const datafeed = useMemo(() => {
|
||||
const feed: IBasicDataFeed = {
|
||||
const feed: IBasicDataFeed & { setSymbol: (symbol: string) => void } = {
|
||||
setSymbol: (symbol: string) => {
|
||||
// re-setting the symbol so it could be consumed by `resolveSymbol`
|
||||
requestedSymbol = symbol;
|
||||
},
|
||||
onReady: (callback) => {
|
||||
requestedSymbol = marketId;
|
||||
setTimeout(() => callback(configurationData));
|
||||
},
|
||||
|
||||
@ -68,7 +76,7 @@ export const useDatafeed = () => {
|
||||
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
||||
query: SymbolDocument,
|
||||
variables: {
|
||||
marketId,
|
||||
marketId: requestedSymbol || marketId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -242,7 +250,7 @@ export const useDatafeed = () => {
|
||||
};
|
||||
|
||||
return feed;
|
||||
}, [client]);
|
||||
}, [client, marketId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
Loading…
Reference in New Issue
Block a user