Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed7b1a7034 | ||
|
|
a1381ed68a | ||
|
|
e2763cc738 |
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
|
||||
|
||||
This repository is managed using [Nx](https://nx.dev).
|
||||
|
||||
## 🔎 Applications in this repo
|
||||
# 🔎 Applications in this repo
|
||||
|
||||
### [Block explorer](./apps/explorer)
|
||||
|
||||
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
|
||||
|
||||
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
|
||||
|
||||
## 🧱 Libraries in this repo
|
||||
# 🧱 Libraries in this repo
|
||||
|
||||
### [UI toolkit](./libs/ui-toolkit)
|
||||
|
||||
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
|
||||
|
||||
Generic react helpers that can be used across multiple applications, along with other utilities.
|
||||
|
||||
## 💻 Develop
|
||||
# 💻 Develop
|
||||
|
||||
### Set up
|
||||
|
||||
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
|
||||
|
||||
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
|
||||
|
||||
## 🐋 Hosting a console
|
||||
# 🐋 Hosting a console
|
||||
|
||||
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
|
||||
|
||||
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
|
||||
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
|
||||
```
|
||||
|
||||
## 📑 License
|
||||
# 📑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type HashProps = React.HTMLProps<HTMLSpanElement> & {
|
||||
export type HashProps = {
|
||||
text: string;
|
||||
truncate?: boolean;
|
||||
};
|
||||
|
||||
@@ -2,5 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
|
||||
export { default as PartyLink } from './party-link/party-link';
|
||||
export { default as NodeLink } from './node-link/node-link';
|
||||
export { default as MarketLink } from './market-link/market-link';
|
||||
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
|
||||
export * from './asset-link/asset-link';
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
|
||||
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
parameter: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Links a given network parameter to the relevant page and anchor on the page
|
||||
*/
|
||||
const NetworkParameterLink = ({
|
||||
parameter,
|
||||
...props
|
||||
}: NetworkParameterLinkProps) => {
|
||||
return (
|
||||
<Link
|
||||
className="underline"
|
||||
{...props}
|
||||
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
|
||||
>
|
||||
<Hash text={parameter} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkParameterLink;
|
||||
@@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
||||
>;
|
||||
|
||||
const base = ENV.dataSources.governanceUrl;
|
||||
const label = proposal?.rationale?.title || id;
|
||||
const label = proposal?.rationale.title || id;
|
||||
|
||||
return (
|
||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||
|
||||
@@ -5,10 +5,5 @@ query ExplorerProposalStatus($id: ID!) {
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
... on BatchProposal {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
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 type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||
|
||||
|
||||
export const ExplorerProposalStatusDocument = gql`
|
||||
@@ -19,11 +19,6 @@ export const ExplorerProposalStatusDocument = gql`
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
... on BatchProposal {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
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,4 +1,6 @@
|
||||
import type { ProposalTerms } from '../tx-proposal';
|
||||
import { useState } from 'react';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
||||
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
||||
import truncate from 'lodash/truncate';
|
||||
@@ -7,12 +9,7 @@ import ReactMarkdown from 'react-markdown';
|
||||
import { ProposalDate } from './proposal-date';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
import type { ProposalTerms } from '../tx-proposal';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { BatchItem } from './batch-item';
|
||||
|
||||
type Rationale = components['schemas']['vegaProposalRationale'];
|
||||
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
|
||||
|
||||
type ProposalTermsDialog = {
|
||||
open: boolean;
|
||||
@@ -24,7 +21,6 @@ interface ProposalSummaryProps {
|
||||
id: string;
|
||||
rationale?: Rationale;
|
||||
terms?: ProposalTerms;
|
||||
batch?: Batch;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +31,6 @@ export const ProposalSummary = ({
|
||||
id,
|
||||
rationale,
|
||||
terms,
|
||||
batch,
|
||||
}: ProposalSummaryProps) => {
|
||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||
open: false,
|
||||
@@ -77,18 +72,6 @@ export const ProposalSummary = ({
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{batch && (
|
||||
<section className="pt-2 text-sm leading-tight my-3">
|
||||
<h2 className="text-lg pb-1">{t('Changes')}</h2>
|
||||
<ol>
|
||||
{batch.map((change, index) => (
|
||||
<li className="ml-4 list-decimal" key={`batch-${index}`}>
|
||||
<BatchItem item={change} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
<div className="pt-5">
|
||||
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
||||
{t('View terms')}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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,7 +33,6 @@ import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
||||
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||
import { TxBatchProposal } from './tx-batch-proposal';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -137,8 +136,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsJoinTeam;
|
||||
case 'Update Margin Mode':
|
||||
return TxDetailsUpdateMarginMode;
|
||||
case 'Batch Proposal':
|
||||
return TxBatchProposal;
|
||||
default:
|
||||
return TxDetailsGeneric;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ export type FilterOption =
|
||||
| 'Amend Order'
|
||||
| 'Apply Referral Code'
|
||||
| 'Batch Market Instructions'
|
||||
| 'Batch Proposal'
|
||||
| 'Cancel LiquidityProvision Order'
|
||||
| 'Cancel Order'
|
||||
| 'Cancel Transfer Funds'
|
||||
@@ -68,13 +67,7 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
||||
'Cancel Transfer Funds',
|
||||
'Withdraw',
|
||||
],
|
||||
Governance: [
|
||||
'Batch Proposal',
|
||||
'Delegate',
|
||||
'Undelegate',
|
||||
'Vote on Proposal',
|
||||
'Proposal',
|
||||
],
|
||||
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
|
||||
Referrals: [
|
||||
'Apply Referral Code',
|
||||
'Create Referral Set',
|
||||
|
||||
@@ -71,27 +71,18 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||
|
||||
if (status === 'confirmed') {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-start gap-2"
|
||||
data-testid="team-creation-success-message"
|
||||
>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||
{code && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
Your team ID is:{' '}
|
||||
<span
|
||||
className="font-mono break-all"
|
||||
data-testid="team-id-display"
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
<span className="font-mono break-all">{code}</span>
|
||||
</p>
|
||||
<TradingAnchorButton
|
||||
href={Links.COMPETITIONS_TEAM(code)}
|
||||
intent={Intent.Info}
|
||||
size="small"
|
||||
data-testid="view-team-button"
|
||||
>
|
||||
{t('View team')}
|
||||
</TradingAnchorButton>
|
||||
@@ -134,7 +125,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
err={err}
|
||||
isCreatingSoloTeam={isSolo}
|
||||
isSolo={isSolo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,6 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
|
||||
import { useTeams } from '../../lib/hooks/use-teams';
|
||||
import take from 'lodash/take';
|
||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||
import { TeamCard } from '../../components/competitions/team-card';
|
||||
import { useMyTeam } from '../../lib/hooks/use-my-team';
|
||||
|
||||
export const CompetitionsHome = () => {
|
||||
const t = useT();
|
||||
@@ -33,14 +31,10 @@ export const CompetitionsHome = () => {
|
||||
currentEpoch,
|
||||
});
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
|
||||
const {
|
||||
team: myTeam,
|
||||
stats: myTeamStats,
|
||||
games: myTeamGames,
|
||||
rank: myTeamRank,
|
||||
} = useMyTeam();
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@@ -52,83 +46,65 @@ export const CompetitionsHome = () => {
|
||||
</p>
|
||||
</CompetitionsHeader>
|
||||
|
||||
{/** Team card */}
|
||||
{myTeam ? (
|
||||
<>
|
||||
<h2 className="text-2xl mb-6">{t('My team')}</h2>
|
||||
<div className="mb-12">
|
||||
<TeamCard
|
||||
team={myTeam}
|
||||
rank={myTeamRank}
|
||||
stats={myTeamStats}
|
||||
games={myTeamGames}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/** Get started */}
|
||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||
{/** Get started */}
|
||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||
|
||||
<CompetitionsActionsContainer>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
data-testId="create-public-team-button"
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
</>
|
||||
)}
|
||||
<CompetitionsActionsContainer>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
|
||||
{/** List of available games */}
|
||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
||||
|
||||
@@ -38,11 +38,12 @@ export const CompetitionsTeam = () => {
|
||||
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||
const t = useT();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, team, partyTeam, stats, members, games, loading, refetch } =
|
||||
useTeam(teamId, pubKey || undefined);
|
||||
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
|
||||
teamId,
|
||||
pubKey || undefined
|
||||
);
|
||||
|
||||
// only show spinner on first load so when users join teams its smoother
|
||||
if (!data && loading) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Splash>
|
||||
<Loader />
|
||||
@@ -185,10 +186,7 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
|
||||
const data = orderBy(
|
||||
members.map((m) => ({
|
||||
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
|
||||
rewards: formatNumber(m.totalQuantumRewards),
|
||||
volume: formatNumber(m.totalQuantumVolume),
|
||||
gamesPlayed: formatNumber(m.totalGamesPlayed),
|
||||
referee: <RefereeLink pubkey={m.referee} />,
|
||||
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||
})),
|
||||
@@ -199,10 +197,7 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'referee', displayName: t('Member ID') },
|
||||
{ name: 'rewards', displayName: t('Rewards earned') },
|
||||
{ name: 'volume', displayName: t('Total volume') },
|
||||
{ name: 'gamesPlayed', displayName: t('Games played') },
|
||||
{ name: 'referee', displayName: t('Referee') },
|
||||
{
|
||||
name: 'joinedAt',
|
||||
displayName: t('Joined at'),
|
||||
@@ -218,24 +213,14 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const RefereeLink = ({
|
||||
pubkey,
|
||||
isCreator,
|
||||
}: {
|
||||
pubkey: string;
|
||||
isCreator: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
||||
const linkCreator = useLinks(DApp.Explorer);
|
||||
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||
{truncateMiddle(pubkey)}
|
||||
</Link>{' '}
|
||||
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
|
||||
</>
|
||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||
{truncateMiddle(pubkey)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@ export const CompetitionsTeams = () => {
|
||||
|
||||
usePageTitle([t('Competitions'), t('Teams')]);
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [filter, setFilter] = useState<string | null | undefined>(undefined);
|
||||
|
||||
@@ -98,7 +98,7 @@ const UpdateTeamFormContainer = ({
|
||||
type={TransactionType.UpdateReferralSet}
|
||||
status={status}
|
||||
err={err}
|
||||
isCreatingSoloTeam={team.closed}
|
||||
isSolo={team.closed}
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useSimpleTransaction,
|
||||
useVegaWallet,
|
||||
type Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { useState } from 'react';
|
||||
@@ -23,8 +27,19 @@ export const JoinTeam = ({
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { send, status } = useSimpleTransaction({
|
||||
onSuccess: refetch,
|
||||
});
|
||||
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
||||
|
||||
const joinTeam = () => {
|
||||
send({
|
||||
joinTeam: {
|
||||
id: team.teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<JoinButton
|
||||
@@ -41,10 +56,11 @@ export const JoinTeam = ({
|
||||
{confirmDialog !== undefined && (
|
||||
<DialogContent
|
||||
type={confirmDialog}
|
||||
status={status}
|
||||
team={team}
|
||||
partyTeam={partyTeam}
|
||||
onConfirm={joinTeam}
|
||||
onCancel={() => setConfirmDialog(undefined)}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -94,7 +110,7 @@ export const JoinButton = ({
|
||||
// Not creator of the team, but still can't switch because
|
||||
// creators cannot leave their own team
|
||||
return (
|
||||
<Tooltip description={t('As a team creator, you cannot switch teams')}>
|
||||
<Tooltip description="As a team creator, you cannot switch teams">
|
||||
<Button intent={Intent.Primary} disabled={true}>
|
||||
{t('Switch team')}{' '}
|
||||
</Button>
|
||||
@@ -105,11 +121,7 @@ export const JoinButton = ({
|
||||
// Party is in a team, but not this one
|
||||
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => onJoin('switch')}
|
||||
intent={Intent.Primary}
|
||||
data-testid="switch-team-button"
|
||||
>
|
||||
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
|
||||
{t('Switch team')}{' '}
|
||||
</Button>
|
||||
);
|
||||
@@ -137,39 +149,21 @@ export const JoinButton = ({
|
||||
|
||||
const DialogContent = ({
|
||||
type,
|
||||
status,
|
||||
team,
|
||||
partyTeam,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
refetch,
|
||||
}: {
|
||||
type: JoinType;
|
||||
status: Status;
|
||||
team: Team;
|
||||
partyTeam?: Team;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const { send, status, error } = useSimpleTransaction({
|
||||
onSuccess: refetch,
|
||||
});
|
||||
|
||||
const joinTeam = () => {
|
||||
send({
|
||||
joinTeam: {
|
||||
id: team.teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<p className="text-vega-red break-words first-letter:capitalize">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
return <p>{t('Confirm in wallet...')}</p>;
|
||||
}
|
||||
@@ -219,11 +213,7 @@ const DialogContent = ({
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button
|
||||
onClick={joinTeam}
|
||||
intent={Intent.Success}
|
||||
data-testid="confirm-switch-button"
|
||||
>
|
||||
<Button onClick={onConfirm} intent={Intent.Success}>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
TextArea,
|
||||
TradingButton,
|
||||
Intent,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||
|
||||
@@ -19,8 +17,6 @@ import type {
|
||||
UpdateReferralSet,
|
||||
Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import classNames from 'classnames';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export type FormFields = {
|
||||
id: string;
|
||||
@@ -32,8 +28,8 @@ export type FormFields = {
|
||||
};
|
||||
|
||||
export enum TransactionType {
|
||||
CreateReferralSet = 'CreateReferralSet',
|
||||
UpdateReferralSet = 'UpdateReferralSet',
|
||||
CreateReferralSet,
|
||||
UpdateReferralSet,
|
||||
}
|
||||
|
||||
const prepareTransaction = (
|
||||
@@ -79,14 +75,14 @@ export const TeamForm = ({
|
||||
type,
|
||||
status,
|
||||
err,
|
||||
isCreatingSoloTeam,
|
||||
isSolo,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: {
|
||||
type: TransactionType;
|
||||
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
||||
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
||||
isCreatingSoloTeam: boolean;
|
||||
isSolo: boolean;
|
||||
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
||||
defaultValues?: FormFields;
|
||||
}) => {
|
||||
@@ -100,7 +96,8 @@ export const TeamForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
private: isCreatingSoloTeam,
|
||||
private: isSolo,
|
||||
allowList: '',
|
||||
...defaultValues,
|
||||
},
|
||||
});
|
||||
@@ -113,14 +110,16 @@ export const TeamForm = ({
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||
<input type="hidden" {...register('id')} />
|
||||
<input
|
||||
type="hidden"
|
||||
{...register('id', {
|
||||
disabled: true,
|
||||
})}
|
||||
/>
|
||||
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||
<TradingInput
|
||||
{...register('name', { required: t('Required') })}
|
||||
data-testid="team-name-input"
|
||||
/>
|
||||
<TradingInput {...register('name', { required: t('Required') })} />
|
||||
{errors.name?.message && (
|
||||
<TradingInputError forInput="name" data-testid="team-name-error">
|
||||
<TradingInputError forInput="name">
|
||||
{errors.name.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@@ -136,10 +135,9 @@ export const TeamForm = ({
|
||||
{...register('url', {
|
||||
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
||||
})}
|
||||
data-testid="team-url-input"
|
||||
/>
|
||||
{errors.url?.message && (
|
||||
<TradingInputError forInput="url" data-testid="team-url-error">
|
||||
<TradingInputError forInput="url">
|
||||
{errors.url.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@@ -156,81 +154,69 @@ export const TeamForm = ({
|
||||
message: t('Invalid image URL'),
|
||||
},
|
||||
})}
|
||||
data-testid="avatar-url-input"
|
||||
/>
|
||||
{errors.avatarUrl?.message && (
|
||||
<TradingInputError
|
||||
forInput="avatarUrl"
|
||||
data-testid="avatar-url-error"
|
||||
>
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.avatarUrl.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
{
|
||||
// allow changing to private/public if editing, but don't show these options if making a solo team
|
||||
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
|
||||
<>
|
||||
{!isSolo && (
|
||||
<>
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
disabled={isSolo}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{isPrivate && (
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
data-testid="team-private-checkbox"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{isPrivate && (
|
||||
<TradingFormGroup
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (
|
||||
publicKeys.every((pk) => isValidVegaPublicKey(pk))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
disabled: isSolo,
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
})}
|
||||
data-testid="team-allow-list-textarea"
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError
|
||||
forInput="avatarUrl"
|
||||
data-testid="team-allow-list-error"
|
||||
>
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{err && (
|
||||
<p className="text-danger text-xs mb-4 first-letter:capitalize">
|
||||
{err}
|
||||
@@ -256,61 +242,20 @@ const SubmitButton = ({
|
||||
text = t('Update');
|
||||
}
|
||||
|
||||
let confirmedText = t('Created');
|
||||
if (type === TransactionType.UpdateReferralSet) {
|
||||
confirmedText = t('Updated');
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
text = t('Confirm in wallet...');
|
||||
} else if (status === 'pending') {
|
||||
text = t('Confirming transaction...');
|
||||
}
|
||||
|
||||
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
|
||||
useLayoutEffect(() => {
|
||||
let to: ReturnType<typeof setTimeout>;
|
||||
if (status === 'confirmed' && !showConfirmed) {
|
||||
to = setTimeout(() => {
|
||||
setShowConfirmed(true);
|
||||
}, 100);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(to);
|
||||
};
|
||||
}, [showConfirmed, status]);
|
||||
|
||||
const confirmed = (
|
||||
<span
|
||||
className={classNames('text-sm transition-opacity opacity-0', {
|
||||
'opacity-100': showConfirmed,
|
||||
})}
|
||||
>
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
size={18}
|
||||
className="text-vega-green-500"
|
||||
/>{' '}
|
||||
{confirmedText}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<TradingButton
|
||||
type="submit"
|
||||
intent={Intent.Info}
|
||||
disabled={disabled}
|
||||
data-testid="team-form-submit-button"
|
||||
>
|
||||
{text}
|
||||
</TradingButton>
|
||||
{status === 'confirmed' && confirmed}
|
||||
</div>
|
||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
||||
{text}
|
||||
</TradingButton>
|
||||
);
|
||||
};
|
||||
|
||||
const parseAllowListText = (str: string = '') => {
|
||||
const parseAllowListText = (str: string) => {
|
||||
return str
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { type ComponentProps } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const UpdateTeamButton = ({
|
||||
team,
|
||||
size = 'medium',
|
||||
}: {
|
||||
team: Pick<Team, 'teamId' | 'referrer'>;
|
||||
size?: ComponentProps<typeof TradingAnchorButton>['size'];
|
||||
}) => {
|
||||
export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
||||
const t = useT();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
|
||||
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||
return (
|
||||
<TradingAnchorButton
|
||||
size={size}
|
||||
data-testid="update-team-button"
|
||||
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||
intent={Intent.Info}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
useFundingRate,
|
||||
useMarketTradingMode,
|
||||
useExternalTwap,
|
||||
getQuoteName,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketState as State } from '@vegaprotocol/types';
|
||||
import { HeaderStat } from '../../components/header';
|
||||
@@ -42,7 +41,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
|
||||
const asset = getAsset(market);
|
||||
const quoteUnit = getQuoteName(market);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -62,8 +60,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
<Last24hVolume
|
||||
marketId={market.id}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
marketDecimals={market.decimalPlaces}
|
||||
quoteUnit={quoteUnit}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useDataGridEvents,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
||||
import { useMarketsColumnDefs } from './use-column-defs';
|
||||
import { useColumnDefs } from './use-column-defs';
|
||||
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { type StateCreator, create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
|
||||
);
|
||||
|
||||
export const MarketListTable = (props: Props) => {
|
||||
const columnDefs = useMarketsColumnDefs();
|
||||
const columnDefs = useColumnDefs();
|
||||
const gridStore = useMarketsStore((store) => store.gridStore);
|
||||
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
|
||||
|
||||
|
||||
@@ -7,31 +7,21 @@ import type {
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type {
|
||||
MarketFieldsFragment,
|
||||
MarketMaybeWithData,
|
||||
MarketMaybeWithDataAndCandles,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketActionsDropdown } from './market-table-actions';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
calcCandleVolumePrice,
|
||||
getAsset,
|
||||
getQuoteName,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
|
||||
import { MarketCodeCell } from './market-code-cell';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||
|
||||
export const useMarketsColumnDefs = () => {
|
||||
export const useColumnDefs = () => {
|
||||
const t = useT();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return useMemo<ColDef[]>(
|
||||
@@ -168,25 +158,11 @@ export const useMarketsColumnDefs = () => {
|
||||
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
||||
const candles = data?.candles;
|
||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||
const quoteName = getQuoteName(data as MarketFieldsFragment);
|
||||
const volPrice =
|
||||
candles &&
|
||||
calcCandleVolumePrice(
|
||||
candles,
|
||||
data.decimalPlaces,
|
||||
data.positionDecimalPlaces
|
||||
);
|
||||
|
||||
const volume =
|
||||
data && vol && vol !== '0'
|
||||
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
||||
: '0.00';
|
||||
const volumePrice =
|
||||
volPrice && formatNumber(volPrice, data?.decimalPlaces);
|
||||
|
||||
return volumePrice
|
||||
? `${volume} (${volumePrice} ${quoteName})`
|
||||
: volume;
|
||||
return volume;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,11 +5,7 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const Box = ({
|
||||
children,
|
||||
backgroundImage,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
|
||||
export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@@ -17,22 +13,9 @@ export const Box = ({
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
'border rounded-lg',
|
||||
'relative p-6 overflow-hidden',
|
||||
'p-6',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{Boolean(backgroundImage?.length) && (
|
||||
<div
|
||||
className={classNames(
|
||||
'pointer-events-none',
|
||||
'bg-no-repeat bg-center bg-[length:500px_500px]',
|
||||
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
|
||||
)}
|
||||
style={{ backgroundImage: `url("${backgroundImage}")` }}
|
||||
></div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import classNames from 'classnames';
|
||||
const NUM_AVATARS = 20;
|
||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||
|
||||
export const getFallbackAvatar = (teamId: string) => {
|
||||
const getFallbackAvatar = (teamId: string) => {
|
||||
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||
.toString()
|
||||
.padStart(2, '0'); // between 01 - 20
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
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,7 +15,6 @@ import {
|
||||
} from '../../lib/hooks/use-team';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const TeamStats = ({
|
||||
stats,
|
||||
@@ -103,13 +102,7 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const FavoriteGame = ({
|
||||
games,
|
||||
noLabel = false,
|
||||
}: {
|
||||
games: TeamGame[];
|
||||
noLabel?: boolean;
|
||||
}) => {
|
||||
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
|
||||
const rewardMetrics = games.map(
|
||||
@@ -135,13 +128,7 @@ export const FavoriteGame = ({
|
||||
|
||||
return (
|
||||
<dl className="flex flex-col gap-1">
|
||||
<dt
|
||||
className={classNames('text-muted text-sm', {
|
||||
hidden: noLabel,
|
||||
})}
|
||||
>
|
||||
{t('Favorite game')}
|
||||
</dt>
|
||||
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
||||
<dd>
|
||||
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||
<VegaIcon
|
||||
@@ -155,7 +142,7 @@ export const FavoriteGame = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||
{children}
|
||||
@@ -163,11 +150,11 @@ export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const StatSectionSeparator = () => {
|
||||
const StatSectionSeparator = () => {
|
||||
return <div className="hidden md:block border-r border-default" />;
|
||||
};
|
||||
|
||||
export const StatList = ({ children }: { children: ReactNode }) => {
|
||||
const StatList = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||
{children}
|
||||
@@ -175,21 +162,19 @@ export const StatList = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Stat = ({
|
||||
const Stat = ({
|
||||
value,
|
||||
label,
|
||||
tooltip,
|
||||
valueTestId,
|
||||
className,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
valueTestId?: string;
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames(className)}>
|
||||
<div>
|
||||
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||
{value}
|
||||
</dd>
|
||||
|
||||
@@ -152,11 +152,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('Active rewards')}
|
||||
className="lg:col-span-full"
|
||||
data-testid="active-rewards-card"
|
||||
>
|
||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
||||
{enrichedTransfers.length > 1 && (
|
||||
<TradingInput
|
||||
onChange={(e) =>
|
||||
@@ -316,30 +312,49 @@ export const ActiveRewardCard = ({
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
|
||||
if (marketSettled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetInActiveMarket =
|
||||
const assetInSettledMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||
return (
|
||||
m?.state &&
|
||||
[
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const marketSuspended = transferNode.markets?.some(
|
||||
// Gray out the cards that are related to suspended markets
|
||||
const suspended = transferNode.markets?.some(
|
||||
(m) =>
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
);
|
||||
|
||||
const assetInSuspendedMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return (
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Gray out the cards that are related to suspended markets
|
||||
// Or settlement assets in markets that are not active and eligible for rewards
|
||||
const { gradientClassName, mainClassName } =
|
||||
marketSuspended || !assetInActiveMarket
|
||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
||||
? {
|
||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||
@@ -356,7 +371,6 @@ export const ActiveRewardCard = ({
|
||||
'rounded-lg',
|
||||
gradientClassName
|
||||
)}
|
||||
data-testid="active-rewards-card"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -368,7 +382,7 @@ export const ActiveRewardCard = ({
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<EntityIcon transfer={transfer} />
|
||||
{entityScope && (
|
||||
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||
<span className="text-muted text-xs">
|
||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||
</span>
|
||||
)}
|
||||
@@ -376,7 +390,7 @@ export const ActiveRewardCard = ({
|
||||
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||
<span className="font-glitch" data-testid="reward-value">
|
||||
<span className="font-glitch">
|
||||
{addDecimalsFormatNumber(
|
||||
transferNode.transfer.amount,
|
||||
transferNode.transfer.asset?.decimals || 0,
|
||||
@@ -397,7 +411,7 @@ export const ActiveRewardCard = ({
|
||||
)}
|
||||
underline={true}
|
||||
>
|
||||
<span className="text-xs" data-testid="distribution-strategy">
|
||||
<span className="text-xs">
|
||||
{
|
||||
DistributionStrategyMapping[
|
||||
dispatchStrategy.distributionStrategy
|
||||
@@ -415,10 +429,7 @@ export const ActiveRewardCard = ({
|
||||
'Number of epochs after distribution to delay vesting of rewards by'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className="text-muted text-xs whitespace-nowrap"
|
||||
data-testid="locked-for"
|
||||
>
|
||||
<span className="text-muted text-xs whitespace-nowrap">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.dispatchStrategy?.lockPeriod,
|
||||
})}
|
||||
@@ -427,15 +438,15 @@ export const ActiveRewardCard = ({
|
||||
</div>
|
||||
|
||||
<span className="border-[0.5px] border-gray-700" />
|
||||
<span data-testid="dispatch-metric-info">
|
||||
<span>
|
||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||
<Tooltip
|
||||
underline={marketSuspended}
|
||||
underline={suspended}
|
||||
description={
|
||||
(marketSuspended || !assetInActiveMarket) &&
|
||||
(suspended || assetInSuspendedMarket) &&
|
||||
(specificMarkets
|
||||
? t('Eligible market(s) currently suspended')
|
||||
: !assetInActiveMarket
|
||||
: assetInSuspendedMarket
|
||||
? t('Currently no markets eligible for reward')
|
||||
: '')
|
||||
}
|
||||
@@ -447,8 +458,8 @@ export const ActiveRewardCard = ({
|
||||
<div className="flex items-center gap-8 flex-wrap">
|
||||
{kind.endEpoch && (
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||
<span data-testid="ends-in">
|
||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
||||
<span>
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.endEpoch - currentEpoch,
|
||||
})}
|
||||
@@ -459,7 +470,7 @@ export const ActiveRewardCard = ({
|
||||
{
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||
<span data-testid="assessed-over">
|
||||
<span>
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: dispatchStrategy.windowLength,
|
||||
})}
|
||||
@@ -502,7 +513,7 @@ const RewardRequirements = ({
|
||||
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||
})}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1" data-testid="scope">
|
||||
<dd className="flex items-center gap-1">
|
||||
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||
</dd>
|
||||
</div>
|
||||
@@ -511,10 +522,7 @@ const RewardRequirements = ({
|
||||
<dt className="flex items-center gap-1 text-muted">
|
||||
{t('Staked VEGA')}
|
||||
</dt>
|
||||
<dd
|
||||
className="flex items-center gap-1"
|
||||
data-testid="staking-requirement"
|
||||
>
|
||||
<dd className="flex items-center gap-1">
|
||||
{addDecimalsFormatNumber(
|
||||
dispatchStrategy?.stakingRequirement || 0,
|
||||
assetDecimalPlaces
|
||||
@@ -526,7 +534,7 @@ const RewardRequirements = ({
|
||||
<dt className="flex items-center gap-1 text-muted">
|
||||
{t('Average position')}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1" data-testid="average-position">
|
||||
<dd className="flex items-center gap-1">
|
||||
{addDecimalsFormatNumber(
|
||||
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||
0,
|
||||
|
||||
@@ -81,7 +81,6 @@ export const Settings = () => {
|
||||
intent={Intent.Primary}
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.74.0-preview.8
|
||||
VEGA_VERSION=v0.74.0-preview.7
|
||||
LOCAL_SERVER=false
|
||||
|
||||
@@ -33,7 +33,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
|
||||
"You may not have enough margin available to open this position.")
|
||||
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
||||
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
||||
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
|
||||
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
|
||||
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
||||
expect(page.get_by_test_id("sidebar-content")
|
||||
).to_contain_text("DepositFrom")
|
||||
|
||||
@@ -37,7 +37,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
||||
# 6002-MDET-004
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||
# 6002-MDET-005
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
# 6002-MDET-008
|
||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
|
||||
"Settlement assettDAI"
|
||||
|
||||
@@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
next_epoch(vega=vega)
|
||||
|
||||
page.reload()
|
||||
expect(page.get_by_test_id("active-rewards-card")).to_be_visible(timeout=15000)
|
||||
expect(page.locator(".from-vega-cdark-400")).to_be_visible(timeout=15000)
|
||||
governance.submit_oracle_data(
|
||||
wallet=vega.wallet,
|
||||
payload={"trading.terminated": "true"},
|
||||
@@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
)
|
||||
next_epoch(vega=vega)
|
||||
page.reload()
|
||||
expect(page.get_by_test_id("active-rewards-card")).not_to_be_in_viewport()
|
||||
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
|
||||
|
||||
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
|
||||
import vega_sim.proto.vega as vega_protos
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from actions.utils import next_epoch, change_keys
|
||||
from actions.utils import next_epoch
|
||||
from fixtures.market import setup_continuous_market
|
||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
||||
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
|
||||
@@ -14,7 +14,6 @@ def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def team_page(vega, browser, request, setup_teams_and_games):
|
||||
with init_page(vega, browser, request) as page:
|
||||
@@ -24,20 +23,9 @@ def team_page(vega, browser, request, setup_teams_and_games):
|
||||
page.goto(f"/#/competitions/teams/{team_id}")
|
||||
yield page
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def competitions_page(vega, browser, request, setup_teams_and_games):
|
||||
with init_page(vega, browser, request) as page:
|
||||
risk_accepted_setup(page)
|
||||
auth_setup(vega, page)
|
||||
team_id = setup_teams_and_games["team_id"]
|
||||
page.goto(f"/#/competitions/")
|
||||
yield page
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def setup_teams_and_games(vega: VegaServiceNull):
|
||||
tDAI_market = setup_continuous_market(vega, custom_quantum=100000)
|
||||
tDAI_market = setup_continuous_market(vega)
|
||||
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
|
||||
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
|
||||
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
|
||||
@@ -58,18 +46,6 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
|
||||
# list_teams actually returns a dictionary {"team_id": Team}
|
||||
team_id = list(teams.keys())[0]
|
||||
vega.create_referral_set(
|
||||
key_name="market_maker",
|
||||
name="test",
|
||||
team_url="https://vega.xyz",
|
||||
avatar_url="http://placekitten.com/200/200",
|
||||
closed=False,
|
||||
)
|
||||
next_epoch(vega)
|
||||
teams = vega.list_teams()
|
||||
|
||||
team_id_2 = list(teams.keys())[0]
|
||||
vega.apply_referral_code("Key 1", team_id_2)
|
||||
|
||||
vega.apply_referral_code(PARTY_B.name, team_id)
|
||||
|
||||
@@ -87,7 +63,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
|
||||
current_epoch = vega.statistics().epoch_seq
|
||||
game_start = current_epoch + 1
|
||||
game_end = current_epoch + 14
|
||||
game_end = current_epoch + 11
|
||||
|
||||
current_epoch = vega.statistics().epoch_seq
|
||||
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
||||
@@ -108,42 +84,9 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
factor=1.0,
|
||||
start_epoch=game_start,
|
||||
end_epoch=game_end,
|
||||
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
|
||||
window_length=10
|
||||
)
|
||||
|
||||
next_epoch(vega)
|
||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
||||
|
||||
@@ -170,22 +113,6 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
side="SIDE_BUY",
|
||||
volume=1,
|
||||
)
|
||||
vega.submit_order(
|
||||
trading_key="Key 1",
|
||||
market_id=tDAI_market,
|
||||
order_type="TYPE_MARKET",
|
||||
time_in_force="TIME_IN_FORCE_IOC",
|
||||
side="SIDE_BUY",
|
||||
volume=1,
|
||||
)
|
||||
vega.submit_order(
|
||||
trading_key="market_maker",
|
||||
market_id=tDAI_market,
|
||||
order_type="TYPE_MARKET",
|
||||
time_in_force="TIME_IN_FORCE_IOC",
|
||||
side="SIDE_BUY",
|
||||
volume=1,
|
||||
)
|
||||
next_epoch(vega)
|
||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
||||
|
||||
@@ -193,7 +120,6 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
"market_id": tDAI_market,
|
||||
"asset_id": tDAI_asset_id,
|
||||
"team_id": team_id,
|
||||
"team_id_2": team_id_2,
|
||||
"team_name": team_name,
|
||||
}
|
||||
|
||||
@@ -210,109 +136,65 @@ def create_team(vega: VegaServiceNull):
|
||||
|
||||
return team_name
|
||||
|
||||
|
||||
def test_team_page_games_table(team_page: Page):
|
||||
team_page.get_by_test_id("games-toggle").click()
|
||||
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
|
||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19")
|
||||
expect(team_page.get_by_test_id("type-0")
|
||||
).to_have_text("Price maker fees paid")
|
||||
expect(team_page.get_by_test_id("amount-0")).to_have_text("74")
|
||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("4")
|
||||
|
||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
|
||||
expect(team_page.get_by_test_id("type-0")).to_have_text("Price maker fees paid")
|
||||
expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
|
||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
|
||||
"1"
|
||||
)
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
|
||||
"2"
|
||||
)
|
||||
|
||||
def test_team_page_members_table(team_page: Page):
|
||||
team_page.get_by_test_id("members-toggle").click()
|
||||
expect(team_page.get_by_test_id("members-toggle")
|
||||
).to_have_text("Members (4)")
|
||||
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (3)")
|
||||
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
|
||||
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
|
||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9")
|
||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("8")
|
||||
|
||||
|
||||
def test_team_page_headline(team_page: Page, setup_teams_and_games):
|
||||
def test_team_page_headline(team_page: Page, setup_teams_and_games
|
||||
):
|
||||
team_name = setup_teams_and_games["team_name"]
|
||||
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
|
||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("4")
|
||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("3")
|
||||
|
||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
|
||||
"1"
|
||||
)
|
||||
|
||||
# TODO this still seems wrong as its always 0
|
||||
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text("0")
|
||||
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
|
||||
"0"
|
||||
)
|
||||
|
||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("214")
|
||||
|
||||
|
||||
def test_switch_teams(team_page: Page, vega: VegaServiceNull):
|
||||
team_page.get_by_test_id("switch-team-button").click()
|
||||
team_page.get_by_test_id("confirm-switch-button").click()
|
||||
expect(team_page.get_by_test_id("dialog-content").first).to_be_visible()
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
next_epoch(vega=vega)
|
||||
team_page.reload()
|
||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("5")
|
||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
|
||||
"100m"
|
||||
)
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def competitions_page(vega, browser, request):
|
||||
with init_page(vega, browser, request) as page:
|
||||
risk_accepted_setup(page)
|
||||
auth_setup(vega, page)
|
||||
yield page
|
||||
|
||||
def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||
team_name = setup_teams_and_games["team_name"]
|
||||
competitions_page.reload()
|
||||
expect(
|
||||
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
|
||||
).to_have_count(1)
|
||||
expect(
|
||||
competitions_page.get_by_test_id(
|
||||
"rank-1").locator(".text-vega-clight-500")
|
||||
).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("team-1")).to_have_text(team_name)
|
||||
expect(competitions_page.get_by_test_id("status-1")).to_have_text("Open")
|
||||
competitions_page.goto(f"/#/competitions/")
|
||||
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
||||
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
|
||||
|
||||
# FIXME: the numbers are different we need to clarify this with the backend
|
||||
# expect(competitions_page.get_by_test_id("earned-1")).to_have_text("160")
|
||||
expect(competitions_page.get_by_test_id("games-1")).to_have_text("2")
|
||||
expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
|
||||
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
|
||||
|
||||
# TODO still odd that this is 0
|
||||
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
||||
|
||||
|
||||
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")
|
||||
#TODO def test_games(competitions_page: Page):
|
||||
#TODO currently no games appear which i think is a bug
|
||||
|
||||
@@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
totalQuantumRewards
|
||||
total_quantum_rewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
@@ -51,13 +51,6 @@ fragment TeamGameFields on Game {
|
||||
}
|
||||
}
|
||||
|
||||
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||
partyId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
}
|
||||
|
||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
@@ -94,14 +87,4 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
teamMembersStatistics(
|
||||
teamId: $teamId
|
||||
aggregationEpochs: $aggregationEpochs
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...TeamMemberStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
fragment TeamsFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
totalMembers
|
||||
}
|
||||
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamsFields
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-22
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> };
|
||||
|
||||
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, totalQuantumRewards: string }> };
|
||||
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
|
||||
|
||||
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
||||
|
||||
@@ -13,8 +13,6 @@ export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number,
|
||||
|
||||
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
|
||||
|
||||
export type TeamMemberStatsFieldsFragment = { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number };
|
||||
|
||||
export type TeamQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
@@ -22,7 +20,7 @@ export type TeamQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamQuery = { __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, allowList: Array<string> } }> } | null, partyTeams?: { __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, allowList: Array<string> } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, totalQuantumRewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null }, teamMembersStatistics?: { __typename?: 'TeamMembersStatisticsConnection', edges: Array<{ __typename?: 'TeamMemberStatisticsEdge', node: { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number } }> } | null };
|
||||
export type TeamQuery = { __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, allowList: Array<string> } }> } | null, partyTeams?: { __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, allowList: Array<string> } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
|
||||
|
||||
export const TeamFieldsFragmentDoc = gql`
|
||||
fragment TeamFields on Team {
|
||||
@@ -45,7 +43,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
totalQuantumRewards
|
||||
total_quantum_rewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
@@ -82,14 +80,6 @@ export const TeamGameFieldsFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
${TeamEntityFragmentDoc}`;
|
||||
export const TeamMemberStatsFieldsFragmentDoc = gql`
|
||||
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||
partyId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
}
|
||||
`;
|
||||
export const TeamDocument = gql`
|
||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
teams(teamId: $teamId) {
|
||||
@@ -127,19 +117,11 @@ export const TeamDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
...TeamMemberStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamFieldsFragmentDoc}
|
||||
${TeamStatsFieldsFragmentDoc}
|
||||
${TeamRefereeFieldsFragmentDoc}
|
||||
${TeamGameFieldsFragmentDoc}
|
||||
${TeamMemberStatsFieldsFragmentDoc}`;
|
||||
${TeamGameFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamQuery__
|
||||
|
||||
+11
-18
@@ -3,40 +3,33 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamsFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number };
|
||||
|
||||
export type TeamsQueryVariables = Types.Exact<{
|
||||
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number } }> } | 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 } }> } | null };
|
||||
|
||||
|
||||
export const TeamsFieldsFragmentDoc = gql`
|
||||
fragment TeamsFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
totalMembers
|
||||
}
|
||||
`;
|
||||
export const TeamsDocument = gql`
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamsFields
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamsFieldsFragmentDoc}`;
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTeamsQuery__
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
||||
import { isActiveReward } from '../../components/rewards-container/active-rewards';
|
||||
import {
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
type TransferNode,
|
||||
} from '@vegaprotocol/types';
|
||||
import { EntityScope, type TransferNode } from '@vegaprotocol/types';
|
||||
|
||||
const isScopedToTeams = (node: TransferNode) =>
|
||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||
// scoped to teams
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS ||
|
||||
// or to individuals
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||
// but they have to be in a team
|
||||
node.transfer.kind.dispatchStrategy.individualScope ===
|
||||
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM));
|
||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS;
|
||||
|
||||
export const useGames = ({
|
||||
currentEpoch,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
@@ -6,24 +6,17 @@ import {
|
||||
type TeamStatsFieldsFragment,
|
||||
type TeamRefereeFieldsFragment,
|
||||
type TeamEntityFragment,
|
||||
type TeamMemberStatsFieldsFragment,
|
||||
} from './__generated__/Team';
|
||||
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
|
||||
|
||||
export type Team = TeamFieldsFragment;
|
||||
export type TeamStats = TeamStatsFieldsFragment;
|
||||
export type Member = TeamRefereeFieldsFragment & {
|
||||
isCreator: boolean;
|
||||
totalGamesPlayed: number;
|
||||
totalQuantumVolume: string;
|
||||
totalQuantumRewards: string;
|
||||
};
|
||||
export type Member = TeamRefereeFieldsFragment;
|
||||
export type TeamEntity = TeamEntityFragment;
|
||||
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
|
||||
export type MemberStats = TeamMemberStatsFieldsFragment;
|
||||
|
||||
export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const queryResult = useTeamQuery({
|
||||
const { data, loading, error, refetch } = useTeamQuery({
|
||||
variables: {
|
||||
teamId: teamId || '',
|
||||
partyId,
|
||||
@@ -33,11 +26,7 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const { data } = queryResult;
|
||||
|
||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||
const team = teamEdge?.node;
|
||||
|
||||
const partyTeam = data?.partyTeams?.edges?.length
|
||||
? data.partyTeams.edges[0].node
|
||||
: undefined;
|
||||
@@ -45,40 +34,9 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const teamStatsEdge = data?.teamsStatistics?.edges.find(
|
||||
(e) => e.node.teamId === teamId
|
||||
);
|
||||
|
||||
const memberStats = data?.teamMembersStatistics?.edges.length
|
||||
? data.teamMembersStatistics.edges.map((e) => e.node)
|
||||
: [];
|
||||
|
||||
const members: Member[] = data?.teamReferees?.edges.length
|
||||
? data.teamReferees.edges
|
||||
.filter((e) => e.node.teamId === teamId)
|
||||
.map((e) => {
|
||||
const member = e.node;
|
||||
const stats = memberStats.find((m) => m.partyId === member.referee);
|
||||
return {
|
||||
...member,
|
||||
isCreator: false,
|
||||
totalQuantumVolume: stats ? stats.totalQuantumVolume : '0',
|
||||
totalQuantumRewards: stats ? stats.totalQuantumRewards : '0',
|
||||
totalGamesPlayed: stats ? stats.totalGamesPlayed : 0,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
if (team) {
|
||||
const ownerStats = memberStats.find((m) => m.partyId === team.referrer);
|
||||
members.unshift({
|
||||
teamId: team.teamId,
|
||||
referee: team.referrer,
|
||||
joinedAt: team?.createdAt,
|
||||
joinedAtEpoch: team?.createdAtEpoch,
|
||||
isCreator: true,
|
||||
totalQuantumVolume: ownerStats ? ownerStats.totalQuantumVolume : '0',
|
||||
totalQuantumRewards: ownerStats ? ownerStats.totalQuantumRewards : '0',
|
||||
totalGamesPlayed: ownerStats ? ownerStats.totalGamesPlayed : 0,
|
||||
});
|
||||
}
|
||||
const members = data?.teamReferees?.edges
|
||||
.filter((e) => e.node.teamId === teamId)
|
||||
.map((e) => e.node);
|
||||
|
||||
// Find games where the current team participated in
|
||||
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
||||
@@ -102,9 +60,12 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
||||
|
||||
return {
|
||||
...queryResult,
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
stats: teamStatsEdge?.node,
|
||||
team,
|
||||
team: teamEdge?.node,
|
||||
members,
|
||||
games,
|
||||
partyTeam,
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { useMemo } from 'react';
|
||||
import { useTeamsQuery } from './__generated__/Teams';
|
||||
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
|
||||
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
|
||||
import {
|
||||
type TeamsStatisticsQuery,
|
||||
useTeamsStatisticsQuery,
|
||||
} from './__generated__/TeamsStatistics';
|
||||
import compact from 'lodash/compact';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { type ArrayElement } from 'type-fest/source/internal';
|
||||
|
||||
// 192
|
||||
export const DEFAULT_AGGREGATION_EPOCHS = 192;
|
||||
type SortableField = keyof Omit<
|
||||
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
|
||||
ArrayElement<
|
||||
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
|
||||
>['node'],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||
type UseTeamsArgs = {
|
||||
aggregationEpochs?: number;
|
||||
sortByField?: SortableField[];
|
||||
order?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export const DEFAULT_AGGREGATION_EPOCHS = 10;
|
||||
|
||||
export const useTeams = ({
|
||||
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
|
||||
sortByField = ['createdAtEpoch'],
|
||||
order = 'asc',
|
||||
}: UseTeamsArgs) => {
|
||||
const {
|
||||
data: teamsData,
|
||||
loading: teamsLoading,
|
||||
@@ -36,8 +57,12 @@ export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||
...stats.find((s) => s.teamId === t.teamId),
|
||||
}));
|
||||
|
||||
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc');
|
||||
}, [teams, stats]);
|
||||
const sorted = sortBy(data, sortByField);
|
||||
if (order === 'desc') {
|
||||
return sorted.reverse();
|
||||
}
|
||||
return sorted;
|
||||
}, [teams, sortByField, order, stats]);
|
||||
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -96,6 +96,11 @@ describe('TransferForm', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
targetText: 'Include transfer fee',
|
||||
tooltipText:
|
||||
'The fee will be taken from the amount you are transferring.',
|
||||
},
|
||||
{
|
||||
targetText: 'Transfer fee',
|
||||
tooltipText: /transfer\.fee\.factor/,
|
||||
@@ -271,6 +276,9 @@ describe('TransferForm', () => {
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '50');
|
||||
|
||||
@@ -280,7 +288,10 @@ describe('TransferForm', () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100.00');
|
||||
|
||||
// If transfering from a vested account fees should be 0
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
const expectedFee = '0';
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
|
||||
@@ -385,7 +396,78 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({ ...props, submitTransfer: mockSubmit });
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
|
||||
// 1003-TRAN-022
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(checkbox).toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
||||
|
||||
// 1003-TRAN-020
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
expectedAmount
|
||||
);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
|
||||
amount
|
||||
);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(expectedAmount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('validates fields when checkbox is not checked', async () => {
|
||||
renderComponent(props);
|
||||
|
||||
@@ -415,8 +497,11 @@ describe('TransferForm', () => {
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(checkbox).not.toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TradingRichSelect,
|
||||
TradingSelect,
|
||||
Tooltip,
|
||||
TradingCheckbox,
|
||||
TradingButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
@@ -134,17 +135,32 @@ export const TransferForm = ({
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Max amount given selected asset and from account
|
||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||
|
||||
const fee = useMemo(
|
||||
() => feeFactor && new BigNumber(feeFactor).times(amount).toString(),
|
||||
[amount, feeFactor]
|
||||
);
|
||||
const transferAmount = useMemo(() => {
|
||||
if (!amount) return undefined;
|
||||
if (includeFee && feeFactor) {
|
||||
return new BigNumber(1).minus(feeFactor).times(amount).toString();
|
||||
}
|
||||
return amount;
|
||||
}, [amount, includeFee, feeFactor]);
|
||||
|
||||
const fee = useMemo(() => {
|
||||
if (!transferAmount) return undefined;
|
||||
if (includeFee) {
|
||||
return new BigNumber(amount).minus(transferAmount).toString();
|
||||
}
|
||||
return (
|
||||
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
|
||||
);
|
||||
}, [amount, includeFee, transferAmount, feeFactor]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!amount) {
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
|
||||
@@ -157,7 +173,7 @@ export const TransferForm = ({
|
||||
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
amount,
|
||||
transferAmount,
|
||||
type,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
@@ -167,7 +183,7 @@ export const TransferForm = ({
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[submitTransfer, amount, assets]
|
||||
[submitTransfer, transferAmount, assets]
|
||||
);
|
||||
|
||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||
@@ -263,6 +279,7 @@ export const TransferForm = ({
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -432,9 +449,27 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
{amount && fee && (
|
||||
<div className="mb-4">
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The fee will be taken from the amount you are transferring.`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<TradingCheckbox
|
||||
name="include-transfer-fee"
|
||||
disabled={!transferAmount || fromVested}
|
||||
label={t('Include transfer fee')}
|
||||
checked={includeFee}
|
||||
onCheckedChange={() => setIncludeFee((x) => !x)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{transferAmount && fee && (
|
||||
<TransferFee
|
||||
amount={amount}
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fromVested ? '0' : fee}
|
||||
decimals={asset?.decimals}
|
||||
@@ -449,22 +484,29 @@ export const TransferForm = ({
|
||||
|
||||
export const TransferFee = ({
|
||||
amount,
|
||||
transferAmount,
|
||||
feeFactor,
|
||||
fee,
|
||||
decimals,
|
||||
}: {
|
||||
amount: string;
|
||||
transferAmount: string;
|
||||
feeFactor: string | null;
|
||||
fee?: string;
|
||||
decimals?: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!feeFactor || !amount || !fee) return null;
|
||||
if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) {
|
||||
if (!feeFactor || !amount || !transferAmount || !fee) return null;
|
||||
if (
|
||||
isNaN(Number(feeFactor)) ||
|
||||
isNaN(Number(amount)) ||
|
||||
isNaN(Number(transferAmount)) ||
|
||||
isNaN(Number(fee))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalValue = new BigNumber(amount).plus(fee).toString();
|
||||
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-2 text-xs">
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AssetsDocument, type AssetsQuery } from './__generated__/Assets';
|
||||
import { AssetStatus } from '@vegaprotocol/types';
|
||||
import { type Asset } from './asset-data-provider';
|
||||
import { DENY_LIST } from './constants';
|
||||
import { type AssetFieldsFragment } from './__generated__/Asset';
|
||||
|
||||
export interface BuiltinAssetSource {
|
||||
__typename: 'BuiltinAsset';
|
||||
@@ -90,24 +89,3 @@ export const useEnabledAssets = () => {
|
||||
variables: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/** Wrapped ETH symbol */
|
||||
const WETH = 'WETH';
|
||||
type WETHDetails = Pick<AssetFieldsFragment, 'symbol' | 'decimals' | 'quantum'>;
|
||||
/**
|
||||
* Tries to find WETH asset configuration on Vega in order to provide its
|
||||
* details, otherwise it returns hardcoded values.
|
||||
*/
|
||||
export const useWETH = (): WETHDetails => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
if (data) {
|
||||
const weth = data.find((a) => a.symbol.toUpperCase() === WETH);
|
||||
if (weth) return weth;
|
||||
}
|
||||
|
||||
return {
|
||||
symbol: WETH,
|
||||
decimals: 18,
|
||||
quantum: '500000000000000', // 1 WETH ~= 2000 qUSD
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,13 +3,27 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import { KeyValue } from './key-value';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useT, ns } from '../../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
@@ -17,9 +31,9 @@ import { emptyValue } from './deal-ticket-fee-details';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
|
||||
export interface DealTicketMarginDetailsProps {
|
||||
generalAccountBalance: string;
|
||||
marginAccountBalance: string;
|
||||
orderMarginAccountBalance: string;
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
orderMarginAccountBalance?: string;
|
||||
market: Market;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
assetSymbol: string;
|
||||
@@ -40,20 +54,118 @@ export const DealTicketMarginDetails = ({
|
||||
const t = useT();
|
||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId: market.id, partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
});
|
||||
const liquidationEstimate = positionEstimate?.liquidation;
|
||||
const marginEstimate = positionEstimate?.margin;
|
||||
const totalMarginAccountBalance =
|
||||
BigInt(marginAccountBalance || '0') +
|
||||
BigInt(orderMarginAccountBalance || '0');
|
||||
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
let marginRequiredBestCase: string | undefined = undefined;
|
||||
let marginRequiredWorstCase: string | undefined = undefined;
|
||||
const marginEstimateBestCase =
|
||||
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) +
|
||||
BigInt(marginEstimate?.bestCase.orderMarginLevel ?? 0);
|
||||
const marginEstimateWorstCase =
|
||||
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) +
|
||||
BigInt(marginEstimate?.worstCase.orderMarginLevel ?? 0);
|
||||
if (marginEstimate) {
|
||||
if (currentMargins) {
|
||||
const currentMargin =
|
||||
BigInt(currentMargins.initialLevel) +
|
||||
BigInt(currentMargins.orderMarginLevel);
|
||||
|
||||
const collateralIncreaseEstimateBestCase = BigInt(
|
||||
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
|
||||
);
|
||||
const collateralIncreaseEstimateWorstCase = BigInt(
|
||||
positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0'
|
||||
);
|
||||
marginRequiredBestCase = (
|
||||
marginEstimateBestCase - currentMargin
|
||||
).toString();
|
||||
if (marginRequiredBestCase.startsWith('-')) {
|
||||
marginRequiredBestCase = '0';
|
||||
}
|
||||
|
||||
marginRequiredWorstCase = (
|
||||
marginEstimateWorstCase - currentMargin
|
||||
).toString();
|
||||
|
||||
if (marginRequiredWorstCase.startsWith('-')) {
|
||||
marginRequiredWorstCase = '0';
|
||||
}
|
||||
} else {
|
||||
marginRequiredBestCase = marginEstimateBestCase.toString();
|
||||
marginRequiredWorstCase = marginEstimateWorstCase.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const totalMarginAvailable = (
|
||||
currentMargins
|
||||
? totalBalance - BigInt(currentMargins.maintenanceLevel)
|
||||
: totalBalance
|
||||
).toString();
|
||||
|
||||
let deductionFromCollateral = null;
|
||||
let projectedMargin = null;
|
||||
if (totalMarginAccountBalance) {
|
||||
const deductionFromCollateralBestCase =
|
||||
marginEstimateBestCase - totalMarginAccountBalance;
|
||||
|
||||
const deductionFromCollateralWorstCase =
|
||||
marginEstimateWorstCase - totalMarginAccountBalance;
|
||||
|
||||
deductionFromCollateral = (
|
||||
<KeyValue
|
||||
indent
|
||||
label={t('Deduction from collateral')}
|
||||
value={formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
projectedMargin = (
|
||||
<KeyValue
|
||||
label={t('Projected margin')}
|
||||
value={formatRange(
|
||||
marginEstimateBestCase.toString(),
|
||||
marginEstimateWorstCase.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
marginEstimateWorstCase.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateRange = emptyValue;
|
||||
@@ -110,50 +222,128 @@ export const DealTicketMarginDetails = ({
|
||||
const quoteName = getQuoteName(market);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-2 mt-2">
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full pt-2',
|
||||
'flex items-center gap-2 text-xs',
|
||||
'group'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-testid={`deal-ticket-fee-margin-required`}
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip
|
||||
description={t(
|
||||
'MARGIN_DIFF_TOOLTIP_TEXT',
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
>
|
||||
<span className="text-muted">{t('Margin required')}</span>
|
||||
</Tooltip>
|
||||
|
||||
<AccordionChevron size={10} />
|
||||
</div>
|
||||
<Tooltip
|
||||
description={
|
||||
formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
) ?? '-'
|
||||
}
|
||||
>
|
||||
<div className="font-mono text-right">
|
||||
{formatValue(
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}{' '}
|
||||
{assetSymbol || ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionPrimitive.Trigger>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<KeyValue
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'TOTAL_MARGIN_AVAILABLE',
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
{
|
||||
generalAccountBalance: formatValue(
|
||||
generalAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginAccountBalance: formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
orderMarginAccountBalance: formatValue(
|
||||
orderMarginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginMaintenance: formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
assetSymbol,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<KeyValue
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
{projectedMargin}
|
||||
<KeyValue
|
||||
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')}
|
||||
label={t('Liquidation')}
|
||||
value={liquidationPriceEstimateRange}
|
||||
formattedValue={liquidationPriceEstimate}
|
||||
symbol={quoteName}
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
} from '../../hooks';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
@@ -177,6 +177,12 @@ export const DealTicket = ({
|
||||
loading: loadingGeneralAccountBalance,
|
||||
} = useAccountBalance(asset.id);
|
||||
|
||||
const balance = (
|
||||
BigInt(marginAccountBalance) +
|
||||
BigInt(generalAccountBalance) +
|
||||
BigInt(orderMarginAccountBalance)
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
const timeInForce = watch('timeInForce');
|
||||
|
||||
@@ -723,11 +729,17 @@ export const DealTicket = ({
|
||||
error={summaryError}
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={generalAccountBalance}
|
||||
margin={
|
||||
positionEstimate?.estimatePosition?.collateralIncreaseEstimate
|
||||
.bestCase || '0'
|
||||
}
|
||||
balance={balance}
|
||||
margin={(
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
||||
'0'
|
||||
) +
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase
|
||||
.orderMarginLevel || '0'
|
||||
)
|
||||
).toString()}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onDeposit={onDeposit}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface KeyValuePros {
|
||||
@@ -18,6 +19,7 @@ export const KeyValue = ({
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
indent,
|
||||
onClick,
|
||||
formattedValue,
|
||||
}: KeyValuePros) => {
|
||||
@@ -41,7 +43,10 @@ export const KeyValue = ({
|
||||
: id
|
||||
}`}
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className="text-xs flex justify-between items-center gap-4 flex-wrap text-right"
|
||||
className={classnames(
|
||||
'text-xs flex justify-between items-center gap-4 flex-wrap text-right',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div className="text-muted text-left">{label}</div>
|
||||
|
||||
@@ -29,7 +29,6 @@ import { usePositionEstimate } from '../../hooks/use-position-estimate';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
||||
import { NoWalletWarning } from './deal-ticket';
|
||||
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
|
||||
|
||||
const defaultLeverage = 10;
|
||||
|
||||
@@ -94,78 +93,66 @@ export const MarginChange = ({
|
||||
},
|
||||
skip
|
||||
);
|
||||
if (!asset || !estimateMargin?.estimatePosition) {
|
||||
if (
|
||||
!asset ||
|
||||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
|
||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const collateralIncreaseEstimate = BigInt(
|
||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
||||
);
|
||||
|
||||
if (!collateralIncreaseEstimate) {
|
||||
return null;
|
||||
}
|
||||
let positionWarning = '';
|
||||
if (orders?.length && openVolume !== '0') {
|
||||
positionWarning = t(
|
||||
'youHaveOpenPositionAndOrders',
|
||||
'You have an existing position and open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
} else if (!orders?.length) {
|
||||
positionWarning = t('You have an existing position on this market.');
|
||||
} else {
|
||||
positionWarning = t(
|
||||
'youHaveOpenOrders',
|
||||
'You have open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
let marginChangeWarning = '';
|
||||
if (collateralIncreaseEstimate) {
|
||||
if (orders?.length && openVolume !== '0') {
|
||||
positionWarning = t(
|
||||
'youHaveOpenPositionAndOrders',
|
||||
'You have an existing position and open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
} else if (!orders?.length) {
|
||||
positionWarning = t('You have an existing position on this market.');
|
||||
} else {
|
||||
positionWarning = t(
|
||||
'youHaveOpenOrders',
|
||||
'You have open orders on this market.',
|
||||
{
|
||||
count: orders.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const amount = addDecimalsFormatNumber(
|
||||
collateralIncreaseEstimate.toString(),
|
||||
asset?.decimals
|
||||
const 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
|
||||
);
|
||||
const { symbol } = asset;
|
||||
const interpolation = { amount, symbol };
|
||||
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
|
||||
marginChangeWarning = t(
|
||||
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||
interpolation
|
||||
);
|
||||
} else {
|
||||
marginChangeWarning = t(
|
||||
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||
interpolation
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="mb-2">
|
||||
{positionWarning && marginChangeWarning && (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<>
|
||||
<p>{positionWarning}</p>
|
||||
<p>{marginChangeWarning}</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<DealTicketMarginDetails
|
||||
marginAccountBalance={marginAccountBalance}
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
orderMarginAccountBalance={orderMarginAccountBalance}
|
||||
assetSymbol={asset.symbol}
|
||||
market={market}
|
||||
positionEstimate={estimateMargin.estimatePosition}
|
||||
side={
|
||||
openVolume.startsWith('-')
|
||||
? Schema.Side.SIDE_SELL
|
||||
: Schema.Side.SIDE_BUY
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<>
|
||||
<p>{positionWarning}</p>
|
||||
<p>{marginChangeWarning}</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '../hooks/use-form-values';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { isPersistentOrder } from './time-in-force-persistence';
|
||||
import { isPersistentOrder } from './time-in-force-persistance';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ import { OrderTimeInForce } from '@vegaprotocol/types';
|
||||
import {
|
||||
isNonPersistentOrder,
|
||||
isPersistentOrder,
|
||||
} from './time-in-force-persistence';
|
||||
} from './time-in-force-persistance';
|
||||
|
||||
it('isNonPersistentOrder', () => {
|
||||
it('isNonPeristentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
||||
@@ -13,7 +13,7 @@ it('isNonPersistentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
||||
});
|
||||
|
||||
it('isPersistentOrder', () => {
|
||||
it('isPeristentOrder', () => {
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@@ -161,6 +162,19 @@ describe('useNodeBasicStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RowData', () => {
|
||||
const props = {
|
||||
id: '0',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { useT } from '../../use-t';
|
||||
import { useResponseTime } from '../../utils/time';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
@@ -108,6 +108,20 @@ export const useNodeBasicStatus = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } =
|
||||
(requests.length && requests[requests.length - 1]) || {};
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const RowData = ({
|
||||
id,
|
||||
url,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
getUserEnabledFeatureFlags,
|
||||
setUserEnabledFeatureFlag,
|
||||
} from './use-environment';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
const noop = () => {
|
||||
/* no op*/
|
||||
@@ -18,10 +17,6 @@ const noop = () => {
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client');
|
||||
jest.mock('zustand');
|
||||
jest.mock('../utils/time');
|
||||
|
||||
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
|
||||
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
|
||||
|
||||
const mockCreateClient = createClient as jest.Mock;
|
||||
const createDefaultMockClient = () => {
|
||||
@@ -160,14 +155,6 @@ describe('useEnvironment', () => {
|
||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||
const fastWait = 1000;
|
||||
const nodes = [slowNode, fastNode];
|
||||
|
||||
mockCanMeasureResponseTime.mockImplementation(() => true);
|
||||
mockMeasureResponseTime.mockImplementation((url: string) => {
|
||||
if (url === slowNode) return slowWait;
|
||||
if (url === fastNode) return fastWait;
|
||||
return Infinity;
|
||||
});
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||
|
||||
@@ -181,7 +168,7 @@ describe('useEnvironment', () => {
|
||||
statistics: {
|
||||
chainId: 'chain-id',
|
||||
blockHeight: '100',
|
||||
vegaTime: new Date(1).toISOString(),
|
||||
vegaTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -209,8 +196,7 @@ describe('useEnvironment', () => {
|
||||
expect(result.current.nodes).toEqual(nodes);
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
// jest.runAllTimers();
|
||||
jest.runAllTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toEqual('success');
|
||||
|
||||
@@ -19,9 +19,6 @@ import { compileErrors } from '../utils/compile-errors';
|
||||
import { envSchema } from '../utils/validate-environment';
|
||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||
import uniq from 'lodash/uniq';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import first from 'lodash/first';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
type Client = ReturnType<typeof createClient>;
|
||||
type ClientCollection = {
|
||||
@@ -41,17 +38,8 @@ export type EnvStore = Env & Actions;
|
||||
|
||||
const VERSION = 1;
|
||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||
|
||||
const QUERY_TIMEOUT = 3000;
|
||||
const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
|
||||
const raceAgainst = (timeout: number): Promise<false> =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch and validate a vega node configuration
|
||||
*/
|
||||
@@ -76,88 +64,53 @@ const fetchConfig = async (url?: string) => {
|
||||
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||
try {
|
||||
const nodes = await Promise.all(tests);
|
||||
const responsiveNodes = nodes
|
||||
.filter(([, q, s]) => q && s)
|
||||
.map(([url, q]) => {
|
||||
return {
|
||||
url,
|
||||
...q,
|
||||
};
|
||||
});
|
||||
|
||||
// more recent and faster at the top
|
||||
const ordered = orderBy(
|
||||
responsiveNodes,
|
||||
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
|
||||
['desc', 'desc', 'asc']
|
||||
);
|
||||
|
||||
const best = first(ordered);
|
||||
return best ? best.url : null;
|
||||
} catch (err) {
|
||||
const url = await Promise.any(tests);
|
||||
return url;
|
||||
} catch {
|
||||
// All tests rejected, no suitable node found
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type Maybe<T> = T | false;
|
||||
type QueryTestResult = {
|
||||
blockHeight: number;
|
||||
vegaTime: Date;
|
||||
responseTime: number;
|
||||
};
|
||||
type SubscriptionTestResult = true;
|
||||
type NodeTestResult = [
|
||||
/** url */
|
||||
string,
|
||||
Maybe<QueryTestResult>,
|
||||
Maybe<SubscriptionTestResult>
|
||||
];
|
||||
/**
|
||||
* Test a node for suitability for connection
|
||||
*/
|
||||
const testNode = async (
|
||||
url: string,
|
||||
client: Client
|
||||
): Promise<NodeTestResult> => {
|
||||
): Promise<string | null> => {
|
||||
const results = await Promise.all([
|
||||
testQuery(client, url),
|
||||
// these promises will only resolve with true/false
|
||||
testQuery(client),
|
||||
testSubscription(client),
|
||||
]);
|
||||
return [url, ...results];
|
||||
if (results[0] && results[1]) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const message = `Tests failed for node: ${url}`;
|
||||
console.warn(message);
|
||||
|
||||
// throwing here will mean this tests is ignored and a different
|
||||
// node that hopefully does resolve will fulfill the Promise.any
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a test query on a client
|
||||
*/
|
||||
const testQuery = (
|
||||
client: Client,
|
||||
url: string
|
||||
): Promise<Maybe<QueryTestResult>> => {
|
||||
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||
client
|
||||
.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result && !result.error) {
|
||||
const res = {
|
||||
blockHeight: Number(result.data.statistics.blockHeight),
|
||||
vegaTime: new Date(result.data.statistics.vegaTime),
|
||||
// only after a request has been sent we can retrieve the response time
|
||||
responseTime: canMeasureResponseTime(url)
|
||||
? measureResponseTime(url) || Infinity
|
||||
: Infinity,
|
||||
} as QueryTestResult;
|
||||
resolve(res);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => resolve(false))
|
||||
);
|
||||
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
|
||||
const testQuery = async (client: Client) => {
|
||||
try {
|
||||
const result = await client.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
});
|
||||
if (!result || result.error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -165,9 +118,7 @@ const testQuery = (
|
||||
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||
* is deemed a failure
|
||||
*/
|
||||
const testSubscription = (
|
||||
client: Client
|
||||
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||
const testSubscription = (client: Client) => {
|
||||
return new Promise((resolve) => {
|
||||
const sub = client
|
||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -21,6 +21,7 @@
|
||||
"Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.": "Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.",
|
||||
"Enter manually": "Enter manually",
|
||||
"From account": "From account",
|
||||
"Include transfer fee": "Include transfer fee",
|
||||
"initial level": "initial level",
|
||||
"maintenance level": "maintenance level",
|
||||
"Margin health": "Margin health",
|
||||
@@ -32,6 +33,7 @@
|
||||
"release level": "release level",
|
||||
"search level": "search level",
|
||||
"Select from wallet": "Select from wallet",
|
||||
"The fee will be taken from the amount you are transferring.": "The fee will be taken from the amount you are transferring.",
|
||||
"The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.",
|
||||
"The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.",
|
||||
"The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)",
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"Markets": "Markets",
|
||||
"Members": "Members",
|
||||
"Members ({{count}})": "Members ({{count}})",
|
||||
"Member ID": "Member ID",
|
||||
"Menu": "Menu",
|
||||
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
||||
"Min. epochs": "Min. epochs",
|
||||
@@ -365,7 +364,7 @@
|
||||
"Type": "Type",
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
"Update team": "Update team",
|
||||
"Update team":, "Update team",
|
||||
"URL": "URL",
|
||||
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
|
||||
"Vega chart": "Vega chart",
|
||||
@@ -417,7 +416,6 @@
|
||||
"myVolume_other": "My volume (last {{count}} epochs)",
|
||||
"numberEpochs": "{{count}} epochs",
|
||||
"numberEpochs_one": "{{count}} epoch",
|
||||
"Rewards earned": "Rewards earned",
|
||||
"Rewards paid out": "Rewards paid out",
|
||||
"{{reward}}x": "{{reward}}x",
|
||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||
@@ -432,19 +430,5 @@
|
||||
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
||||
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
||||
"{{distance}} ago": "{{distance}} ago",
|
||||
"{{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"
|
||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision"
|
||||
}
|
||||
|
||||
@@ -47,11 +47,5 @@
|
||||
"Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.": "Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.",
|
||||
"Withdrawals ready": "Withdrawals ready",
|
||||
"You have no assets to withdraw": "You have no assets to withdraw",
|
||||
"Your funds have been unlocked for withdrawal - <0>View in block explorer<0>": "Your funds have been unlocked for withdrawal - <0>View in block explorer<0>",
|
||||
"Gas fee": "Gas fee",
|
||||
"Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)": "Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)",
|
||||
"It seems that the current gas prices are exceeding the amount you're trying to withdraw": "It seems that the current gas prices are exceeding the amount you're trying to withdraw",
|
||||
"The current gas price range": "The current gas price range",
|
||||
"min": "min",
|
||||
"max": "max"
|
||||
"Your funds have been unlocked for withdrawal - <0>View in block explorer<0>": "Your funds have been unlocked for withdrawal - <0>View in block explorer<0>"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { calcCandleVolume } from '../../market-utils';
|
||||
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCandles } from '../../hooks';
|
||||
import { useT } from '../../use-t';
|
||||
@@ -13,17 +9,13 @@ interface Props {
|
||||
positionDecimalPlaces?: number;
|
||||
formatDecimals?: number;
|
||||
initialValue?: string;
|
||||
marketDecimals?: number;
|
||||
quoteUnit?: string;
|
||||
}
|
||||
|
||||
export const Last24hVolume = ({
|
||||
marketId,
|
||||
marketDecimals,
|
||||
positionDecimalPlaces,
|
||||
formatDecimals,
|
||||
initialValue,
|
||||
quoteUnit,
|
||||
}: Props) => {
|
||||
const t = useT();
|
||||
const { oneDayCandles, fiveDaysCandles } = useCandles({
|
||||
@@ -36,11 +28,6 @@ export const Last24hVolume = ({
|
||||
(!oneDayCandles || oneDayCandles?.length === 0)
|
||||
) {
|
||||
const candleVolume = calcCandleVolume(fiveDaysCandles);
|
||||
const candleVolumePrice = calcCandleVolumePrice(
|
||||
fiveDaysCandles,
|
||||
marketDecimals,
|
||||
positionDecimalPlaces
|
||||
);
|
||||
const candleVolumeValue =
|
||||
candleVolume && isNumeric(positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
@@ -55,8 +42,8 @@ export const Last24hVolume = ({
|
||||
<div>
|
||||
<span className="flex flex-col">
|
||||
{t(
|
||||
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} ({{candleVolumePrice}} {{quoteUnit}})',
|
||||
{ candleVolumeValue, candleVolumePrice, quoteUnit }
|
||||
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}',
|
||||
{ candleVolumeValue }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -70,18 +57,10 @@ export const Last24hVolume = ({
|
||||
? calcCandleVolume(oneDayCandles)
|
||||
: initialValue;
|
||||
|
||||
const candleVolumePrice = oneDayCandles
|
||||
? calcCandleVolumePrice(
|
||||
oneDayCandles,
|
||||
marketDecimals,
|
||||
positionDecimalPlaces
|
||||
)
|
||||
: initialValue;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
description={t(
|
||||
'The total number of contracts traded in the last 24 hours. (Total value of contracts traded in the last 24 hours)'
|
||||
'The total number of contracts traded in the last 24 hours.'
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
@@ -91,12 +70,7 @@ export const Last24hVolume = ({
|
||||
positionDecimalPlaces,
|
||||
formatDecimals
|
||||
)
|
||||
: '-'}{' '}
|
||||
(
|
||||
{candleVolumePrice && isNumeric(positionDecimalPlaces)
|
||||
? formatNumber(candleVolumePrice, formatDecimals)
|
||||
: '-'}{' '}
|
||||
{quoteUnit})
|
||||
: '-'}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -155,7 +155,6 @@ export const MarketVolumeInfoPanel = ({ market }: MarketInfoProps) => {
|
||||
<Last24hVolume
|
||||
marketId={market.id}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
marketDecimals={market.decimalPlaces}
|
||||
/>
|
||||
),
|
||||
openInterest: dash(data?.openInterest),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
|
||||
import {
|
||||
calcCandleVolumePrice,
|
||||
calcTradedFactor,
|
||||
filterAndSortMarkets,
|
||||
sumFeesFactors,
|
||||
@@ -146,31 +145,3 @@ describe('sumFeesFactors', () => {
|
||||
).toEqual(0.6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcCandleVolumePrice', () => {
|
||||
it('calculates the volume price', () => {
|
||||
const candles = [
|
||||
{
|
||||
volume: '1000',
|
||||
high: '100',
|
||||
low: '10',
|
||||
open: '15',
|
||||
close: '90',
|
||||
periodStart: '2022-05-18T13:08:27.693537312Z',
|
||||
},
|
||||
{
|
||||
volume: '1000',
|
||||
high: '100',
|
||||
low: '10',
|
||||
open: '15',
|
||||
close: '90',
|
||||
periodStart: '2022-05-18T14:08:27.693537312Z',
|
||||
},
|
||||
];
|
||||
const marketDecimals = 3;
|
||||
const positionDecimalPlaces = 2;
|
||||
expect(
|
||||
calcCandleVolumePrice(candles, marketDecimals, positionDecimalPlaces)
|
||||
).toEqual('2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
addDecimal,
|
||||
formatNumberPercentage,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { formatNumberPercentage, toBigNum } from '@vegaprotocol/utils';
|
||||
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
@@ -151,51 +147,10 @@ export const calcCandleHigh = (candles: Candle[]): string | undefined => {
|
||||
.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* The total number of contracts traded in the last 24 hours.
|
||||
*
|
||||
* @param candles
|
||||
* @returns the volume of a given set of candles
|
||||
*/
|
||||
export const calcCandleVolume = (candles: Candle[]): string | undefined =>
|
||||
candles &&
|
||||
candles.reduce((acc, c) => new BigNumber(acc).plus(c.volume).toString(), '0');
|
||||
|
||||
/**
|
||||
* The total number of contracts traded in the last 24 hours. (Total value of contracts traded in the last 24 hours)
|
||||
* The volume is calculated as the sum of the product of the volume and the high price of each candle.
|
||||
* The result is formatted using positionDecimalPlaces to account for the position size.
|
||||
* The result is formatted using marketDecimals to account for the market precision.
|
||||
*
|
||||
* @param candles
|
||||
* @param marketDecimals
|
||||
* @param positionDecimalPlaces
|
||||
* @returns the volume (in quote price) of a given set of candles
|
||||
*/
|
||||
export const calcCandleVolumePrice = (
|
||||
candles: Candle[],
|
||||
marketDecimals: number = 1,
|
||||
positionDecimalPlaces: number = 1
|
||||
): string | undefined =>
|
||||
candles &&
|
||||
candles.reduce(
|
||||
(acc, c) =>
|
||||
new BigNumber(acc)
|
||||
.plus(
|
||||
BigNumber(addDecimal(c.volume, positionDecimalPlaces)).times(
|
||||
addDecimal(c.high, marketDecimals)
|
||||
)
|
||||
)
|
||||
.toString(),
|
||||
'0'
|
||||
);
|
||||
|
||||
/**
|
||||
* Calculates the traded factor of a given market.
|
||||
*
|
||||
* @param m
|
||||
* @returns
|
||||
*/
|
||||
export const calcTradedFactor = (m: MarketMaybeWithDataAndCandles) => {
|
||||
const volume = Number(calcCandleVolume(m.candles || []) || 0);
|
||||
const price = m.data?.markPrice ? Number(m.data.markPrice) : 0;
|
||||
|
||||
@@ -67,6 +67,26 @@ query EstimatePosition(
|
||||
# we can set this variable to true so that we can format with market.decimalPlaces
|
||||
scaleLiquidationPriceToMarketDecimals: true
|
||||
) {
|
||||
margin {
|
||||
worstCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
bestCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
}
|
||||
collateralIncreaseEstimate {
|
||||
worstCase
|
||||
bestCase
|
||||
|
||||
+21
-1
@@ -33,7 +33,7 @@ export type EstimatePositionQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
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 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 const PositionFieldsFragmentDoc = gql`
|
||||
fragment PositionFields on Position {
|
||||
@@ -144,6 +144,26 @@ export const EstimatePositionDocument = gql`
|
||||
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
||||
scaleLiquidationPriceToMarketDecimals: true
|
||||
) {
|
||||
margin {
|
||||
worstCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
bestCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
}
|
||||
collateralIncreaseEstimate {
|
||||
worstCase
|
||||
bestCase
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import merge from 'lodash/merge';
|
||||
import type { EstimatePositionQuery } from './__generated__/Positions';
|
||||
import { MarginMode } from '@vegaprotocol/types';
|
||||
|
||||
export const estimatePositionQuery = (
|
||||
override?: PartialDeep<EstimatePositionQuery>
|
||||
@@ -8,6 +9,26 @@ export const estimatePositionQuery = (
|
||||
const defaultResult: EstimatePositionQuery = {
|
||||
estimatePosition: {
|
||||
__typename: 'PositionEstimate',
|
||||
margin: {
|
||||
bestCase: {
|
||||
collateralReleaseLevel: '1000000',
|
||||
initialLevel: '500000',
|
||||
maintenanceLevel: '200000',
|
||||
searchLevel: '300000',
|
||||
marginFactor: '1',
|
||||
orderMarginLevel: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
worstCase: {
|
||||
collateralReleaseLevel: '1100000',
|
||||
initialLevel: '600000',
|
||||
maintenanceLevel: '300000',
|
||||
searchLevel: '400000',
|
||||
marginFactor: '1',
|
||||
orderMarginLevel: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
},
|
||||
collateralIncreaseEstimate: {
|
||||
bestCase: '0',
|
||||
worstCase: '0',
|
||||
|
||||
@@ -30,6 +30,26 @@ describe('LiquidationPrice', () => {
|
||||
result: {
|
||||
data: {
|
||||
estimatePosition: {
|
||||
margin: {
|
||||
worstCase: {
|
||||
maintenanceLevel: '100',
|
||||
searchLevel: '100',
|
||||
initialLevel: '100',
|
||||
collateralReleaseLevel: '100',
|
||||
orderMarginLevel: '0',
|
||||
marginFactor: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
bestCase: {
|
||||
maintenanceLevel: '100',
|
||||
searchLevel: '100',
|
||||
initialLevel: '100',
|
||||
collateralReleaseLevel: '100',
|
||||
orderMarginLevel: '0',
|
||||
marginFactor: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
},
|
||||
collateralIncreaseEstimate: {
|
||||
bestCase: '0',
|
||||
worstCase: '0',
|
||||
|
||||
@@ -41,17 +41,16 @@ export const TradingView = ({
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||
const widgetRef = useRef<IChartingLibraryWidget>();
|
||||
|
||||
const datafeed = useDatafeed();
|
||||
|
||||
const prevMarketId = usePrevious(marketId);
|
||||
const prevTheme = usePrevious(theme);
|
||||
|
||||
const datafeed = useDatafeed(marketId);
|
||||
|
||||
useEffect(() => {
|
||||
// Widget already created
|
||||
if (widgetRef.current !== undefined) {
|
||||
// Update the symbol if changed
|
||||
if (marketId !== prevMarketId) {
|
||||
datafeed.setSymbol(marketId);
|
||||
widgetRef.current.setSymbol(
|
||||
marketId,
|
||||
(interval ? interval : '15') as TVResolutionString,
|
||||
|
||||
@@ -44,22 +44,14 @@ const configurationData: DatafeedConfiguration = {
|
||||
supported_resolutions: supportedResolutions as ResolutionString[],
|
||||
} as const;
|
||||
|
||||
// HACK: local handle for market id
|
||||
let requestedSymbol: string | undefined = undefined;
|
||||
|
||||
export const useDatafeed = (marketId: string) => {
|
||||
export const useDatafeed = () => {
|
||||
const hasHistory = useRef(false);
|
||||
const subRef = useRef<Subscription>();
|
||||
const client = useApolloClient();
|
||||
|
||||
const datafeed = useMemo(() => {
|
||||
const feed: IBasicDataFeed & { setSymbol: (symbol: string) => void } = {
|
||||
setSymbol: (symbol: string) => {
|
||||
// re-setting the symbol so it could be consumed by `resolveSymbol`
|
||||
requestedSymbol = symbol;
|
||||
},
|
||||
const feed: IBasicDataFeed = {
|
||||
onReady: (callback) => {
|
||||
requestedSymbol = marketId;
|
||||
setTimeout(() => callback(configurationData));
|
||||
},
|
||||
|
||||
@@ -76,7 +68,7 @@ export const useDatafeed = (marketId: string) => {
|
||||
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
||||
query: SymbolDocument,
|
||||
variables: {
|
||||
marketId: requestedSymbol || marketId,
|
||||
marketId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -250,7 +242,7 @@ export const useDatafeed = (marketId: string) => {
|
||||
};
|
||||
|
||||
return feed;
|
||||
}, [client, marketId]);
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
Generated
+1
-1
@@ -4682,7 +4682,7 @@ export type QuantumRewardsPerEpoch = {
|
||||
/** Epoch for which this information is valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
|
||||
totalQuantumRewards: Scalars['String'];
|
||||
total_quantum_rewards: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { EtherUnit, formatEther, unitiseEther } from './ether';
|
||||
|
||||
describe('unitiseEther', () => {
|
||||
it.each([
|
||||
[1, '1', EtherUnit.wei],
|
||||
[999, '999', EtherUnit.wei],
|
||||
[1000, '1', EtherUnit.kwei],
|
||||
[9999, '9.999', EtherUnit.kwei],
|
||||
[10000, '10', EtherUnit.kwei],
|
||||
[999999, '999.999', EtherUnit.kwei],
|
||||
[1000000, '1', EtherUnit.mwei],
|
||||
[999999999, '999.999999', EtherUnit.mwei],
|
||||
[1000000000, '1', EtherUnit.gwei],
|
||||
['999999999999999999', '999999999.999999999', EtherUnit.gwei], // max gwei
|
||||
[1e18, '1', EtherUnit.ether], // 1 ETH
|
||||
[1234e18, '1234', EtherUnit.ether], // 1234 ETH
|
||||
])('unitises %s to [%s, %s]', (value, expectedOutput, expectedUnit) => {
|
||||
const [output, unit] = unitiseEther(value);
|
||||
expect(output.toFixed()).toEqual(expectedOutput);
|
||||
expect(unit).toEqual(expectedUnit);
|
||||
});
|
||||
|
||||
it('unitises to requested unit', () => {
|
||||
const [output, unit] = unitiseEther(1, EtherUnit.kwei);
|
||||
expect(output).toEqual(BigNumber(0.001));
|
||||
expect(unit).toEqual(EtherUnit.kwei);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatEther', () => {
|
||||
it.each([
|
||||
[1, EtherUnit.wei, '1 wei'],
|
||||
[12, EtherUnit.kwei, '12 kwei'],
|
||||
[123, EtherUnit.gwei, '123 gwei'],
|
||||
[3, EtherUnit.ether, '3 ETH'],
|
||||
[234.67776331, EtherUnit.gwei, '235 gwei'],
|
||||
[12.12, EtherUnit.gwei, '12 gwei'],
|
||||
])('formats [%s, %s] to "%s"', (value, unit, expectedOutput) => {
|
||||
expect(formatEther([BigNumber(value), unit])).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { formatNumber, toBigNum } from './number';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
|
||||
export enum EtherUnit {
|
||||
/** 1 wei = 10^-18 ETH */
|
||||
wei = '0',
|
||||
/** 1 kwei = 1000 wei */
|
||||
kwei = '3',
|
||||
/** 1 mwei = 1000 kwei */
|
||||
mwei = '6',
|
||||
/** 1 gwei = 1000 kwei */
|
||||
gwei = '9',
|
||||
|
||||
// other denominations:
|
||||
// microether = '12', // aka szabo, µETH
|
||||
// milliether = '15', // aka finney, mETH
|
||||
|
||||
/** 1 ETH = 1B gwei = 10^18 wei */
|
||||
ether = '18',
|
||||
}
|
||||
|
||||
export const etherUnitMapping: Record<EtherUnit, string> = {
|
||||
[EtherUnit.wei]: 'wei',
|
||||
[EtherUnit.kwei]: 'kwei',
|
||||
[EtherUnit.mwei]: 'mwei',
|
||||
[EtherUnit.gwei]: 'gwei',
|
||||
// [EtherUnit.microether]: 'µETH', // szabo
|
||||
// [EtherUnit.milliether]: 'mETH', // finney
|
||||
[EtherUnit.ether]: 'ETH',
|
||||
};
|
||||
|
||||
type InputValue = string | number | BigNumber;
|
||||
type UnitisedTuple = [value: BigNumber, unit: EtherUnit];
|
||||
|
||||
/**
|
||||
* Converts given raw value to the unitised tuple of amount and unit
|
||||
*/
|
||||
export const unitiseEther = (
|
||||
input: InputValue,
|
||||
forceUnit?: EtherUnit
|
||||
): UnitisedTuple => {
|
||||
const units = Object.values(EtherUnit).reverse();
|
||||
|
||||
let value = toBigNum(input, Number(forceUnit || EtherUnit.ether));
|
||||
let unit = forceUnit || EtherUnit.ether;
|
||||
|
||||
if (!forceUnit) {
|
||||
for (const u of units) {
|
||||
const v = toBigNum(input, Number(u));
|
||||
value = v;
|
||||
unit = u;
|
||||
if (v.isGreaterThanOrEqualTo(1)) break;
|
||||
}
|
||||
}
|
||||
|
||||
return [value, unit];
|
||||
};
|
||||
|
||||
/**
|
||||
* `formatNumber` wrapper for unitised ether values (attaches unit name)
|
||||
*/
|
||||
export const formatEther = (
|
||||
input: UnitisedTuple,
|
||||
decimals = 0,
|
||||
noUnit = false
|
||||
) => {
|
||||
const [value, unit] = input;
|
||||
const num = formatNumber(value, decimals);
|
||||
const unitName = noUnit ? '' : etherUnitMapping[unit];
|
||||
|
||||
return `${num} ${unitName}`.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function that formats given raw amount as ETH.
|
||||
* Example:
|
||||
* Given value of `1` this will return `0.000000000000000001 ETH`
|
||||
*/
|
||||
export const asETH = (input: InputValue, noUnit = false) =>
|
||||
formatEther(
|
||||
unitiseEther(input, EtherUnit.ether),
|
||||
Number(EtherUnit.ether),
|
||||
noUnit
|
||||
);
|
||||
@@ -4,4 +4,3 @@ export * from './range';
|
||||
export * from './size';
|
||||
export * from './strings';
|
||||
export * from './trigger';
|
||||
export * from './ether';
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
toDecimal,
|
||||
toNumberParts,
|
||||
formatNumberRounded,
|
||||
toQUSD,
|
||||
} from './number';
|
||||
|
||||
describe('number utils', () => {
|
||||
@@ -283,22 +282,3 @@ describe('formatNumberRounded', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toQUSD', () => {
|
||||
it.each([
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 10, 0.1],
|
||||
[1, 100, 0.01],
|
||||
// real life examples
|
||||
[1000000, 1000000, 1], // USDC -> 1 USDC ~= 1 qUSD
|
||||
[500000, 1000000, 0.5], // USDC => 0.6 USDC ~= 0.5 qUSD
|
||||
[1e18, 1e18, 1], // VEGA -> 1 VEGA ~= 1 qUSD
|
||||
[123.45e18, 1e18, 123.45], // VEGA -> 1 VEGA ~= 1 qUSD
|
||||
[1e18, 5e14, 2000], // WETH -> 1 WETH ~= 2000 qUSD
|
||||
[1e9, 5e14, 0.000002], // gwei -> 1 gwei ~= 0.000002 qUSD
|
||||
[50000e9, 5e14, 0.1], // gwei -> 50000 gwei ~= 0.1 qUSD
|
||||
])('converts (%d, %d) to %d qUSD', (amount, quantum, expected) => {
|
||||
expect(toQUSD(amount, quantum).toNumber()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export function toDecimal(numberOfDecimals: number) {
|
||||
}
|
||||
|
||||
export function toBigNum(
|
||||
rawValue: string | number | BigNumber,
|
||||
rawValue: string | number,
|
||||
decimals: number
|
||||
): BigNumber {
|
||||
const divides = new BigNumber(10).exponentiatedBy(decimals);
|
||||
@@ -233,24 +233,3 @@ export const formatNumberRounded = (
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts given amount in one asset (determined by raw amount
|
||||
* and quantum values) to qUSD.
|
||||
* @param amount The raw amount
|
||||
* @param quantum The quantum value of the asset.
|
||||
*/
|
||||
export const toQUSD = (
|
||||
amount: string | number | BigNumber,
|
||||
quantum: string | number
|
||||
) => {
|
||||
const value = new BigNumber(amount);
|
||||
let q = new BigNumber(quantum);
|
||||
|
||||
if (q.isNaN() || q.isLessThanOrEqualTo(0)) {
|
||||
q = new BigNumber(1);
|
||||
}
|
||||
|
||||
const qUSD = value.dividedBy(q);
|
||||
return qUSD;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ export * from './lib/use-ethereum-transaction';
|
||||
export * from './lib/use-ethereum-withdraw-approval-toasts';
|
||||
export * from './lib/use-ethereum-withdraw-approvals-manager';
|
||||
export * from './lib/use-ethereum-withdraw-approvals-store';
|
||||
export * from './lib/use-gas-price';
|
||||
export * from './lib/use-get-withdraw-delay';
|
||||
export * from './lib/use-get-withdraw-threshold';
|
||||
export * from './lib/use-token-contract';
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useEthereumConfig } from './use-ethereum-config';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
const DEFAULT_INTERVAL = 15000; // 15 seconds
|
||||
|
||||
/**
|
||||
* These are the hex values of the collateral bridge contract methods.
|
||||
*
|
||||
* Collateral bridge address: 0x23872549cE10B40e31D6577e0A920088B0E0666a
|
||||
* Etherscan: https://etherscan.io/address/0x23872549cE10B40e31D6577e0A920088B0E0666a#writeContract
|
||||
*/
|
||||
export enum ContractMethod {
|
||||
DEPOSIT_ASSET = '0xf7683932',
|
||||
EXEMPT_DEPOSITOR = '0xb76fbb75',
|
||||
GLOBAL_RESUME = '0xd72ed529',
|
||||
GLOBAL_STOP = '0x9dfd3c88',
|
||||
LIST_ASSET = '0x0ff3562c',
|
||||
REMOVE_ASSET = '0xc76de358',
|
||||
REVOKE_EXEMPT_DEPOSITOR = '0x6a1c6fa4',
|
||||
SET_ASSET_LIMITS = '0x41fb776d',
|
||||
SET_WITHDRAW_DELAY = '0x5a246728',
|
||||
WITHDRAW_ASSET = '0x3ad90635',
|
||||
}
|
||||
|
||||
export type GasData = {
|
||||
/** The base (minimum) price of 1 unit of gas */
|
||||
basePrice: BigNumber;
|
||||
/** The maximum price of 1 unit of gas */
|
||||
maxPrice: BigNumber;
|
||||
/** The amount of gas (units) needed to process a transaction */
|
||||
gas: BigNumber;
|
||||
};
|
||||
|
||||
type Provider = NonNullable<ReturnType<typeof useWeb3React>['provider']>;
|
||||
|
||||
const retrieveGasData = async (
|
||||
provider: Provider,
|
||||
account: string,
|
||||
contractAddress: string,
|
||||
contractMethod: ContractMethod
|
||||
) => {
|
||||
try {
|
||||
const data = await provider.getFeeData();
|
||||
const estGasAmount = await provider.estimateGas({
|
||||
to: account,
|
||||
from: contractAddress,
|
||||
data: contractMethod,
|
||||
});
|
||||
|
||||
if (data.lastBaseFeePerGas && data.maxFeePerGas) {
|
||||
return {
|
||||
// converts also form ethers BigNumber to "normal" BigNumber
|
||||
basePrice: BigNumber(data.lastBaseFeePerGas.toString()),
|
||||
maxPrice: BigNumber(data.maxFeePerGas.toString()),
|
||||
gas: BigNumber(estGasAmount.toString()),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// NOOP - could not get the estimated gas or the fee data from
|
||||
// the network. This could happen if there's an issue with transaction
|
||||
// request parameters (e.g. to/from mismatch)
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the "current" gas price from the ethereum network.
|
||||
*/
|
||||
export const useGasPrice = (
|
||||
method: ContractMethod,
|
||||
interval = DEFAULT_INTERVAL
|
||||
): GasData | undefined => {
|
||||
const [gas, setGas] = useState<GasData | undefined>(undefined);
|
||||
const { provider, account } = useWeb3React();
|
||||
const { config } = useEthereumConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider || !config || !account) return;
|
||||
|
||||
const retrieve = async () => {
|
||||
retrieveGasData(
|
||||
provider,
|
||||
account,
|
||||
config.collateral_bridge_contract.address,
|
||||
method
|
||||
).then((gasData) => {
|
||||
if (gasData) {
|
||||
setGas(gasData);
|
||||
}
|
||||
});
|
||||
};
|
||||
retrieve();
|
||||
|
||||
// Retrieves another estimation and prices in [interval] ms.
|
||||
let i: ReturnType<typeof setInterval>;
|
||||
if (interval > 0) {
|
||||
i = setInterval(() => {
|
||||
retrieve();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (i) clearInterval(i);
|
||||
};
|
||||
}, [account, config, interval, method, provider]);
|
||||
|
||||
return gas;
|
||||
};
|
||||
@@ -27,7 +27,6 @@ import { useForm, Controller, useWatch } from 'react-hook-form';
|
||||
import { WithdrawLimits } from './withdraw-limits';
|
||||
import {
|
||||
ETHEREUM_EAGER_CONNECT,
|
||||
type GasData,
|
||||
useWeb3ConnectStore,
|
||||
useWeb3Disconnect,
|
||||
} from '@vegaprotocol/web3';
|
||||
@@ -57,7 +56,6 @@ export interface WithdrawFormProps {
|
||||
delay: number | undefined;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
||||
gasPrice?: GasData;
|
||||
}
|
||||
|
||||
const WithdrawDelayNotification = ({
|
||||
@@ -119,7 +117,6 @@ export const WithdrawForm = ({
|
||||
delay,
|
||||
onSelectAsset,
|
||||
submitWithdraw,
|
||||
gasPrice,
|
||||
}: WithdrawFormProps) => {
|
||||
const t = useT();
|
||||
const ethereumAddress = useEthereumAddress();
|
||||
@@ -250,7 +247,6 @@ export const WithdrawForm = ({
|
||||
delay={delay}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
gas={gasPrice}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT, useWETH } from '@vegaprotocol/assets';
|
||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from '@vegaprotocol/assets';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -9,16 +9,6 @@ import {
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useT } from './use-t';
|
||||
import { type GasData } from '@vegaprotocol/web3';
|
||||
import {
|
||||
asETH,
|
||||
formatEther,
|
||||
formatNumber,
|
||||
removeDecimal,
|
||||
toQUSD,
|
||||
unitiseEther,
|
||||
} from '@vegaprotocol/utils';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface WithdrawLimitsProps {
|
||||
amount: string;
|
||||
@@ -26,7 +16,6 @@ interface WithdrawLimitsProps {
|
||||
balance: BigNumber;
|
||||
delay: number | undefined;
|
||||
asset: Asset;
|
||||
gas?: GasData;
|
||||
}
|
||||
|
||||
export const WithdrawLimits = ({
|
||||
@@ -35,7 +24,6 @@ export const WithdrawLimits = ({
|
||||
balance,
|
||||
delay,
|
||||
asset,
|
||||
gas,
|
||||
}: WithdrawLimitsProps) => {
|
||||
const t = useT();
|
||||
const delayTime =
|
||||
@@ -76,24 +64,6 @@ export const WithdrawLimits = ({
|
||||
label: t('Delay time'),
|
||||
value: threshold && delay ? delayTime : '-',
|
||||
},
|
||||
{
|
||||
key: 'GAS_FEE',
|
||||
tooltip: t(
|
||||
'Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)'
|
||||
),
|
||||
label: t('Gas fee'),
|
||||
value: gas ? (
|
||||
<GasPrice
|
||||
gasPrice={gas}
|
||||
amount={{
|
||||
value: removeDecimal(amount, asset.decimals),
|
||||
quantum: asset.quantum,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -121,117 +91,3 @@ export const WithdrawLimits = ({
|
||||
</KeyValueTable>
|
||||
);
|
||||
};
|
||||
|
||||
const GasPrice = ({
|
||||
gasPrice,
|
||||
amount,
|
||||
}: {
|
||||
gasPrice: WithdrawLimitsProps['gas'];
|
||||
amount: { value: string; quantum: string };
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { quantum: wethQuantum } = useWETH();
|
||||
const { value, quantum } = amount;
|
||||
if (gasPrice) {
|
||||
const {
|
||||
basePrice: basePricePerGas,
|
||||
maxPrice: maxPricePerGas,
|
||||
gas,
|
||||
} = gasPrice;
|
||||
const basePrice = basePricePerGas.multipliedBy(gas);
|
||||
const maxPrice = maxPricePerGas.multipliedBy(gas);
|
||||
|
||||
const basePriceQUSD = toQUSD(basePrice, wethQuantum);
|
||||
const maxPriceQUSD = toQUSD(maxPrice, wethQuantum);
|
||||
|
||||
const withdrawalAmountQUSD = toQUSD(value, quantum);
|
||||
|
||||
const isExpensive =
|
||||
!withdrawalAmountQUSD.isLessThanOrEqualTo(0) &&
|
||||
withdrawalAmountQUSD.isLessThanOrEqualTo(maxPriceQUSD);
|
||||
const expensiveClassNames = {
|
||||
'text-vega-red-500':
|
||||
isExpensive && withdrawalAmountQUSD.isLessThanOrEqualTo(basePriceQUSD),
|
||||
'text-vega-orange-500':
|
||||
isExpensive &&
|
||||
withdrawalAmountQUSD.isGreaterThan(basePriceQUSD) &&
|
||||
withdrawalAmountQUSD.isLessThanOrEqualTo(maxPriceQUSD),
|
||||
};
|
||||
|
||||
const uBasePricePerGas = unitiseEther(basePricePerGas);
|
||||
const uMaxPricePerGas = unitiseEther(
|
||||
maxPricePerGas,
|
||||
uBasePricePerGas[1] // forces the same unit as min price
|
||||
);
|
||||
|
||||
const uBasePrice = unitiseEther(basePrice);
|
||||
const uMaxPrice = unitiseEther(maxPrice, uBasePrice[1]);
|
||||
|
||||
let range = (
|
||||
<span>
|
||||
{formatEther(uBasePrice, 0, true)} - {formatEther(uMaxPrice)}
|
||||
</span>
|
||||
);
|
||||
// displays range as ETH when it's greater that 1000000 gwei
|
||||
if (uBasePrice[0].isGreaterThan(1e6)) {
|
||||
range = (
|
||||
<span className="flex flex-col font-mono md:text-[11px]">
|
||||
<span>
|
||||
{t('min')}: {asETH(basePrice)}
|
||||
</span>
|
||||
<span>
|
||||
{t('max')}: {asETH(maxPrice)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col items-end self-end')}>
|
||||
<Tooltip description={t('The current gas price range')}>
|
||||
<span>
|
||||
{/* base price per gas unit */}
|
||||
{formatEther(uBasePricePerGas, 0, true)} -{' '}
|
||||
{formatEther(uMaxPricePerGas)} / gas
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
description={
|
||||
<div className="flex flex-col gap-1">
|
||||
{isExpensive && (
|
||||
<span className={classNames(expensiveClassNames)}>
|
||||
{t(
|
||||
"It seems that the current gas prices are exceeding the amount you're trying to withdraw"
|
||||
)}{' '}
|
||||
<strong>
|
||||
(~{formatNumber(withdrawalAmountQUSD, 2)} qUSD)
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{formatNumber(gas)} gas × {asETH(basePricePerGas)} <br />{' '}
|
||||
= {asETH(basePrice)}
|
||||
</span>
|
||||
<span>
|
||||
{formatNumber(gas)} gas × {asETH(maxPricePerGas)} <br /> ={' '}
|
||||
{asETH(maxPrice)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className={classNames(expensiveClassNames, 'text-xs')}>
|
||||
{range}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<span className="text-muted text-xs">
|
||||
~{formatNumber(basePriceQUSD, 2)} - {formatNumber(maxPriceQUSD, 2)}{' '}
|
||||
qUSD
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '-';
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@ jest.mock('@vegaprotocol/web3', () => ({
|
||||
useGetWithdrawDelay: () => {
|
||||
return () => Promise.resolve(10000);
|
||||
},
|
||||
useGasPrice: () => undefined,
|
||||
}));
|
||||
|
||||
describe('WithdrawManager', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { WithdrawForm } from './withdraw-form';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import type { AccountFieldsFragment } from '@vegaprotocol/accounts';
|
||||
import { useWithdrawAsset } from './use-withdraw-asset';
|
||||
import { ContractMethod, useGasPrice } from '@vegaprotocol/web3';
|
||||
|
||||
export interface WithdrawManagerProps {
|
||||
assets: Asset[];
|
||||
@@ -21,8 +20,6 @@ export const WithdrawManager = ({
|
||||
}: WithdrawManagerProps) => {
|
||||
const { asset, balance, min, threshold, delay, handleSelectAsset } =
|
||||
useWithdrawAsset(assets, accounts, assetId);
|
||||
const gasPrice = useGasPrice(ContractMethod.WITHDRAW_ASSET);
|
||||
|
||||
return (
|
||||
<WithdrawForm
|
||||
selectedAsset={asset}
|
||||
@@ -33,7 +30,6 @@ export const WithdrawManager = ({
|
||||
submitWithdraw={submit}
|
||||
threshold={threshold}
|
||||
delay={delay}
|
||||
gasPrice={gasPrice}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
"echo $NX_VEGA_URL",
|
||||
"echo $NX_TENDERMINT_URL",
|
||||
"echo $NX_TENDERMINT_WEBSOCKET_URL",
|
||||
"echo $NX_ETHEREUM_PROVIDER_URL",
|
||||
"echo $NX_CHARTING_LIBRARY_PATH",
|
||||
"echo $NX_CHARTING_LIBRARY_HASH"
|
||||
"echo $NX_ETHEREUM_PROVIDER_URL"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,22 @@
|
||||
|
||||
## Transfer
|
||||
|
||||
- **Must** can select include transfer fee (<a name="1003-TRAN-015" href="#1003-TRAN-015">1003-TRAN-015</a>)
|
||||
|
||||
- **Must** display tooltip for "Include transfer fee" when hovered over.(<a name="1003-TRAN-016" href="#1003-TRAN-016">1003-TRAN-016</a>)
|
||||
|
||||
- **Must** display tooltip for "Transfer fee when hovered over.(<a name="1003-TRAN-017" href="#1003-TRAN-017">1003-TRAN-017</a>)
|
||||
|
||||
- **Must** display tooltip for "Amount to be transferred" when hovered over.(<a name="1003-TRAN-018" href="#1003-TRAN-018">1003-TRAN-018</a>)
|
||||
|
||||
- **Must** display tooltip for "Total amount (with fee)" when hovered over.(<a name="1003-TRAN-019" href="#1003-TRAN-019">1003-TRAN-019</a>)
|
||||
|
||||
- **Must** amount to be transferred and transfer fee update correctly when include transfer fee is selected (<a name="1003-TRAN-020" href="#1003-TRAN-020">1003-TRAN-020</a>)
|
||||
|
||||
- **Must** total amount with fee is correct with and without "Include transfer fee" selected (<a name="1003-TRAN-021" href="#1003-TRAN-021">1003-TRAN-021</a>)
|
||||
|
||||
- **Must** i cannot select include transfer fee unless amount is entered (<a name="1003-TRAN-022" href="#1003-TRAN-022">1003-TRAN-022</a>)
|
||||
|
||||
- **Must** With all fields entered correctly, clicking "confirm transfer" button will start transaction(<a name="1003-TRAN-023" href="#1003-TRAN-023">1003-TRAN-023</a>)
|
||||
|
||||
### Transfer page
|
||||
|
||||
Reference in New Issue
Block a user