Merge branch 'feat/mobile-layout' of github.com:vegaprotocol/frontend-monorepo into feat/mobile-buttons
This commit is contained in:
commit
934eed87bf
10
README.md
10
README.md
@ -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).
|
This repository is managed using [Nx](https://nx.dev).
|
||||||
|
|
||||||
# 🔎 Applications in this repo
|
## 🔎 Applications in this repo
|
||||||
|
|
||||||
### [Block explorer](./apps/explorer)
|
### [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.
|
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)
|
### [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.
|
Generic react helpers that can be used across multiple applications, along with other utilities.
|
||||||
|
|
||||||
# 💻 Develop
|
## 💻 Develop
|
||||||
|
|
||||||
### Set up
|
### 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.
|
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).
|
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
|
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)
|
[MIT](./LICENSE)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export type HashProps = {
|
export type HashProps = React.HTMLProps<HTMLSpanElement> & {
|
||||||
text: string;
|
text: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -2,4 +2,5 @@ export { default as BlockLink } from './block-link/block-link';
|
|||||||
export { default as PartyLink } from './party-link/party-link';
|
export { default as PartyLink } from './party-link/party-link';
|
||||||
export { default as NodeLink } from './node-link/node-link';
|
export { default as NodeLink } from './node-link/node-link';
|
||||||
export { default as MarketLink } from './market-link/market-link';
|
export { default as MarketLink } from './market-link/market-link';
|
||||||
|
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
|
||||||
export * from './asset-link/asset-link';
|
export * from './asset-link/asset-link';
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import Hash from '../hash';
|
||||||
|
|
||||||
|
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
parameter: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links a given network parameter to the relevant page and anchor on the page
|
||||||
|
*/
|
||||||
|
const NetworkParameterLink = ({
|
||||||
|
parameter,
|
||||||
|
...props
|
||||||
|
}: NetworkParameterLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="underline"
|
||||||
|
{...props}
|
||||||
|
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
|
||||||
|
>
|
||||||
|
<Hash text={parameter} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkParameterLink;
|
@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const base = ENV.dataSources.governanceUrl;
|
const base = ENV.dataSources.governanceUrl;
|
||||||
const label = proposal?.rationale.title || id;
|
const label = proposal?.rationale?.title || id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||||
|
@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
|
|||||||
state
|
state
|
||||||
rejectionReason
|
rejectionReason
|
||||||
}
|
}
|
||||||
|
... on BatchProposal {
|
||||||
|
id
|
||||||
|
state
|
||||||
|
rejectionReason
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||||
|
|
||||||
|
|
||||||
export const ExplorerProposalStatusDocument = gql`
|
export const ExplorerProposalStatusDocument = gql`
|
||||||
@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
|
|||||||
state
|
state
|
||||||
rejectionReason
|
rejectionReason
|
||||||
}
|
}
|
||||||
|
... on BatchProposal {
|
||||||
|
id
|
||||||
|
state
|
||||||
|
rejectionReason
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -0,0 +1,257 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { BatchItem } from './batch-item';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
type Item = components['schemas']['vegaBatchProposalTermsChange'];
|
||||||
|
|
||||||
|
describe('BatchItem', () => {
|
||||||
|
it('Renders "Unknown proposal type" by default', () => {
|
||||||
|
const item = {};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Unknown proposal type" for unknown items', () => {
|
||||||
|
const item = {
|
||||||
|
newLochNessMonster: {
|
||||||
|
location: 'Loch Ness',
|
||||||
|
},
|
||||||
|
} as unknown as Item;
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New spot market"', () => {
|
||||||
|
const item = {
|
||||||
|
newSpotMarket: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New spot market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Cancel transfer"', () => {
|
||||||
|
const item = {
|
||||||
|
cancelTransfer: {
|
||||||
|
changes: {
|
||||||
|
transferId: 'transfer123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('transf')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Cancel transfer" without an id', () => {
|
||||||
|
const item = {
|
||||||
|
cancelTransfer: {
|
||||||
|
changes: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New freeform"', () => {
|
||||||
|
const item = {
|
||||||
|
newFreeform: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New freeform proposal')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New market"', () => {
|
||||||
|
const item = {
|
||||||
|
newMarket: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New transfer"', () => {
|
||||||
|
const item = {
|
||||||
|
newTransfer: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New transfer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update asset" with assetId', () => {
|
||||||
|
const item = {
|
||||||
|
updateAsset: {
|
||||||
|
assetId: 'asset123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update asset')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('asset123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update asset" even if assetId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateAsset: {
|
||||||
|
assetId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Update asset')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update market state" with marketId', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarketState: {
|
||||||
|
changes: {
|
||||||
|
marketId: 'market123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market state')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update market state" even if marketId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarketState: {
|
||||||
|
changes: {
|
||||||
|
marketId: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market state')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update network parameter" with parameter', () => {
|
||||||
|
const item = {
|
||||||
|
updateNetworkParameter: {
|
||||||
|
changes: {
|
||||||
|
key: 'parameter123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('parameter123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update network parameter" even if parameter is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateNetworkParameter: {
|
||||||
|
changes: {
|
||||||
|
key: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update referral program"', () => {
|
||||||
|
const item = {
|
||||||
|
updateReferralProgram: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Update referral program')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update spot market" with marketId', () => {
|
||||||
|
const item = {
|
||||||
|
updateSpotMarket: {
|
||||||
|
marketId: 'market123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update spot market')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update spot market" even if marketId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateSpotMarket: {
|
||||||
|
marketId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update spot market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Renders "Update market" with marketId', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarket: {
|
||||||
|
marketId: 'market123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update market" even if marketId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarket: {
|
||||||
|
marketId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update volume discount program"', () => {
|
||||||
|
const item = {
|
||||||
|
updateVolumeDiscountProgram: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Update volume discount program')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,87 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
import Hash from '../../../links/hash';
|
||||||
|
|
||||||
|
type Item = components['schemas']['vegaBatchProposalTermsChange'];
|
||||||
|
|
||||||
|
export interface BatchItemProps {
|
||||||
|
item: Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a one line summary for an item in a batch proposal. Could
|
||||||
|
* easily be adapted to summarise individual proposals, but there is no
|
||||||
|
* place for that yet.
|
||||||
|
*
|
||||||
|
* Details (like IDs) should be shown and linked if available, but handled
|
||||||
|
* if not available. This is adequate as the ProposalSummary component contains
|
||||||
|
* a JSON viewer for the full proposal.
|
||||||
|
*/
|
||||||
|
export const BatchItem = ({ item }: BatchItemProps) => {
|
||||||
|
if (item.cancelTransfer) {
|
||||||
|
const transferId = item?.cancelTransfer?.changes?.transferId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Cancel transfer')}
|
||||||
|
{transferId && (
|
||||||
|
<Hash className="ml-1" truncate={true} text={transferId} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.newFreeform) {
|
||||||
|
return <span>{t('New freeform proposal')}</span>;
|
||||||
|
} else if (item.newMarket) {
|
||||||
|
return <span>{t('New market')}</span>;
|
||||||
|
} else if (item.newSpotMarket) {
|
||||||
|
return <span>{t('New spot market')}</span>;
|
||||||
|
} else if (item.newTransfer) {
|
||||||
|
return <span>{t('New transfer')}</span>;
|
||||||
|
} else if (item.updateAsset) {
|
||||||
|
const assetId = item?.updateAsset?.assetId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update asset')}
|
||||||
|
{assetId && <AssetLink className="ml-1" assetId={assetId} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateMarket) {
|
||||||
|
const marketId = item?.updateMarket?.marketId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update market')}{' '}
|
||||||
|
{marketId && <MarketLink className="ml-1" id={marketId} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateMarketState) {
|
||||||
|
const marketId = item?.updateMarketState?.changes?.marketId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update market state')}
|
||||||
|
{marketId && <MarketLink className="ml-1" id={marketId} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateNetworkParameter) {
|
||||||
|
const param = item?.updateNetworkParameter?.changes?.key || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update network parameter')}
|
||||||
|
{param && <NetworkParameterLink className="ml-1" parameter={param} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateReferralProgram) {
|
||||||
|
return <span>{t('Update referral program')}</span>;
|
||||||
|
} else if (item.updateSpotMarket) {
|
||||||
|
const marketId = item?.updateSpotMarket?.marketId || '';
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update spot market')}
|
||||||
|
<MarketLink className="ml-1" id={marketId} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateVolumeDiscountProgram) {
|
||||||
|
return <span>{t('Update volume discount program')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{t('Unknown proposal type')}</span>;
|
||||||
|
};
|
@ -1,6 +1,4 @@
|
|||||||
import type { ProposalTerms } from '../tx-proposal';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { components } from '../../../../../types/explorer';
|
|
||||||
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
||||||
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
||||||
import truncate from 'lodash/truncate';
|
import truncate from 'lodash/truncate';
|
||||||
@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
|
|||||||
import { ProposalDate } from './proposal-date';
|
import { ProposalDate } from './proposal-date';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
|
import type { ProposalTerms } from '../tx-proposal';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
import { BatchItem } from './batch-item';
|
||||||
|
|
||||||
type Rationale = components['schemas']['vegaProposalRationale'];
|
type Rationale = components['schemas']['vegaProposalRationale'];
|
||||||
|
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
|
||||||
|
|
||||||
type ProposalTermsDialog = {
|
type ProposalTermsDialog = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -21,6 +24,7 @@ interface ProposalSummaryProps {
|
|||||||
id: string;
|
id: string;
|
||||||
rationale?: Rationale;
|
rationale?: Rationale;
|
||||||
terms?: ProposalTerms;
|
terms?: ProposalTerms;
|
||||||
|
batch?: Batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +35,7 @@ export const ProposalSummary = ({
|
|||||||
id,
|
id,
|
||||||
rationale,
|
rationale,
|
||||||
terms,
|
terms,
|
||||||
|
batch,
|
||||||
}: ProposalSummaryProps) => {
|
}: ProposalSummaryProps) => {
|
||||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||||
open: false,
|
open: false,
|
||||||
@ -72,6 +77,18 @@ export const ProposalSummary = ({
|
|||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{batch && (
|
||||||
|
<section className="pt-2 text-sm leading-tight my-3">
|
||||||
|
<h2 className="text-lg pb-1">{t('Changes')}</h2>
|
||||||
|
<ol>
|
||||||
|
{batch.map((change, index) => (
|
||||||
|
<li className="ml-4 list-decimal" key={`batch-${index}`}>
|
||||||
|
<BatchItem item={change} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
<div className="pt-5">
|
<div className="pt-5">
|
||||||
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
||||||
{t('View terms')}
|
{t('View terms')}
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
import type { components } from '../../../../types/explorer';
|
||||||
|
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||||
|
import { ProposalSummary } from './proposal/summary';
|
||||||
|
import Hash from '../../links/hash';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
|
export type Proposal = components['schemas']['v1BatchProposalSubmission'];
|
||||||
|
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
|
||||||
|
|
||||||
|
interface TxBatchProposalProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const TxBatchProposal = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxBatchProposalProps) => {
|
||||||
|
if (!txData || !txData.command.batchProposalSubmission) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
let deterministicId = '';
|
||||||
|
|
||||||
|
const proposal: Proposal = txData.command.batchProposalSubmission;
|
||||||
|
const sig = txData?.signature?.value;
|
||||||
|
if (sig) {
|
||||||
|
deterministicId = txSignatureToDeterministicId(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||||
|
<TableCell>{t('Batch proposal')}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TxDetailsShared
|
||||||
|
txData={txData}
|
||||||
|
pubKey={pubKey}
|
||||||
|
blockData={blockData}
|
||||||
|
hideTypeRow={true}
|
||||||
|
/>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Batch size')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{proposal.terms?.changes?.length || t('No changes')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Proposal ID')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Hash text={deterministicId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
{proposal && (
|
||||||
|
<ProposalSummary
|
||||||
|
id={deterministicId}
|
||||||
|
rationale={proposal?.rationale}
|
||||||
|
terms={proposal.terms}
|
||||||
|
batch={proposal.terms?.changes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -33,6 +33,7 @@ import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
|||||||
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||||
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||||
|
import { TxBatchProposal } from './tx-batch-proposal';
|
||||||
|
|
||||||
interface TxDetailsWrapperProps {
|
interface TxDetailsWrapperProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@ -136,6 +137,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
|||||||
return TxDetailsJoinTeam;
|
return TxDetailsJoinTeam;
|
||||||
case 'Update Margin Mode':
|
case 'Update Margin Mode':
|
||||||
return TxDetailsUpdateMarginMode;
|
return TxDetailsUpdateMarginMode;
|
||||||
|
case 'Batch Proposal':
|
||||||
|
return TxBatchProposal;
|
||||||
default:
|
default:
|
||||||
return TxDetailsGeneric;
|
return TxDetailsGeneric;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export type FilterOption =
|
|||||||
| 'Amend Order'
|
| 'Amend Order'
|
||||||
| 'Apply Referral Code'
|
| 'Apply Referral Code'
|
||||||
| 'Batch Market Instructions'
|
| 'Batch Market Instructions'
|
||||||
|
| 'Batch Proposal'
|
||||||
| 'Cancel LiquidityProvision Order'
|
| 'Cancel LiquidityProvision Order'
|
||||||
| 'Cancel Order'
|
| 'Cancel Order'
|
||||||
| 'Cancel Transfer Funds'
|
| 'Cancel Transfer Funds'
|
||||||
@ -67,7 +68,13 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
|||||||
'Cancel Transfer Funds',
|
'Cancel Transfer Funds',
|
||||||
'Withdraw',
|
'Withdraw',
|
||||||
],
|
],
|
||||||
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
|
Governance: [
|
||||||
|
'Batch Proposal',
|
||||||
|
'Delegate',
|
||||||
|
'Undelegate',
|
||||||
|
'Vote on Proposal',
|
||||||
|
'Proposal',
|
||||||
|
],
|
||||||
Referrals: [
|
Referrals: [
|
||||||
'Apply Referral Code',
|
'Apply Referral Code',
|
||||||
'Create Referral Set',
|
'Create Referral Set',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Mainnet Stats",
|
"short_name": "Explorer VEGA",
|
||||||
"name": "Vega Mainnet statistics",
|
"name": "Vega Protocol - Explorer",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Mainnet Stats",
|
"short_name": "Governance VEGA",
|
||||||
"name": "Vega Mainnet statistics",
|
"name": "Vega Protocol - Governance",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<title>Vega Protocol static asseets</title>
|
<title>Vega Protocol static assets</title>
|
||||||
<link rel="stylesheet" href="fonts.css" />
|
<link rel="stylesheet" href="fonts.css" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Mainnet Stats",
|
"name": "Vega Protocol - Trading",
|
||||||
"name": "Vega Mainnet statistics",
|
"short_name": "Console",
|
||||||
|
"description": "Vega Protocol - Trading dApp",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
@ -12,9 +18,5 @@
|
|||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ export const CompetitionsCreateTeam = () => {
|
|||||||
<div className="mx-auto md:w-2/3 max-w-xl">
|
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||||
<Box className="flex flex-col gap-4">
|
<Box className="flex flex-col gap-4">
|
||||||
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
|
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
|
||||||
{t('Create a team')}
|
{isSolo ? t('Create solo team') : t('Create a team')}
|
||||||
</h1>
|
</h1>
|
||||||
{pubKey && !isReadOnly ? (
|
{pubKey && !isReadOnly ? (
|
||||||
<CreateTeamFormContainer isSolo={isSolo} />
|
<CreateTeamFormContainer isSolo={isSolo} />
|
||||||
@ -71,18 +71,27 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
|||||||
|
|
||||||
if (status === 'confirmed') {
|
if (status === 'confirmed') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div
|
||||||
|
className="flex flex-col items-start gap-2"
|
||||||
|
data-testid="team-creation-success-message"
|
||||||
|
>
|
||||||
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||||
{code && (
|
{code && (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
Your team ID is:{' '}
|
Your team ID is:{' '}
|
||||||
<span className="font-mono break-all">{code}</span>
|
<span
|
||||||
|
className="font-mono break-all"
|
||||||
|
data-testid="team-id-display"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<TradingAnchorButton
|
<TradingAnchorButton
|
||||||
href={Links.COMPETITIONS_TEAM(code)}
|
href={Links.COMPETITIONS_TEAM(code)}
|
||||||
intent={Intent.Info}
|
intent={Intent.Info}
|
||||||
size="small"
|
size="small"
|
||||||
|
data-testid="view-team-button"
|
||||||
>
|
>
|
||||||
{t('View team')}
|
{t('View team')}
|
||||||
</TradingAnchorButton>
|
</TradingAnchorButton>
|
||||||
@ -125,7 +134,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
status={status}
|
status={status}
|
||||||
err={err}
|
err={err}
|
||||||
isSolo={isSolo}
|
isCreatingSoloTeam={isSolo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,8 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
|
|||||||
import { useTeams } from '../../lib/hooks/use-teams';
|
import { useTeams } from '../../lib/hooks/use-teams';
|
||||||
import take from 'lodash/take';
|
import take from 'lodash/take';
|
||||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { TeamCard } from '../../components/competitions/team-card';
|
||||||
|
import { useMyTeam } from '../../lib/hooks/use-my-team';
|
||||||
|
|
||||||
export const CompetitionsHome = () => {
|
export const CompetitionsHome = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -31,10 +33,14 @@ export const CompetitionsHome = () => {
|
|||||||
currentEpoch,
|
currentEpoch,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||||
sortByField: ['totalQuantumRewards'],
|
|
||||||
order: 'desc',
|
const {
|
||||||
});
|
team: myTeam,
|
||||||
|
stats: myTeamStats,
|
||||||
|
games: myTeamGames,
|
||||||
|
rank: myTeamRank,
|
||||||
|
} = useMyTeam();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@ -46,65 +52,83 @@ export const CompetitionsHome = () => {
|
|||||||
</p>
|
</p>
|
||||||
</CompetitionsHeader>
|
</CompetitionsHeader>
|
||||||
|
|
||||||
{/** Get started */}
|
{/** Team card */}
|
||||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
{myTeam ? (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl mb-6">{t('My team')}</h2>
|
||||||
|
<div className="mb-12">
|
||||||
|
<TeamCard
|
||||||
|
team={myTeam}
|
||||||
|
rank={myTeamRank}
|
||||||
|
stats={myTeamStats}
|
||||||
|
games={myTeamGames}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/** Get started */}
|
||||||
|
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||||
|
|
||||||
<CompetitionsActionsContainer>
|
<CompetitionsActionsContainer>
|
||||||
<CompetitionsAction
|
<CompetitionsAction
|
||||||
variant="A"
|
variant="A"
|
||||||
title={t('Create a team')}
|
title={t('Create a team')}
|
||||||
description={t(
|
description={t(
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||||
)}
|
)}
|
||||||
actionElement={
|
actionElement={
|
||||||
<TradingButton
|
<TradingButton
|
||||||
intent={Intent.Primary}
|
intent={Intent.Primary}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||||
}}
|
}}
|
||||||
>
|
data-testId="create-public-team-button"
|
||||||
{t('Create a public team')}
|
>
|
||||||
</TradingButton>
|
{t('Create a public team')}
|
||||||
}
|
</TradingButton>
|
||||||
/>
|
}
|
||||||
<CompetitionsAction
|
/>
|
||||||
variant="B"
|
<CompetitionsAction
|
||||||
title={t('Solo team / lone wolf')}
|
variant="B"
|
||||||
description={t(
|
title={t('Solo team / lone wolf')}
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
description={t(
|
||||||
)}
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||||
actionElement={
|
)}
|
||||||
<TradingButton
|
actionElement={
|
||||||
intent={Intent.Primary}
|
<TradingButton
|
||||||
onClick={(e) => {
|
intent={Intent.Primary}
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
e.preventDefault();
|
||||||
}}
|
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||||
>
|
}}
|
||||||
{t('Create a private team')}
|
>
|
||||||
</TradingButton>
|
{t('Create a private team')}
|
||||||
}
|
</TradingButton>
|
||||||
/>
|
}
|
||||||
<CompetitionsAction
|
/>
|
||||||
variant="C"
|
<CompetitionsAction
|
||||||
title={t('Join a team')}
|
variant="C"
|
||||||
description={t(
|
title={t('Join a team')}
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
description={t(
|
||||||
)}
|
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||||
actionElement={
|
)}
|
||||||
<TradingButton
|
actionElement={
|
||||||
intent={Intent.Primary}
|
<TradingButton
|
||||||
onClick={(e) => {
|
intent={Intent.Primary}
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
navigate(Links.COMPETITIONS_TEAMS());
|
e.preventDefault();
|
||||||
}}
|
navigate(Links.COMPETITIONS_TEAMS());
|
||||||
>
|
}}
|
||||||
{t('Choose a team')}
|
>
|
||||||
</TradingButton>
|
{t('Choose a team')}
|
||||||
}
|
</TradingButton>
|
||||||
/>
|
}
|
||||||
</CompetitionsActionsContainer>
|
/>
|
||||||
|
</CompetitionsActionsContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/** List of available games */}
|
{/** List of available games */}
|
||||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
||||||
|
@ -38,12 +38,11 @@ export const CompetitionsTeam = () => {
|
|||||||
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
|
const { data, team, partyTeam, stats, members, games, loading, refetch } =
|
||||||
teamId,
|
useTeam(teamId, pubKey || undefined);
|
||||||
pubKey || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
// only show spinner on first load so when users join teams its smoother
|
||||||
|
if (!data && loading) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
<Loader />
|
<Loader />
|
||||||
@ -100,8 +99,10 @@ const TeamPage = ({
|
|||||||
>
|
>
|
||||||
{team.name}
|
{team.name}
|
||||||
</h1>
|
</h1>
|
||||||
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
<div className="flex gap-2">
|
||||||
<UpdateTeamButton team={team} />
|
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||||
|
<UpdateTeamButton team={team} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<TeamStats stats={stats} members={members} games={games} />
|
<TeamStats stats={stats} members={members} games={games} />
|
||||||
@ -184,7 +185,10 @@ const Members = ({ members }: { members?: Member[] }) => {
|
|||||||
|
|
||||||
const data = orderBy(
|
const data = orderBy(
|
||||||
members.map((m) => ({
|
members.map((m) => ({
|
||||||
referee: <RefereeLink pubkey={m.referee} />,
|
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
|
||||||
|
rewards: formatNumber(m.totalQuantumRewards),
|
||||||
|
volume: formatNumber(m.totalQuantumVolume),
|
||||||
|
gamesPlayed: formatNumber(m.totalGamesPlayed),
|
||||||
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||||
joinedAtEpoch: Number(m.joinedAtEpoch),
|
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||||
})),
|
})),
|
||||||
@ -195,7 +199,10 @@ const Members = ({ members }: { members?: Member[] }) => {
|
|||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
columns={[
|
columns={[
|
||||||
{ name: 'referee', displayName: t('Referee') },
|
{ 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: 'joinedAt',
|
name: 'joinedAt',
|
||||||
displayName: t('Joined at'),
|
displayName: t('Joined at'),
|
||||||
@ -211,14 +218,24 @@ const Members = ({ members }: { members?: Member[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
const RefereeLink = ({
|
||||||
|
pubkey,
|
||||||
|
isCreator,
|
||||||
|
}: {
|
||||||
|
pubkey: string;
|
||||||
|
isCreator: boolean;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
const linkCreator = useLinks(DApp.Explorer);
|
const linkCreator = useLinks(DApp.Explorer);
|
||||||
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
<>
|
||||||
{truncateMiddle(pubkey)}
|
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||||
</Link>
|
{truncateMiddle(pubkey)}
|
||||||
|
</Link>{' '}
|
||||||
|
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,10 +17,7 @@ export const CompetitionsTeams = () => {
|
|||||||
|
|
||||||
usePageTitle([t('Competitions'), t('Teams')]);
|
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 inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [filter, setFilter] = useState<string | null | undefined>(undefined);
|
const [filter, setFilter] = useState<string | null | undefined>(undefined);
|
||||||
|
@ -98,7 +98,7 @@ const UpdateTeamFormContainer = ({
|
|||||||
type={TransactionType.UpdateReferralSet}
|
type={TransactionType.UpdateReferralSet}
|
||||||
status={status}
|
status={status}
|
||||||
err={err}
|
err={err}
|
||||||
isSolo={team.closed}
|
isCreatingSoloTeam={team.closed}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
/>
|
/>
|
||||||
|
@ -6,11 +6,7 @@ import {
|
|||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
useSimpleTransaction,
|
|
||||||
useVegaWallet,
|
|
||||||
type Status,
|
|
||||||
} from '@vegaprotocol/wallet';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { type Team } from '../../lib/hooks/use-team';
|
import { type Team } from '../../lib/hooks/use-team';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -27,19 +23,8 @@ export const JoinTeam = ({
|
|||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const { send, status } = useSimpleTransaction({
|
|
||||||
onSuccess: refetch,
|
|
||||||
});
|
|
||||||
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
||||||
|
|
||||||
const joinTeam = () => {
|
|
||||||
send({
|
|
||||||
joinTeam: {
|
|
||||||
id: team.teamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<JoinButton
|
<JoinButton
|
||||||
@ -56,11 +41,10 @@ export const JoinTeam = ({
|
|||||||
{confirmDialog !== undefined && (
|
{confirmDialog !== undefined && (
|
||||||
<DialogContent
|
<DialogContent
|
||||||
type={confirmDialog}
|
type={confirmDialog}
|
||||||
status={status}
|
|
||||||
team={team}
|
team={team}
|
||||||
partyTeam={partyTeam}
|
partyTeam={partyTeam}
|
||||||
onConfirm={joinTeam}
|
|
||||||
onCancel={() => setConfirmDialog(undefined)}
|
onCancel={() => setConfirmDialog(undefined)}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -110,7 +94,7 @@ export const JoinButton = ({
|
|||||||
// Not creator of the team, but still can't switch because
|
// Not creator of the team, but still can't switch because
|
||||||
// creators cannot leave their own team
|
// creators cannot leave their own team
|
||||||
return (
|
return (
|
||||||
<Tooltip description="As a team creator, you cannot switch teams">
|
<Tooltip description={t('As a team creator, you cannot switch teams')}>
|
||||||
<Button intent={Intent.Primary} disabled={true}>
|
<Button intent={Intent.Primary} disabled={true}>
|
||||||
{t('Switch team')}{' '}
|
{t('Switch team')}{' '}
|
||||||
</Button>
|
</Button>
|
||||||
@ -121,7 +105,11 @@ export const JoinButton = ({
|
|||||||
// Party is in a team, but not this one
|
// Party is in a team, but not this one
|
||||||
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
|
<Button
|
||||||
|
onClick={() => onJoin('switch')}
|
||||||
|
intent={Intent.Primary}
|
||||||
|
data-testid="switch-team-button"
|
||||||
|
>
|
||||||
{t('Switch team')}{' '}
|
{t('Switch team')}{' '}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -149,21 +137,39 @@ export const JoinButton = ({
|
|||||||
|
|
||||||
const DialogContent = ({
|
const DialogContent = ({
|
||||||
type,
|
type,
|
||||||
status,
|
|
||||||
team,
|
team,
|
||||||
partyTeam,
|
partyTeam,
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
|
refetch,
|
||||||
}: {
|
}: {
|
||||||
type: JoinType;
|
type: JoinType;
|
||||||
status: Status;
|
|
||||||
team: Team;
|
team: Team;
|
||||||
partyTeam?: Team;
|
partyTeam?: Team;
|
||||||
onConfirm: () => void;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
refetch: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
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') {
|
if (status === 'requested') {
|
||||||
return <p>{t('Confirm in wallet...')}</p>;
|
return <p>{t('Confirm in wallet...')}</p>;
|
||||||
}
|
}
|
||||||
@ -213,7 +219,11 @@ const DialogContent = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<Button onClick={onConfirm} intent={Intent.Success}>
|
<Button
|
||||||
|
onClick={joinTeam}
|
||||||
|
intent={Intent.Success}
|
||||||
|
data-testid="confirm-switch-button"
|
||||||
|
>
|
||||||
{t('Confirm')}
|
{t('Confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onCancel} intent={Intent.Danger}>
|
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||||
|
@ -6,6 +6,8 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
TradingButton,
|
TradingButton,
|
||||||
Intent,
|
Intent,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
@ -17,6 +19,8 @@ import type {
|
|||||||
UpdateReferralSet,
|
UpdateReferralSet,
|
||||||
Status,
|
Status,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
export type FormFields = {
|
export type FormFields = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -28,8 +32,8 @@ export type FormFields = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum TransactionType {
|
export enum TransactionType {
|
||||||
CreateReferralSet,
|
CreateReferralSet = 'CreateReferralSet',
|
||||||
UpdateReferralSet,
|
UpdateReferralSet = 'UpdateReferralSet',
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepareTransaction = (
|
const prepareTransaction = (
|
||||||
@ -75,14 +79,14 @@ export const TeamForm = ({
|
|||||||
type,
|
type,
|
||||||
status,
|
status,
|
||||||
err,
|
err,
|
||||||
isSolo,
|
isCreatingSoloTeam,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
}: {
|
}: {
|
||||||
type: TransactionType;
|
type: TransactionType;
|
||||||
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
||||||
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
||||||
isSolo: boolean;
|
isCreatingSoloTeam: boolean;
|
||||||
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
||||||
defaultValues?: FormFields;
|
defaultValues?: FormFields;
|
||||||
}) => {
|
}) => {
|
||||||
@ -96,7 +100,7 @@ export const TeamForm = ({
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormFields>({
|
} = useForm<FormFields>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
private: isSolo,
|
private: isCreatingSoloTeam,
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -109,16 +113,14 @@ export const TeamForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(sendTransaction)}>
|
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||||
<input
|
<input type="hidden" {...register('id')} />
|
||||||
type="hidden"
|
|
||||||
{...register('id', {
|
|
||||||
disabled: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<TradingFormGroup label={t('Team name')} labelFor="name">
|
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||||
<TradingInput {...register('name', { required: t('Required') })} />
|
<TradingInput
|
||||||
|
{...register('name', { required: t('Required') })}
|
||||||
|
data-testid="team-name-input"
|
||||||
|
/>
|
||||||
{errors.name?.message && (
|
{errors.name?.message && (
|
||||||
<TradingInputError forInput="name">
|
<TradingInputError forInput="name" data-testid="team-name-error">
|
||||||
{errors.name.message}
|
{errors.name.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
@ -134,9 +136,10 @@ export const TeamForm = ({
|
|||||||
{...register('url', {
|
{...register('url', {
|
||||||
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
||||||
})}
|
})}
|
||||||
|
data-testid="team-url-input"
|
||||||
/>
|
/>
|
||||||
{errors.url?.message && (
|
{errors.url?.message && (
|
||||||
<TradingInputError forInput="url">
|
<TradingInputError forInput="url" data-testid="team-url-error">
|
||||||
{errors.url.message}
|
{errors.url.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
@ -153,66 +156,86 @@ export const TeamForm = ({
|
|||||||
message: t('Invalid image URL'),
|
message: t('Invalid image URL'),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
data-testid="avatar-url-input"
|
||||||
/>
|
/>
|
||||||
{errors.avatarUrl?.message && (
|
{errors.avatarUrl?.message && (
|
||||||
<TradingInputError forInput="avatarUrl">
|
<TradingInputError
|
||||||
|
forInput="avatarUrl"
|
||||||
|
data-testid="avatar-url-error"
|
||||||
|
>
|
||||||
{errors.avatarUrl.message}
|
{errors.avatarUrl.message}
|
||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
<TradingFormGroup
|
{
|
||||||
label={t('Make team private')}
|
// allow changing to private/public if editing, but don't show these options if making a solo team
|
||||||
labelFor="private"
|
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
|
||||||
hideLabel={true}
|
<>
|
||||||
>
|
<TradingFormGroup
|
||||||
<Controller
|
label={t('Make team private')}
|
||||||
name="private"
|
labelFor="private"
|
||||||
control={control}
|
hideLabel={true}
|
||||||
render={({ field }) => {
|
>
|
||||||
return (
|
<Controller
|
||||||
<TradingCheckbox
|
name="private"
|
||||||
label={t('Make team private')}
|
control={control}
|
||||||
checked={field.value}
|
render={({ field }) => {
|
||||||
onCheckedChange={(value) => {
|
return (
|
||||||
field.onChange(value);
|
<TradingCheckbox
|
||||||
|
label={t('Make team private')}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
data-testid="team-private-checkbox"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
disabled={isSolo}
|
|
||||||
/>
|
/>
|
||||||
);
|
</TradingFormGroup>
|
||||||
}}
|
{isPrivate && (
|
||||||
/>
|
<TradingFormGroup
|
||||||
</TradingFormGroup>
|
label={t('Public key allow list')}
|
||||||
{isPrivate && (
|
labelFor="allowList"
|
||||||
<TradingFormGroup
|
labelDescription={t(
|
||||||
label={t('Public key allow list')}
|
'Use a comma separated list to allow only specific public keys to join the team'
|
||||||
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'),
|
||||||
<TextArea
|
validate: {
|
||||||
{...register('allowList', {
|
allowList: (value) => {
|
||||||
required: t('Required'),
|
const publicKeys = parseAllowListText(value);
|
||||||
disabled: isSolo,
|
if (
|
||||||
validate: {
|
publicKeys.every((pk) => isValidVegaPublicKey(pk))
|
||||||
allowList: (value) => {
|
) {
|
||||||
const publicKeys = parseAllowListText(value);
|
return true;
|
||||||
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
}
|
||||||
return true;
|
return t('Invalid public key found in allow list');
|
||||||
}
|
},
|
||||||
return t('Invalid public key found in allow list');
|
},
|
||||||
},
|
})}
|
||||||
},
|
data-testid="team-allow-list-textarea"
|
||||||
})}
|
/>
|
||||||
/>
|
{errors.allowList?.message && (
|
||||||
{errors.allowList?.message && (
|
<TradingInputError
|
||||||
<TradingInputError forInput="avatarUrl">
|
forInput="avatarUrl"
|
||||||
{errors.allowList.message}
|
data-testid="team-allow-list-error"
|
||||||
</TradingInputError>
|
>
|
||||||
)}
|
{errors.allowList.message}
|
||||||
</TradingFormGroup>
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{err && (
|
||||||
|
<p className="text-danger text-xs mb-4 first-letter:capitalize">
|
||||||
|
{err}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
|
|
||||||
<SubmitButton type={type} status={status} />
|
<SubmitButton type={type} status={status} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
@ -233,20 +256,61 @@ const SubmitButton = ({
|
|||||||
text = t('Update');
|
text = t('Update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let confirmedText = t('Created');
|
||||||
|
if (type === TransactionType.UpdateReferralSet) {
|
||||||
|
confirmedText = t('Updated');
|
||||||
|
}
|
||||||
|
|
||||||
if (status === 'requested') {
|
if (status === 'requested') {
|
||||||
text = t('Confirm in wallet...');
|
text = t('Confirm in wallet...');
|
||||||
} else if (status === 'pending') {
|
} else if (status === 'pending') {
|
||||||
text = t('Confirming transaction...');
|
text = t('Confirming transaction...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let to: ReturnType<typeof setTimeout>;
|
||||||
|
if (status === 'confirmed' && !showConfirmed) {
|
||||||
|
to = setTimeout(() => {
|
||||||
|
setShowConfirmed(true);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
clearTimeout(to);
|
||||||
|
};
|
||||||
|
}, [showConfirmed, status]);
|
||||||
|
|
||||||
|
const confirmed = (
|
||||||
|
<span
|
||||||
|
className={classNames('text-sm transition-opacity opacity-0', {
|
||||||
|
'opacity-100': showConfirmed,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.TICK}
|
||||||
|
size={18}
|
||||||
|
className="text-vega-green-500"
|
||||||
|
/>{' '}
|
||||||
|
{confirmedText}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
<div className="flex gap-2 items-baseline">
|
||||||
{text}
|
<TradingButton
|
||||||
</TradingButton>
|
type="submit"
|
||||||
|
intent={Intent.Info}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="team-form-submit-button"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</TradingButton>
|
||||||
|
{status === 'confirmed' && confirmed}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseAllowListText = (str: string) => {
|
const parseAllowListText = (str: string = '') => {
|
||||||
return str
|
return str
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((v) => v.trim())
|
.map((v) => v.trim())
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
|
||||||
import { type Team } from '../../lib/hooks/use-team';
|
import { type Team } from '../../lib/hooks/use-team';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Links } from '../../lib/links';
|
import { Links } from '../../lib/links';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
export const UpdateTeamButton = ({
|
||||||
|
team,
|
||||||
|
size = 'medium',
|
||||||
|
}: {
|
||||||
|
team: Pick<Team, 'teamId' | 'referrer'>;
|
||||||
|
size?: ComponentProps<typeof TradingAnchorButton>['size'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
|
||||||
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||||
return (
|
return (
|
||||||
<TradingAnchorButton
|
<TradingAnchorButton
|
||||||
|
size={size}
|
||||||
data-testid="update-team-button"
|
data-testid="update-team-button"
|
||||||
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||||
intent={Intent.Info}
|
intent={Intent.Info}
|
||||||
/>
|
>
|
||||||
|
{t('Update team')}
|
||||||
|
</TradingAnchorButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
useFundingRate,
|
useFundingRate,
|
||||||
useMarketTradingMode,
|
useMarketTradingMode,
|
||||||
useExternalTwap,
|
useExternalTwap,
|
||||||
|
getQuoteName,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketState as State } from '@vegaprotocol/types';
|
import { MarketState as State } from '@vegaprotocol/types';
|
||||||
import { HeaderStat } from '../../components/header';
|
import { HeaderStat } from '../../components/header';
|
||||||
@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
|||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
|
|
||||||
const asset = getAsset(market);
|
const asset = getAsset(market);
|
||||||
|
const quoteUnit = getQuoteName(market);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -54,12 +56,16 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
|||||||
<Last24hPriceChange
|
<Last24hPriceChange
|
||||||
marketId={market.id}
|
marketId={market.id}
|
||||||
decimalPlaces={market.decimalPlaces}
|
decimalPlaces={market.decimalPlaces}
|
||||||
/>
|
>
|
||||||
|
<span>-</span>
|
||||||
|
</Last24hPriceChange>
|
||||||
</HeaderStat>
|
</HeaderStat>
|
||||||
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
||||||
<Last24hVolume
|
<Last24hVolume
|
||||||
marketId={market.id}
|
marketId={market.id}
|
||||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||||
|
marketDecimals={market.decimalPlaces}
|
||||||
|
quoteUnit={quoteUnit}
|
||||||
/>
|
/>
|
||||||
</HeaderStat>
|
</HeaderStat>
|
||||||
<HeaderStatMarketTradingMode
|
<HeaderStatMarketTradingMode
|
||||||
|
@ -20,10 +20,10 @@ interface TradePanelsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||||
const [view1, setView1] = useState<TradingView>('chart');
|
const [topView, setTopView] = useState<TradingView>('chart');
|
||||||
const viewCfg1 = TradingViews[view1];
|
const topViewCfg = TradingViews[topView];
|
||||||
const [view2, setView2] = useState<TradingView>('positions');
|
const [bottomView, setBottomView] = useState<TradingView>('positions');
|
||||||
const viewCfg2 = TradingViews[view2];
|
const bottomViewCfg = TradingViews[bottomView];
|
||||||
|
|
||||||
const renderView = (view: TradingView) => {
|
const renderView = (view: TradingView) => {
|
||||||
const Component = TradingViews[view].component;
|
const Component = TradingViews[view].component;
|
||||||
@ -94,58 +94,56 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
|||||||
})
|
})
|
||||||
.map((_key) => {
|
.map((_key) => {
|
||||||
const key = _key as TradingView;
|
const key = _key as TradingView;
|
||||||
const isActive = view1 === key;
|
const isActive = topView === key;
|
||||||
return (
|
return (
|
||||||
<ViewButton
|
<ViewButton
|
||||||
key={key}
|
key={key}
|
||||||
view={key}
|
view={key}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setView1(key);
|
setTopView(key);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[376px] sm:h-[460px] lg:h-full relative">
|
<div className="h-[50vh] lg:h-full relative">
|
||||||
<div>{renderMenu(viewCfg1)}</div>
|
<div>{renderMenu(topViewCfg)}</div>
|
||||||
<div className="overflow-auto h-full">{renderView(view1)}</div>
|
<div className="overflow-auto h-full">{renderView(topView)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
<div className="flex flex-col w-full grow">
|
||||||
<div className="flex flex-col w-full grow">
|
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
{[
|
||||||
{[
|
'positions',
|
||||||
'positions',
|
'activeOrders',
|
||||||
'activeOrders',
|
'closedOrders',
|
||||||
'closedOrders',
|
'rejectedOrders',
|
||||||
'rejectedOrders',
|
'orders',
|
||||||
'orders',
|
'stopOrders',
|
||||||
'stopOrders',
|
'collateral',
|
||||||
'collateral',
|
'fills',
|
||||||
'fills',
|
].map((_key) => {
|
||||||
].map((_key) => {
|
const key = _key as TradingView;
|
||||||
const key = _key as TradingView;
|
const isActive = bottomView === key;
|
||||||
const isActive = view2 === key;
|
return (
|
||||||
return (
|
<ViewButton
|
||||||
<ViewButton
|
key={key}
|
||||||
key={key}
|
view={key}
|
||||||
view={key}
|
isActive={isActive}
|
||||||
isActive={isActive}
|
onClick={() => {
|
||||||
onClick={() => {
|
setBottomView(key);
|
||||||
setView2(key);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="relative grow">
|
|
||||||
<div className="flex flex-col">{renderMenu(viewCfg2)}</div>
|
|
||||||
<div className="overflow-auto h-full">{renderView(view2)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className="relative grow">
|
||||||
|
<div className="flex flex-col">{renderMenu(bottomViewCfg)}</div>
|
||||||
|
<div className="overflow-auto h-full">{renderView(bottomView)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
useDataGridEvents,
|
useDataGridEvents,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
||||||
import { useColumnDefs } from './use-column-defs';
|
import { useMarketsColumnDefs } from './use-column-defs';
|
||||||
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||||
import { type StateCreator, create } from 'zustand';
|
import { type StateCreator, create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const MarketListTable = (props: Props) => {
|
export const MarketListTable = (props: Props) => {
|
||||||
const columnDefs = useColumnDefs();
|
const columnDefs = useMarketsColumnDefs();
|
||||||
const gridStore = useMarketsStore((store) => store.gridStore);
|
const gridStore = useMarketsStore((store) => store.gridStore);
|
||||||
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
|
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
|
||||||
|
|
||||||
|
@ -7,21 +7,31 @@ import type {
|
|||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
import type {
|
import type {
|
||||||
|
MarketFieldsFragment,
|
||||||
MarketMaybeWithData,
|
MarketMaybeWithData,
|
||||||
MarketMaybeWithDataAndCandles,
|
MarketMaybeWithDataAndCandles,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketActionsDropdown } from './market-table-actions';
|
import { MarketActionsDropdown } from './market-table-actions';
|
||||||
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
|
import {
|
||||||
|
calcCandleVolume,
|
||||||
|
calcCandleVolumePrice,
|
||||||
|
getAsset,
|
||||||
|
getQuoteName,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketCodeCell } from './market-code-cell';
|
import { MarketCodeCell } from './market-code-cell';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||||
|
|
||||||
export const useColumnDefs = () => {
|
export const useMarketsColumnDefs = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
return useMemo<ColDef[]>(
|
return useMemo<ColDef[]>(
|
||||||
@ -158,11 +168,25 @@ export const useColumnDefs = () => {
|
|||||||
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
||||||
const candles = data?.candles;
|
const candles = data?.candles;
|
||||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||||
|
const quoteName = getQuoteName(data as MarketFieldsFragment);
|
||||||
|
const volPrice =
|
||||||
|
candles &&
|
||||||
|
calcCandleVolumePrice(
|
||||||
|
candles,
|
||||||
|
data.decimalPlaces,
|
||||||
|
data.positionDecimalPlaces
|
||||||
|
);
|
||||||
|
|
||||||
const volume =
|
const volume =
|
||||||
data && vol && vol !== '0'
|
data && vol && vol !== '0'
|
||||||
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
||||||
: '0.00';
|
: '0.00';
|
||||||
return volume;
|
const volumePrice =
|
||||||
|
volPrice && formatNumber(volPrice, data?.decimalPlaces);
|
||||||
|
|
||||||
|
return volumePrice
|
||||||
|
? `${volume} (${volumePrice} ${quoteName})`
|
||||||
|
: volume;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,11 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
|||||||
export const GRADIENT =
|
export const GRADIENT =
|
||||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||||
|
|
||||||
export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
export const Box = ({
|
||||||
|
children,
|
||||||
|
backgroundImage,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
@ -13,9 +17,22 @@ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
|||||||
BORDER_COLOR,
|
BORDER_COLOR,
|
||||||
GRADIENT,
|
GRADIENT,
|
||||||
'border rounded-lg',
|
'border rounded-lg',
|
||||||
'p-6',
|
'relative p-6 overflow-hidden',
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
|
{Boolean(backgroundImage?.length) && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'pointer-events-none',
|
||||||
|
'bg-no-repeat bg-center bg-[length:500px_500px]',
|
||||||
|
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
|
||||||
|
)}
|
||||||
|
style={{ backgroundImage: `url("${backgroundImage}")` }}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||||||
const NUM_AVATARS = 20;
|
const NUM_AVATARS = 20;
|
||||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||||
|
|
||||||
const getFallbackAvatar = (teamId: string) => {
|
export const getFallbackAvatar = (teamId: string) => {
|
||||||
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0'); // between 01 - 20
|
.padStart(2, '0'); // between 01 - 20
|
||||||
|
154
apps/trading/components/competitions/team-card.tsx
Normal file
154
apps/trading/components/competitions/team-card.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { type TeamGame, type TeamStats } from '../../lib/hooks/use-team';
|
||||||
|
import { type TeamsFieldsFragment } from '../../lib/hooks/__generated__/Teams';
|
||||||
|
import { TeamAvatar, getFallbackAvatar } from './team-avatar';
|
||||||
|
import { FavoriteGame, Stat } from './team-stats';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { formatNumberRounded } from '@vegaprotocol/utils';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { Box } from './box';
|
||||||
|
import { Intent, Tooltip, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { take } from 'lodash';
|
||||||
|
import { DispatchMetricLabels } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { UpdateTeamButton } from '../../client-pages/competitions/update-team-button';
|
||||||
|
|
||||||
|
export const TeamCard = ({
|
||||||
|
rank,
|
||||||
|
team,
|
||||||
|
stats,
|
||||||
|
games,
|
||||||
|
}: {
|
||||||
|
rank: number;
|
||||||
|
team: TeamsFieldsFragment;
|
||||||
|
stats?: TeamStats;
|
||||||
|
games?: TeamGame[];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const lastGames = take(
|
||||||
|
orderBy(
|
||||||
|
games?.map((g) => ({
|
||||||
|
rank: g.team.rank,
|
||||||
|
metric: g.team.rewardMetric,
|
||||||
|
epoch: g.epoch,
|
||||||
|
})),
|
||||||
|
(i) => i.epoch,
|
||||||
|
'desc'
|
||||||
|
),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'gap-6 grid grid-cols-1 grid-rows-1',
|
||||||
|
'md:grid-cols-3'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/** Card */}
|
||||||
|
<Box
|
||||||
|
backgroundImage={team.avatarUrl || getFallbackAvatar(team.teamId)}
|
||||||
|
className="flex flex-col items-center gap-3 min-w-[80px] lg:min-w-[112px]"
|
||||||
|
>
|
||||||
|
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||||
|
<h1 className="calt lg:text-2xl" data-testid="team-name">
|
||||||
|
{team.name}
|
||||||
|
</h1>
|
||||||
|
{games && <FavoriteGame games={games} noLabel />}
|
||||||
|
<TradingAnchorButton
|
||||||
|
size="extra-small"
|
||||||
|
intent={Intent.Primary}
|
||||||
|
href={Links.COMPETITIONS_TEAM(team.teamId)}
|
||||||
|
>
|
||||||
|
{t('Profile')}
|
||||||
|
</TradingAnchorButton>
|
||||||
|
<UpdateTeamButton team={team} size="extra-small" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/** Tiles */}
|
||||||
|
<Box className="w-full md:col-span-2">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'grid gap-3 w-full mb-4',
|
||||||
|
'md:grid-cols-3 md:grid-rows-2',
|
||||||
|
'grid-cols-2 grid-rows-3'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={rank}
|
||||||
|
label={t('Rank')}
|
||||||
|
valueTestId="team-rank"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={team.totalMembers || 0}
|
||||||
|
label={t('Members')}
|
||||||
|
valueTestId="members-count-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={stats?.totalGamesPlayed || 0}
|
||||||
|
label={t('Total games')}
|
||||||
|
valueTestId="total-games-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={
|
||||||
|
stats?.totalQuantumVolume
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumVolume || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Total volume')}
|
||||||
|
valueTestId="total-volume-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={
|
||||||
|
stats?.totalQuantumRewards
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumRewards || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Rewards paid out')}
|
||||||
|
valueTestId="rewards-paid-stat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="w-full pt-4 border-t border-vega-clight-700 dark:border-vega-cdark-700">
|
||||||
|
<dt className="mb-1 text-sm text-muted">
|
||||||
|
{t('Last {{games}} games result', {
|
||||||
|
replace: { games: lastGames.length || '' },
|
||||||
|
})}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex flex-row flex-wrap gap-2">
|
||||||
|
{lastGames.length === 0 && t('None available')}
|
||||||
|
{lastGames.map((game, i) => (
|
||||||
|
<Tooltip key={i} description={DispatchMetricLabels[game.metric]}>
|
||||||
|
<button className="cursor-help text-sm bg-vega-clight-700 dark:bg-vega-cdark-700 px-2 py-1 rounded-full">
|
||||||
|
<RankLabel rank={game.rank} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the english ordinal for given rank only if the current language is set
|
||||||
|
* to english.
|
||||||
|
*/
|
||||||
|
const RankLabel = ({ rank }: { rank: number }) => {
|
||||||
|
const t = useT();
|
||||||
|
return t('place', { count: rank, ordinal: true });
|
||||||
|
};
|
@ -15,6 +15,7 @@ import {
|
|||||||
} from '../../lib/hooks/use-team';
|
} from '../../lib/hooks/use-team';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export const TeamStats = ({
|
export const TeamStats = ({
|
||||||
stats,
|
stats,
|
||||||
@ -102,7 +103,13 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
export const FavoriteGame = ({
|
||||||
|
games,
|
||||||
|
noLabel = false,
|
||||||
|
}: {
|
||||||
|
games: TeamGame[];
|
||||||
|
noLabel?: boolean;
|
||||||
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
|
|
||||||
const rewardMetrics = games.map(
|
const rewardMetrics = games.map(
|
||||||
@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="flex flex-col gap-1">
|
<dl className="flex flex-col gap-1">
|
||||||
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
<dt
|
||||||
|
className={classNames('text-muted text-sm', {
|
||||||
|
hidden: noLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('Favorite game')}
|
||||||
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||||
<VegaIcon
|
<VegaIcon
|
||||||
@ -142,7 +155,7 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||||
{children}
|
{children}
|
||||||
@ -150,11 +163,11 @@ const StatSection = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatSectionSeparator = () => {
|
export const StatSectionSeparator = () => {
|
||||||
return <div className="hidden md:block border-r border-default" />;
|
return <div className="hidden md:block border-r border-default" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatList = ({ children }: { children: ReactNode }) => {
|
export const StatList = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||||
{children}
|
{children}
|
||||||
@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stat = ({
|
export const Stat = ({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
tooltip,
|
tooltip,
|
||||||
valueTestId,
|
valueTestId,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
value: ReactNode;
|
value: ReactNode;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
valueTestId?: string;
|
valueTestId?: string;
|
||||||
|
className?: classNames.Argument;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classNames(className)}>
|
||||||
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||||
{value}
|
{value}
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -27,7 +27,7 @@ export const LayoutWithSidebar = ({
|
|||||||
<div className="col-span-full">{header}</div>
|
<div className="col-span-full">{header}</div>
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'col-start-1 col-end-1 overflow-y-auto grow lg:grow-0',
|
'col-start-1 col-end-1 overflow-hidden lg:overflow-y-auto grow lg:grow-0',
|
||||||
{
|
{
|
||||||
'lg:col-end-3': !sidebarOpen,
|
'lg:col-end-3': !sidebarOpen,
|
||||||
'hidden lg:block lg:col-end-2': sidebarOpen,
|
'hidden lg:block lg:col-end-2': sidebarOpen,
|
||||||
|
@ -29,7 +29,7 @@ export const MobileMarketHeader = () => {
|
|||||||
if (!marketId) return null;
|
if (!marketId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2 flex justify-between gap-2 items-center h-10 pr-1 border-b border-default bg-vega-clight-700 dark:bg-vega-cdark-700">
|
<div className="p-2 flex justify-between gap-2 items-center h-10 border-b border-default bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||||
<FullScreenPopover
|
<FullScreenPopover
|
||||||
open={openMarket}
|
open={openMarket}
|
||||||
onOpenChange={(x) => {
|
onOpenChange={(x) => {
|
||||||
@ -42,7 +42,7 @@ export const MobileMarketHeader = () => {
|
|||||||
: t('Select market')}
|
: t('Select market')}
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'transition-transform ease-in-out duration-300',
|
'transition-transform ease-in-out duration-300 flex',
|
||||||
{
|
{
|
||||||
'rotate-180': openMarket,
|
'rotate-180': openMarket,
|
||||||
}
|
}
|
||||||
@ -71,7 +71,6 @@ export const MobileMarketHeader = () => {
|
|||||||
<Last24hPriceChange
|
<Last24hPriceChange
|
||||||
marketId={data.id}
|
marketId={data.id}
|
||||||
decimalPlaces={data.decimalPlaces}
|
decimalPlaces={data.decimalPlaces}
|
||||||
hideZero={true}
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<MarketMarkPrice
|
<MarketMarkPrice
|
||||||
|
@ -152,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
|||||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
<Card
|
||||||
|
title={t('Active rewards')}
|
||||||
|
className="lg:col-span-full"
|
||||||
|
data-testid="active-rewards-card"
|
||||||
|
>
|
||||||
{enrichedTransfers.length > 1 && (
|
{enrichedTransfers.length > 1 && (
|
||||||
<TradingInput
|
<TradingInput
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -312,49 +316,30 @@ export const ActiveRewardCard = ({
|
|||||||
MarketState.STATE_CLOSED,
|
MarketState.STATE_CLOSED,
|
||||||
].includes(m.state)
|
].includes(m.state)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (marketSettled) {
|
if (marketSettled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetInSettledMarket =
|
const assetInActiveMarket =
|
||||||
allMarkets &&
|
allMarkets &&
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||||
return (
|
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||||
m?.state &&
|
|
||||||
[
|
|
||||||
MarketState.STATE_TRADING_TERMINATED,
|
|
||||||
MarketState.STATE_SETTLED,
|
|
||||||
MarketState.STATE_CANCELLED,
|
|
||||||
MarketState.STATE_CLOSED,
|
|
||||||
].includes(m.state)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
const marketSuspended = transferNode.markets?.some(
|
||||||
const suspended = transferNode.markets?.some(
|
|
||||||
(m) =>
|
(m) =>
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
m?.state === MarketState.STATE_SUSPENDED ||
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||||
);
|
);
|
||||||
|
|
||||||
const assetInSuspendedMarket =
|
|
||||||
allMarkets &&
|
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
|
||||||
return (
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
// Gray out the cards that are related to suspended markets
|
||||||
|
// Or settlement assets in markets that are not active and eligible for rewards
|
||||||
const { gradientClassName, mainClassName } =
|
const { gradientClassName, mainClassName } =
|
||||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
marketSuspended || !assetInActiveMarket
|
||||||
? {
|
? {
|
||||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||||
@ -371,6 +356,7 @@ export const ActiveRewardCard = ({
|
|||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
gradientClassName
|
gradientClassName
|
||||||
)}
|
)}
|
||||||
|
data-testid="active-rewards-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -382,7 +368,7 @@ export const ActiveRewardCard = ({
|
|||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<EntityIcon transfer={transfer} />
|
<EntityIcon transfer={transfer} />
|
||||||
{entityScope && (
|
{entityScope && (
|
||||||
<span className="text-muted text-xs">
|
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -390,7 +376,7 @@ export const ActiveRewardCard = ({
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||||
<span className="font-glitch">
|
<span className="font-glitch" data-testid="reward-value">
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
transferNode.transfer.amount,
|
transferNode.transfer.amount,
|
||||||
transferNode.transfer.asset?.decimals || 0,
|
transferNode.transfer.asset?.decimals || 0,
|
||||||
@ -411,7 +397,7 @@ export const ActiveRewardCard = ({
|
|||||||
)}
|
)}
|
||||||
underline={true}
|
underline={true}
|
||||||
>
|
>
|
||||||
<span className="text-xs">
|
<span className="text-xs" data-testid="distribution-strategy">
|
||||||
{
|
{
|
||||||
DistributionStrategyMapping[
|
DistributionStrategyMapping[
|
||||||
dispatchStrategy.distributionStrategy
|
dispatchStrategy.distributionStrategy
|
||||||
@ -429,7 +415,10 @@ export const ActiveRewardCard = ({
|
|||||||
'Number of epochs after distribution to delay vesting of rewards by'
|
'Number of epochs after distribution to delay vesting of rewards by'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted text-xs whitespace-nowrap">
|
<span
|
||||||
|
className="text-muted text-xs whitespace-nowrap"
|
||||||
|
data-testid="locked-for"
|
||||||
|
>
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.dispatchStrategy?.lockPeriod,
|
count: kind.dispatchStrategy?.lockPeriod,
|
||||||
})}
|
})}
|
||||||
@ -438,15 +427,15 @@ export const ActiveRewardCard = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
<span>
|
<span data-testid="dispatch-metric-info">
|
||||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
underline={suspended}
|
underline={marketSuspended}
|
||||||
description={
|
description={
|
||||||
(suspended || assetInSuspendedMarket) &&
|
(marketSuspended || !assetInActiveMarket) &&
|
||||||
(specificMarkets
|
(specificMarkets
|
||||||
? t('Eligible market(s) currently suspended')
|
? t('Eligible market(s) currently suspended')
|
||||||
: assetInSuspendedMarket
|
: !assetInActiveMarket
|
||||||
? t('Currently no markets eligible for reward')
|
? t('Currently no markets eligible for reward')
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
@ -458,8 +447,8 @@ export const ActiveRewardCard = ({
|
|||||||
<div className="flex items-center gap-8 flex-wrap">
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
{kind.endEpoch && (
|
{kind.endEpoch && (
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||||
<span>
|
<span data-testid="ends-in">
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.endEpoch - currentEpoch,
|
count: kind.endEpoch - currentEpoch,
|
||||||
})}
|
})}
|
||||||
@ -470,7 +459,7 @@ export const ActiveRewardCard = ({
|
|||||||
{
|
{
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||||
<span>
|
<span data-testid="assessed-over">
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: dispatchStrategy.windowLength,
|
count: dispatchStrategy.windowLength,
|
||||||
})}
|
})}
|
||||||
@ -513,7 +502,7 @@ const RewardRequirements = ({
|
|||||||
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||||
})}
|
})}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd className="flex items-center gap-1" data-testid="scope">
|
||||||
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -522,7 +511,10 @@ const RewardRequirements = ({
|
|||||||
<dt className="flex items-center gap-1 text-muted">
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
{t('Staked VEGA')}
|
{t('Staked VEGA')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
data-testid="staking-requirement"
|
||||||
|
>
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
dispatchStrategy?.stakingRequirement || 0,
|
dispatchStrategy?.stakingRequirement || 0,
|
||||||
assetDecimalPlaces
|
assetDecimalPlaces
|
||||||
@ -534,7 +526,7 @@ const RewardRequirements = ({
|
|||||||
<dt className="flex items-center gap-1 text-muted">
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
{t('Average position')}
|
{t('Average position')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="flex items-center gap-1">
|
<dd className="flex items-center gap-1" data-testid="average-position">
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||||
0,
|
0,
|
||||||
|
@ -81,6 +81,7 @@ export const Settings = () => {
|
|||||||
intent={Intent.Primary}
|
intent={Intent.Primary}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||||
VEGA_VERSION=v0.74.0-preview.7
|
VEGA_VERSION=v0.74.0-preview.8
|
||||||
LOCAL_SERVER=false
|
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.")
|
"You may not have enough margin available to open this position.")
|
||||||
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
||||||
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
||||||
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
|
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
|
||||||
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
||||||
expect(page.get_by_test_id("sidebar-content")
|
expect(page.get_by_test_id("sidebar-content")
|
||||||
).to_contain_text("DepositFrom")
|
).to_contain_text("DepositFrom")
|
||||||
|
@ -37,7 +37,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
|||||||
# 6002-MDET-004
|
# 6002-MDET-004
|
||||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||||
# 6002-MDET-005
|
# 6002-MDET-005
|
||||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
|
||||||
# 6002-MDET-008
|
# 6002-MDET-008
|
||||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
|
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
|
||||||
"Settlement assettDAI"
|
"Settlement assettDAI"
|
||||||
|
@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
|||||||
next_epoch(vega=vega)
|
next_epoch(vega=vega)
|
||||||
|
|
||||||
page.reload()
|
page.reload()
|
||||||
expect(page.locator(".from-vega-cdark-400")).to_be_visible(timeout=15000)
|
expect(page.get_by_test_id("active-rewards-card")).to_be_visible(timeout=15000)
|
||||||
governance.submit_oracle_data(
|
governance.submit_oracle_data(
|
||||||
wallet=vega.wallet,
|
wallet=vega.wallet,
|
||||||
payload={"trading.terminated": "true"},
|
payload={"trading.terminated": "true"},
|
||||||
@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
|||||||
)
|
)
|
||||||
next_epoch(vega=vega)
|
next_epoch(vega=vega)
|
||||||
page.reload()
|
page.reload()
|
||||||
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
|
expect(page.get_by_test_id("active-rewards-card")).not_to_be_in_viewport()
|
||||||
|
@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
|
|||||||
import vega_sim.proto.vega as vega_protos
|
import vega_sim.proto.vega as vega_protos
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from conftest import init_vega
|
from conftest import init_vega
|
||||||
from actions.utils import next_epoch
|
from actions.utils import next_epoch, change_keys
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
||||||
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
|
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
|
||||||
@ -14,6 +14,7 @@ def vega(request):
|
|||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega:
|
||||||
yield vega
|
yield vega
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def team_page(vega, browser, request, setup_teams_and_games):
|
def team_page(vega, browser, request, setup_teams_and_games):
|
||||||
with init_page(vega, browser, request) as page:
|
with init_page(vega, browser, request) as page:
|
||||||
@ -23,9 +24,20 @@ def team_page(vega, browser, request, setup_teams_and_games):
|
|||||||
page.goto(f"/#/competitions/teams/{team_id}")
|
page.goto(f"/#/competitions/teams/{team_id}")
|
||||||
yield page
|
yield page
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def competitions_page(vega, browser, request, setup_teams_and_games):
|
||||||
|
with init_page(vega, browser, request) as page:
|
||||||
|
risk_accepted_setup(page)
|
||||||
|
auth_setup(vega, page)
|
||||||
|
team_id = setup_teams_and_games["team_id"]
|
||||||
|
page.goto(f"/#/competitions/")
|
||||||
|
yield page
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def setup_teams_and_games(vega: VegaServiceNull):
|
def setup_teams_and_games(vega: VegaServiceNull):
|
||||||
tDAI_market = setup_continuous_market(vega)
|
tDAI_market = setup_continuous_market(vega, custom_quantum=100000)
|
||||||
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
|
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
|
||||||
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
|
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
|
||||||
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
|
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
|
||||||
@ -46,6 +58,18 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
|
|
||||||
# list_teams actually returns a dictionary {"team_id": Team}
|
# list_teams actually returns a dictionary {"team_id": Team}
|
||||||
team_id = list(teams.keys())[0]
|
team_id = list(teams.keys())[0]
|
||||||
|
vega.create_referral_set(
|
||||||
|
key_name="market_maker",
|
||||||
|
name="test",
|
||||||
|
team_url="https://vega.xyz",
|
||||||
|
avatar_url="http://placekitten.com/200/200",
|
||||||
|
closed=False,
|
||||||
|
)
|
||||||
|
next_epoch(vega)
|
||||||
|
teams = vega.list_teams()
|
||||||
|
|
||||||
|
team_id_2 = list(teams.keys())[0]
|
||||||
|
vega.apply_referral_code("Key 1", team_id_2)
|
||||||
|
|
||||||
vega.apply_referral_code(PARTY_B.name, team_id)
|
vega.apply_referral_code(PARTY_B.name, team_id)
|
||||||
|
|
||||||
@ -63,7 +87,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
|
|
||||||
current_epoch = vega.statistics().epoch_seq
|
current_epoch = vega.statistics().epoch_seq
|
||||||
game_start = current_epoch + 1
|
game_start = current_epoch + 1
|
||||||
game_end = current_epoch + 11
|
game_end = current_epoch + 14
|
||||||
|
|
||||||
current_epoch = vega.statistics().epoch_seq
|
current_epoch = vega.statistics().epoch_seq
|
||||||
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
||||||
@ -84,9 +108,42 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
factor=1.0,
|
factor=1.0,
|
||||||
start_epoch=game_start,
|
start_epoch=game_start,
|
||||||
end_epoch=game_end,
|
end_epoch=game_end,
|
||||||
window_length=10
|
window_length=15,
|
||||||
|
)
|
||||||
|
vega.wait_fn(1)
|
||||||
|
vega.wait_for_total_catchup()
|
||||||
|
vega.recurring_transfer(
|
||||||
|
from_key_name=PARTY_B.name,
|
||||||
|
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
|
||||||
|
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||||
|
asset=tDAI_asset_id,
|
||||||
|
reference="reward",
|
||||||
|
asset_for_metric=tDAI_asset_id,
|
||||||
|
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
|
||||||
|
entity_scope=vega_protos.vega.ENTITY_SCOPE_INDIVIDUALS,
|
||||||
|
individual_scope=vega_protos.vega.INDIVIDUAL_SCOPE_IN_TEAM,
|
||||||
|
n_top_performers=1,
|
||||||
|
amount=100,
|
||||||
|
factor=1.0,
|
||||||
|
window_length=15
|
||||||
|
)
|
||||||
|
vega.wait_fn(1)
|
||||||
|
vega.wait_for_total_catchup()
|
||||||
|
vega.recurring_transfer(
|
||||||
|
from_key_name=PARTY_C.name,
|
||||||
|
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
|
||||||
|
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||||
|
asset=tDAI_asset_id,
|
||||||
|
reference="reward",
|
||||||
|
asset_for_metric=tDAI_asset_id,
|
||||||
|
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
|
||||||
|
entity_scope=vega_protos.vega.ENTITY_SCOPE_INDIVIDUALS,
|
||||||
|
individual_scope=vega_protos.vega.INDIVIDUAL_SCOPE_NOT_IN_TEAM,
|
||||||
|
n_top_performers=1,
|
||||||
|
amount=100,
|
||||||
|
factor=1.0,
|
||||||
|
window_length=15
|
||||||
)
|
)
|
||||||
|
|
||||||
next_epoch(vega)
|
next_epoch(vega)
|
||||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
||||||
|
|
||||||
@ -113,6 +170,22 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
side="SIDE_BUY",
|
side="SIDE_BUY",
|
||||||
volume=1,
|
volume=1,
|
||||||
)
|
)
|
||||||
|
vega.submit_order(
|
||||||
|
trading_key="Key 1",
|
||||||
|
market_id=tDAI_market,
|
||||||
|
order_type="TYPE_MARKET",
|
||||||
|
time_in_force="TIME_IN_FORCE_IOC",
|
||||||
|
side="SIDE_BUY",
|
||||||
|
volume=1,
|
||||||
|
)
|
||||||
|
vega.submit_order(
|
||||||
|
trading_key="market_maker",
|
||||||
|
market_id=tDAI_market,
|
||||||
|
order_type="TYPE_MARKET",
|
||||||
|
time_in_force="TIME_IN_FORCE_IOC",
|
||||||
|
side="SIDE_BUY",
|
||||||
|
volume=1,
|
||||||
|
)
|
||||||
next_epoch(vega)
|
next_epoch(vega)
|
||||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
||||||
|
|
||||||
@ -120,6 +193,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
|||||||
"market_id": tDAI_market,
|
"market_id": tDAI_market,
|
||||||
"asset_id": tDAI_asset_id,
|
"asset_id": tDAI_asset_id,
|
||||||
"team_id": team_id,
|
"team_id": team_id,
|
||||||
|
"team_id_2": team_id_2,
|
||||||
"team_name": team_name,
|
"team_name": team_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,65 +210,109 @@ def create_team(vega: VegaServiceNull):
|
|||||||
|
|
||||||
return team_name
|
return team_name
|
||||||
|
|
||||||
|
|
||||||
def test_team_page_games_table(team_page: Page):
|
def test_team_page_games_table(team_page: Page):
|
||||||
team_page.get_by_test_id("games-toggle").click()
|
team_page.get_by_test_id("games-toggle").click()
|
||||||
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
|
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
|
||||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
|
expect(team_page.get_by_test_id("rank-0")).to_have_text("2")
|
||||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
|
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19")
|
||||||
expect(team_page.get_by_test_id("type-0")).to_have_text("Price maker fees paid")
|
expect(team_page.get_by_test_id("type-0")
|
||||||
expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
|
).to_have_text("Price maker fees paid")
|
||||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
|
expect(team_page.get_by_test_id("amount-0")).to_have_text("74")
|
||||||
"1"
|
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text("2")
|
||||||
)
|
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("4")
|
||||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
|
|
||||||
"2"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_team_page_members_table(team_page: Page):
|
def test_team_page_members_table(team_page: Page):
|
||||||
team_page.get_by_test_id("members-toggle").click()
|
team_page.get_by_test_id("members-toggle").click()
|
||||||
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (3)")
|
expect(team_page.get_by_test_id("members-toggle")
|
||||||
|
).to_have_text("Members (4)")
|
||||||
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
|
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
|
||||||
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
|
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
|
||||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("8")
|
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9")
|
||||||
|
|
||||||
def test_team_page_headline(team_page: Page, setup_teams_and_games
|
|
||||||
):
|
def test_team_page_headline(team_page: Page, setup_teams_and_games):
|
||||||
team_name = setup_teams_and_games["team_name"]
|
team_name = setup_teams_and_games["team_name"]
|
||||||
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
|
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
|
||||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("3")
|
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("4")
|
||||||
|
|
||||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
|
expect(team_page.get_by_test_id("total-games-stat")).to_have_text("2")
|
||||||
"1"
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO this still seems wrong as its always 0
|
# TODO this still seems wrong as its always 0
|
||||||
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
|
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text("0")
|
||||||
"0"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
|
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("214")
|
||||||
"100m"
|
|
||||||
)
|
|
||||||
|
def test_switch_teams(team_page: Page, vega: VegaServiceNull):
|
||||||
|
team_page.get_by_test_id("switch-team-button").click()
|
||||||
|
team_page.get_by_test_id("confirm-switch-button").click()
|
||||||
|
expect(team_page.get_by_test_id("dialog-content").first).to_be_visible()
|
||||||
|
vega.wait_fn(1)
|
||||||
|
vega.wait_for_total_catchup()
|
||||||
|
next_epoch(vega=vega)
|
||||||
|
team_page.reload()
|
||||||
|
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("5")
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def competitions_page(vega, browser, request):
|
|
||||||
with init_page(vega, browser, request) as page:
|
|
||||||
risk_accepted_setup(page)
|
|
||||||
auth_setup(vega, page)
|
|
||||||
yield page
|
|
||||||
|
|
||||||
def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||||
team_name = setup_teams_and_games["team_name"]
|
team_name = setup_teams_and_games["team_name"]
|
||||||
competitions_page.goto(f"/#/competitions/")
|
competitions_page.reload()
|
||||||
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
|
expect(
|
||||||
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
|
||||||
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
|
).to_have_count(1)
|
||||||
|
expect(
|
||||||
|
competitions_page.get_by_test_id(
|
||||||
|
"rank-1").locator(".text-vega-clight-500")
|
||||||
|
).to_have_count(1)
|
||||||
|
expect(competitions_page.get_by_test_id("team-1")).to_have_text(team_name)
|
||||||
|
expect(competitions_page.get_by_test_id("status-1")).to_have_text("Open")
|
||||||
|
|
||||||
expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
|
# FIXME: the numbers are different we need to clarify this with the backend
|
||||||
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
|
# expect(competitions_page.get_by_test_id("earned-1")).to_have_text("160")
|
||||||
|
expect(competitions_page.get_by_test_id("games-1")).to_have_text("2")
|
||||||
|
|
||||||
# TODO still odd that this is 0
|
# TODO still odd that this is 0
|
||||||
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
||||||
|
|
||||||
#TODO def test_games(competitions_page: Page):
|
|
||||||
#TODO currently no games appear which i think is a bug
|
def test_game_card(competitions_page: Page):
|
||||||
|
expect(competitions_page.get_by_test_id(
|
||||||
|
"active-rewards-card")).to_have_count(2)
|
||||||
|
game_1 = competitions_page.get_by_test_id("active-rewards-card").first
|
||||||
|
expect(game_1).to_be_visible()
|
||||||
|
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Individual")
|
||||||
|
expect(game_1.get_by_test_id("locked-for")).to_have_text("1 epoch")
|
||||||
|
expect(game_1.get_by_test_id("reward-value")).to_have_text("100.00")
|
||||||
|
expect(game_1.get_by_test_id("distribution-strategy")
|
||||||
|
).to_have_text("Pro rata")
|
||||||
|
expect(game_1.get_by_test_id("dispatch-metric-info")
|
||||||
|
).to_have_text("Price maker fees paid • ")
|
||||||
|
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
|
||||||
|
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
|
||||||
|
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
|
||||||
|
expect(game_1.get_by_test_id("average-position")).to_have_text("0.00")
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_team(competitions_page: Page, vega: VegaServiceNull):
|
||||||
|
change_keys(competitions_page, vega, "market_maker_2")
|
||||||
|
competitions_page.get_by_test_id("create-public-team-button").click()
|
||||||
|
competitions_page.get_by_test_id("team-name-input").fill("e2e")
|
||||||
|
competitions_page.get_by_test_id("team-url-input").fill("https://vega.xyz")
|
||||||
|
competitions_page.get_by_test_id("avatar-url-input").fill(
|
||||||
|
"http://placekitten.com/200/200"
|
||||||
|
)
|
||||||
|
competitions_page.get_by_test_id("team-form-submit-button").click()
|
||||||
|
expect(competitions_page.get_by_test_id("team-form-submit-button")).to_have_text(
|
||||||
|
"Confirming transaction..."
|
||||||
|
)
|
||||||
|
vega.wait_fn(2)
|
||||||
|
vega.wait_for_total_catchup()
|
||||||
|
expect(
|
||||||
|
competitions_page.get_by_test_id("team-creation-success-message")
|
||||||
|
).to_be_visible()
|
||||||
|
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
|
||||||
|
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
|
||||||
|
competitions_page.get_by_test_id("view-team-button").click()
|
||||||
|
expect(competitions_page.get_by_test_id("team-name")).to_have_text("e2e")
|
||||||
|
@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
|
|||||||
totalGamesPlayed
|
totalGamesPlayed
|
||||||
quantumRewards {
|
quantumRewards {
|
||||||
epoch
|
epoch
|
||||||
total_quantum_rewards
|
totalQuantumRewards
|
||||||
}
|
}
|
||||||
gamesPlayed
|
gamesPlayed
|
||||||
}
|
}
|
||||||
@ -51,6 +51,13 @@ fragment TeamGameFields on Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||||
|
partyId
|
||||||
|
totalQuantumVolume
|
||||||
|
totalQuantumRewards
|
||||||
|
totalGamesPlayed
|
||||||
|
}
|
||||||
|
|
||||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||||
teams(teamId: $teamId) {
|
teams(teamId: $teamId) {
|
||||||
edges {
|
edges {
|
||||||
@ -87,4 +94,14 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
teamMembersStatistics(
|
||||||
|
teamId: $teamId
|
||||||
|
aggregationEpochs: $aggregationEpochs
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamMemberStatsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
|
fragment TeamsFields on Team {
|
||||||
|
teamId
|
||||||
|
referrer
|
||||||
|
name
|
||||||
|
teamUrl
|
||||||
|
avatarUrl
|
||||||
|
createdAt
|
||||||
|
createdAtEpoch
|
||||||
|
closed
|
||||||
|
totalMembers
|
||||||
|
}
|
||||||
|
|
||||||
query Teams($teamId: ID, $partyId: ID) {
|
query Teams($teamId: ID, $partyId: ID) {
|
||||||
teams(teamId: $teamId, partyId: $partyId) {
|
teams(teamId: $teamId, partyId: $partyId) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
teamId
|
...TeamsFields
|
||||||
referrer
|
|
||||||
name
|
|
||||||
teamUrl
|
|
||||||
avatarUrl
|
|
||||||
createdAt
|
|
||||||
createdAtEpoch
|
|
||||||
closed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
apps/trading/lib/hooks/__generated__/Team.ts
generated
26
apps/trading/lib/hooks/__generated__/Team.ts
generated
@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
|
|||||||
const defaultOptions = {} as const;
|
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 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, total_quantum_rewards: 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 TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
||||||
|
|
||||||
@ -13,6 +13,8 @@ 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 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<{
|
export type TeamQueryVariables = Types.Exact<{
|
||||||
teamId: Types.Scalars['ID'];
|
teamId: Types.Scalars['ID'];
|
||||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||||
@ -20,7 +22,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, 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 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 const TeamFieldsFragmentDoc = gql`
|
export const TeamFieldsFragmentDoc = gql`
|
||||||
fragment TeamFields on Team {
|
fragment TeamFields on Team {
|
||||||
@ -43,7 +45,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
|
|||||||
totalGamesPlayed
|
totalGamesPlayed
|
||||||
quantumRewards {
|
quantumRewards {
|
||||||
epoch
|
epoch
|
||||||
total_quantum_rewards
|
totalQuantumRewards
|
||||||
}
|
}
|
||||||
gamesPlayed
|
gamesPlayed
|
||||||
}
|
}
|
||||||
@ -80,6 +82,14 @@ export const TeamGameFieldsFragmentDoc = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
${TeamEntityFragmentDoc}`;
|
${TeamEntityFragmentDoc}`;
|
||||||
|
export const TeamMemberStatsFieldsFragmentDoc = gql`
|
||||||
|
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||||
|
partyId
|
||||||
|
totalQuantumVolume
|
||||||
|
totalQuantumRewards
|
||||||
|
totalGamesPlayed
|
||||||
|
}
|
||||||
|
`;
|
||||||
export const TeamDocument = gql`
|
export const TeamDocument = gql`
|
||||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||||
teams(teamId: $teamId) {
|
teams(teamId: $teamId) {
|
||||||
@ -117,11 +127,19 @@ export const TeamDocument = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamMemberStatsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
${TeamFieldsFragmentDoc}
|
${TeamFieldsFragmentDoc}
|
||||||
${TeamStatsFieldsFragmentDoc}
|
${TeamStatsFieldsFragmentDoc}
|
||||||
${TeamRefereeFieldsFragmentDoc}
|
${TeamRefereeFieldsFragmentDoc}
|
||||||
${TeamGameFieldsFragmentDoc}`;
|
${TeamGameFieldsFragmentDoc}
|
||||||
|
${TeamMemberStatsFieldsFragmentDoc}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useTeamQuery__
|
* __useTeamQuery__
|
||||||
|
29
apps/trading/lib/hooks/__generated__/Teams.ts
generated
29
apps/trading/lib/hooks/__generated__/Teams.ts
generated
@ -3,33 +3,40 @@ import * as Types from '@vegaprotocol/types';
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
const defaultOptions = {} as const;
|
const defaultOptions = {} as const;
|
||||||
|
export type TeamsFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number };
|
||||||
|
|
||||||
export type TeamsQueryVariables = Types.Exact<{
|
export type TeamsQueryVariables = Types.Exact<{
|
||||||
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null };
|
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number } }> } | null };
|
||||||
|
|
||||||
|
|
||||||
|
export const TeamsFieldsFragmentDoc = gql`
|
||||||
|
fragment TeamsFields on Team {
|
||||||
|
teamId
|
||||||
|
referrer
|
||||||
|
name
|
||||||
|
teamUrl
|
||||||
|
avatarUrl
|
||||||
|
createdAt
|
||||||
|
createdAtEpoch
|
||||||
|
closed
|
||||||
|
totalMembers
|
||||||
|
}
|
||||||
|
`;
|
||||||
export const TeamsDocument = gql`
|
export const TeamsDocument = gql`
|
||||||
query Teams($teamId: ID, $partyId: ID) {
|
query Teams($teamId: ID, $partyId: ID) {
|
||||||
teams(teamId: $teamId, partyId: $partyId) {
|
teams(teamId: $teamId, partyId: $partyId) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
teamId
|
...TeamsFields
|
||||||
referrer
|
|
||||||
name
|
|
||||||
teamUrl
|
|
||||||
avatarUrl
|
|
||||||
createdAt
|
|
||||||
createdAtEpoch
|
|
||||||
closed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
${TeamsFieldsFragmentDoc}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useTeamsQuery__
|
* __useTeamsQuery__
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
||||||
import { isActiveReward } from '../../components/rewards-container/active-rewards';
|
import { isActiveReward } from '../../components/rewards-container/active-rewards';
|
||||||
import { EntityScope, type TransferNode } from '@vegaprotocol/types';
|
import {
|
||||||
|
EntityScope,
|
||||||
|
IndividualScope,
|
||||||
|
type TransferNode,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
const isScopedToTeams = (node: TransferNode) =>
|
const isScopedToTeams = (node: TransferNode) =>
|
||||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
// scoped to teams
|
||||||
EntityScope.ENTITY_SCOPE_TEAMS;
|
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||||
|
EntityScope.ENTITY_SCOPE_TEAMS ||
|
||||||
|
// or to individuals
|
||||||
|
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||||
|
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||||
|
// but they have to be in a team
|
||||||
|
node.transfer.kind.dispatchStrategy.individualScope ===
|
||||||
|
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM));
|
||||||
|
|
||||||
export const useGames = ({
|
export const useGames = ({
|
||||||
currentEpoch,
|
currentEpoch,
|
||||||
|
25
apps/trading/lib/hooks/use-my-team.ts
Normal file
25
apps/trading/lib/hooks/use-my-team.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import first from 'lodash/first';
|
||||||
|
import { useTeamsQuery } from './__generated__/Teams';
|
||||||
|
import { useTeam } from './use-team';
|
||||||
|
import { useTeams } from './use-teams';
|
||||||
|
|
||||||
|
export const useMyTeam = () => {
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
const { data: teams } = useTeams();
|
||||||
|
|
||||||
|
const { data: maybeMyTeam } = useTeamsQuery({
|
||||||
|
variables: {
|
||||||
|
partyId: pubKey,
|
||||||
|
},
|
||||||
|
skip: !pubKey,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const team = first(compact(maybeMyTeam?.teams?.edges.map((n) => n.node)));
|
||||||
|
const rank = teams.findIndex((t) => t.teamId === team?.teamId) + 1;
|
||||||
|
const { games, stats } = useTeam(team?.teamId);
|
||||||
|
|
||||||
|
return { team, stats, games, rank };
|
||||||
|
};
|
@ -6,17 +6,24 @@ import {
|
|||||||
type TeamStatsFieldsFragment,
|
type TeamStatsFieldsFragment,
|
||||||
type TeamRefereeFieldsFragment,
|
type TeamRefereeFieldsFragment,
|
||||||
type TeamEntityFragment,
|
type TeamEntityFragment,
|
||||||
|
type TeamMemberStatsFieldsFragment,
|
||||||
} from './__generated__/Team';
|
} from './__generated__/Team';
|
||||||
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
|
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
|
||||||
|
|
||||||
export type Team = TeamFieldsFragment;
|
export type Team = TeamFieldsFragment;
|
||||||
export type TeamStats = TeamStatsFieldsFragment;
|
export type TeamStats = TeamStatsFieldsFragment;
|
||||||
export type Member = TeamRefereeFieldsFragment;
|
export type Member = TeamRefereeFieldsFragment & {
|
||||||
|
isCreator: boolean;
|
||||||
|
totalGamesPlayed: number;
|
||||||
|
totalQuantumVolume: string;
|
||||||
|
totalQuantumRewards: string;
|
||||||
|
};
|
||||||
export type TeamEntity = TeamEntityFragment;
|
export type TeamEntity = TeamEntityFragment;
|
||||||
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
|
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
|
||||||
|
export type MemberStats = TeamMemberStatsFieldsFragment;
|
||||||
|
|
||||||
export const useTeam = (teamId?: string, partyId?: string) => {
|
export const useTeam = (teamId?: string, partyId?: string) => {
|
||||||
const { data, loading, error, refetch } = useTeamQuery({
|
const queryResult = useTeamQuery({
|
||||||
variables: {
|
variables: {
|
||||||
teamId: teamId || '',
|
teamId: teamId || '',
|
||||||
partyId,
|
partyId,
|
||||||
@ -26,7 +33,11 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
|||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data } = queryResult;
|
||||||
|
|
||||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||||
|
const team = teamEdge?.node;
|
||||||
|
|
||||||
const partyTeam = data?.partyTeams?.edges?.length
|
const partyTeam = data?.partyTeams?.edges?.length
|
||||||
? data.partyTeams.edges[0].node
|
? data.partyTeams.edges[0].node
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -34,9 +45,40 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
|||||||
const teamStatsEdge = data?.teamsStatistics?.edges.find(
|
const teamStatsEdge = data?.teamsStatistics?.edges.find(
|
||||||
(e) => e.node.teamId === teamId
|
(e) => e.node.teamId === teamId
|
||||||
);
|
);
|
||||||
const members = data?.teamReferees?.edges
|
|
||||||
.filter((e) => e.node.teamId === teamId)
|
const memberStats = data?.teamMembersStatistics?.edges.length
|
||||||
.map((e) => e.node);
|
? 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Find games where the current team participated in
|
// Find games where the current team participated in
|
||||||
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
||||||
@ -60,12 +102,9 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
|||||||
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
...queryResult,
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
stats: teamStatsEdge?.node,
|
stats: teamStatsEdge?.node,
|
||||||
team: teamEdge?.node,
|
team,
|
||||||
members,
|
members,
|
||||||
games,
|
games,
|
||||||
partyTeam,
|
partyTeam,
|
||||||
|
@ -1,34 +1,13 @@
|
|||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
|
import { useTeamsQuery } from './__generated__/Teams';
|
||||||
import {
|
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
|
||||||
type TeamsStatisticsQuery,
|
|
||||||
useTeamsStatisticsQuery,
|
|
||||||
} from './__generated__/TeamsStatistics';
|
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import { type ArrayElement } from 'type-fest/source/internal';
|
|
||||||
|
|
||||||
type SortableField = keyof Omit<
|
// 192
|
||||||
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
|
export const DEFAULT_AGGREGATION_EPOCHS = 192;
|
||||||
ArrayElement<
|
|
||||||
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
|
|
||||||
>['node'],
|
|
||||||
'__typename'
|
|
||||||
>;
|
|
||||||
|
|
||||||
type UseTeamsArgs = {
|
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||||
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 {
|
const {
|
||||||
data: teamsData,
|
data: teamsData,
|
||||||
loading: teamsLoading,
|
loading: teamsLoading,
|
||||||
@ -57,12 +36,8 @@ export const useTeams = ({
|
|||||||
...stats.find((s) => s.teamId === t.teamId),
|
...stats.find((s) => s.teamId === t.teamId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const sorted = sortBy(data, sortByField);
|
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc');
|
||||||
if (order === 'desc') {
|
}, [teams, stats]);
|
||||||
return sorted.reverse();
|
|
||||||
}
|
|
||||||
return sorted;
|
|
||||||
}, [teams, sortByField, order, stats]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
@ -24,11 +24,14 @@ export default function Document() {
|
|||||||
|
|
||||||
{/* scripts */}
|
{/* scripts */}
|
||||||
<script src="/theme-setter.js" type="text/javascript" async />
|
<script src="/theme-setter.js" type="text/javascript" async />
|
||||||
|
|
||||||
|
{/* manifest */}
|
||||||
|
<link rel="manifest" href="/apps/trading/public/manifest.json" />
|
||||||
</Head>
|
</Head>
|
||||||
<Html>
|
<Html>
|
||||||
<body
|
<body
|
||||||
// Nextjs will set body to display none until js runs. Because the entire app is client rendered
|
// Next.js will set body to display none until js runs. Because the entire app is client rendered
|
||||||
// and delivered via ipfs we override this to show a server side render loading animation until the
|
// and delivered via IPFS we override this to show a server side render loading animation until the
|
||||||
// js is downloaded and react takes over rendering
|
// js is downloaded and react takes over rendering
|
||||||
style={{ display: 'block' }}
|
style={{ display: 'block' }}
|
||||||
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
|
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
|
||||||
|
22
apps/trading/public/manifest.json
Normal file
22
apps/trading/public/manifest.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "Vega Protocol - Trading",
|
||||||
|
"short_name": "Console",
|
||||||
|
"description": "Vega Protocol - Trading dApp",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "cover.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -96,11 +96,6 @@ describe('TransferForm', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
|
||||||
targetText: 'Include transfer fee',
|
|
||||||
tooltipText:
|
|
||||||
'The fee will be taken from the amount you are transferring.',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
targetText: 'Transfer fee',
|
targetText: 'Transfer fee',
|
||||||
tooltipText: /transfer\.fee\.factor/,
|
tooltipText: /transfer\.fee\.factor/,
|
||||||
@ -276,9 +271,6 @@ describe('TransferForm', () => {
|
|||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
|
|
||||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
|
||||||
expect(checkbox).not.toBeChecked();
|
|
||||||
|
|
||||||
await userEvent.clear(amountInput);
|
await userEvent.clear(amountInput);
|
||||||
await userEvent.type(amountInput, '50');
|
await userEvent.type(amountInput, '50');
|
||||||
|
|
||||||
@ -288,10 +280,7 @@ describe('TransferForm', () => {
|
|||||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||||
expect(amountInput).toHaveValue('100.00');
|
expect(amountInput).toHaveValue('100.00');
|
||||||
|
|
||||||
// If transfering from a vested account 'include fees' checkbox should
|
// If transfering from a vested account fees should be 0
|
||||||
// be disabled and fees should be 0
|
|
||||||
expect(checkbox).not.toBeChecked();
|
|
||||||
expect(checkbox).toBeDisabled();
|
|
||||||
const expectedFee = '0';
|
const expectedFee = '0';
|
||||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||||
|
|
||||||
@ -396,78 +385,7 @@ describe('TransferForm', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('IncludeFeesCheckbox', () => {
|
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 () => {
|
it('validates fields when checkbox is not checked', async () => {
|
||||||
renderComponent(props);
|
renderComponent(props);
|
||||||
|
|
||||||
@ -497,11 +415,8 @@ describe('TransferForm', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const amountInput = screen.getByLabelText('Amount');
|
const amountInput = screen.getByLabelText('Amount');
|
||||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
|
||||||
expect(checkbox).not.toBeChecked();
|
|
||||||
|
|
||||||
await userEvent.type(amountInput, amount);
|
await userEvent.type(amountInput, amount);
|
||||||
expect(checkbox).not.toBeChecked();
|
|
||||||
const expectedFee = new BigNumber(amount)
|
const expectedFee = new BigNumber(amount)
|
||||||
.times(props.feeFactor)
|
.times(props.feeFactor)
|
||||||
.toFixed();
|
.toFixed();
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
TradingRichSelect,
|
TradingRichSelect,
|
||||||
TradingSelect,
|
TradingSelect,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TradingCheckbox,
|
|
||||||
TradingButton,
|
TradingButton,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { Transfer } from '@vegaprotocol/wallet';
|
import type { Transfer } from '@vegaprotocol/wallet';
|
||||||
@ -135,32 +134,17 @@ export const TransferForm = ({
|
|||||||
const accountBalance =
|
const accountBalance =
|
||||||
account && addDecimal(account.balance, account.asset.decimals);
|
account && addDecimal(account.balance, account.asset.decimals);
|
||||||
|
|
||||||
const [includeFee, setIncludeFee] = useState(false);
|
|
||||||
|
|
||||||
// Max amount given selected asset and from account
|
// Max amount given selected asset and from account
|
||||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||||
|
|
||||||
const transferAmount = useMemo(() => {
|
const fee = useMemo(
|
||||||
if (!amount) return undefined;
|
() => feeFactor && new BigNumber(feeFactor).times(amount).toString(),
|
||||||
if (includeFee && feeFactor) {
|
[amount, 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(
|
const onSubmit = useCallback(
|
||||||
(fields: FormFields) => {
|
(fields: FormFields) => {
|
||||||
if (!transferAmount) {
|
if (!amount) {
|
||||||
throw new Error('Submitted transfer with no amount selected');
|
throw new Error('Submitted transfer with no amount selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +157,7 @@ export const TransferForm = ({
|
|||||||
|
|
||||||
const transfer = normalizeTransfer(
|
const transfer = normalizeTransfer(
|
||||||
fields.toVegaKey,
|
fields.toVegaKey,
|
||||||
transferAmount,
|
amount,
|
||||||
type,
|
type,
|
||||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||||
{
|
{
|
||||||
@ -183,7 +167,7 @@ export const TransferForm = ({
|
|||||||
);
|
);
|
||||||
submitTransfer(transfer);
|
submitTransfer(transfer);
|
||||||
},
|
},
|
||||||
[submitTransfer, transferAmount, assets]
|
[submitTransfer, amount, assets]
|
||||||
);
|
);
|
||||||
|
|
||||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||||
@ -279,7 +263,6 @@ export const TransferForm = ({
|
|||||||
) {
|
) {
|
||||||
setValue('toVegaKey', pubKey);
|
setValue('toVegaKey', pubKey);
|
||||||
setToVegaKeyMode('select');
|
setToVegaKeyMode('select');
|
||||||
setIncludeFee(false);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -449,27 +432,9 @@ export const TransferForm = ({
|
|||||||
</TradingInputError>
|
</TradingInputError>
|
||||||
)}
|
)}
|
||||||
</TradingFormGroup>
|
</TradingFormGroup>
|
||||||
<div className="mb-4">
|
{amount && fee && (
|
||||||
<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
|
<TransferFee
|
||||||
amount={transferAmount}
|
amount={amount}
|
||||||
transferAmount={transferAmount}
|
|
||||||
feeFactor={feeFactor}
|
feeFactor={feeFactor}
|
||||||
fee={fromVested ? '0' : fee}
|
fee={fromVested ? '0' : fee}
|
||||||
decimals={asset?.decimals}
|
decimals={asset?.decimals}
|
||||||
@ -484,29 +449,22 @@ export const TransferForm = ({
|
|||||||
|
|
||||||
export const TransferFee = ({
|
export const TransferFee = ({
|
||||||
amount,
|
amount,
|
||||||
transferAmount,
|
|
||||||
feeFactor,
|
feeFactor,
|
||||||
fee,
|
fee,
|
||||||
decimals,
|
decimals,
|
||||||
}: {
|
}: {
|
||||||
amount: string;
|
amount: string;
|
||||||
transferAmount: string;
|
|
||||||
feeFactor: string | null;
|
feeFactor: string | null;
|
||||||
fee?: string;
|
fee?: string;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
if (!feeFactor || !amount || !transferAmount || !fee) return null;
|
if (!feeFactor || !amount || !fee) return null;
|
||||||
if (
|
if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) {
|
||||||
isNaN(Number(feeFactor)) ||
|
|
||||||
isNaN(Number(amount)) ||
|
|
||||||
isNaN(Number(transferAmount)) ||
|
|
||||||
isNaN(Number(fee))
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
|
const totalValue = new BigNumber(amount).plus(fee).toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 flex flex-col gap-2 text-xs">
|
<div className="mb-4 flex flex-col gap-2 text-xs">
|
||||||
|
@ -7,6 +7,7 @@ import { AssetsDocument, type AssetsQuery } from './__generated__/Assets';
|
|||||||
import { AssetStatus } from '@vegaprotocol/types';
|
import { AssetStatus } from '@vegaprotocol/types';
|
||||||
import { type Asset } from './asset-data-provider';
|
import { type Asset } from './asset-data-provider';
|
||||||
import { DENY_LIST } from './constants';
|
import { DENY_LIST } from './constants';
|
||||||
|
import { type AssetFieldsFragment } from './__generated__/Asset';
|
||||||
|
|
||||||
export interface BuiltinAssetSource {
|
export interface BuiltinAssetSource {
|
||||||
__typename: 'BuiltinAsset';
|
__typename: 'BuiltinAsset';
|
||||||
@ -89,3 +90,24 @@ export const useEnabledAssets = () => {
|
|||||||
variables: undefined,
|
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,27 +3,13 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
|||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||||
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
|
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import {
|
import {
|
||||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
|
||||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
|
||||||
TOTAL_MARGIN_AVAILABLE,
|
|
||||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
|
||||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { KeyValue } from './key-value';
|
import { KeyValue } from './key-value';
|
||||||
import {
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
Accordion,
|
|
||||||
AccordionChevron,
|
|
||||||
AccordionPanel,
|
|
||||||
ExternalLink,
|
|
||||||
Tooltip,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useT, ns } from '../../use-t';
|
import { useT, ns } from '../../use-t';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
@ -31,9 +17,9 @@ import { emptyValue } from './deal-ticket-fee-details';
|
|||||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||||
|
|
||||||
export interface DealTicketMarginDetailsProps {
|
export interface DealTicketMarginDetailsProps {
|
||||||
generalAccountBalance?: string;
|
generalAccountBalance: string;
|
||||||
marginAccountBalance?: string;
|
marginAccountBalance: string;
|
||||||
orderMarginAccountBalance?: string;
|
orderMarginAccountBalance: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
assetSymbol: string;
|
assetSymbol: string;
|
||||||
@ -54,118 +40,20 @@ export const DealTicketMarginDetails = ({
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||||
const { pubKey: partyId } = useVegaWallet();
|
const { pubKey: partyId } = useVegaWallet();
|
||||||
const { data: currentMargins } = useDataProvider({
|
|
||||||
dataProvider: marketMarginDataProvider,
|
|
||||||
variables: { marketId: market.id, partyId: partyId || '' },
|
|
||||||
skip: !partyId,
|
|
||||||
});
|
|
||||||
const liquidationEstimate = positionEstimate?.liquidation;
|
const liquidationEstimate = positionEstimate?.liquidation;
|
||||||
const marginEstimate = positionEstimate?.margin;
|
|
||||||
const totalMarginAccountBalance =
|
const totalMarginAccountBalance =
|
||||||
BigInt(marginAccountBalance || '0') +
|
BigInt(marginAccountBalance || '0') +
|
||||||
BigInt(orderMarginAccountBalance || '0');
|
BigInt(orderMarginAccountBalance || '0');
|
||||||
const totalBalance =
|
|
||||||
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
|
|
||||||
const asset = getAsset(market);
|
const asset = getAsset(market);
|
||||||
const { decimals: assetDecimals, quantum } = asset;
|
const { decimals: assetDecimals, quantum } = asset;
|
||||||
let marginRequiredBestCase: string | undefined = undefined;
|
|
||||||
let marginRequiredWorstCase: string | undefined = undefined;
|
|
||||||
const 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);
|
|
||||||
|
|
||||||
marginRequiredBestCase = (
|
const collateralIncreaseEstimateBestCase = BigInt(
|
||||||
marginEstimateBestCase - currentMargin
|
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
|
||||||
).toString();
|
);
|
||||||
if (marginRequiredBestCase.startsWith('-')) {
|
const collateralIncreaseEstimateWorstCase = BigInt(
|
||||||
marginRequiredBestCase = '0';
|
positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0'
|
||||||
}
|
);
|
||||||
|
|
||||||
marginRequiredWorstCase = (
|
|
||||||
marginEstimateWorstCase - currentMargin
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
if (marginRequiredWorstCase.startsWith('-')) {
|
|
||||||
marginRequiredWorstCase = '0';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
marginRequiredBestCase = marginEstimateBestCase.toString();
|
|
||||||
marginRequiredWorstCase = marginEstimateWorstCase.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalMarginAvailable = (
|
|
||||||
currentMargins
|
|
||||||
? totalBalance - BigInt(currentMargins.maintenanceLevel)
|
|
||||||
: totalBalance
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
let deductionFromCollateral = null;
|
|
||||||
let projectedMargin = null;
|
|
||||||
if (totalMarginAccountBalance) {
|
|
||||||
const deductionFromCollateralBestCase =
|
|
||||||
marginEstimateBestCase - totalMarginAccountBalance;
|
|
||||||
|
|
||||||
const deductionFromCollateralWorstCase =
|
|
||||||
marginEstimateWorstCase - totalMarginAccountBalance;
|
|
||||||
|
|
||||||
deductionFromCollateral = (
|
|
||||||
<KeyValue
|
|
||||||
indent
|
|
||||||
label={t('Deduction from collateral')}
|
|
||||||
value={formatRange(
|
|
||||||
deductionFromCollateralBestCase > 0
|
|
||||||
? deductionFromCollateralBestCase.toString()
|
|
||||||
: '0',
|
|
||||||
deductionFromCollateralWorstCase > 0
|
|
||||||
? deductionFromCollateralWorstCase.toString()
|
|
||||||
: '0',
|
|
||||||
assetDecimals
|
|
||||||
)}
|
|
||||||
formattedValue={formatValue(
|
|
||||||
deductionFromCollateralWorstCase > 0
|
|
||||||
? deductionFromCollateralWorstCase.toString()
|
|
||||||
: '0',
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
)}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
labelDescription={t(
|
|
||||||
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
|
|
||||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
|
||||||
{ assetSymbol }
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
projectedMargin = (
|
|
||||||
<KeyValue
|
|
||||||
label={t('Projected margin')}
|
|
||||||
value={formatRange(
|
|
||||||
marginEstimateBestCase.toString(),
|
|
||||||
marginEstimateWorstCase.toString(),
|
|
||||||
assetDecimals
|
|
||||||
)}
|
|
||||||
formattedValue={formatValue(
|
|
||||||
marginEstimateWorstCase.toString(),
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
)}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
labelDescription={t(
|
|
||||||
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
|
|
||||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let liquidationPriceEstimate = emptyValue;
|
let liquidationPriceEstimate = emptyValue;
|
||||||
let liquidationPriceEstimateRange = emptyValue;
|
let liquidationPriceEstimateRange = emptyValue;
|
||||||
@ -222,128 +110,50 @@ export const DealTicketMarginDetails = ({
|
|||||||
const quoteName = getQuoteName(market);
|
const quoteName = getQuoteName(market);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full gap-2">
|
<div className="flex flex-col w-full gap-2 mt-2">
|
||||||
<Accordion>
|
|
||||||
<AccordionPanel
|
|
||||||
itemId="margin"
|
|
||||||
trigger={
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
data-testid="accordion-toggle"
|
|
||||||
className={classNames(
|
|
||||||
'w-full pt-2',
|
|
||||||
'flex items-center gap-2 text-xs',
|
|
||||||
'group'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-testid={`deal-ticket-fee-margin-required`}
|
|
||||||
key={'value-dropdown'}
|
|
||||||
className="flex items-center justify-between w-full gap-2"
|
|
||||||
>
|
|
||||||
<div className="flex items-center text-left gap-1">
|
|
||||||
<Tooltip
|
|
||||||
description={t(
|
|
||||||
'MARGIN_DIFF_TOOLTIP_TEXT',
|
|
||||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
|
||||||
{ assetSymbol }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="text-muted">{t('Margin required')}</span>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<AccordionChevron size={10} />
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
formatRange(
|
|
||||||
marginRequiredBestCase,
|
|
||||||
marginRequiredWorstCase,
|
|
||||||
assetDecimals
|
|
||||||
) ?? '-'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="font-mono text-right">
|
|
||||||
{formatValue(
|
|
||||||
marginRequiredWorstCase,
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
)}{' '}
|
|
||||||
{assetSymbol || ''}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col w-full gap-2">
|
|
||||||
<KeyValue
|
|
||||||
label={t('Total margin available')}
|
|
||||||
indent
|
|
||||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
|
||||||
formattedValue={formatValue(
|
|
||||||
totalMarginAvailable,
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
)}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
labelDescription={t(
|
|
||||||
'TOTAL_MARGIN_AVAILABLE',
|
|
||||||
TOTAL_MARGIN_AVAILABLE,
|
|
||||||
{
|
|
||||||
generalAccountBalance: formatValue(
|
|
||||||
generalAccountBalance,
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
),
|
|
||||||
marginAccountBalance: formatValue(
|
|
||||||
marginAccountBalance,
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
),
|
|
||||||
orderMarginAccountBalance: formatValue(
|
|
||||||
orderMarginAccountBalance,
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
),
|
|
||||||
marginMaintenance: formatValue(
|
|
||||||
currentMargins?.maintenanceLevel,
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
),
|
|
||||||
assetSymbol,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{deductionFromCollateral}
|
|
||||||
<KeyValue
|
|
||||||
label={t('Current margin allocation')}
|
|
||||||
indent
|
|
||||||
onClick={
|
|
||||||
generalAccountBalance
|
|
||||||
? () => setBreakdownDialog(true)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
value={formatValue(
|
|
||||||
totalMarginAccountBalance.toString(),
|
|
||||||
assetDecimals
|
|
||||||
)}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
labelDescription={t(
|
|
||||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
|
||||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
|
||||||
)}
|
|
||||||
formattedValue={formatValue(
|
|
||||||
totalMarginAccountBalance.toString(),
|
|
||||||
assetDecimals,
|
|
||||||
quantum
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionPanel>
|
|
||||||
</Accordion>
|
|
||||||
{projectedMargin}
|
|
||||||
<KeyValue
|
<KeyValue
|
||||||
label={t('Liquidation')}
|
label={t('Current margin')}
|
||||||
|
onClick={
|
||||||
|
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
|
||||||
|
}
|
||||||
|
value={formatValue(totalMarginAccountBalance.toString(), assetDecimals)}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
labelDescription={t(
|
||||||
|
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||||
|
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||||
|
)}
|
||||||
|
formattedValue={formatValue(
|
||||||
|
totalMarginAccountBalance.toString(),
|
||||||
|
assetDecimals,
|
||||||
|
quantum
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<KeyValue
|
||||||
|
label={t('Available collateral')}
|
||||||
|
value={formatValue(generalAccountBalance, assetDecimals)}
|
||||||
|
formattedValue={formatValue(
|
||||||
|
generalAccountBalance.toString(),
|
||||||
|
assetDecimals,
|
||||||
|
quantum
|
||||||
|
)}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
/>
|
||||||
|
<KeyValue
|
||||||
|
label={t('Additional margin required')}
|
||||||
|
value={formatRange(
|
||||||
|
collateralIncreaseEstimateBestCase.toString(),
|
||||||
|
collateralIncreaseEstimateWorstCase.toString(),
|
||||||
|
assetDecimals
|
||||||
|
)}
|
||||||
|
formattedValue={formatValue(
|
||||||
|
collateralIncreaseEstimateBestCase.toString(),
|
||||||
|
assetDecimals,
|
||||||
|
quantum
|
||||||
|
)}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
/>
|
||||||
|
<KeyValue
|
||||||
|
label={t('Liquidation estimate')}
|
||||||
value={liquidationPriceEstimateRange}
|
value={liquidationPriceEstimateRange}
|
||||||
formattedValue={liquidationPriceEstimate}
|
formattedValue={liquidationPriceEstimate}
|
||||||
symbol={quoteName}
|
symbol={quoteName}
|
||||||
|
@ -73,7 +73,7 @@ import {
|
|||||||
} from '../../hooks';
|
} from '../../hooks';
|
||||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||||
import { KeyValue } from './key-value';
|
import { KeyValue } from './key-value';
|
||||||
import { DocsLinks } from '@vegaprotocol/environment';
|
import { DocsLinks } from '@vegaprotocol/environment';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
@ -177,12 +177,6 @@ export const DealTicket = ({
|
|||||||
loading: loadingGeneralAccountBalance,
|
loading: loadingGeneralAccountBalance,
|
||||||
} = useAccountBalance(asset.id);
|
} = useAccountBalance(asset.id);
|
||||||
|
|
||||||
const balance = (
|
|
||||||
BigInt(marginAccountBalance) +
|
|
||||||
BigInt(generalAccountBalance) +
|
|
||||||
BigInt(orderMarginAccountBalance)
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
const { marketState, marketTradingMode } = marketData;
|
const { marketState, marketTradingMode } = marketData;
|
||||||
const timeInForce = watch('timeInForce');
|
const timeInForce = watch('timeInForce');
|
||||||
|
|
||||||
@ -729,17 +723,11 @@ export const DealTicket = ({
|
|||||||
error={summaryError}
|
error={summaryError}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
marketTradingMode={marketData.marketTradingMode}
|
marketTradingMode={marketData.marketTradingMode}
|
||||||
balance={balance}
|
balance={generalAccountBalance}
|
||||||
margin={(
|
margin={
|
||||||
BigInt(
|
positionEstimate?.estimatePosition?.collateralIncreaseEstimate
|
||||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
.bestCase || '0'
|
||||||
'0'
|
}
|
||||||
) +
|
|
||||||
BigInt(
|
|
||||||
positionEstimate?.estimatePosition?.margin.bestCase
|
|
||||||
.orderMarginLevel || '0'
|
|
||||||
)
|
|
||||||
).toString()}
|
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
pubKey={pubKey}
|
pubKey={pubKey}
|
||||||
onDeposit={onDeposit}
|
onDeposit={onDeposit}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import classnames from 'classnames';
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface KeyValuePros {
|
export interface KeyValuePros {
|
||||||
@ -19,7 +18,6 @@ export const KeyValue = ({
|
|||||||
value,
|
value,
|
||||||
labelDescription,
|
labelDescription,
|
||||||
symbol,
|
symbol,
|
||||||
indent,
|
|
||||||
onClick,
|
onClick,
|
||||||
formattedValue,
|
formattedValue,
|
||||||
}: KeyValuePros) => {
|
}: KeyValuePros) => {
|
||||||
@ -43,10 +41,7 @@ export const KeyValue = ({
|
|||||||
: id
|
: id
|
||||||
}`}
|
}`}
|
||||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||||
className={classnames(
|
className="text-xs flex justify-between items-center gap-4 flex-wrap text-right"
|
||||||
'text-xs flex justify-between items-center gap-4 flex-wrap text-right',
|
|
||||||
{ 'ml-2': indent }
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Tooltip description={labelDescription}>
|
<Tooltip description={labelDescription}>
|
||||||
<div className="text-muted text-left">{label}</div>
|
<div className="text-muted text-left">{label}</div>
|
||||||
|
@ -29,6 +29,7 @@ import { usePositionEstimate } from '../../hooks/use-position-estimate';
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
||||||
import { NoWalletWarning } from './deal-ticket';
|
import { NoWalletWarning } from './deal-ticket';
|
||||||
|
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
|
||||||
|
|
||||||
const defaultLeverage = 10;
|
const defaultLeverage = 10;
|
||||||
|
|
||||||
@ -93,66 +94,78 @@ export const MarginChange = ({
|
|||||||
},
|
},
|
||||||
skip
|
skip
|
||||||
);
|
);
|
||||||
if (
|
if (!asset || !estimateMargin?.estimatePosition) {
|
||||||
!asset ||
|
|
||||||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
|
|
||||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const collateralIncreaseEstimate = BigInt(
|
const collateralIncreaseEstimate = BigInt(
|
||||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
||||||
);
|
);
|
||||||
if (!collateralIncreaseEstimate) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let positionWarning = '';
|
let positionWarning = '';
|
||||||
if (orders?.length && openVolume !== '0') {
|
|
||||||
positionWarning = t(
|
|
||||||
'youHaveOpenPositionAndOrders',
|
|
||||||
'You have an existing position and open orders on this market.',
|
|
||||||
{
|
|
||||||
count: orders.length,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (!orders?.length) {
|
|
||||||
positionWarning = t('You have an existing position on this market.');
|
|
||||||
} else {
|
|
||||||
positionWarning = t(
|
|
||||||
'youHaveOpenOrders',
|
|
||||||
'You have open orders on this market.',
|
|
||||||
{
|
|
||||||
count: orders.length,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let marginChangeWarning = '';
|
let marginChangeWarning = '';
|
||||||
const amount = addDecimalsFormatNumber(
|
if (collateralIncreaseEstimate) {
|
||||||
collateralIncreaseEstimate.toString(),
|
if (orders?.length && openVolume !== '0') {
|
||||||
asset?.decimals
|
positionWarning = t(
|
||||||
);
|
'youHaveOpenPositionAndOrders',
|
||||||
const { symbol } = asset;
|
'You have an existing position and open orders on this market.',
|
||||||
const interpolation = { amount, symbol };
|
{
|
||||||
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
|
count: orders.length,
|
||||||
marginChangeWarning = t(
|
}
|
||||||
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
|
);
|
||||||
interpolation
|
} else if (!orders?.length) {
|
||||||
);
|
positionWarning = t('You have an existing position on this market.');
|
||||||
} else {
|
} else {
|
||||||
marginChangeWarning = t(
|
positionWarning = t(
|
||||||
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
|
'youHaveOpenOrders',
|
||||||
interpolation
|
'You have open orders on this market.',
|
||||||
|
{
|
||||||
|
count: orders.length,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = addDecimalsFormatNumber(
|
||||||
|
collateralIncreaseEstimate.toString(),
|
||||||
|
asset?.decimals
|
||||||
);
|
);
|
||||||
|
const { symbol } = asset;
|
||||||
|
const interpolation = { amount, symbol };
|
||||||
|
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
|
||||||
|
marginChangeWarning = t(
|
||||||
|
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||||
|
interpolation
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
marginChangeWarning = t(
|
||||||
|
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
|
||||||
|
interpolation
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Notification
|
{positionWarning && marginChangeWarning && (
|
||||||
intent={Intent.Warning}
|
<Notification
|
||||||
message={
|
intent={Intent.Warning}
|
||||||
<>
|
message={
|
||||||
<p>{positionWarning}</p>
|
<>
|
||||||
<p>{marginChangeWarning}</p>
|
<p>{positionWarning}</p>
|
||||||
</>
|
<p>{marginChangeWarning}</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DealTicketMarginDetails
|
||||||
|
marginAccountBalance={marginAccountBalance}
|
||||||
|
generalAccountBalance={generalAccountBalance}
|
||||||
|
orderMarginAccountBalance={orderMarginAccountBalance}
|
||||||
|
assetSymbol={asset.symbol}
|
||||||
|
market={market}
|
||||||
|
positionEstimate={estimateMargin.estimatePosition}
|
||||||
|
side={
|
||||||
|
openVolume.startsWith('-')
|
||||||
|
? Schema.Side.SIDE_SELL
|
||||||
|
: Schema.Side.SIDE_BUY
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@ import type {
|
|||||||
} from '../hooks/use-form-values';
|
} from '../hooks/use-form-values';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||||
import { isPersistentOrder } from './time-in-force-persistance';
|
import { isPersistentOrder } from './time-in-force-persistence';
|
||||||
|
|
||||||
export const mapFormValuesToOrderSubmission = (
|
export const mapFormValuesToOrderSubmission = (
|
||||||
order: OrderFormValues,
|
order: OrderFormValues,
|
||||||
|
@ -2,9 +2,9 @@ import { OrderTimeInForce } from '@vegaprotocol/types';
|
|||||||
import {
|
import {
|
||||||
isNonPersistentOrder,
|
isNonPersistentOrder,
|
||||||
isPersistentOrder,
|
isPersistentOrder,
|
||||||
} from './time-in-force-persistance';
|
} from './time-in-force-persistence';
|
||||||
|
|
||||||
it('isNonPeristentOrder', () => {
|
it('isNonPersistentOrder', () => {
|
||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
|
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
|
||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
||||||
@ -13,7 +13,7 @@ it('isNonPeristentOrder', () => {
|
|||||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('isPeristentOrder', () => {
|
it('isPersistentOrder', () => {
|
||||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
|
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
|
||||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
||||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
@ -23,7 +23,6 @@ import {
|
|||||||
SUBSCRIPTION_TIMEOUT,
|
SUBSCRIPTION_TIMEOUT,
|
||||||
useNodeBasicStatus,
|
useNodeBasicStatus,
|
||||||
useNodeSubscriptionStatus,
|
useNodeSubscriptionStatus,
|
||||||
useResponseTime,
|
|
||||||
} from './row-data';
|
} from './row-data';
|
||||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||||
import { CUSTOM_NODE_KEY } from '../../types';
|
import { CUSTOM_NODE_KEY } from '../../types';
|
||||||
@ -162,19 +161,6 @@ describe('useNodeBasicStatus', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useResponseTime', () => {
|
|
||||||
it('returns response time when url is valid', () => {
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useResponseTime('https://localhost:1234')
|
|
||||||
);
|
|
||||||
expect(result.current.responseTime).toBe(50);
|
|
||||||
});
|
|
||||||
it('does not return response time when url is invalid', () => {
|
|
||||||
const { result } = renderHook(() => useResponseTime('nope'));
|
|
||||||
expect(result.current.responseTime).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('RowData', () => {
|
describe('RowData', () => {
|
||||||
const props = {
|
const props = {
|
||||||
id: '0',
|
id: '0',
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { isValidUrl } from '@vegaprotocol/utils';
|
|
||||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { CUSTOM_NODE_KEY } from '../../types';
|
import { CUSTOM_NODE_KEY } from '../../types';
|
||||||
@ -8,6 +7,7 @@ import {
|
|||||||
} from '../../utils/__generated__/NodeCheck';
|
} from '../../utils/__generated__/NodeCheck';
|
||||||
import { LayoutCell } from './layout-cell';
|
import { LayoutCell } from './layout-cell';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
|
import { useResponseTime } from '../../utils/time';
|
||||||
|
|
||||||
export const POLL_INTERVAL = 1000;
|
export const POLL_INTERVAL = 1000;
|
||||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||||
@ -108,20 +108,6 @@ export const useNodeBasicStatus = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
|
||||||
const [responseTime, setResponseTime] = useState<number>();
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isValidUrl(url)) return;
|
|
||||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
|
||||||
const requestUrl = new URL(url);
|
|
||||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
|
||||||
const { duration } =
|
|
||||||
(requests.length && requests[requests.length - 1]) || {};
|
|
||||||
setResponseTime(duration);
|
|
||||||
}, [url, trigger]);
|
|
||||||
return { responseTime };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RowData = ({
|
export const RowData = ({
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
getUserEnabledFeatureFlags,
|
getUserEnabledFeatureFlags,
|
||||||
setUserEnabledFeatureFlag,
|
setUserEnabledFeatureFlag,
|
||||||
} from './use-environment';
|
} from './use-environment';
|
||||||
|
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||||
|
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
/* no op*/
|
/* no op*/
|
||||||
@ -17,6 +18,10 @@ const noop = () => {
|
|||||||
|
|
||||||
jest.mock('@vegaprotocol/apollo-client');
|
jest.mock('@vegaprotocol/apollo-client');
|
||||||
jest.mock('zustand');
|
jest.mock('zustand');
|
||||||
|
jest.mock('../utils/time');
|
||||||
|
|
||||||
|
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
|
||||||
|
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
|
||||||
|
|
||||||
const mockCreateClient = createClient as jest.Mock;
|
const mockCreateClient = createClient as jest.Mock;
|
||||||
const createDefaultMockClient = () => {
|
const createDefaultMockClient = () => {
|
||||||
@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
|||||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||||
const fastWait = 1000;
|
const fastWait = 1000;
|
||||||
const nodes = [slowNode, fastNode];
|
const nodes = [slowNode, fastNode];
|
||||||
|
|
||||||
|
mockCanMeasureResponseTime.mockImplementation(() => true);
|
||||||
|
mockMeasureResponseTime.mockImplementation((url: string) => {
|
||||||
|
if (url === slowNode) return slowWait;
|
||||||
|
if (url === fastNode) return fastWait;
|
||||||
|
return Infinity;
|
||||||
|
});
|
||||||
|
|
||||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||||
|
|
||||||
@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
|||||||
statistics: {
|
statistics: {
|
||||||
chainId: 'chain-id',
|
chainId: 'chain-id',
|
||||||
blockHeight: '100',
|
blockHeight: '100',
|
||||||
vegaTime: new Date().toISOString(),
|
vegaTime: new Date(1).toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
|||||||
expect(result.current.nodes).toEqual(nodes);
|
expect(result.current.nodes).toEqual(nodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.runAllTimers();
|
jest.advanceTimersByTime(2000);
|
||||||
|
// jest.runAllTimers();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.status).toEqual('success');
|
expect(result.current.status).toEqual('success');
|
||||||
|
@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
|||||||
import { envSchema } from '../utils/validate-environment';
|
import { envSchema } from '../utils/validate-environment';
|
||||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import first from 'lodash/first';
|
||||||
|
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||||
|
|
||||||
type Client = ReturnType<typeof createClient>;
|
type Client = ReturnType<typeof createClient>;
|
||||||
type ClientCollection = {
|
type ClientCollection = {
|
||||||
@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
|||||||
|
|
||||||
const VERSION = 1;
|
const VERSION = 1;
|
||||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||||
|
|
||||||
|
const QUERY_TIMEOUT = 3000;
|
||||||
const SUBSCRIPTION_TIMEOUT = 3000;
|
const SUBSCRIPTION_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
const raceAgainst = (timeout: number): Promise<false> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(false);
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch and validate a vega node configuration
|
* Fetch and validate a vega node configuration
|
||||||
*/
|
*/
|
||||||
@ -64,53 +76,88 @@ const fetchConfig = async (url?: string) => {
|
|||||||
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||||
try {
|
try {
|
||||||
const url = await Promise.any(tests);
|
const nodes = await Promise.all(tests);
|
||||||
return url;
|
const responsiveNodes = nodes
|
||||||
} catch {
|
.filter(([, q, s]) => q && s)
|
||||||
|
.map(([url, q]) => {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
...q,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// more recent and faster at the top
|
||||||
|
const ordered = orderBy(
|
||||||
|
responsiveNodes,
|
||||||
|
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
|
||||||
|
['desc', 'desc', 'asc']
|
||||||
|
);
|
||||||
|
|
||||||
|
const best = first(ordered);
|
||||||
|
return best ? best.url : null;
|
||||||
|
} catch (err) {
|
||||||
// All tests rejected, no suitable node found
|
// All tests rejected, no suitable node found
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Maybe<T> = T | false;
|
||||||
|
type QueryTestResult = {
|
||||||
|
blockHeight: number;
|
||||||
|
vegaTime: Date;
|
||||||
|
responseTime: number;
|
||||||
|
};
|
||||||
|
type SubscriptionTestResult = true;
|
||||||
|
type NodeTestResult = [
|
||||||
|
/** url */
|
||||||
|
string,
|
||||||
|
Maybe<QueryTestResult>,
|
||||||
|
Maybe<SubscriptionTestResult>
|
||||||
|
];
|
||||||
/**
|
/**
|
||||||
* Test a node for suitability for connection
|
* Test a node for suitability for connection
|
||||||
*/
|
*/
|
||||||
const testNode = async (
|
const testNode = async (
|
||||||
url: string,
|
url: string,
|
||||||
client: Client
|
client: Client
|
||||||
): Promise<string | null> => {
|
): Promise<NodeTestResult> => {
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
// these promises will only resolve with true/false
|
testQuery(client, url),
|
||||||
testQuery(client),
|
|
||||||
testSubscription(client),
|
testSubscription(client),
|
||||||
]);
|
]);
|
||||||
if (results[0] && results[1]) {
|
return [url, ...results];
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = `Tests failed for node: ${url}`;
|
|
||||||
console.warn(message);
|
|
||||||
|
|
||||||
// throwing here will mean this tests is ignored and a different
|
|
||||||
// node that hopefully does resolve will fulfill the Promise.any
|
|
||||||
throw new Error(message);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a test query on a client
|
* Run a test query on a client
|
||||||
*/
|
*/
|
||||||
const testQuery = async (client: Client) => {
|
const testQuery = (
|
||||||
try {
|
client: Client,
|
||||||
const result = await client.query<NodeCheckQuery>({
|
url: string
|
||||||
query: NodeCheckDocument,
|
): Promise<Maybe<QueryTestResult>> => {
|
||||||
});
|
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||||
if (!result || result.error) {
|
client
|
||||||
return false;
|
.query<NodeCheckQuery>({
|
||||||
}
|
query: NodeCheckDocument,
|
||||||
return true;
|
})
|
||||||
} catch (err) {
|
.then((result) => {
|
||||||
return false;
|
if (result && !result.error) {
|
||||||
}
|
const res = {
|
||||||
|
blockHeight: Number(result.data.statistics.blockHeight),
|
||||||
|
vegaTime: new Date(result.data.statistics.vegaTime),
|
||||||
|
// only after a request has been sent we can retrieve the response time
|
||||||
|
responseTime: canMeasureResponseTime(url)
|
||||||
|
? measureResponseTime(url) || Infinity
|
||||||
|
: Infinity,
|
||||||
|
} as QueryTestResult;
|
||||||
|
resolve(res);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => resolve(false))
|
||||||
|
);
|
||||||
|
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,7 +165,9 @@ const testQuery = async (client: Client) => {
|
|||||||
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||||
* is deemed a failure
|
* is deemed a failure
|
||||||
*/
|
*/
|
||||||
const testSubscription = (client: Client) => {
|
const testSubscription = (
|
||||||
|
client: Client
|
||||||
|
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const sub = client
|
const sub = client
|
||||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
.subscribe<NodeCheckTimeUpdateSubscription>({
|
||||||
|
22
libs/environment/src/utils/time.spec.ts
Normal file
22
libs/environment/src/utils/time.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useResponseTime } from './time';
|
||||||
|
|
||||||
|
const mockResponseTime = 50;
|
||||||
|
global.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
duration: mockResponseTime,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe('useResponseTime', () => {
|
||||||
|
it('returns response time when url is valid', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useResponseTime('https://localhost:1234')
|
||||||
|
);
|
||||||
|
expect(result.current.responseTime).toBe(50);
|
||||||
|
});
|
||||||
|
it('does not return response time when url is invalid', () => {
|
||||||
|
const { result } = renderHook(() => useResponseTime('nope'));
|
||||||
|
expect(result.current.responseTime).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
25
libs/environment/src/utils/time.ts
Normal file
25
libs/environment/src/utils/time.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { isValidUrl } from '@vegaprotocol/utils';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||||
|
const [responseTime, setResponseTime] = useState<number>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canMeasureResponseTime(url)) return;
|
||||||
|
const duration = measureResponseTime(url);
|
||||||
|
setResponseTime(duration);
|
||||||
|
}, [url, trigger]);
|
||||||
|
return { responseTime };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canMeasureResponseTime = (url: string) => {
|
||||||
|
if (!isValidUrl(url)) return false;
|
||||||
|
if (typeof window.performance.getEntriesByName !== 'function') return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const measureResponseTime = (url: string) => {
|
||||||
|
const requestUrl = new URL(url);
|
||||||
|
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||||
|
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||||
|
return duration;
|
||||||
|
};
|
@ -21,7 +21,6 @@
|
|||||||
"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.",
|
"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",
|
"Enter manually": "Enter manually",
|
||||||
"From account": "From account",
|
"From account": "From account",
|
||||||
"Include transfer fee": "Include transfer fee",
|
|
||||||
"initial level": "initial level",
|
"initial level": "initial level",
|
||||||
"maintenance level": "maintenance level",
|
"maintenance level": "maintenance level",
|
||||||
"Margin health": "Margin health",
|
"Margin health": "Margin health",
|
||||||
@ -33,7 +32,6 @@
|
|||||||
"release level": "release level",
|
"release level": "release level",
|
||||||
"search level": "search level",
|
"search level": "search level",
|
||||||
"Select from wallet": "Select from wallet",
|
"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 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 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)",
|
"The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)",
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Create a team": "Create a team",
|
"Create a team": "Create a team",
|
||||||
"Create a simple referral code to enjoy the referrer commission outlined in the current referral program": "Create a simple referral code to enjoy the referrer commission outlined in the current referral program",
|
"Create a simple referral code to enjoy the referrer commission outlined in the current referral program": "Create a simple referral code to enjoy the referrer commission outlined in the current referral program",
|
||||||
|
"Create solo team": "Create solo team",
|
||||||
"Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards",
|
"Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards",
|
||||||
"Current tier": "Current tier",
|
"Current tier": "Current tier",
|
||||||
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
|
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
|
||||||
@ -181,6 +182,7 @@
|
|||||||
"Markets": "Markets",
|
"Markets": "Markets",
|
||||||
"Members": "Members",
|
"Members": "Members",
|
||||||
"Members ({{count}})": "Members ({{count}})",
|
"Members ({{count}})": "Members ({{count}})",
|
||||||
|
"Member ID": "Member ID",
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
||||||
"Min. epochs": "Min. epochs",
|
"Min. epochs": "Min. epochs",
|
||||||
@ -363,6 +365,7 @@
|
|||||||
"Type": "Type",
|
"Type": "Type",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Unknown settlement date": "Unknown settlement date",
|
"Unknown settlement date": "Unknown settlement date",
|
||||||
|
"Update team": "Update team",
|
||||||
"URL": "URL",
|
"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",
|
"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",
|
"Vega chart": "Vega chart",
|
||||||
@ -414,6 +417,7 @@
|
|||||||
"myVolume_other": "My volume (last {{count}} epochs)",
|
"myVolume_other": "My volume (last {{count}} epochs)",
|
||||||
"numberEpochs": "{{count}} epochs",
|
"numberEpochs": "{{count}} epochs",
|
||||||
"numberEpochs_one": "{{count}} epoch",
|
"numberEpochs_one": "{{count}} epoch",
|
||||||
|
"Rewards earned": "Rewards earned",
|
||||||
"Rewards paid out": "Rewards paid out",
|
"Rewards paid out": "Rewards paid out",
|
||||||
"{{reward}}x": "{{reward}}x",
|
"{{reward}}x": "{{reward}}x",
|
||||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||||
@ -428,5 +432,19 @@
|
|||||||
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
||||||
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
||||||
"{{distance}} ago": "{{distance}} ago",
|
"{{distance}} ago": "{{distance}} ago",
|
||||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision"
|
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision",
|
||||||
|
"My team": "My team",
|
||||||
|
"Profile": "Profile",
|
||||||
|
"Last {{games}} games result_one": "Last game result",
|
||||||
|
"Last {{games}} games result_other": "Last {{games}} games result",
|
||||||
|
"Leaderboard": "Leaderboard",
|
||||||
|
"View all teams": "View all teams",
|
||||||
|
"Competitions": "Competitions",
|
||||||
|
"Be a team player! Participate in games and work together to rake in as much profit to win.": "Be a team player! Participate in games and work together to rake in as much profit to win.",
|
||||||
|
"Create a public team": "Create a public team",
|
||||||
|
"Create a private team": "Create a private team",
|
||||||
|
"Choose a team": "Choose a team",
|
||||||
|
"Join a team": "Join a team",
|
||||||
|
"Solo team / lone wolf": "Solo team / lone wolf",
|
||||||
|
"Choose a team to get involved": "Choose a team to get involved"
|
||||||
}
|
}
|
||||||
|
@ -47,5 +47,11 @@
|
|||||||
"Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.": "Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.",
|
"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",
|
"Withdrawals ready": "Withdrawals ready",
|
||||||
"You have no assets to withdraw": "You have no assets to withdraw",
|
"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>"
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,15 @@ interface Props {
|
|||||||
initialValue?: string[];
|
initialValue?: string[];
|
||||||
isHeader?: boolean;
|
isHeader?: boolean;
|
||||||
noUpdate?: boolean;
|
noUpdate?: boolean;
|
||||||
// to render nothing instead of '-' when there is no price change
|
// render prop for no price change
|
||||||
hideZero?: boolean;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Last24hPriceChange = ({
|
export const Last24hPriceChange = ({
|
||||||
marketId,
|
marketId,
|
||||||
decimalPlaces,
|
decimalPlaces,
|
||||||
initialValue,
|
initialValue,
|
||||||
hideZero,
|
children,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { oneDayCandles, error, fiveDaysCandles } = useCandles({
|
const { oneDayCandles, error, fiveDaysCandles } = useCandles({
|
||||||
@ -37,10 +37,6 @@ export const Last24hPriceChange = ({
|
|||||||
fiveDaysCandles.length > 0 &&
|
fiveDaysCandles.length > 0 &&
|
||||||
(!oneDayCandles || oneDayCandles?.length === 0)
|
(!oneDayCandles || oneDayCandles?.length === 0)
|
||||||
) {
|
) {
|
||||||
// render nothing instead of '-' when there is no price change
|
|
||||||
if (hideZero) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
description={
|
description={
|
||||||
@ -55,24 +51,19 @@ export const Last24hPriceChange = ({
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>-</span>
|
<span>{children}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !isNumeric(decimalPlaces)) {
|
if (error || !isNumeric(decimalPlaces)) {
|
||||||
return <span>-</span>;
|
return <span>{children}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
|
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
|
||||||
const change = priceChange(candles);
|
const change = priceChange(candles);
|
||||||
const changePercentage = priceChangePercentage(candles);
|
const changePercentage = priceChangePercentage(candles);
|
||||||
|
|
||||||
// render nothing instead of '-' when there is no price change
|
|
||||||
if (!change && !changePercentage && hideZero) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { calcCandleVolume } from '../../market-utils';
|
import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
|
||||||
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
isNumeric,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useCandles } from '../../hooks';
|
import { useCandles } from '../../hooks';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
@ -9,13 +13,17 @@ interface Props {
|
|||||||
positionDecimalPlaces?: number;
|
positionDecimalPlaces?: number;
|
||||||
formatDecimals?: number;
|
formatDecimals?: number;
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
|
marketDecimals?: number;
|
||||||
|
quoteUnit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Last24hVolume = ({
|
export const Last24hVolume = ({
|
||||||
marketId,
|
marketId,
|
||||||
|
marketDecimals,
|
||||||
positionDecimalPlaces,
|
positionDecimalPlaces,
|
||||||
formatDecimals,
|
formatDecimals,
|
||||||
initialValue,
|
initialValue,
|
||||||
|
quoteUnit,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { oneDayCandles, fiveDaysCandles } = useCandles({
|
const { oneDayCandles, fiveDaysCandles } = useCandles({
|
||||||
@ -28,6 +36,11 @@ export const Last24hVolume = ({
|
|||||||
(!oneDayCandles || oneDayCandles?.length === 0)
|
(!oneDayCandles || oneDayCandles?.length === 0)
|
||||||
) {
|
) {
|
||||||
const candleVolume = calcCandleVolume(fiveDaysCandles);
|
const candleVolume = calcCandleVolume(fiveDaysCandles);
|
||||||
|
const candleVolumePrice = calcCandleVolumePrice(
|
||||||
|
fiveDaysCandles,
|
||||||
|
marketDecimals,
|
||||||
|
positionDecimalPlaces
|
||||||
|
);
|
||||||
const candleVolumeValue =
|
const candleVolumeValue =
|
||||||
candleVolume && isNumeric(positionDecimalPlaces)
|
candleVolume && isNumeric(positionDecimalPlaces)
|
||||||
? addDecimalsFormatNumber(
|
? addDecimalsFormatNumber(
|
||||||
@ -42,8 +55,8 @@ export const Last24hVolume = ({
|
|||||||
<div>
|
<div>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
{t(
|
{t(
|
||||||
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}',
|
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} ({{candleVolumePrice}} {{quoteUnit}})',
|
||||||
{ candleVolumeValue }
|
{ candleVolumeValue, candleVolumePrice, quoteUnit }
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -57,10 +70,18 @@ export const Last24hVolume = ({
|
|||||||
? calcCandleVolume(oneDayCandles)
|
? calcCandleVolume(oneDayCandles)
|
||||||
: initialValue;
|
: initialValue;
|
||||||
|
|
||||||
|
const candleVolumePrice = oneDayCandles
|
||||||
|
? calcCandleVolumePrice(
|
||||||
|
oneDayCandles,
|
||||||
|
marketDecimals,
|
||||||
|
positionDecimalPlaces
|
||||||
|
)
|
||||||
|
: initialValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
description={t(
|
description={t(
|
||||||
'The total number of contracts traded in the last 24 hours.'
|
'The total number of contracts traded in the last 24 hours. (Total value of contracts traded in the last 24 hours)'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@ -70,7 +91,12 @@ export const Last24hVolume = ({
|
|||||||
positionDecimalPlaces,
|
positionDecimalPlaces,
|
||||||
formatDecimals
|
formatDecimals
|
||||||
)
|
)
|
||||||
: '-'}
|
: '-'}{' '}
|
||||||
|
(
|
||||||
|
{candleVolumePrice && isNumeric(positionDecimalPlaces)
|
||||||
|
? formatNumber(candleVolumePrice, formatDecimals)
|
||||||
|
: '-'}{' '}
|
||||||
|
{quoteUnit})
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
@ -155,6 +155,7 @@ export const MarketVolumeInfoPanel = ({ market }: MarketInfoProps) => {
|
|||||||
<Last24hVolume
|
<Last24hVolume
|
||||||
marketId={market.id}
|
marketId={market.id}
|
||||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||||
|
marketDecimals={market.decimalPlaces}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
openInterest: dash(data?.openInterest),
|
openInterest: dash(data?.openInterest),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
|
import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
|
||||||
import {
|
import {
|
||||||
|
calcCandleVolumePrice,
|
||||||
calcTradedFactor,
|
calcTradedFactor,
|
||||||
filterAndSortMarkets,
|
filterAndSortMarkets,
|
||||||
sumFeesFactors,
|
sumFeesFactors,
|
||||||
@ -145,3 +146,31 @@ describe('sumFeesFactors', () => {
|
|||||||
).toEqual(0.6);
|
).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,4 +1,8 @@
|
|||||||
import { formatNumberPercentage, toBigNum } from '@vegaprotocol/utils';
|
import {
|
||||||
|
addDecimal,
|
||||||
|
formatNumberPercentage,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
@ -147,10 +151,51 @@ export const calcCandleHigh = (candles: Candle[]): string | undefined => {
|
|||||||
.toString();
|
.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 =>
|
export const calcCandleVolume = (candles: Candle[]): string | undefined =>
|
||||||
candles &&
|
candles &&
|
||||||
candles.reduce((acc, c) => new BigNumber(acc).plus(c.volume).toString(), '0');
|
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) => {
|
export const calcTradedFactor = (m: MarketMaybeWithDataAndCandles) => {
|
||||||
const volume = Number(calcCandleVolume(m.candles || []) || 0);
|
const volume = Number(calcCandleVolume(m.candles || []) || 0);
|
||||||
const price = m.data?.markPrice ? Number(m.data.markPrice) : 0;
|
const price = m.data?.markPrice ? Number(m.data.markPrice) : 0;
|
||||||
|
@ -67,26 +67,6 @@ query EstimatePosition(
|
|||||||
# we can set this variable to true so that we can format with market.decimalPlaces
|
# we can set this variable to true so that we can format with market.decimalPlaces
|
||||||
scaleLiquidationPriceToMarketDecimals: true
|
scaleLiquidationPriceToMarketDecimals: true
|
||||||
) {
|
) {
|
||||||
margin {
|
|
||||||
worstCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
bestCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collateralIncreaseEstimate {
|
collateralIncreaseEstimate {
|
||||||
worstCase
|
worstCase
|
||||||
bestCase
|
bestCase
|
||||||
|
22
libs/positions/src/lib/__generated__/Positions.ts
generated
22
libs/positions/src/lib/__generated__/Positions.ts
generated
@ -33,7 +33,7 @@ export type EstimatePositionQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string } }, collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
|
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
|
||||||
|
|
||||||
export const PositionFieldsFragmentDoc = gql`
|
export const PositionFieldsFragmentDoc = gql`
|
||||||
fragment PositionFields on Position {
|
fragment PositionFields on Position {
|
||||||
@ -144,26 +144,6 @@ export const EstimatePositionDocument = gql`
|
|||||||
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
||||||
scaleLiquidationPriceToMarketDecimals: true
|
scaleLiquidationPriceToMarketDecimals: true
|
||||||
) {
|
) {
|
||||||
margin {
|
|
||||||
worstCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
bestCase {
|
|
||||||
maintenanceLevel
|
|
||||||
searchLevel
|
|
||||||
initialLevel
|
|
||||||
collateralReleaseLevel
|
|
||||||
marginMode
|
|
||||||
marginFactor
|
|
||||||
orderMarginLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collateralIncreaseEstimate {
|
collateralIncreaseEstimate {
|
||||||
worstCase
|
worstCase
|
||||||
bestCase
|
bestCase
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { EstimatePositionQuery } from './__generated__/Positions';
|
import type { EstimatePositionQuery } from './__generated__/Positions';
|
||||||
import { MarginMode } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
export const estimatePositionQuery = (
|
export const estimatePositionQuery = (
|
||||||
override?: PartialDeep<EstimatePositionQuery>
|
override?: PartialDeep<EstimatePositionQuery>
|
||||||
@ -9,26 +8,6 @@ export const estimatePositionQuery = (
|
|||||||
const defaultResult: EstimatePositionQuery = {
|
const defaultResult: EstimatePositionQuery = {
|
||||||
estimatePosition: {
|
estimatePosition: {
|
||||||
__typename: 'PositionEstimate',
|
__typename: 'PositionEstimate',
|
||||||
margin: {
|
|
||||||
bestCase: {
|
|
||||||
collateralReleaseLevel: '1000000',
|
|
||||||
initialLevel: '500000',
|
|
||||||
maintenanceLevel: '200000',
|
|
||||||
searchLevel: '300000',
|
|
||||||
marginFactor: '1',
|
|
||||||
orderMarginLevel: '0',
|
|
||||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
|
||||||
},
|
|
||||||
worstCase: {
|
|
||||||
collateralReleaseLevel: '1100000',
|
|
||||||
initialLevel: '600000',
|
|
||||||
maintenanceLevel: '300000',
|
|
||||||
searchLevel: '400000',
|
|
||||||
marginFactor: '1',
|
|
||||||
orderMarginLevel: '0',
|
|
||||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
collateralIncreaseEstimate: {
|
collateralIncreaseEstimate: {
|
||||||
bestCase: '0',
|
bestCase: '0',
|
||||||
worstCase: '0',
|
worstCase: '0',
|
||||||
|
@ -30,26 +30,6 @@ describe('LiquidationPrice', () => {
|
|||||||
result: {
|
result: {
|
||||||
data: {
|
data: {
|
||||||
estimatePosition: {
|
estimatePosition: {
|
||||||
margin: {
|
|
||||||
worstCase: {
|
|
||||||
maintenanceLevel: '100',
|
|
||||||
searchLevel: '100',
|
|
||||||
initialLevel: '100',
|
|
||||||
collateralReleaseLevel: '100',
|
|
||||||
orderMarginLevel: '0',
|
|
||||||
marginFactor: '0',
|
|
||||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
|
||||||
},
|
|
||||||
bestCase: {
|
|
||||||
maintenanceLevel: '100',
|
|
||||||
searchLevel: '100',
|
|
||||||
initialLevel: '100',
|
|
||||||
collateralReleaseLevel: '100',
|
|
||||||
orderMarginLevel: '0',
|
|
||||||
marginFactor: '0',
|
|
||||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
collateralIncreaseEstimate: {
|
collateralIncreaseEstimate: {
|
||||||
bestCase: '0',
|
bestCase: '0',
|
||||||
worstCase: '0',
|
worstCase: '0',
|
||||||
|
@ -41,16 +41,17 @@ export const TradingView = ({
|
|||||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const widgetRef = useRef<IChartingLibraryWidget>();
|
const widgetRef = useRef<IChartingLibraryWidget>();
|
||||||
|
|
||||||
const datafeed = useDatafeed();
|
|
||||||
|
|
||||||
const prevMarketId = usePrevious(marketId);
|
const prevMarketId = usePrevious(marketId);
|
||||||
const prevTheme = usePrevious(theme);
|
const prevTheme = usePrevious(theme);
|
||||||
|
|
||||||
|
const datafeed = useDatafeed(marketId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Widget already created
|
// Widget already created
|
||||||
if (widgetRef.current !== undefined) {
|
if (widgetRef.current !== undefined) {
|
||||||
// Update the symbol if changed
|
// Update the symbol if changed
|
||||||
if (marketId !== prevMarketId) {
|
if (marketId !== prevMarketId) {
|
||||||
|
datafeed.setSymbol(marketId);
|
||||||
widgetRef.current.setSymbol(
|
widgetRef.current.setSymbol(
|
||||||
marketId,
|
marketId,
|
||||||
(interval ? interval : '15') as TVResolutionString,
|
(interval ? interval : '15') as TVResolutionString,
|
||||||
|
@ -44,14 +44,22 @@ const configurationData: DatafeedConfiguration = {
|
|||||||
supported_resolutions: supportedResolutions as ResolutionString[],
|
supported_resolutions: supportedResolutions as ResolutionString[],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const useDatafeed = () => {
|
// HACK: local handle for market id
|
||||||
|
let requestedSymbol: string | undefined = undefined;
|
||||||
|
|
||||||
|
export const useDatafeed = (marketId: string) => {
|
||||||
const hasHistory = useRef(false);
|
const hasHistory = useRef(false);
|
||||||
const subRef = useRef<Subscription>();
|
const subRef = useRef<Subscription>();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
const datafeed = useMemo(() => {
|
const datafeed = useMemo(() => {
|
||||||
const feed: IBasicDataFeed = {
|
const feed: IBasicDataFeed & { setSymbol: (symbol: string) => void } = {
|
||||||
|
setSymbol: (symbol: string) => {
|
||||||
|
// re-setting the symbol so it could be consumed by `resolveSymbol`
|
||||||
|
requestedSymbol = symbol;
|
||||||
|
},
|
||||||
onReady: (callback) => {
|
onReady: (callback) => {
|
||||||
|
requestedSymbol = marketId;
|
||||||
setTimeout(() => callback(configurationData));
|
setTimeout(() => callback(configurationData));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -68,7 +76,7 @@ export const useDatafeed = () => {
|
|||||||
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
||||||
query: SymbolDocument,
|
query: SymbolDocument,
|
||||||
variables: {
|
variables: {
|
||||||
marketId,
|
marketId: requestedSymbol || marketId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -242,7 +250,7 @@ export const useDatafeed = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return feed;
|
return feed;
|
||||||
}, [client]);
|
}, [client, marketId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
2
libs/types/src/__generated__/types.ts
generated
2
libs/types/src/__generated__/types.ts
generated
@ -4682,7 +4682,7 @@ export type QuantumRewardsPerEpoch = {
|
|||||||
/** Epoch for which this information is valid. */
|
/** Epoch for which this information is valid. */
|
||||||
epoch: Scalars['Int'];
|
epoch: Scalars['Int'];
|
||||||
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
|
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
|
||||||
total_quantum_rewards: Scalars['String'];
|
totalQuantumRewards: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||||
|
42
libs/utils/src/lib/format/ether.spec.ts
Normal file
42
libs/utils/src/lib/format/ether.spec.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
84
libs/utils/src/lib/format/ether.ts
Normal file
84
libs/utils/src/lib/format/ether.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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,3 +4,4 @@ export * from './range';
|
|||||||
export * from './size';
|
export * from './size';
|
||||||
export * from './strings';
|
export * from './strings';
|
||||||
export * from './trigger';
|
export * from './trigger';
|
||||||
|
export * from './ether';
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
toDecimal,
|
toDecimal,
|
||||||
toNumberParts,
|
toNumberParts,
|
||||||
formatNumberRounded,
|
formatNumberRounded,
|
||||||
|
toQUSD,
|
||||||
} from './number';
|
} from './number';
|
||||||
|
|
||||||
describe('number utils', () => {
|
describe('number utils', () => {
|
||||||
@ -282,3 +283,22 @@ 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(
|
export function toBigNum(
|
||||||
rawValue: string | number,
|
rawValue: string | number | BigNumber,
|
||||||
decimals: number
|
decimals: number
|
||||||
): BigNumber {
|
): BigNumber {
|
||||||
const divides = new BigNumber(10).exponentiatedBy(decimals);
|
const divides = new BigNumber(10).exponentiatedBy(decimals);
|
||||||
@ -233,3 +233,24 @@ export const formatNumberRounded = (
|
|||||||
|
|
||||||
return value;
|
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,6 +19,7 @@ export * from './lib/use-ethereum-transaction';
|
|||||||
export * from './lib/use-ethereum-withdraw-approval-toasts';
|
export * from './lib/use-ethereum-withdraw-approval-toasts';
|
||||||
export * from './lib/use-ethereum-withdraw-approvals-manager';
|
export * from './lib/use-ethereum-withdraw-approvals-manager';
|
||||||
export * from './lib/use-ethereum-withdraw-approvals-store';
|
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-delay';
|
||||||
export * from './lib/use-get-withdraw-threshold';
|
export * from './lib/use-get-withdraw-threshold';
|
||||||
export * from './lib/use-token-contract';
|
export * from './lib/use-token-contract';
|
||||||
|
111
libs/web3/src/lib/use-gas-price.ts
Normal file
111
libs/web3/src/lib/use-gas-price.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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,6 +27,7 @@ import { useForm, Controller, useWatch } from 'react-hook-form';
|
|||||||
import { WithdrawLimits } from './withdraw-limits';
|
import { WithdrawLimits } from './withdraw-limits';
|
||||||
import {
|
import {
|
||||||
ETHEREUM_EAGER_CONNECT,
|
ETHEREUM_EAGER_CONNECT,
|
||||||
|
type GasData,
|
||||||
useWeb3ConnectStore,
|
useWeb3ConnectStore,
|
||||||
useWeb3Disconnect,
|
useWeb3Disconnect,
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
@ -56,6 +57,7 @@ export interface WithdrawFormProps {
|
|||||||
delay: number | undefined;
|
delay: number | undefined;
|
||||||
onSelectAsset: (assetId: string) => void;
|
onSelectAsset: (assetId: string) => void;
|
||||||
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
||||||
|
gasPrice?: GasData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WithdrawDelayNotification = ({
|
const WithdrawDelayNotification = ({
|
||||||
@ -117,6 +119,7 @@ export const WithdrawForm = ({
|
|||||||
delay,
|
delay,
|
||||||
onSelectAsset,
|
onSelectAsset,
|
||||||
submitWithdraw,
|
submitWithdraw,
|
||||||
|
gasPrice,
|
||||||
}: WithdrawFormProps) => {
|
}: WithdrawFormProps) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const ethereumAddress = useEthereumAddress();
|
const ethereumAddress = useEthereumAddress();
|
||||||
@ -247,6 +250,7 @@ export const WithdrawForm = ({
|
|||||||
delay={delay}
|
delay={delay}
|
||||||
balance={balance}
|
balance={balance}
|
||||||
asset={selectedAsset}
|
asset={selectedAsset}
|
||||||
|
gas={gasPrice}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from '@vegaprotocol/assets';
|
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT, useWETH } from '@vegaprotocol/assets';
|
||||||
import {
|
import {
|
||||||
KeyValueTable,
|
KeyValueTable,
|
||||||
KeyValueTableRow,
|
KeyValueTableRow,
|
||||||
@ -9,6 +9,16 @@ import {
|
|||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { useT } from './use-t';
|
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 {
|
interface WithdrawLimitsProps {
|
||||||
amount: string;
|
amount: string;
|
||||||
@ -16,6 +26,7 @@ interface WithdrawLimitsProps {
|
|||||||
balance: BigNumber;
|
balance: BigNumber;
|
||||||
delay: number | undefined;
|
delay: number | undefined;
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
|
gas?: GasData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithdrawLimits = ({
|
export const WithdrawLimits = ({
|
||||||
@ -24,6 +35,7 @@ export const WithdrawLimits = ({
|
|||||||
balance,
|
balance,
|
||||||
delay,
|
delay,
|
||||||
asset,
|
asset,
|
||||||
|
gas,
|
||||||
}: WithdrawLimitsProps) => {
|
}: WithdrawLimitsProps) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const delayTime =
|
const delayTime =
|
||||||
@ -64,6 +76,24 @@ export const WithdrawLimits = ({
|
|||||||
label: t('Delay time'),
|
label: t('Delay time'),
|
||||||
value: threshold && delay ? delayTime : '-',
|
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 (
|
return (
|
||||||
@ -91,3 +121,117 @@ export const WithdrawLimits = ({
|
|||||||
</KeyValueTable>
|
</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,6 +38,7 @@ jest.mock('@vegaprotocol/web3', () => ({
|
|||||||
useGetWithdrawDelay: () => {
|
useGetWithdrawDelay: () => {
|
||||||
return () => Promise.resolve(10000);
|
return () => Promise.resolve(10000);
|
||||||
},
|
},
|
||||||
|
useGasPrice: () => undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('WithdrawManager', () => {
|
describe('WithdrawManager', () => {
|
||||||
|
@ -4,6 +4,7 @@ import { WithdrawForm } from './withdraw-form';
|
|||||||
import type { Asset } from '@vegaprotocol/assets';
|
import type { Asset } from '@vegaprotocol/assets';
|
||||||
import type { AccountFieldsFragment } from '@vegaprotocol/accounts';
|
import type { AccountFieldsFragment } from '@vegaprotocol/accounts';
|
||||||
import { useWithdrawAsset } from './use-withdraw-asset';
|
import { useWithdrawAsset } from './use-withdraw-asset';
|
||||||
|
import { ContractMethod, useGasPrice } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
export interface WithdrawManagerProps {
|
export interface WithdrawManagerProps {
|
||||||
assets: Asset[];
|
assets: Asset[];
|
||||||
@ -20,6 +21,8 @@ export const WithdrawManager = ({
|
|||||||
}: WithdrawManagerProps) => {
|
}: WithdrawManagerProps) => {
|
||||||
const { asset, balance, min, threshold, delay, handleSelectAsset } =
|
const { asset, balance, min, threshold, delay, handleSelectAsset } =
|
||||||
useWithdrawAsset(assets, accounts, assetId);
|
useWithdrawAsset(assets, accounts, assetId);
|
||||||
|
const gasPrice = useGasPrice(ContractMethod.WITHDRAW_ASSET);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithdrawForm
|
<WithdrawForm
|
||||||
selectedAsset={asset}
|
selectedAsset={asset}
|
||||||
@ -30,6 +33,7 @@ export const WithdrawManager = ({
|
|||||||
submitWithdraw={submit}
|
submitWithdraw={submit}
|
||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
|
gasPrice={gasPrice}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
4
nx.json
4
nx.json
@ -11,7 +11,9 @@
|
|||||||
"echo $NX_VEGA_URL",
|
"echo $NX_VEGA_URL",
|
||||||
"echo $NX_TENDERMINT_URL",
|
"echo $NX_TENDERMINT_URL",
|
||||||
"echo $NX_TENDERMINT_WEBSOCKET_URL",
|
"echo $NX_TENDERMINT_WEBSOCKET_URL",
|
||||||
"echo $NX_ETHEREUM_PROVIDER_URL"
|
"echo $NX_ETHEREUM_PROVIDER_URL",
|
||||||
|
"echo $NX_CHARTING_LIBRARY_PATH",
|
||||||
|
"echo $NX_CHARTING_LIBRARY_HASH"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,22 +40,12 @@
|
|||||||
|
|
||||||
## Transfer
|
## 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 "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 "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** 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>)
|
- **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
|
### Transfer page
|
||||||
|
Loading…
Reference in New Issue
Block a user