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;
|
text: string;
|
||||||
truncate?: boolean;
|
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 PartyLink } from './party-link/party-link';
|
||||||
export { default as NodeLink } from './node-link/node-link';
|
export { default as NodeLink } from './node-link/node-link';
|
||||||
export { default as MarketLink } from './market-link/market-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';
|
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 base = ENV.dataSources.governanceUrl;
|
||||||
const label = proposal?.rationale.title || id;
|
const label = proposal?.rationale?.title || id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||||
|
@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
|
|||||||
state
|
state
|
||||||
rejectionReason
|
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`
|
export const ExplorerProposalStatusDocument = gql`
|
||||||
@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
|
|||||||
state
|
state
|
||||||
rejectionReason
|
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 { useState } from 'react';
|
||||||
import type { components } from '../../../../../types/explorer';
|
|
||||||
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
||||||
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
||||||
import truncate from 'lodash/truncate';
|
import truncate from 'lodash/truncate';
|
||||||
@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
|
|||||||
import { ProposalDate } from './proposal-date';
|
import { ProposalDate } from './proposal-date';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
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 Rationale = components['schemas']['vegaProposalRationale'];
|
||||||
|
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
|
||||||
|
|
||||||
type ProposalTermsDialog = {
|
type ProposalTermsDialog = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -21,6 +24,7 @@ interface ProposalSummaryProps {
|
|||||||
id: string;
|
id: string;
|
||||||
rationale?: Rationale;
|
rationale?: Rationale;
|
||||||
terms?: ProposalTerms;
|
terms?: ProposalTerms;
|
||||||
|
batch?: Batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +35,7 @@ export const ProposalSummary = ({
|
|||||||
id,
|
id,
|
||||||
rationale,
|
rationale,
|
||||||
terms,
|
terms,
|
||||||
|
batch,
|
||||||
}: ProposalSummaryProps) => {
|
}: ProposalSummaryProps) => {
|
||||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||||
open: false,
|
open: false,
|
||||||
@ -72,6 +77,18 @@ export const ProposalSummary = ({
|
|||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</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">
|
<div className="pt-5">
|
||||||
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
||||||
{t('View terms')}
|
{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 { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||||
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||||
|
import { TxBatchProposal } from './tx-batch-proposal';
|
||||||
|
|
||||||
interface TxDetailsWrapperProps {
|
interface TxDetailsWrapperProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@ -136,6 +137,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
|||||||
return TxDetailsJoinTeam;
|
return TxDetailsJoinTeam;
|
||||||
case 'Update Margin Mode':
|
case 'Update Margin Mode':
|
||||||
return TxDetailsUpdateMarginMode;
|
return TxDetailsUpdateMarginMode;
|
||||||
|
case 'Batch Proposal':
|
||||||
|
return TxBatchProposal;
|
||||||
default:
|
default:
|
||||||
return TxDetailsGeneric;
|
return TxDetailsGeneric;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export type FilterOption =
|
|||||||
| 'Amend Order'
|
| 'Amend Order'
|
||||||
| 'Apply Referral Code'
|
| 'Apply Referral Code'
|
||||||
| 'Batch Market Instructions'
|
| 'Batch Market Instructions'
|
||||||
|
| 'Batch Proposal'
|
||||||
| 'Cancel LiquidityProvision Order'
|
| 'Cancel LiquidityProvision Order'
|
||||||
| 'Cancel Order'
|
| 'Cancel Order'
|
||||||
| 'Cancel Transfer Funds'
|
| 'Cancel Transfer Funds'
|
||||||
@ -67,7 +68,13 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
|||||||
'Cancel Transfer Funds',
|
'Cancel Transfer Funds',
|
||||||
'Withdraw',
|
'Withdraw',
|
||||||
],
|
],
|
||||||
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
|
Governance: [
|
||||||
|
'Batch Proposal',
|
||||||
|
'Delegate',
|
||||||
|
'Undelegate',
|
||||||
|
'Vote on Proposal',
|
||||||
|
'Proposal',
|
||||||
|
],
|
||||||
Referrals: [
|
Referrals: [
|
||||||
'Apply Referral Code',
|
'Apply Referral Code',
|
||||||
'Create Referral Set',
|
'Create Referral Set',
|
||||||
|
@ -71,18 +71,27 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
|||||||
|
|
||||||
if (status === 'confirmed') {
|
if (status === 'confirmed') {
|
||||||
return (
|
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>
|
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||||
{code && (
|
{code && (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Your team ID is:{' '}
|
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>
|
</p>
|
||||||
<TradingAnchorButton
|
<TradingAnchorButton
|
||||||
href={Links.COMPETITIONS_TEAM(code)}
|
href={Links.COMPETITIONS_TEAM(code)}
|
||||||
intent={Intent.Info}
|
intent={Intent.Info}
|
||||||
size="small"
|
size="small"
|
||||||
|
data-testid="view-team-button"
|
||||||
>
|
>
|
||||||
{t('View team')}
|
{t('View team')}
|
||||||
</TradingAnchorButton>
|
</TradingAnchorButton>
|
||||||
|
@ -16,6 +16,8 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
|
|||||||
import { useTeams } from '../../lib/hooks/use-teams';
|
import { useTeams } from '../../lib/hooks/use-teams';
|
||||||
import take from 'lodash/take';
|
import take from 'lodash/take';
|
||||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
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 = () => {
|
export const CompetitionsHome = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -33,6 +35,13 @@ export const CompetitionsHome = () => {
|
|||||||
|
|
||||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||||
|
|
||||||
|
const {
|
||||||
|
team: myTeam,
|
||||||
|
stats: myTeamStats,
|
||||||
|
games: myTeamGames,
|
||||||
|
rank: myTeamRank,
|
||||||
|
} = useMyTeam();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<CompetitionsHeader title={t('Competitions')}>
|
<CompetitionsHeader title={t('Competitions')}>
|
||||||
@ -43,65 +52,83 @@ export const CompetitionsHome = () => {
|
|||||||
</p>
|
</p>
|
||||||
</CompetitionsHeader>
|
</CompetitionsHeader>
|
||||||
|
|
||||||
{/** Get started */}
|
{/** Team card */}
|
||||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
{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>
|
<CompetitionsActionsContainer>
|
||||||
<CompetitionsAction
|
<CompetitionsAction
|
||||||
variant="A"
|
variant="A"
|
||||||
title={t('Create a team')}
|
title={t('Create a team')}
|
||||||
description={t(
|
description={t(
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||||
)}
|
)}
|
||||||
actionElement={
|
actionElement={
|
||||||
<TradingButton
|
<TradingButton
|
||||||
intent={Intent.Primary}
|
intent={Intent.Primary}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||||
}}
|
}}
|
||||||
>
|
data-testId="create-public-team-button"
|
||||||
{t('Create a public team')}
|
>
|
||||||
</TradingButton>
|
{t('Create a public team')}
|
||||||
}
|
</TradingButton>
|
||||||
/>
|
}
|
||||||
<CompetitionsAction
|
/>
|
||||||
variant="B"
|
<CompetitionsAction
|
||||||
title={t('Solo team / lone wolf')}
|
variant="B"
|
||||||
description={t(
|
title={t('Solo team / lone wolf')}
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
description={t(
|
||||||
)}
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||||
actionElement={
|
)}
|
||||||
<TradingButton
|
actionElement={
|
||||||
intent={Intent.Primary}
|
<TradingButton
|
||||||
onClick={(e) => {
|
intent={Intent.Primary}
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
e.preventDefault();
|
||||||
}}
|
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||||
>
|
}}
|
||||||
{t('Create a private team')}
|
>
|
||||||
</TradingButton>
|
{t('Create a private team')}
|
||||||
}
|
</TradingButton>
|
||||||
/>
|
}
|
||||||
<CompetitionsAction
|
/>
|
||||||
variant="C"
|
<CompetitionsAction
|
||||||
title={t('Join a team')}
|
variant="C"
|
||||||
description={t(
|
title={t('Join a team')}
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
description={t(
|
||||||
)}
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||||
actionElement={
|
)}
|
||||||
<TradingButton
|
actionElement={
|
||||||
intent={Intent.Primary}
|
<TradingButton
|
||||||
onClick={(e) => {
|
intent={Intent.Primary}
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate(Links.COMPETITIONS_TEAMS());
|
e.preventDefault();
|
||||||
}}
|
navigate(Links.COMPETITIONS_TEAMS());
|
||||||
>
|
}}
|
||||||
{t('Choose a team')}
|
>
|
||||||
</TradingButton>
|
{t('Choose a team')}
|
||||||
}
|
</TradingButton>
|
||||||
/>
|
}
|
||||||
</CompetitionsActionsContainer>
|
/>
|
||||||
|
</CompetitionsActionsContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/** List of available games */}
|
{/** List of available games */}
|
||||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
<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
|
// Party is in a team, but not this one
|
||||||
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
|
<Button
|
||||||
|
onClick={() => onJoin('switch')}
|
||||||
|
intent={Intent.Primary}
|
||||||
|
data-testid="switch-team-button"
|
||||||
|
>
|
||||||
{t('Switch team')}{' '}
|
{t('Switch team')}{' '}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -215,7 +219,11 @@ const DialogContent = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between gap-2">
|
<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')}
|
{t('Confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onCancel} intent={Intent.Danger}>
|
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
TradingButton,
|
TradingButton,
|
||||||
Intent,
|
Intent,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
@ -17,6 +19,8 @@ import type {
|
|||||||
UpdateReferralSet,
|
UpdateReferralSet,
|
||||||
Status,
|
Status,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
export type FormFields = {
|
export type FormFields = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -111,9 +115,12 @@ export const TeamForm = ({
|
|||||||
<form onSubmit={handleSubmit(sendTransaction)}>
|
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||||
<input type="hidden" {...register('id')} />
|
<input type="hidden" {...register('id')} />
|
||||||
<TradingFormGroup label={t('Team name')} labelFor="name">
|
<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 && (
|
{errors.name?.message && (
|
||||||
<TradingInputError forInput="name">
|
<TradingInputError forInput="name" data-testid="team-name-error">
|
||||||
{errors.name.message}
|
{errors.name.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
@ -129,9 +136,10 @@ export const TeamForm = ({
|
|||||||
{...register('url', {
|
{...register('url', {
|
||||||
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
||||||
})}
|
})}
|
||||||
|
data-testid="team-url-input"
|
||||||
/>
|
/>
|
||||||
{errors.url?.message && (
|
{errors.url?.message && (
|
||||||
<TradingInputError forInput="url">
|
<TradingInputError forInput="url" data-testid="team-url-error">
|
||||||
{errors.url.message}
|
{errors.url.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
@ -148,9 +156,13 @@ export const TeamForm = ({
|
|||||||
message: t('Invalid image URL'),
|
message: t('Invalid image URL'),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
data-testid="avatar-url-input"
|
||||||
/>
|
/>
|
||||||
{errors.avatarUrl?.message && (
|
{errors.avatarUrl?.message && (
|
||||||
<TradingInputError forInput="avatarUrl">
|
<TradingInputError
|
||||||
|
forInput="avatarUrl"
|
||||||
|
data-testid="avatar-url-error"
|
||||||
|
>
|
||||||
{errors.avatarUrl.message}
|
{errors.avatarUrl.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
@ -175,6 +187,7 @@ export const TeamForm = ({
|
|||||||
onCheckedChange={(value) => {
|
onCheckedChange={(value) => {
|
||||||
field.onChange(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 && (
|
{errors.allowList?.message && (
|
||||||
<TradingInputError forInput="avatarUrl">
|
<TradingInputError
|
||||||
|
forInput="avatarUrl"
|
||||||
|
data-testid="team-allow-list-error"
|
||||||
|
>
|
||||||
{errors.allowList.message}
|
{errors.allowList.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
@ -239,16 +256,57 @@ const SubmitButton = ({
|
|||||||
text = t('Update');
|
text = t('Update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let confirmedText = t('Created');
|
||||||
|
if (type === TransactionType.UpdateReferralSet) {
|
||||||
|
confirmedText = t('Updated');
|
||||||
|
}
|
||||||
|
|
||||||
if (status === 'requested') {
|
if (status === 'requested') {
|
||||||
text = t('Confirm in wallet...');
|
text = t('Confirm in wallet...');
|
||||||
} else if (status === 'pending') {
|
} else if (status === 'pending') {
|
||||||
text = t('Confirming transaction...');
|
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 (
|
return (
|
||||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
<div className="flex gap-2 items-baseline">
|
||||||
{text}
|
<TradingButton
|
||||||
</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 Team } from '../../lib/hooks/use-team';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Links } from '../../lib/links';
|
import { Links } from '../../lib/links';
|
||||||
import { useT } from '../../lib/use-t';
|
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 t = useT();
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
|
||||||
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||||
return (
|
return (
|
||||||
<TradingAnchorButton
|
<TradingAnchorButton
|
||||||
|
size={size}
|
||||||
data-testid="update-team-button"
|
data-testid="update-team-button"
|
||||||
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||||
intent={Intent.Info}
|
intent={Intent.Info}
|
||||||
|
@ -5,7 +5,11 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
|||||||
export const GRADIENT =
|
export const GRADIENT =
|
||||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
'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 (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
@ -13,9 +17,22 @@ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
|||||||
BORDER_COLOR,
|
BORDER_COLOR,
|
||||||
GRADIENT,
|
GRADIENT,
|
||||||
'border rounded-lg',
|
'border rounded-lg',
|
||||||
'p-6',
|
'relative p-6 overflow-hidden',
|
||||||
props.className
|
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 NUM_AVATARS = 20;
|
||||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
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)
|
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0'); // between 01 - 20
|
.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';
|
} from '../../lib/hooks/use-team';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export const TeamStats = ({
|
export const TeamStats = ({
|
||||||
stats,
|
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 t = useT();
|
||||||
|
|
||||||
const rewardMetrics = games.map(
|
const rewardMetrics = games.map(
|
||||||
@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="flex flex-col gap-1">
|
<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>
|
<dd>
|
||||||
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||||
<VegaIcon
|
<VegaIcon
|
||||||
@ -142,7 +155,7 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||||
{children}
|
{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" />;
|
return <div className="hidden md:block border-r border-default" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatList = ({ children }: { children: ReactNode }) => {
|
export const StatList = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||||
{children}
|
{children}
|
||||||
@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stat = ({
|
export const Stat = ({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
tooltip,
|
tooltip,
|
||||||
valueTestId,
|
valueTestId,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
value: ReactNode;
|
value: ReactNode;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
valueTestId?: string;
|
valueTestId?: string;
|
||||||
|
className?: classNames.Argument;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classNames(className)}>
|
||||||
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||||
{value}
|
{value}
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -152,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
|||||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||||
|
|
||||||
return (
|
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 && (
|
{enrichedTransfers.length > 1 && (
|
||||||
<TradingInput
|
<TradingInput
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -312,49 +316,30 @@ export const ActiveRewardCard = ({
|
|||||||
MarketState.STATE_CLOSED,
|
MarketState.STATE_CLOSED,
|
||||||
].includes(m.state)
|
].includes(m.state)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (marketSettled) {
|
if (marketSettled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetInSettledMarket =
|
const assetInActiveMarket =
|
||||||
allMarkets &&
|
allMarkets &&
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||||
return (
|
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||||
m?.state &&
|
|
||||||
[
|
|
||||||
MarketState.STATE_TRADING_TERMINATED,
|
|
||||||
MarketState.STATE_SETTLED,
|
|
||||||
MarketState.STATE_CANCELLED,
|
|
||||||
MarketState.STATE_CLOSED,
|
|
||||||
].includes(m.state)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
const marketSuspended = transferNode.markets?.some(
|
||||||
const suspended = transferNode.markets?.some(
|
|
||||||
(m) =>
|
(m) =>
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
m?.state === MarketState.STATE_SUSPENDED ||
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
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
|
// 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 } =
|
const { gradientClassName, mainClassName } =
|
||||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
marketSuspended || !assetInActiveMarket
|
||||||
? {
|
? {
|
||||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||||
@ -371,6 +356,7 @@ export const ActiveRewardCard = ({
|
|||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
gradientClassName
|
gradientClassName
|
||||||
)}
|
)}
|
||||||
|
data-testid="active-rewards-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -382,7 +368,7 @@ export const ActiveRewardCard = ({
|
|||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<EntityIcon transfer={transfer} />
|
<EntityIcon transfer={transfer} />
|
||||||
{entityScope && (
|
{entityScope && (
|
||||||
<span className="text-muted text-xs">
|
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -390,7 +376,7 @@ export const ActiveRewardCard = ({
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 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(
|
{addDecimalsFormatNumber(
|
||||||
transferNode.transfer.amount,
|
transferNode.transfer.amount,
|
||||||
transferNode.transfer.asset?.decimals || 0,
|
transferNode.transfer.asset?.decimals || 0,
|
||||||
@ -411,7 +397,7 @@ export const ActiveRewardCard = ({
|
|||||||
)}
|
)}
|
||||||
underline={true}
|
underline={true}
|
||||||
>
|
>
|
||||||
<span className="text-xs">
|
<span className="text-xs" data-testid="distribution-strategy">
|
||||||
{
|
{
|
||||||
DistributionStrategyMapping[
|
DistributionStrategyMapping[
|
||||||
dispatchStrategy.distributionStrategy
|
dispatchStrategy.distributionStrategy
|
||||||
@ -429,7 +415,10 @@ export const ActiveRewardCard = ({
|
|||||||
'Number of epochs after distribution to delay vesting of rewards by'
|
'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', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.dispatchStrategy?.lockPeriod,
|
count: kind.dispatchStrategy?.lockPeriod,
|
||||||
})}
|
})}
|
||||||
@ -438,15 +427,15 @@ export const ActiveRewardCard = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
<span>
|
<span data-testid="dispatch-metric-info">
|
||||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
underline={suspended}
|
underline={marketSuspended}
|
||||||
description={
|
description={
|
||||||
(suspended || assetInSuspendedMarket) &&
|
(marketSuspended || !assetInActiveMarket) &&
|
||||||
(specificMarkets
|
(specificMarkets
|
||||||
? t('Eligible market(s) currently suspended')
|
? t('Eligible market(s) currently suspended')
|
||||||
: assetInSuspendedMarket
|
: !assetInActiveMarket
|
||||||
? t('Currently no markets eligible for reward')
|
? t('Currently no markets eligible for reward')
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
@ -458,8 +447,8 @@ export const ActiveRewardCard = ({
|
|||||||
<div className="flex items-center gap-8 flex-wrap">
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
{kind.endEpoch && (
|
{kind.endEpoch && (
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||||
<span>
|
<span data-testid="ends-in">
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.endEpoch - currentEpoch,
|
count: kind.endEpoch - currentEpoch,
|
||||||
})}
|
})}
|
||||||
@ -470,7 +459,7 @@ export const ActiveRewardCard = ({
|
|||||||
{
|
{
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||||
<span>
|
<span data-testid="assessed-over">
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: dispatchStrategy.windowLength,
|
count: dispatchStrategy.windowLength,
|
||||||
})}
|
})}
|
||||||
@ -513,7 +502,7 @@ const RewardRequirements = ({
|
|||||||
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||||
})}
|
})}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd className="flex items-center gap-1" data-testid="scope">
|
||||||
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -522,7 +511,10 @@ const RewardRequirements = ({
|
|||||||
<dt className="flex items-center gap-1 text-muted">
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
{t('Staked VEGA')}
|
{t('Staked VEGA')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
data-testid="staking-requirement"
|
||||||
|
>
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
dispatchStrategy?.stakingRequirement || 0,
|
dispatchStrategy?.stakingRequirement || 0,
|
||||||
assetDecimalPlaces
|
assetDecimalPlaces
|
||||||
@ -534,7 +526,7 @@ const RewardRequirements = ({
|
|||||||
<dt className="flex items-center gap-1 text-muted">
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
{t('Average position')}
|
{t('Average position')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd className="flex items-center gap-1" data-testid="average-position">
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||||
0,
|
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.")
|
"You may not have enough margin available to open this position.")
|
||||||
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
||||||
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
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()
|
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
||||||
expect(page.get_by_test_id("sidebar-content")
|
expect(page.get_by_test_id("sidebar-content")
|
||||||
).to_contain_text("DepositFrom")
|
).to_contain_text("DepositFrom")
|
||||||
|
@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
|||||||
next_epoch(vega=vega)
|
next_epoch(vega=vega)
|
||||||
|
|
||||||
page.reload()
|
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(
|
governance.submit_oracle_data(
|
||||||
wallet=vega.wallet,
|
wallet=vega.wallet,
|
||||||
payload={"trading.terminated": "true"},
|
payload={"trading.terminated": "true"},
|
||||||
@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
|||||||
)
|
)
|
||||||
next_epoch(vega=vega)
|
next_epoch(vega=vega)
|
||||||
page.reload()
|
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
|
import vega_sim.proto.vega as vega_protos
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from conftest import init_vega
|
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 fixtures.market import setup_continuous_market
|
||||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
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
|
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:
|
with init_vega(request) as vega:
|
||||||
yield vega
|
yield vega
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def team_page(vega, browser, request, setup_teams_and_games):
|
def team_page(vega, browser, request, setup_teams_and_games):
|
||||||
with init_page(vega, browser, request) as page:
|
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}")
|
page.goto(f"/#/competitions/teams/{team_id}")
|
||||||
yield page
|
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")
|
@pytest.fixture(scope="module")
|
||||||
def setup_teams_and_games(vega: VegaServiceNull):
|
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")
|
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_B.name, asset=tDAI_asset_id, amount=100000)
|
||||||
vega.mint(key_name=PARTY_C.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}
|
# list_teams actually returns a dictionary {"team_id": Team}
|
||||||
team_id = list(teams.keys())[0]
|
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)
|
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
|
current_epoch = vega.statistics().epoch_seq
|
||||||
game_start = current_epoch + 1
|
game_start = current_epoch + 1
|
||||||
game_end = current_epoch + 11
|
game_end = current_epoch + 14
|
||||||
|
|
||||||
current_epoch = vega.statistics().epoch_seq
|
current_epoch = vega.statistics().epoch_seq
|
||||||
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
||||||
@ -84,9 +108,42 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
factor=1.0,
|
factor=1.0,
|
||||||
start_epoch=game_start,
|
start_epoch=game_start,
|
||||||
end_epoch=game_end,
|
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)
|
next_epoch(vega)
|
||||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
||||||
|
|
||||||
@ -113,6 +170,22 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
side="SIDE_BUY",
|
side="SIDE_BUY",
|
||||||
volume=1,
|
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)
|
next_epoch(vega)
|
||||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
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,
|
"market_id": tDAI_market,
|
||||||
"asset_id": tDAI_asset_id,
|
"asset_id": tDAI_asset_id,
|
||||||
"team_id": team_id,
|
"team_id": team_id,
|
||||||
|
"team_id_2": team_id_2,
|
||||||
"team_name": team_name,
|
"team_name": team_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,66 +210,109 @@ def create_team(vega: VegaServiceNull):
|
|||||||
|
|
||||||
return team_name
|
return team_name
|
||||||
|
|
||||||
|
|
||||||
def test_team_page_games_table(team_page: Page):
|
def test_team_page_games_table(team_page: Page):
|
||||||
team_page.pause()
|
|
||||||
team_page.get_by_test_id("games-toggle").click()
|
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("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("rank-0")).to_have_text("2")
|
||||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
|
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("type-0")
|
||||||
expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
|
).to_have_text("Price maker fees paid")
|
||||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
|
expect(team_page.get_by_test_id("amount-0")).to_have_text("74")
|
||||||
"1"
|
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")
|
||||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
|
|
||||||
"2"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_team_page_members_table(team_page: Page):
|
def test_team_page_members_table(team_page: Page):
|
||||||
team_page.get_by_test_id("members-toggle").click()
|
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("referee-0")).to_be_visible()
|
||||||
expect(team_page.get_by_test_id("joinedAt-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"]
|
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("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("members-count-stat")).to_have_text("4")
|
||||||
|
|
||||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
|
expect(team_page.get_by_test_id("total-games-stat")).to_have_text("2")
|
||||||
"1"
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO this still seems wrong as its always 0
|
# TODO this still seems wrong as its always 0
|
||||||
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
|
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text("0")
|
||||||
"0"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
|
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("214")
|
||||||
"100m"
|
|
||||||
)
|
|
||||||
|
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):
|
def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||||
team_name = setup_teams_and_games["team_name"]
|
team_name = setup_teams_and_games["team_name"]
|
||||||
competitions_page.goto(f"/#/competitions/")
|
competitions_page.reload()
|
||||||
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
|
expect(
|
||||||
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
|
||||||
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
|
).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")
|
# FIXME: the numbers are different we need to clarify this with the backend
|
||||||
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
|
# 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
|
# TODO still odd that this is 0
|
||||||
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
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) {
|
query Teams($teamId: ID, $partyId: ID) {
|
||||||
teams(teamId: $teamId, partyId: $partyId) {
|
teams(teamId: $teamId, partyId: $partyId) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
teamId
|
...TeamsFields
|
||||||
referrer
|
|
||||||
name
|
|
||||||
teamUrl
|
|
||||||
avatarUrl
|
|
||||||
createdAt
|
|
||||||
createdAtEpoch
|
|
||||||
closed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
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<{
|
export type TeamsQueryVariables = Types.Exact<{
|
||||||
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||||
partyId?: 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`
|
export const TeamsDocument = gql`
|
||||||
query Teams($teamId: ID, $partyId: ID) {
|
query Teams($teamId: ID, $partyId: ID) {
|
||||||
teams(teamId: $teamId, partyId: $partyId) {
|
teams(teamId: $teamId, partyId: $partyId) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
teamId
|
...TeamsFields
|
||||||
referrer
|
|
||||||
name
|
|
||||||
teamUrl
|
|
||||||
avatarUrl
|
|
||||||
createdAt
|
|
||||||
createdAtEpoch
|
|
||||||
closed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
${TeamsFieldsFragmentDoc}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useTeamsQuery__
|
* __useTeamsQuery__
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
||||||
import { isActiveReward } from '../../components/rewards-container/active-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) =>
|
const isScopedToTeams = (node: TransferNode) =>
|
||||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
// scoped to teams
|
||||||
EntityScope.ENTITY_SCOPE_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 = ({
|
export const useGames = ({
|
||||||
currentEpoch,
|
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 { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
|
||||||
import compact from 'lodash/compact';
|
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) => {
|
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||||
const {
|
const {
|
||||||
|
@ -3,27 +3,13 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
|||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
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 * as Schema from '@vegaprotocol/types';
|
||||||
import {
|
import {
|
||||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
|
||||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
|
||||||
TOTAL_MARGIN_AVAILABLE,
|
|
||||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
|
||||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { KeyValue } from './key-value';
|
import { KeyValue } from './key-value';
|
||||||
import {
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
Accordion,
|
|
||||||
AccordionChevron,
|
|
||||||
AccordionPanel,
|
|
||||||
ExternalLink,
|
|
||||||
Tooltip,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useT, ns } from '../../use-t';
|
import { useT, ns } from '../../use-t';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
@ -31,9 +17,9 @@ import { emptyValue } from './deal-ticket-fee-details';
|
|||||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||||
|
|
||||||
export interface DealTicketMarginDetailsProps {
|
export interface DealTicketMarginDetailsProps {
|
||||||
generalAccountBalance?: string;
|
generalAccountBalance: string;
|
||||||
marginAccountBalance?: string;
|
marginAccountBalance: string;
|
||||||
orderMarginAccountBalance?: string;
|
orderMarginAccountBalance: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
assetSymbol: string;
|
assetSymbol: string;
|
||||||
@ -54,25 +40,13 @@ export const DealTicketMarginDetails = ({
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||||
const { pubKey: partyId } = useVegaWallet();
|
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 liquidationEstimate = positionEstimate?.liquidation;
|
||||||
const marginEstimate = positionEstimate?.margin;
|
|
||||||
const totalMarginAccountBalance =
|
const totalMarginAccountBalance =
|
||||||
BigInt(marginAccountBalance || '0') +
|
BigInt(marginAccountBalance || '0') +
|
||||||
BigInt(orderMarginAccountBalance || '0');
|
BigInt(orderMarginAccountBalance || '0');
|
||||||
const totalBalance =
|
|
||||||
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
|
|
||||||
const asset = getAsset(market);
|
const asset = getAsset(market);
|
||||||
const { decimals: assetDecimals, quantum } = asset;
|
const { decimals: assetDecimals, quantum } = asset;
|
||||||
let marginRequiredBestCase: string | undefined = undefined;
|
|
||||||
let marginRequiredWorstCase: string | undefined = undefined;
|
|
||||||
|
|
||||||
const collateralIncreaseEstimateBestCase = BigInt(
|
const collateralIncreaseEstimateBestCase = BigInt(
|
||||||
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
|
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
|
||||||
@ -80,102 +54,6 @@ export const DealTicketMarginDetails = ({
|
|||||||
const collateralIncreaseEstimateWorstCase = BigInt(
|
const collateralIncreaseEstimateWorstCase = BigInt(
|
||||||
positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0'
|
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 liquidationPriceEstimate = emptyValue;
|
||||||
let liquidationPriceEstimateRange = emptyValue;
|
let liquidationPriceEstimateRange = emptyValue;
|
||||||
@ -232,128 +110,50 @@ export const DealTicketMarginDetails = ({
|
|||||||
const quoteName = getQuoteName(market);
|
const quoteName = getQuoteName(market);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full gap-2">
|
<div className="flex flex-col w-full gap-2 mt-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}
|
|
||||||
<KeyValue
|
<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}
|
value={liquidationPriceEstimateRange}
|
||||||
formattedValue={liquidationPriceEstimate}
|
formattedValue={liquidationPriceEstimate}
|
||||||
symbol={quoteName}
|
symbol={quoteName}
|
||||||
|
@ -73,7 +73,7 @@ import {
|
|||||||
} from '../../hooks';
|
} from '../../hooks';
|
||||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||||
import noop from 'lodash/noop';
|
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 { KeyValue } from './key-value';
|
||||||
import { DocsLinks } from '@vegaprotocol/environment';
|
import { DocsLinks } from '@vegaprotocol/environment';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
@ -177,12 +177,6 @@ export const DealTicket = ({
|
|||||||
loading: loadingGeneralAccountBalance,
|
loading: loadingGeneralAccountBalance,
|
||||||
} = useAccountBalance(asset.id);
|
} = useAccountBalance(asset.id);
|
||||||
|
|
||||||
const balance = (
|
|
||||||
BigInt(marginAccountBalance) +
|
|
||||||
BigInt(generalAccountBalance) +
|
|
||||||
BigInt(orderMarginAccountBalance)
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
const { marketState, marketTradingMode } = marketData;
|
const { marketState, marketTradingMode } = marketData;
|
||||||
const timeInForce = watch('timeInForce');
|
const timeInForce = watch('timeInForce');
|
||||||
|
|
||||||
@ -729,17 +723,11 @@ export const DealTicket = ({
|
|||||||
error={summaryError}
|
error={summaryError}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
marketTradingMode={marketData.marketTradingMode}
|
marketTradingMode={marketData.marketTradingMode}
|
||||||
balance={balance}
|
balance={generalAccountBalance}
|
||||||
margin={(
|
margin={
|
||||||
BigInt(
|
positionEstimate?.estimatePosition?.collateralIncreaseEstimate
|
||||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
.bestCase || '0'
|
||||||
'0'
|
}
|
||||||
) +
|
|
||||||
BigInt(
|
|
||||||
positionEstimate?.estimatePosition?.margin.bestCase
|
|
||||||
.orderMarginLevel || '0'
|
|
||||||
)
|
|
||||||
).toString()}
|
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
pubKey={pubKey}
|
pubKey={pubKey}
|
||||||
onDeposit={onDeposit}
|
onDeposit={onDeposit}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import classnames from 'classnames';
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface KeyValuePros {
|
export interface KeyValuePros {
|
||||||
@ -19,7 +18,6 @@ export const KeyValue = ({
|
|||||||
value,
|
value,
|
||||||
labelDescription,
|
labelDescription,
|
||||||
symbol,
|
symbol,
|
||||||
indent,
|
|
||||||
onClick,
|
onClick,
|
||||||
formattedValue,
|
formattedValue,
|
||||||
}: KeyValuePros) => {
|
}: KeyValuePros) => {
|
||||||
@ -43,10 +41,7 @@ export const KeyValue = ({
|
|||||||
: id
|
: id
|
||||||
}`}
|
}`}
|
||||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||||
className={classnames(
|
className="text-xs flex justify-between items-center gap-4 flex-wrap text-right"
|
||||||
'text-xs flex justify-between items-center gap-4 flex-wrap text-right',
|
|
||||||
{ 'ml-2': indent }
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Tooltip description={labelDescription}>
|
<Tooltip description={labelDescription}>
|
||||||
<div className="text-muted text-left">{label}</div>
|
<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 { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
||||||
import { NoWalletWarning } from './deal-ticket';
|
import { NoWalletWarning } from './deal-ticket';
|
||||||
|
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
|
||||||
|
|
||||||
const defaultLeverage = 10;
|
const defaultLeverage = 10;
|
||||||
|
|
||||||
@ -93,66 +94,78 @@ export const MarginChange = ({
|
|||||||
},
|
},
|
||||||
skip
|
skip
|
||||||
);
|
);
|
||||||
if (
|
if (!asset || !estimateMargin?.estimatePosition) {
|
||||||
!asset ||
|
|
||||||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
|
|
||||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const collateralIncreaseEstimate = BigInt(
|
const collateralIncreaseEstimate = BigInt(
|
||||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
||||||
);
|
);
|
||||||
if (!collateralIncreaseEstimate) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let positionWarning = '';
|
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 = '';
|
let marginChangeWarning = '';
|
||||||
const amount = addDecimalsFormatNumber(
|
if (collateralIncreaseEstimate) {
|
||||||
collateralIncreaseEstimate.toString(),
|
if (orders?.length && openVolume !== '0') {
|
||||||
asset?.decimals
|
positionWarning = t(
|
||||||
);
|
'youHaveOpenPositionAndOrders',
|
||||||
const { symbol } = asset;
|
'You have an existing position and open orders on this market.',
|
||||||
const interpolation = { amount, symbol };
|
{
|
||||||
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
|
count: orders.length,
|
||||||
marginChangeWarning = t(
|
}
|
||||||
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
|
);
|
||||||
interpolation
|
} else if (!orders?.length) {
|
||||||
);
|
positionWarning = t('You have an existing position on this market.');
|
||||||
} else {
|
} else {
|
||||||
marginChangeWarning = t(
|
positionWarning = t(
|
||||||
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
|
'youHaveOpenOrders',
|
||||||
interpolation
|
'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 (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Notification
|
{positionWarning && marginChangeWarning && (
|
||||||
intent={Intent.Warning}
|
<Notification
|
||||||
message={
|
intent={Intent.Warning}
|
||||||
<>
|
message={
|
||||||
<p>{positionWarning}</p>
|
<>
|
||||||
<p>{marginChangeWarning}</p>
|
<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>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@ import type {
|
|||||||
} from '../hooks/use-form-values';
|
} from '../hooks/use-form-values';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||||
import { isPersistentOrder } from './time-in-force-persistance';
|
import { isPersistentOrder } from './time-in-force-persistence';
|
||||||
|
|
||||||
export const mapFormValuesToOrderSubmission = (
|
export const mapFormValuesToOrderSubmission = (
|
||||||
order: OrderFormValues,
|
order: OrderFormValues,
|
||||||
|
@ -2,9 +2,9 @@ import { OrderTimeInForce } from '@vegaprotocol/types';
|
|||||||
import {
|
import {
|
||||||
isNonPersistentOrder,
|
isNonPersistentOrder,
|
||||||
isPersistentOrder,
|
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_FOK)).toBe(true);
|
||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
||||||
@ -13,7 +13,7 @@ it('isNonPeristentOrder', () => {
|
|||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
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_FOK)).toBe(false);
|
||||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
||||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
@ -23,7 +23,6 @@ import {
|
|||||||
SUBSCRIPTION_TIMEOUT,
|
SUBSCRIPTION_TIMEOUT,
|
||||||
useNodeBasicStatus,
|
useNodeBasicStatus,
|
||||||
useNodeSubscriptionStatus,
|
useNodeSubscriptionStatus,
|
||||||
useResponseTime,
|
|
||||||
} from './row-data';
|
} from './row-data';
|
||||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||||
import { CUSTOM_NODE_KEY } from '../../types';
|
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', () => {
|
describe('RowData', () => {
|
||||||
const props = {
|
const props = {
|
||||||
id: '0',
|
id: '0',
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { isValidUrl } from '@vegaprotocol/utils';
|
|
||||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { CUSTOM_NODE_KEY } from '../../types';
|
import { CUSTOM_NODE_KEY } from '../../types';
|
||||||
@ -8,6 +7,7 @@ import {
|
|||||||
} from '../../utils/__generated__/NodeCheck';
|
} from '../../utils/__generated__/NodeCheck';
|
||||||
import { LayoutCell } from './layout-cell';
|
import { LayoutCell } from './layout-cell';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
|
import { useResponseTime } from '../../utils/time';
|
||||||
|
|
||||||
export const POLL_INTERVAL = 1000;
|
export const POLL_INTERVAL = 1000;
|
||||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
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 = ({
|
export const RowData = ({
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
getUserEnabledFeatureFlags,
|
getUserEnabledFeatureFlags,
|
||||||
setUserEnabledFeatureFlag,
|
setUserEnabledFeatureFlag,
|
||||||
} from './use-environment';
|
} from './use-environment';
|
||||||
|
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||||
|
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
/* no op*/
|
/* no op*/
|
||||||
@ -17,6 +18,10 @@ const noop = () => {
|
|||||||
|
|
||||||
jest.mock('@vegaprotocol/apollo-client');
|
jest.mock('@vegaprotocol/apollo-client');
|
||||||
jest.mock('zustand');
|
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 mockCreateClient = createClient as jest.Mock;
|
||||||
const createDefaultMockClient = () => {
|
const createDefaultMockClient = () => {
|
||||||
@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
|||||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||||
const fastWait = 1000;
|
const fastWait = 1000;
|
||||||
const nodes = [slowNode, fastNode];
|
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
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||||
|
|
||||||
@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
|||||||
statistics: {
|
statistics: {
|
||||||
chainId: 'chain-id',
|
chainId: 'chain-id',
|
||||||
blockHeight: '100',
|
blockHeight: '100',
|
||||||
vegaTime: new Date().toISOString(),
|
vegaTime: new Date(1).toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
|||||||
expect(result.current.nodes).toEqual(nodes);
|
expect(result.current.nodes).toEqual(nodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.runAllTimers();
|
jest.advanceTimersByTime(2000);
|
||||||
|
// jest.runAllTimers();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.status).toEqual('success');
|
expect(result.current.status).toEqual('success');
|
||||||
|
@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
|||||||
import { envSchema } from '../utils/validate-environment';
|
import { envSchema } from '../utils/validate-environment';
|
||||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||||
import uniq from 'lodash/uniq';
|
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 Client = ReturnType<typeof createClient>;
|
||||||
type ClientCollection = {
|
type ClientCollection = {
|
||||||
@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
|||||||
|
|
||||||
const VERSION = 1;
|
const VERSION = 1;
|
||||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||||
|
|
||||||
|
const QUERY_TIMEOUT = 3000;
|
||||||
const SUBSCRIPTION_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
|
* 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 findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||||
try {
|
try {
|
||||||
const url = await Promise.any(tests);
|
const nodes = await Promise.all(tests);
|
||||||
return url;
|
const responsiveNodes = nodes
|
||||||
} catch {
|
.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
|
// All tests rejected, no suitable node found
|
||||||
return null;
|
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
|
* Test a node for suitability for connection
|
||||||
*/
|
*/
|
||||||
const testNode = async (
|
const testNode = async (
|
||||||
url: string,
|
url: string,
|
||||||
client: Client
|
client: Client
|
||||||
): Promise<string | null> => {
|
): Promise<NodeTestResult> => {
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
// these promises will only resolve with true/false
|
testQuery(client, url),
|
||||||
testQuery(client),
|
|
||||||
testSubscription(client),
|
testSubscription(client),
|
||||||
]);
|
]);
|
||||||
if (results[0] && results[1]) {
|
return [url, ...results];
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a test query on a client
|
* Run a test query on a client
|
||||||
*/
|
*/
|
||||||
const testQuery = async (client: Client) => {
|
const testQuery = (
|
||||||
try {
|
client: Client,
|
||||||
const result = await client.query<NodeCheckQuery>({
|
url: string
|
||||||
query: NodeCheckDocument,
|
): Promise<Maybe<QueryTestResult>> => {
|
||||||
});
|
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||||
if (!result || result.error) {
|
client
|
||||||
return false;
|
.query<NodeCheckQuery>({
|
||||||
}
|
query: NodeCheckDocument,
|
||||||
return true;
|
})
|
||||||
} catch (err) {
|
.then((result) => {
|
||||||
return false;
|
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
|
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||||
* is deemed a failure
|
* is deemed a failure
|
||||||
*/
|
*/
|
||||||
const testSubscription = (client: Client) => {
|
const testSubscription = (
|
||||||
|
client: Client
|
||||||
|
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const sub = client
|
const sub = client
|
||||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
.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)",
|
"myVolume_other": "My volume (last {{count}} epochs)",
|
||||||
"numberEpochs": "{{count}} epochs",
|
"numberEpochs": "{{count}} epochs",
|
||||||
"numberEpochs_one": "{{count}} epoch",
|
"numberEpochs_one": "{{count}} epoch",
|
||||||
|
"Rewards earned": "Rewards earned",
|
||||||
"Rewards paid out": "Rewards paid out",
|
"Rewards paid out": "Rewards paid out",
|
||||||
"{{reward}}x": "{{reward}}x",
|
"{{reward}}x": "{{reward}}x",
|
||||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||||
@ -431,5 +432,19 @@
|
|||||||
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
||||||
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
||||||
"{{distance}} ago": "{{distance}} ago",
|
"{{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
|
# we can set this variable to true so that we can format with market.decimalPlaces
|
||||||
scaleLiquidationPriceToMarketDecimals: true
|
scaleLiquidationPriceToMarketDecimals: true
|
||||||
) {
|
) {
|
||||||
margin {
|
|
||||||
worstCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
bestCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collateralIncreaseEstimate {
|
collateralIncreaseEstimate {
|
||||||
worstCase
|
worstCase
|
||||||
bestCase
|
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`
|
export const PositionFieldsFragmentDoc = gql`
|
||||||
fragment PositionFields on Position {
|
fragment PositionFields on Position {
|
||||||
@ -144,26 +144,6 @@ export const EstimatePositionDocument = gql`
|
|||||||
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
||||||
scaleLiquidationPriceToMarketDecimals: true
|
scaleLiquidationPriceToMarketDecimals: true
|
||||||
) {
|
) {
|
||||||
margin {
|
|
||||||
worstCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
bestCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collateralIncreaseEstimate {
|
collateralIncreaseEstimate {
|
||||||
worstCase
|
worstCase
|
||||||
bestCase
|
bestCase
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { EstimatePositionQuery } from './__generated__/Positions';
|
import type { EstimatePositionQuery } from './__generated__/Positions';
|
||||||
import { MarginMode } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
export const estimatePositionQuery = (
|
export const estimatePositionQuery = (
|
||||||
override?: PartialDeep<EstimatePositionQuery>
|
override?: PartialDeep<EstimatePositionQuery>
|
||||||
@ -9,26 +8,6 @@ export const estimatePositionQuery = (
|
|||||||
const defaultResult: EstimatePositionQuery = {
|
const defaultResult: EstimatePositionQuery = {
|
||||||
estimatePosition: {
|
estimatePosition: {
|
||||||
__typename: 'PositionEstimate',
|
__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: {
|
collateralIncreaseEstimate: {
|
||||||
bestCase: '0',
|
bestCase: '0',
|
||||||
worstCase: '0',
|
worstCase: '0',
|
||||||
|
@ -30,26 +30,6 @@ describe('LiquidationPrice', () => {
|
|||||||
result: {
|
result: {
|
||||||
data: {
|
data: {
|
||||||
estimatePosition: {
|
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: {
|
collateralIncreaseEstimate: {
|
||||||
bestCase: '0',
|
bestCase: '0',
|
||||||
worstCase: '0',
|
worstCase: '0',
|
||||||
|
@ -41,16 +41,17 @@ export const TradingView = ({
|
|||||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const widgetRef = useRef<IChartingLibraryWidget>();
|
const widgetRef = useRef<IChartingLibraryWidget>();
|
||||||
|
|
||||||
const datafeed = useDatafeed();
|
|
||||||
|
|
||||||
const prevMarketId = usePrevious(marketId);
|
const prevMarketId = usePrevious(marketId);
|
||||||
const prevTheme = usePrevious(theme);
|
const prevTheme = usePrevious(theme);
|
||||||
|
|
||||||
|
const datafeed = useDatafeed(marketId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Widget already created
|
// Widget already created
|
||||||
if (widgetRef.current !== undefined) {
|
if (widgetRef.current !== undefined) {
|
||||||
// Update the symbol if changed
|
// Update the symbol if changed
|
||||||
if (marketId !== prevMarketId) {
|
if (marketId !== prevMarketId) {
|
||||||
|
datafeed.setSymbol(marketId);
|
||||||
widgetRef.current.setSymbol(
|
widgetRef.current.setSymbol(
|
||||||
marketId,
|
marketId,
|
||||||
(interval ? interval : '15') as TVResolutionString,
|
(interval ? interval : '15') as TVResolutionString,
|
||||||
|
@ -44,14 +44,22 @@ const configurationData: DatafeedConfiguration = {
|
|||||||
supported_resolutions: supportedResolutions as ResolutionString[],
|
supported_resolutions: supportedResolutions as ResolutionString[],
|
||||||
} as const;
|
} 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 hasHistory = useRef(false);
|
||||||
const subRef = useRef<Subscription>();
|
const subRef = useRef<Subscription>();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
const datafeed = useMemo(() => {
|
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) => {
|
onReady: (callback) => {
|
||||||
|
requestedSymbol = marketId;
|
||||||
setTimeout(() => callback(configurationData));
|
setTimeout(() => callback(configurationData));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -68,7 +76,7 @@ export const useDatafeed = () => {
|
|||||||
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
||||||
query: SymbolDocument,
|
query: SymbolDocument,
|
||||||
variables: {
|
variables: {
|
||||||
marketId,
|
marketId: requestedSymbol || marketId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -242,7 +250,7 @@ export const useDatafeed = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return feed;
|
return feed;
|
||||||
}, [client]);
|
}, [client, marketId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user