Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9691e0950 | ||
|
|
688eb6f5cb | ||
|
|
09778a5d3f | ||
|
|
59993e3ea6 | ||
|
|
54161a6dba | ||
|
|
26f524ea07 | ||
|
|
db13e6f21d | ||
|
|
4a679813ba | ||
|
|
a4279d5b5e | ||
|
|
1a274a67c3 | ||
|
|
1e9ff31fd7 | ||
|
|
aa2b755b93 | ||
|
|
4a7296312a | ||
|
|
1418a2259f | ||
|
|
7e5ef0157a | ||
|
|
0cf764f11e | ||
|
|
1c626aeeb2 | ||
|
|
c4cec47b0b | ||
|
|
fbe1233178 | ||
|
|
d4abb4db39 | ||
|
|
fc994ba52b | ||
|
|
1a67c41bb4 | ||
|
|
fc66240390 | ||
|
|
fc0da98683 | ||
|
|
4d0dafb113 | ||
|
|
eda431be9e | ||
|
|
4ff1749e54 | ||
|
|
e7a108fd7a | ||
|
|
5d3f2ea7d8 | ||
|
|
1ab27574ca | ||
|
|
0ea3c43e78 | ||
|
|
9e183568b0 | ||
|
|
69fd5b26b6 | ||
|
|
2d5b926609 | ||
|
|
98a2ba79a8 | ||
|
|
0e6e986937 | ||
|
|
b3b86f95c2 | ||
|
|
15d6ea0c05 | ||
|
|
0358ce6293 | ||
|
|
0791be4636 | ||
|
|
07442848ee | ||
|
|
d5f0bd419e | ||
|
|
2c7934a8c7 | ||
|
|
fae61c28ca | ||
|
|
fcd4541625 | ||
|
|
8e0a92fdec | ||
|
|
6e7109ed87 | ||
|
|
f2c698ce06 | ||
|
|
ad9a3a3400 | ||
|
|
efeac49d58 | ||
|
|
2d430710ab | ||
|
|
bca1a98985 | ||
|
|
8a080d3279 | ||
|
|
f121836b4e | ||
|
|
b641c82ad8 | ||
|
|
bf73559c30 | ||
|
|
653cec2592 | ||
|
|
579c884a5a | ||
|
|
b2279c7e47 | ||
|
|
a8c17b6807 | ||
|
|
b4c7dc6f59 |
@@ -33,5 +33,4 @@ jobs:
|
||||
config: baseUrl=${{ github.event.inputs.url }}
|
||||
env: grepTags=@live
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
######
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
|
||||
@@ -148,6 +148,7 @@ jobs:
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
vegaprotocol/${{ matrix.app }}:${{ github.ref_name }}
|
||||
vegaprotocol/${{ matrix.app }}:mainnet
|
||||
|
||||
# bucket creation in github.com/vegaprotocol/terraform//frontend
|
||||
- name: Publish dist to s3
|
||||
@@ -174,3 +175,53 @@ jobs:
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
files: ${{ matrix.app }}-ipfs-hash
|
||||
|
||||
- name: Trigger fleek deployment
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
# display info about app
|
||||
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
|
||||
https://api.fleek.co/graphql
|
||||
|
||||
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
|
||||
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
|
||||
https://api.fleek.co/graphql
|
||||
|
||||
- name: Checkout vega.xyz
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: vegaprotocol/vega.xyz
|
||||
path: './vega-xyz'
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
|
||||
- name: Update hash on interstitial page
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||
tar -xzf kubo.tgz
|
||||
export PATH="$PATH:$PWD/kubo"
|
||||
which ipfs
|
||||
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
|
||||
cd vega-xyz
|
||||
./interstital-allow-update.sh ${new_hash}
|
||||
git config --global user.email "vega-ci-bot@vega.xyz"
|
||||
git config --global user.name "vega-ci-bot"
|
||||
git add interstitial-allow.json netlify.toml
|
||||
commit_msg="feat(ci): Update CID for console release @ ${{ github.ref }}"
|
||||
git commit -m "${commit_msg}"
|
||||
git push origin main
|
||||
|
||||
# branch_name=${{ github.ref_name }}-hash-update
|
||||
# git checkout -b "${branch_name}"
|
||||
# git add interstitial-allow.json netlify.toml
|
||||
# git push -u origin "${branch_name}"
|
||||
# pr_url="$(gh pr create --title "${commit_msg}" --body 'update ipfs hash for console @ ${{ github.ref }}')"
|
||||
# echo $pr_url
|
||||
# gh pr merge --admin --auto $pr_url
|
||||
|
||||
@@ -11,7 +11,7 @@ recalculate-ipfs:
|
||||
echo "ipfs hash inside the image"
|
||||
docker run --rm ${TAG} cat /ipfs-hash
|
||||
echo "recalculating ipfs hash"
|
||||
docker run --rm ${TAG} ipfs add -rw /usr/share/nginx/html
|
||||
docker run --rm ${TAG} ipfs add -r /usr/share/nginx/html
|
||||
|
||||
.PHONY: eject-ipfs-hash
|
||||
unpack:
|
||||
|
||||
+8
-2
@@ -1,3 +1,5 @@
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
|
||||
export const ErrorCodes = new Map([
|
||||
[51, 'Transaction failed validation'],
|
||||
@@ -28,7 +30,11 @@ export const ChainResponseCode = ({
|
||||
}: ChainResponseCodeProps) => {
|
||||
const isSuccess = successCodes.has(code);
|
||||
|
||||
const icon = isSuccess ? '✅' : '❌';
|
||||
const icon = isSuccess ? (
|
||||
<Icon name="tick-circle" className="fill-vega-green-550" />
|
||||
) : (
|
||||
<Icon name="cross" className="fill-vega-pink-550" />
|
||||
);
|
||||
const label = ErrorCodes.get(code) || 'Unknown response code';
|
||||
|
||||
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
|
||||
@@ -36,7 +42,7 @@ export const ChainResponseCode = ({
|
||||
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
||||
|
||||
return (
|
||||
<div title={`Response code: ${code} - ${label}`} className="inline-block">
|
||||
<div title={`Response code: ${code} - ${label}`} className=" inline-block">
|
||||
<span
|
||||
className="mr-2"
|
||||
aria-label={isSuccess ? 'Success' : 'Warning'}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import ProposalLink from '../../links/proposal-link/proposal-link';
|
||||
import { VoteIcon } from '../../vote-icon/vote-icon';
|
||||
|
||||
interface TxProposalVoteProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -30,27 +31,22 @@ export const TxProposalVote = ({
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
|
||||
const vote = txData.command.voteSubmission.value === 'VALUE_YES';
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Proposal ID')}</TableCell>
|
||||
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Proposal details')}</TableCell>
|
||||
<TableCell>
|
||||
<ProposalLink id={txData.command.voteSubmission.proposalId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Proposal')}</TableCell>
|
||||
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Vote')}</TableCell>
|
||||
<TableCell>{vote}</TableCell>
|
||||
<TableCell>
|
||||
<VoteIcon vote={vote} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { components } from '../../../types/explorer';
|
||||
import { VoteIcon } from '../vote-icon/vote-icon';
|
||||
|
||||
interface TxOrderTypeProps {
|
||||
orderType: string;
|
||||
@@ -137,12 +138,15 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
||||
let type = displayString[orderType] || orderType;
|
||||
|
||||
let colours =
|
||||
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
|
||||
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250';
|
||||
|
||||
// This will get unwieldy and should probably produce a different colour of tag
|
||||
if (type === 'Chain Event' && !!command?.chainEvent) {
|
||||
type = getLabelForChainEvent(command.chainEvent);
|
||||
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
|
||||
} else if (type === 'Validator Heartbeat') {
|
||||
colours =
|
||||
'text-white dark-text-white bg-vega-light-200 dark:bg-vega-dark-100';
|
||||
} else if (type === 'Proposal' || type === 'Governance Proposal') {
|
||||
if (command && !!command.proposalSubmission) {
|
||||
type = getLabelForProposal(command.proposalSubmission);
|
||||
@@ -150,6 +154,16 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
||||
colours = 'text-black bg-vega-yellow';
|
||||
}
|
||||
|
||||
if (type === 'Vote on Proposal') {
|
||||
return (
|
||||
<VoteIcon
|
||||
vote={command?.voteSubmission?.value === 'VALUE_YES'}
|
||||
yesText="Proposal vote"
|
||||
noText="Proposal vote"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'Vote on Proposal' || type === 'Vote Submission') {
|
||||
colours = 'text-black bg-vega-yellow';
|
||||
}
|
||||
|
||||
@@ -98,6 +98,6 @@ describe('Txs infinite list item', () => {
|
||||
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
||||
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
|
||||
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success: ✅');
|
||||
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export const TxsInfiniteListItem = ({
|
||||
return (
|
||||
<div
|
||||
data-testid="transaction-row"
|
||||
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
|
||||
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10"
|
||||
>
|
||||
<div
|
||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
||||
@@ -83,7 +83,7 @@ export const TxsInfiniteListItem = ({
|
||||
data-testid="tx-success"
|
||||
>
|
||||
<span className="md:hidden uppercase text-vega-dark-300">
|
||||
Success:
|
||||
Success
|
||||
</span>
|
||||
{isNumber(code) ? (
|
||||
<ChainResponseCode code={code} hideLabel={true} />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { VoteIcon } from './vote-icon';
|
||||
|
||||
describe('Vote TX icon', () => {
|
||||
it('should use the text For by default for yes votes', () => {
|
||||
const yes = render(<VoteIcon vote={true} />);
|
||||
expect(yes.getByTestId('label')).toHaveTextContent('For');
|
||||
});
|
||||
|
||||
it('should use the yesText for yes votes if specified', () => {
|
||||
const yes = render(<VoteIcon vote={true} yesText="Test" />);
|
||||
expect(yes.getByTestId('label')).toHaveTextContent('Test');
|
||||
});
|
||||
|
||||
it('should display the tick icon for yes votes', () => {
|
||||
const no = render(<VoteIcon vote={true} />);
|
||||
expect(no.getByRole('img')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'tick-circle icon'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the text Against by default for no votes', () => {
|
||||
const no = render(<VoteIcon vote={false} />);
|
||||
expect(no.getByTestId('label')).toHaveTextContent('Against');
|
||||
});
|
||||
|
||||
it('should use the noText for no votes if specified', () => {
|
||||
const no = render(<VoteIcon vote={false} noText="Test" />);
|
||||
expect(no.getByTestId('label')).toHaveTextContent('Test');
|
||||
});
|
||||
|
||||
it('should display the delete icon for no votes', () => {
|
||||
const no = render(<VoteIcon vote={false} />);
|
||||
expect(no.getByRole('img')).toHaveAttribute('aria-label', 'delete icon');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconName } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export interface VoteIconProps {
|
||||
// True is a yes vote, false is undefined or no vorte
|
||||
vote: boolean;
|
||||
// Defaults to 'For', but can be any text
|
||||
yesText?: string;
|
||||
// Defaults to 'Against', but can be any text
|
||||
noText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a lozenge with an icon representing the way a user voted for a proposal.
|
||||
* The yes and no text can be overridden
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export function VoteIcon({
|
||||
vote,
|
||||
yesText = 'For',
|
||||
noText = 'Against',
|
||||
}: VoteIconProps) {
|
||||
const label = vote ? yesText : noText;
|
||||
const bg = vote ? 'bg-vega-green-550' : 'bg-vega-pink-550';
|
||||
const icon: IconName = vote ? 'tick-circle' : 'delete';
|
||||
const fill = vote ? 'vega-green-300' : 'vega-pink-300';
|
||||
const text = vote ? 'vega-green-200' : 'vega-pink-200';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`voteicon inline-block my-1 py-1 px-2 py rounded-md text-white leading-one sm align-top ${bg}`}
|
||||
>
|
||||
<Icon name={icon} size={3} className={`mr-2 p-0 fill-${fill}`} />
|
||||
<span className={`text-base text-${text}`} data-testid="label">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -60,3 +60,7 @@
|
||||
--ag-row-hover-color: theme(colors.neutral[800]);
|
||||
--ag-font-size: 12px;
|
||||
}
|
||||
|
||||
.voteicon svg {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ context(
|
||||
// 2001-STKE-002, 2001-STKE-032
|
||||
before('visit staking tab and connect vega wallet', function () {
|
||||
cy.visit('/');
|
||||
cy.validatorsSelfDelegate();
|
||||
ethereumWalletConnect();
|
||||
// this is a workaround for #2422 which can be removed once issue is resolved
|
||||
cy.associateTokensToVegaWallet('4');
|
||||
|
||||
@@ -109,7 +109,7 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-status').should(
|
||||
'have.text',
|
||||
'Approved '
|
||||
'Approved by validators '
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -136,7 +136,7 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-state').should(
|
||||
'have.text',
|
||||
'Approved'
|
||||
'Approved by validators'
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-release-tag').should(
|
||||
'have.text',
|
||||
|
||||
@@ -31,9 +31,6 @@ const totalPenaltyToolTip = '[data-testid="total-penalty-tooltip"]';
|
||||
const epochCountDown = '[data-testid="epoch-countdown"]';
|
||||
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
|
||||
|
||||
// If running locally, validators need to have self-stake to be displayed
|
||||
// Run cy.validatorsSelfDelegate() in before hook
|
||||
|
||||
context('Validators Page - verify elements on page', function () {
|
||||
before('navigate to validators page', function () {
|
||||
cy.visit('/validators');
|
||||
|
||||
@@ -33,4 +33,6 @@ before(() => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
});
|
||||
// Self stake validators so they are displayed
|
||||
cy.validatorsSelfDelegate();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
StakingBridge,
|
||||
Token,
|
||||
TokenVesting,
|
||||
TokenFaucetable,
|
||||
CollateralBridge,
|
||||
} from '@vegaprotocol/smart-contracts';
|
||||
import { ethers, Wallet } from 'ethers';
|
||||
@@ -41,7 +40,7 @@ export async function depositAsset(
|
||||
decimalPlaces: number
|
||||
) {
|
||||
// Approve asset
|
||||
const faucet = new TokenFaucetable(assetEthAddress, signer);
|
||||
const faucet = new Token(assetEthAddress, signer);
|
||||
cy.wrap(
|
||||
faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)),
|
||||
{
|
||||
@@ -62,7 +61,7 @@ export async function depositAsset(
|
||||
}
|
||||
|
||||
export async function faucetAsset(assetEthAddress: string) {
|
||||
const faucet = new TokenFaucetable(assetEthAddress, signer);
|
||||
const faucet = new Token(assetEthAddress, signer);
|
||||
await promiseWithTimeout(faucet.faucet(), 10 * 60 * 1000, 'faucet asset');
|
||||
}
|
||||
|
||||
|
||||
+53
-13
@@ -2,7 +2,6 @@ import './i18n';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { BrowserRouter as Router, useLocation } from 'react-router-dom';
|
||||
import { AppLoader } from './app-loader';
|
||||
import { NetworkInfo } from '@vegaprotocol/network-info';
|
||||
@@ -184,6 +183,10 @@ const ScrollToTop = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const removeQueryParams = (url: string) => {
|
||||
return url.split('?')[0];
|
||||
};
|
||||
|
||||
const AppContainer = () => {
|
||||
const { config, loading, error } = useEthereumConfig();
|
||||
const {
|
||||
@@ -201,25 +204,62 @@ const AppContainer = () => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ENV.dsn && telemetryOn) {
|
||||
if (ENV.dsn && telemetryOn === 'true') {
|
||||
Sentry.init({
|
||||
dsn: ENV.dsn,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0.1,
|
||||
enabled: true,
|
||||
environment: VEGA_ENV,
|
||||
release: GIT_COMMIT_HASH,
|
||||
beforeSend(event) {
|
||||
if (event.request?.url?.includes('/claim?')) {
|
||||
return {
|
||||
...event,
|
||||
request: {
|
||||
...event.request,
|
||||
url: event.request?.url.split('?')[0],
|
||||
},
|
||||
};
|
||||
beforeSend(event, hint) {
|
||||
const error = hint?.originalException;
|
||||
const errorIsString = typeof error === 'string';
|
||||
const errorIsObject = error instanceof Error;
|
||||
const requestUrl = event.request?.url;
|
||||
const transaction = event.transaction;
|
||||
|
||||
if (
|
||||
(errorIsString && error.includes('failed to get party ID')) ||
|
||||
(errorIsObject &&
|
||||
error?.message?.includes('failed to get party ID'))
|
||||
) {
|
||||
// This error is caused by a pubkey making an API request before
|
||||
// it has interacted with the chain. This isn't needed in Sentry.
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
|
||||
const updatedRequest =
|
||||
requestUrl && requestUrl.includes('/claim?')
|
||||
? { ...event.request, url: removeQueryParams(requestUrl) }
|
||||
: event.request;
|
||||
|
||||
const updatedTransaction =
|
||||
transaction && transaction.includes('/claim?')
|
||||
? removeQueryParams(transaction)
|
||||
: transaction;
|
||||
|
||||
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
|
||||
if (
|
||||
breadcrumb.type === 'navigation' &&
|
||||
breadcrumb.data?.to?.includes('/claim?')
|
||||
) {
|
||||
return {
|
||||
...breadcrumb,
|
||||
data: {
|
||||
...breadcrumb.data,
|
||||
to: removeQueryParams(breadcrumb.data.to),
|
||||
},
|
||||
};
|
||||
}
|
||||
return breadcrumb;
|
||||
});
|
||||
|
||||
return {
|
||||
...event,
|
||||
request: updatedRequest,
|
||||
transaction: updatedTransaction,
|
||||
breadcrumbs: updatedBreadcrumbs ?? event.breadcrumbs,
|
||||
};
|
||||
},
|
||||
});
|
||||
Sentry.setTag('branch', GIT_BRANCH);
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
|
||||
export const EthConnectPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { open } = useWeb3ConnectStore();
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
onClick={open}
|
||||
fill={true}
|
||||
data-testid="connect-to-eth-btn"
|
||||
>
|
||||
|
||||
@@ -2,10 +2,7 @@ import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
|
||||
export const EthWalletContainer = ({
|
||||
children,
|
||||
@@ -14,21 +11,12 @@ export const EthWalletContainer = ({
|
||||
}) => {
|
||||
const { account } = useWeb3React();
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { open } = useWeb3ConnectStore();
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className="w-full text-center">
|
||||
<Button
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('connectEthWallet')}
|
||||
</Button>
|
||||
<Button onClick={open}>{t('connectEthWallet')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ import {
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import colors from 'tailwindcss/colors';
|
||||
import { useBalances } from '../../lib/balances/balances-store';
|
||||
import { useEthereumConfig, useWeb3Disconnect } from '@vegaprotocol/web3';
|
||||
import {
|
||||
useEthereumConfig,
|
||||
useWeb3ConnectStore,
|
||||
useWeb3Disconnect,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { getChainName } from '@vegaprotocol/web3';
|
||||
|
||||
const removeLeadingAddressSymbol = (key: string) => {
|
||||
@@ -189,6 +193,7 @@ export const EthWallet = () => {
|
||||
const pendingTxs = usePendingTransactions();
|
||||
const disconnect = useWeb3Disconnect(connector);
|
||||
const { config } = useEthereumConfig();
|
||||
const { open } = useWeb3ConnectStore();
|
||||
|
||||
return (
|
||||
<WalletCard>
|
||||
@@ -233,12 +238,7 @@ export const EthWallet = () => {
|
||||
) : (
|
||||
<Button
|
||||
fill={true}
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
onClick={open}
|
||||
data-testid="connect-to-eth-wallet-button"
|
||||
>
|
||||
{t('connectEthWalletToAssociate')}
|
||||
|
||||
@@ -25,20 +25,26 @@ export function Web3Connector({
|
||||
connectors,
|
||||
chainId,
|
||||
}: Web3ConnectorProps) {
|
||||
const { appState, appDispatch } = useAppState();
|
||||
const { open, close, isOpen } = useWeb3ConnectStore();
|
||||
|
||||
const setDialogOpen = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
appDispatch({ type: AppStateActionType.SET_ETH_WALLET_OVERLAY, isOpen });
|
||||
if (isOpen) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
[appDispatch]
|
||||
[open, close]
|
||||
);
|
||||
|
||||
const appChainId = Number(chainId);
|
||||
return (
|
||||
<>
|
||||
<Web3Content appChainId={appChainId}>{children}</Web3Content>
|
||||
<Web3ConnectDialog
|
||||
connectors={connectors}
|
||||
dialogOpen={appState.ethConnectOverlay}
|
||||
dialogOpen={isOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
desiredChainId={appChainId}
|
||||
/>
|
||||
|
||||
@@ -34,9 +34,6 @@ export interface AppState {
|
||||
/** Whether or not the manage VEGA wallet overlay is open */
|
||||
vegaWalletManageOverlay: boolean;
|
||||
|
||||
/** Whether or not the connect to Ethereum wallet overlay is open */
|
||||
ethConnectOverlay: boolean;
|
||||
|
||||
/** Whether or not the transaction modal is open */
|
||||
transactionOverlay: boolean;
|
||||
/**
|
||||
@@ -57,7 +54,6 @@ export enum AppStateActionType {
|
||||
REFRESH_BALANCES,
|
||||
SET_VEGA_WALLET_OVERLAY,
|
||||
SET_VEGA_WALLET_MANAGE_OVERLAY,
|
||||
SET_ETH_WALLET_OVERLAY,
|
||||
SET_DRAWER,
|
||||
REFRESH_ASSOCIATED_BALANCES,
|
||||
SET_ASSOCIATION_BREAKDOWN,
|
||||
@@ -81,10 +77,6 @@ export type AppStateAction =
|
||||
type: AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY;
|
||||
isOpen: boolean;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY;
|
||||
isOpen: boolean;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_DRAWER;
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -16,7 +16,6 @@ const initialAppState: AppState = {
|
||||
totalSupply: new BigNumber(0),
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
@@ -45,12 +44,6 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
|
||||
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_ETH_WALLET_OVERLAY: {
|
||||
return {
|
||||
...state,
|
||||
ethConnectOverlay: action.isOpen,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_DRAWER: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"tokenVotes": "Token votes",
|
||||
"liquidityVotes": "Liquidity votes",
|
||||
"castYourVote": "Cast your vote",
|
||||
"yourVote": "Your vote",
|
||||
"for": "For",
|
||||
"against": "Against",
|
||||
"majorityRequired": "Majority Required",
|
||||
@@ -787,9 +788,9 @@
|
||||
"homeVegaTokenButtonText": "Manage tokens",
|
||||
"downloadProposalJson": "Download proposal as JSON",
|
||||
"networkUpgrade": "Network Upgrade",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved by validators",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Waiting for validator votes",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Declined by validators",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED": "Unspecified",
|
||||
"vegaRelease{release}": "Vega Release {{release}}",
|
||||
"upgradeBlockHeight": "Upgrade block height",
|
||||
|
||||
@@ -50,7 +50,22 @@ export const useTranches = create<TranchesStore>()((set) => ({
|
||||
?.map((t) => {
|
||||
const tranche_progress =
|
||||
t.duration !== 0 ? (now - t.cliff_start) / t.duration : 0;
|
||||
const lockedDecimal = tranche_progress < 0 ? 1 : 1 - tranche_progress;
|
||||
let lockedDecimal;
|
||||
if (t.duration !== 0) {
|
||||
if (tranche_progress < 0) {
|
||||
lockedDecimal = 1;
|
||||
} else {
|
||||
lockedDecimal = 1 - tranche_progress;
|
||||
}
|
||||
} else {
|
||||
if (now < t.cliff_start) {
|
||||
lockedDecimal = 1;
|
||||
} else {
|
||||
lockedDecimal = 0;
|
||||
}
|
||||
}
|
||||
const clampedLockedDecimal = Math.max(0, Math.min(1, lockedDecimal));
|
||||
|
||||
return {
|
||||
tranche_id: t.tranche_id,
|
||||
tranche_start: secondsToDate(t.cliff_start),
|
||||
@@ -60,7 +75,7 @@ export const useTranches = create<TranchesStore>()((set) => ({
|
||||
toBigNum(t.current_balance, decimals)
|
||||
),
|
||||
locked_amount: toBigNum(t.initial_balance, decimals).times(
|
||||
lockedDecimal
|
||||
clampedLockedDecimal
|
||||
),
|
||||
users: t.users,
|
||||
};
|
||||
|
||||
@@ -53,7 +53,6 @@ const mockAppState: AppState = {
|
||||
totalSupply: new BigNumber(65000000),
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
|
||||
@@ -24,7 +24,6 @@ const openProposalClosesNextMonth = generateProposal({
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextMonth.toString(),
|
||||
enactmentDatetime: nextMonth.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,7 +35,6 @@ const openProposalClosesNextWeek = generateProposal({
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +43,6 @@ const enactedProposalClosedLastWeek = generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,7 +51,6 @@ const failedProposalClosedLastMonth = generateProposal({
|
||||
state: ProposalState.STATE_FAILED,
|
||||
terms: {
|
||||
closingDatetime: lastMonth.toString(),
|
||||
enactmentDatetime: lastMonth.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -32,7 +32,9 @@ describe('ProtocolUpgradeProposalDetailInfo', () => {
|
||||
|
||||
it('should render the state', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
|
||||
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent(
|
||||
'Waiting for validator votes'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the vega release tag', () => {
|
||||
|
||||
+7
-1
@@ -26,28 +26,34 @@ describe('ProtocolUpgradeProposalsListItem', () => {
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-rejected',
|
||||
text: 'Declined by validators',
|
||||
},
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-pending',
|
||||
text: 'Waiting for validator votes',
|
||||
},
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-approved',
|
||||
text: 'Approved by validators',
|
||||
},
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
|
||||
text: 'Unspecified',
|
||||
},
|
||||
];
|
||||
|
||||
statuses.forEach(({ status, icon }) => {
|
||||
statuses.forEach(({ status, icon, text }) => {
|
||||
renderComponent({ ...proposal, status });
|
||||
const statusIcon = screen.getByTestId(icon);
|
||||
const textContent = screen.getByText(text);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(textContent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -111,7 +111,9 @@ describe('Vote buttons', () => {
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(
|
||||
screen.getByText('You need some VEGA tokens to participate in governance')
|
||||
screen.getByText(
|
||||
'You need some VEGA tokens to participate in governance.'
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,12 @@ import { format } from 'date-fns';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Button,
|
||||
ButtonLink,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { addDecimal, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import {
|
||||
@@ -161,7 +166,12 @@ export const VoteButtons = ({
|
||||
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
|
||||
<>
|
||||
{currentStakeAvailable.isLessThanOrEqualTo(0) && (
|
||||
<p data-testid="no-stake-available">{t('noGovernanceTokens')}</p>
|
||||
<>
|
||||
<p data-testid="no-stake-available">{t('noGovernanceTokens')}.</p>
|
||||
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
|
||||
{t('findOutMoreAboutHowToVote')}
|
||||
</ExternalLink>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4" data-testid="vote-buttons">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { RoundedWrapper, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
|
||||
@@ -202,7 +202,12 @@ export const VoteDetails = ({
|
||||
)}
|
||||
|
||||
<section className="mt-10">
|
||||
<SubHeading title={t('castYourVote')} />
|
||||
{proposal?.state === ProposalState.STATE_OPEN ? (
|
||||
<SubHeading title={t('castYourVote')} />
|
||||
) : (
|
||||
<SubHeading title={t('yourVote')} />
|
||||
)}
|
||||
|
||||
{pubKey ? (
|
||||
proposal && (
|
||||
<VoteButtonsContainer
|
||||
@@ -224,6 +229,9 @@ export const VoteDetails = ({
|
||||
<Icon name={'info-sign'} />
|
||||
<div>{t('connectAVegaWalletToVote')}</div>
|
||||
</div>
|
||||
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
|
||||
{t('findOutMoreAboutHowToVote')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<ConnectToVega />
|
||||
</RoundedWrapper>
|
||||
|
||||
@@ -16,7 +16,6 @@ const mockAppState: AppState = {
|
||||
totalSupply: mockTotalSupply,
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
|
||||
@@ -20,8 +20,11 @@ import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
|
||||
const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
||||
orderBy(
|
||||
arr,
|
||||
[(p) => new Date(p?.terms?.closingDatetime).getTime(), (p) => p.id],
|
||||
['desc', 'desc']
|
||||
[
|
||||
(p) => new Date(p?.terms?.closingDatetime).getTime(),
|
||||
(p) => new Date(p?.datetime).getTime(),
|
||||
],
|
||||
['asc', 'asc']
|
||||
);
|
||||
|
||||
const orderByUpgradeBlockHeight = (
|
||||
|
||||
@@ -54,22 +54,16 @@ export const TrancheItem = ({
|
||||
{formatNumber(total, 2)}
|
||||
</span>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{t('Starts unlocking')}</td>
|
||||
<td className="text-right">
|
||||
{format(tranche.tranche_start, DATE_FORMAT_LONG)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('Fully unlocked')}</td>
|
||||
<td className="text-right">
|
||||
{format(tranche.tranche_end, DATE_FORMAT_LONG)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="grid grid-cols-2 my-2">
|
||||
<div>
|
||||
<span>{t('Starts unlocking')}:</span>{' '}
|
||||
<span>{format(tranche.tranche_start, DATE_FORMAT_LONG)}</span>
|
||||
</div>
|
||||
<div className="justify-self-end">
|
||||
<span>{t('Fully unlocked')}:</span>{' '}
|
||||
<span>{format(tranche.tranche_end, DATE_FORMAT_LONG)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<LockedProgress
|
||||
locked={locked}
|
||||
unlocked={unlocked}
|
||||
|
||||
+18
-1
@@ -79,7 +79,24 @@ export const generateEpochIndividualRewardsList = ({
|
||||
epoch?.rewards.push(asset);
|
||||
}
|
||||
|
||||
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
|
||||
if (!asset.rewardTypes[rewardType]) {
|
||||
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
|
||||
} else {
|
||||
const previousAmount = asset.rewardTypes[rewardType]?.amount;
|
||||
const previousPercentageOfTotal =
|
||||
asset.rewardTypes[rewardType]?.percentageOfTotal;
|
||||
|
||||
asset.rewardTypes[rewardType] = {
|
||||
amount: previousAmount
|
||||
? new BigNumber(previousAmount).plus(amount).toString()
|
||||
: amount,
|
||||
percentageOfTotal: previousPercentageOfTotal
|
||||
? new BigNumber(previousPercentageOfTotal)
|
||||
.plus(percentageOfTotal)
|
||||
.toString()
|
||||
: percentageOfTotal,
|
||||
};
|
||||
}
|
||||
|
||||
// totalAmount is the sum of all rewardTypes amounts
|
||||
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
|
||||
|
||||
@@ -28,6 +28,7 @@ export const NodeContainer = ({
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { delegationsPagination } = ENV;
|
||||
|
||||
const { data, loading, error, refetch } = useStakingQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
@@ -37,7 +38,9 @@ export const NodeContainer = ({
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
errorPolicy: 'all',
|
||||
});
|
||||
|
||||
const { data: previousEpochData } = usePreviousEpochQuery({
|
||||
variables: {
|
||||
epochId: (Number(data?.epoch.id) - 1).toString(),
|
||||
@@ -47,7 +50,7 @@ export const NodeContainer = ({
|
||||
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
if (error) {
|
||||
if (error && !error.message.includes('failed to get party for ID')) {
|
||||
return (
|
||||
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
|
||||
<pre>
|
||||
|
||||
@@ -1,4 +1,37 @@
|
||||
[
|
||||
{
|
||||
"tranche_id": 59,
|
||||
"tranche_start": "2024-05-01T00:00:00.000Z",
|
||||
"tranche_end": "2024-11-01T00:00:00.000Z",
|
||||
"total_added": "15000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "15000",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "15000",
|
||||
"user": "0xA530ac2B576eF27C09B7b55C83b3E163D167e9AE",
|
||||
"tx": "0x90af73d7321833fc32445850655a1210f0298845b9222e695e1229801f7a95b0"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"users": [
|
||||
{
|
||||
"address": "0xA530ac2B576eF27C09B7b55C83b3E163D167e9AE",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "15000",
|
||||
"user": "0xA530ac2B576eF27C09B7b55C83b3E163D167e9AE",
|
||||
"tranche_id": 59,
|
||||
"tx": "0x90af73d7321833fc32445850655a1210f0298845b9222e695e1229801f7a95b0"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "15000",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "15000"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tranche_id": 58,
|
||||
"tranche_start": "2023-05-11T00:00:00.000Z",
|
||||
@@ -48,8 +81,8 @@
|
||||
"tranche_start": "2023-04-20T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-20T00:00:00.000Z",
|
||||
"total_added": "19242.125",
|
||||
"total_removed": "1979.64045368475",
|
||||
"locked_amount": "5608.9383879726076446",
|
||||
"total_removed": "2667.900599530005",
|
||||
"locked_amount": "1760.572777247300418465",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "188",
|
||||
@@ -238,6 +271,21 @@
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
"tx": "0x47f5bf2c758c5270dd1b6519ac649ddabef8199f8a0bae319e36f8ac5c9c142e"
|
||||
},
|
||||
{
|
||||
"amount": "269.870659721775",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tx": "0x156ef84c345adecdf7b1c55756b1f4326716d1b0191ce1a208f614fc807ce033"
|
||||
},
|
||||
{
|
||||
"amount": "219.675555560475",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tx": "0x682c6453a6c681312a81e15f244e4885c0ceaab35c7dc041c02d536ff42ca62f"
|
||||
},
|
||||
{
|
||||
"amount": "198.713930563005",
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
"tx": "0x9cdddebcfa4f7e189e435c3a0b5f69ecc6240d59b4d9f959739458f610d8fa95"
|
||||
},
|
||||
{
|
||||
"amount": "202.093666077975",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
@@ -311,6 +359,18 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "269.870659721775",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tranche_id": 56,
|
||||
"tx": "0x156ef84c345adecdf7b1c55756b1f4326716d1b0191ce1a208f614fc807ce033"
|
||||
},
|
||||
{
|
||||
"amount": "219.675555560475",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tranche_id": 56,
|
||||
"tx": "0x682c6453a6c681312a81e15f244e4885c0ceaab35c7dc041c02d536ff42ca62f"
|
||||
},
|
||||
{
|
||||
"amount": "202.093666077975",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
@@ -337,8 +397,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "1207.5",
|
||||
"withdrawn_tokens": "597.002068860225",
|
||||
"remaining_tokens": "610.497931139775"
|
||||
"withdrawn_tokens": "1086.548284142475",
|
||||
"remaining_tokens": "120.951715857525"
|
||||
},
|
||||
{
|
||||
"address": "0x33Ce1D9E53AFb7367E34749517C086405a651a95",
|
||||
@@ -378,6 +438,12 @@
|
||||
"tranche_id": 56,
|
||||
"tx": "0x47f5bf2c758c5270dd1b6519ac649ddabef8199f8a0bae319e36f8ac5c9c142e"
|
||||
},
|
||||
{
|
||||
"amount": "198.713930563005",
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
"tranche_id": 56,
|
||||
"tx": "0x9cdddebcfa4f7e189e435c3a0b5f69ecc6240d59b4d9f959739458f610d8fa95"
|
||||
},
|
||||
{
|
||||
"amount": "195.89040769089",
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
@@ -386,8 +452,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "914.25",
|
||||
"withdrawn_tokens": "624.076855605525",
|
||||
"remaining_tokens": "290.173144394475"
|
||||
"withdrawn_tokens": "822.79078616853",
|
||||
"remaining_tokens": "91.45921383147"
|
||||
},
|
||||
{
|
||||
"address": "0x9573BDF7FfC5519912d293e4D1f750eab2E471E7",
|
||||
@@ -899,7 +965,7 @@
|
||||
"tranche_start": "2023-04-06T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-06T00:00:00.000Z",
|
||||
"total_added": "14610",
|
||||
"total_removed": "6675.45090157707",
|
||||
"total_removed": "7227.45090157707",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -1104,6 +1170,26 @@
|
||||
"user": "0x2586bA83696a92b5467Aaa0CF9EEC052F28F2c02",
|
||||
"tx": "0x5aa922056cad64f97a7dfa750da57d63abfdaea4499192d4d57958bfb4fba2ea"
|
||||
},
|
||||
{
|
||||
"amount": "33",
|
||||
"user": "0xBf1AaB792D729fA125e6D7122D4b916a1E1C44B1",
|
||||
"tx": "0xd2c2f11c150797924c49f6022957d701052268c7335ef3927597fca4f1b544a2"
|
||||
},
|
||||
{
|
||||
"amount": "114",
|
||||
"user": "0x8D416A61bCccf4E6aF5598302BC4e41f97401652",
|
||||
"tx": "0x619088e473d63826e10d3516619b505cb0a82884a5b8158146ff8ab79307967d"
|
||||
},
|
||||
{
|
||||
"amount": "96",
|
||||
"user": "0x6324334064d1d807940191C658d925DA7d323b12",
|
||||
"tx": "0xd4d9926de15e6dc4992c47e9fbb9d25bf873d7e331524994bb6139a2953ea96a"
|
||||
},
|
||||
{
|
||||
"amount": "309",
|
||||
"user": "0xf9c6C5F942B94Db9dedC550956865cf99D743959",
|
||||
"tx": "0x4fa5d02d920d90835694cbab1e41a66d317b2bc35860d07a5ccd70d12182116b"
|
||||
},
|
||||
{
|
||||
"amount": "106.53500000286",
|
||||
"user": "0x6F32AA5A6198329c16e438512F992a0548C856f9",
|
||||
@@ -1249,10 +1335,17 @@
|
||||
"tx": "0xc7736d362d5ffdd16dc26f25047f52fa6238f60837e831b149b6ebb25a0d9281"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "33",
|
||||
"user": "0xBf1AaB792D729fA125e6D7122D4b916a1E1C44B1",
|
||||
"tranche_id": 54,
|
||||
"tx": "0xd2c2f11c150797924c49f6022957d701052268c7335ef3927597fca4f1b544a2"
|
||||
}
|
||||
],
|
||||
"total_tokens": "33",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "33"
|
||||
"withdrawn_tokens": "33",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xB0Dbf35C0918536B62a2FbcCC852bCffDB41d6aF",
|
||||
@@ -1399,10 +1492,17 @@
|
||||
"tx": "0x96b16197328b59556848409bcc4ebf1f71648485fabedde2848461247aa4be32"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "96",
|
||||
"user": "0x6324334064d1d807940191C658d925DA7d323b12",
|
||||
"tranche_id": 54,
|
||||
"tx": "0xd4d9926de15e6dc4992c47e9fbb9d25bf873d7e331524994bb6139a2953ea96a"
|
||||
}
|
||||
],
|
||||
"total_tokens": "96",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "96"
|
||||
"withdrawn_tokens": "96",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x268070d5EEd5b24E34a5F4C17B5482178b18089D",
|
||||
@@ -1573,10 +1673,17 @@
|
||||
"tx": "0x09d7342bca4e0fdc9f5a40e18e402b0328991d0df5da17b834c67b149e270808"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "114",
|
||||
"user": "0x8D416A61bCccf4E6aF5598302BC4e41f97401652",
|
||||
"tranche_id": 54,
|
||||
"tx": "0x619088e473d63826e10d3516619b505cb0a82884a5b8158146ff8ab79307967d"
|
||||
}
|
||||
],
|
||||
"total_tokens": "114",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "114"
|
||||
"withdrawn_tokens": "114",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x9E53F72210479BF46aE7ABF77c48B0d611b3c3dB",
|
||||
@@ -1714,10 +1821,17 @@
|
||||
"tx": "0x8a0328460c26941859c8ce400e7b18d41701fd620535f757f17e4c1d2c930831"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "309",
|
||||
"user": "0xf9c6C5F942B94Db9dedC550956865cf99D743959",
|
||||
"tranche_id": 54,
|
||||
"tx": "0x4fa5d02d920d90835694cbab1e41a66d317b2bc35860d07a5ccd70d12182116b"
|
||||
}
|
||||
],
|
||||
"total_tokens": "309",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "309"
|
||||
"withdrawn_tokens": "309",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
|
||||
@@ -1860,7 +1974,7 @@
|
||||
"tranche_start": "2023-03-06T00:00:00.000Z",
|
||||
"tranche_end": "2023-04-06T00:00:00.000Z",
|
||||
"total_added": "14099",
|
||||
"total_removed": "3785.49002352036",
|
||||
"total_removed": "4373.49002352036",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -2630,6 +2744,16 @@
|
||||
"user": "0x2a65Ae527C6Ff4665e048B0E0883c486A7BA4DBc",
|
||||
"tx": "0xb0edcc25e422bc3db3ad8027dfdfc0928abd7a8af8957a1699e4cf2dc8cee8f7"
|
||||
},
|
||||
{
|
||||
"amount": "558",
|
||||
"user": "0xA7f89FF809549F4577993de3d1B3017241a53F40",
|
||||
"tx": "0x84e5ff587c312b578fa946e2dce386cdf19c7638602b12eceaaab7de563f8394"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0x8D416A61bCccf4E6aF5598302BC4e41f97401652",
|
||||
"tx": "0x6a6dc596ec40beefbe40b32666cbef8ea31d1ce73c0c11f57a1e60f9ea242b8d"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0xBe9F912Ad481C61B653463E8F1D2b2b310D49861",
|
||||
@@ -3861,10 +3985,17 @@
|
||||
"tx": "0xecbb3424e425019a8ebdb9326f662c3520f220fbecdfdaa4770c282001a0f5eb"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0x8D416A61bCccf4E6aF5598302BC4e41f97401652",
|
||||
"tranche_id": 53,
|
||||
"tx": "0x6a6dc596ec40beefbe40b32666cbef8ea31d1ce73c0c11f57a1e60f9ea242b8d"
|
||||
}
|
||||
],
|
||||
"total_tokens": "30",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "30"
|
||||
"withdrawn_tokens": "30",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x83D7eD53E7CB97b542F1F71f40561d51F8019C8B",
|
||||
@@ -4446,10 +4577,17 @@
|
||||
"tx": "0x46d80a145d2f49ef3152cbfaa6d2bb054deabe21396ab43553a3f076e2db62e1"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "558",
|
||||
"user": "0xA7f89FF809549F4577993de3d1B3017241a53F40",
|
||||
"tranche_id": 53,
|
||||
"tx": "0x84e5ff587c312b578fa946e2dce386cdf19c7638602b12eceaaab7de563f8394"
|
||||
}
|
||||
],
|
||||
"total_tokens": "558",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "558"
|
||||
"withdrawn_tokens": "558",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x363D3d06d70761c34D6EfEB25E409A9549E6970D",
|
||||
@@ -4907,7 +5045,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "49327.3172923405910546417",
|
||||
"locked_amount": "47902.6878201683513969045",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -4973,7 +5111,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "284.955770502645375",
|
||||
"locked_amount": "202.53945996133505",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -5006,7 +5144,7 @@
|
||||
"tranche_end": "2023-11-01T00:00:00.000Z",
|
||||
"total_added": "15000.000000000000015",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "14163.9766379830905141639766379830905",
|
||||
"locked_amount": "13674.853751509662013674853751509662",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1.5e-14",
|
||||
@@ -5048,10 +5186,15 @@
|
||||
"tranche_id": 46,
|
||||
"tranche_start": "2023-11-01T00:00:00.000Z",
|
||||
"tranche_end": "2024-05-01T00:00:00.000Z",
|
||||
"total_added": "7500",
|
||||
"total_added": "22500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "7500",
|
||||
"locked_amount": "22500",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "15000",
|
||||
"user": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
"tx": "0x959f5eed214f5376e834c2c20ed2a23a75c4465240c2776452ade80afa11ae2e"
|
||||
},
|
||||
{
|
||||
"amount": "7500",
|
||||
"user": "0xA530ac2B576eF27C09B7b55C83b3E163D167e9AE",
|
||||
@@ -5060,6 +5203,21 @@
|
||||
],
|
||||
"withdrawals": [],
|
||||
"users": [
|
||||
{
|
||||
"address": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "15000",
|
||||
"user": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
"tranche_id": 46,
|
||||
"tx": "0x959f5eed214f5376e834c2c20ed2a23a75c4465240c2776452ade80afa11ae2e"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "15000",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "15000"
|
||||
},
|
||||
{
|
||||
"address": "0xA530ac2B576eF27C09B7b55C83b3E163D167e9AE",
|
||||
"deposits": [
|
||||
@@ -5094,7 +5252,7 @@
|
||||
"tranche_end": "2023-09-01T00:00:00.000Z",
|
||||
"total_added": "17500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "10723.00897619766325",
|
||||
"locked_amount": "10152.36560864532825",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12500",
|
||||
@@ -5360,8 +5518,8 @@
|
||||
"tranche_start": "2023-02-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-08-01T00:00:00.000Z",
|
||||
"total_added": "37500",
|
||||
"total_removed": "18302.01762945",
|
||||
"locked_amount": "16936.07322360343725",
|
||||
"total_removed": "18500.56159815",
|
||||
"locked_amount": "15692.99848449969375",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -5380,6 +5538,11 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x0a46fa3cc0154727ae4c7abca97bd7e26e6835018f96cf3a3860a9fcc791b9a4"
|
||||
},
|
||||
{
|
||||
"amount": "198.5439687",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xc25a025176872981def8433ef0982ba306737aaf2cf0867f2259fd19492af2b5"
|
||||
},
|
||||
{
|
||||
"amount": "164.727209925",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -5564,6 +5727,12 @@
|
||||
"tranche_id": 34,
|
||||
"tx": "0x0a46fa3cc0154727ae4c7abca97bd7e26e6835018f96cf3a3860a9fcc791b9a4"
|
||||
},
|
||||
{
|
||||
"amount": "198.5439687",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 34,
|
||||
"tx": "0xc25a025176872981def8433ef0982ba306737aaf2cf0867f2259fd19492af2b5"
|
||||
},
|
||||
{
|
||||
"amount": "164.727209925",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -5746,8 +5915,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "4092.25895865",
|
||||
"remaining_tokens": "3407.74104135"
|
||||
"withdrawn_tokens": "4290.80292735",
|
||||
"remaining_tokens": "3209.19707265"
|
||||
},
|
||||
{
|
||||
"address": "0x0B4e6fcE839B01ef43DA6F890FAC7B1Afb004600",
|
||||
@@ -5791,7 +5960,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "49282.312321912380332265",
|
||||
"locked_amount": "47858.982644879740029465",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -5824,7 +5993,7 @@
|
||||
"tranche_end": "2024-04-01T00:00:00.000Z",
|
||||
"total_added": "54144.7663",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "48189.54915726316779685587",
|
||||
"locked_amount": "47301.94373541695517923511",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "54144.7663",
|
||||
@@ -5857,7 +6026,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "19679.51570903094634",
|
||||
"locked_amount": "18650.49049340436032",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -6050,7 +6219,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1763.627124556063",
|
||||
"locked_amount": "1681.4366121258245",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -7119,7 +7288,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "1709370.7872515768348",
|
||||
"locked_amount": "115570.728817751808474185",
|
||||
"locked_amount": "83681.9872917193018111752",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -40991,7 +41160,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "715655.108029600523393",
|
||||
"locked_amount": "202093.2974680882797136777084",
|
||||
"locked_amount": "153091.4042231713229007715616",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -42383,8 +42552,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "873375.18460711694221852",
|
||||
"locked_amount": "6016297.4428328087189762607645230040375777",
|
||||
"total_removed": "885240.49984321970795352",
|
||||
"locked_amount": "5842539.8756085922637222718130454678926737",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -42903,6 +43072,26 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x989d22d5ba66353cb132b5643100a99e78133e5d40aef1ac91ed6ab0a2290299"
|
||||
},
|
||||
{
|
||||
"amount": "2195.39447101000955",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0xa515dafd399b366ba7f510ea6314587eb19e83fa92dbd5c2a212f3541a0cc4ef"
|
||||
},
|
||||
{
|
||||
"amount": "6626.72344",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tx": "0xdedcbf90e8f19c2a732ba2da663f59b5a19ad94800ddca6a3575019f19e9ca20"
|
||||
},
|
||||
{
|
||||
"amount": "856.50757224183006",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0xc8835acd2e148eeaacde2a0df0ecad19c22717c7c51b207fdeac9a1af3f98c88"
|
||||
},
|
||||
{
|
||||
"amount": "2186.689752850926125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xb048cb2dcdfc30508770a3a112ddbacd63210f740cc887559660522bd506612c"
|
||||
},
|
||||
{
|
||||
"amount": "10150.87581603206683",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -45089,6 +45278,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x989d22d5ba66353cb132b5643100a99e78133e5d40aef1ac91ed6ab0a2290299"
|
||||
},
|
||||
{
|
||||
"amount": "2186.689752850926125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xb048cb2dcdfc30508770a3a112ddbacd63210f740cc887559660522bd506612c"
|
||||
},
|
||||
{
|
||||
"amount": "913.910324501590625",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -46645,8 +46840,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "161285.6283934563635",
|
||||
"remaining_tokens": "98713.2591065436365"
|
||||
"withdrawn_tokens": "163472.318146307289625",
|
||||
"remaining_tokens": "96526.569353692710375"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -46873,6 +47068,18 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "2195.39447101000955",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xa515dafd399b366ba7f510ea6314587eb19e83fa92dbd5c2a212f3541a0cc4ef"
|
||||
},
|
||||
{
|
||||
"amount": "856.50757224183006",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xc8835acd2e148eeaacde2a0df0ecad19c22717c7c51b207fdeac9a1af3f98c88"
|
||||
},
|
||||
{
|
||||
"amount": "10150.87581603206683",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -47163,8 +47370,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "150551.801",
|
||||
"withdrawn_tokens": "91577.204932811647",
|
||||
"remaining_tokens": "58974.596067188353"
|
||||
"withdrawn_tokens": "94629.10697606348661",
|
||||
"remaining_tokens": "55922.69402393651339"
|
||||
},
|
||||
{
|
||||
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
|
||||
@@ -47384,6 +47591,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "6626.72344",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xdedcbf90e8f19c2a732ba2da663f59b5a19ad94800ddca6a3575019f19e9ca20"
|
||||
},
|
||||
{
|
||||
"amount": "1784.722222",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
@@ -47644,8 +47857,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200000",
|
||||
"withdrawn_tokens": "118742.2403",
|
||||
"remaining_tokens": "81257.7597"
|
||||
"withdrawn_tokens": "125368.96374",
|
||||
"remaining_tokens": "74631.03626"
|
||||
},
|
||||
{
|
||||
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
|
||||
@@ -59170,8 +59383,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "44544.1737890903416",
|
||||
"locked_amount": "32022.838198356764871382775241",
|
||||
"total_removed": "45345.4733895423416",
|
||||
"locked_amount": "24258.2081069361612058598496600864",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@@ -65800,6 +66013,41 @@
|
||||
"user": "0x3F9E884B459a6AaC3f66b72555611AdE68a7F472",
|
||||
"tx": "0x4697229c7f272bd3fc06ecdc1b15143d28e20083e15f1b3cadb3fc80e80af180"
|
||||
},
|
||||
{
|
||||
"amount": "186.817193048",
|
||||
"user": "0x0003423D0A1858B6Eaf9a67914aBc67cDF989c2F",
|
||||
"tx": "0x7f120bd664d54bdabddad1ddb7ffdd84fbaa2630f1657289e3a7b11503ec4018"
|
||||
},
|
||||
{
|
||||
"amount": "65.737747336",
|
||||
"user": "0xAc5bd5a3890B12B0da7cF1Ad846F6b8b260B60CC",
|
||||
"tx": "0x8a4862b1fdacb7e24492397fac234152aae4146d9cca2f0a476e918cbe130af7"
|
||||
},
|
||||
{
|
||||
"amount": "84.67351598",
|
||||
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
"tx": "0x891254ca1ea63a87ea7e14c3a985f58a403f4a0baf9fba86582785da6d2ae806"
|
||||
},
|
||||
{
|
||||
"amount": "84.673820396",
|
||||
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
|
||||
"tx": "0x26941fa486bf2aa16da2748a2326f87c3491c4f6f939f4329bd1eb3171ce6c3d"
|
||||
},
|
||||
{
|
||||
"amount": "189.69504693",
|
||||
"user": "0xC198534Ee3D619770C21FFAF5e9F067F8bbC07FD",
|
||||
"tx": "0x803ba71987573a0ab20d4da7eb1dc7fe5e2fd8184199f0ee5d7a949b3d9f4e3e"
|
||||
},
|
||||
{
|
||||
"amount": "189.696721206",
|
||||
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
|
||||
"tx": "0xed29b9b07eff1d6c43d586e545bf5557390f5c6040924718d0f28968fa34a8d5"
|
||||
},
|
||||
{
|
||||
"amount": "0.005555556",
|
||||
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
|
||||
"tx": "0xca745232557385fff568386375d868633bd9542981dd07a6c471b4633dd5c9b8"
|
||||
},
|
||||
{
|
||||
"amount": "182.662252664",
|
||||
"user": "0xb1169C6daAc76bAcaf0D8f87641Fc38fbabe569F",
|
||||
@@ -70786,10 +71034,23 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "189.696721206",
|
||||
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xed29b9b07eff1d6c43d586e545bf5557390f5c6040924718d0f28968fa34a8d5"
|
||||
},
|
||||
{
|
||||
"amount": "0.005555556",
|
||||
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xca745232557385fff568386375d868633bd9542981dd07a6c471b4633dd5c9b8"
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "200"
|
||||
"withdrawn_tokens": "189.702276762",
|
||||
"remaining_tokens": "10.297723238"
|
||||
},
|
||||
{
|
||||
"address": "0x7Cb540c27E6cAD1133a4942293B9293489A2Df19",
|
||||
@@ -70846,10 +71107,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "189.69504693",
|
||||
"user": "0xC198534Ee3D619770C21FFAF5e9F067F8bbC07FD",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x803ba71987573a0ab20d4da7eb1dc7fe5e2fd8184199f0ee5d7a949b3d9f4e3e"
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "200"
|
||||
"withdrawn_tokens": "189.69504693",
|
||||
"remaining_tokens": "10.30495307"
|
||||
},
|
||||
{
|
||||
"address": "0xFFDAE9319517529E0081710bF5A018E06C841eFf",
|
||||
@@ -71055,10 +71323,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "186.817193048",
|
||||
"user": "0x0003423D0A1858B6Eaf9a67914aBc67cDF989c2F",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x7f120bd664d54bdabddad1ddb7ffdd84fbaa2630f1657289e3a7b11503ec4018"
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "200"
|
||||
"withdrawn_tokens": "186.817193048",
|
||||
"remaining_tokens": "13.182806952"
|
||||
},
|
||||
{
|
||||
"address": "0x659ce8E49DA0c872AAC44c70496122044CbcA911",
|
||||
@@ -71379,6 +71654,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "65.737747336",
|
||||
"user": "0xAc5bd5a3890B12B0da7cF1Ad846F6b8b260B60CC",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x8a4862b1fdacb7e24492397fac234152aae4146d9cca2f0a476e918cbe130af7"
|
||||
},
|
||||
{
|
||||
"amount": "309.708054284",
|
||||
"user": "0xAc5bd5a3890B12B0da7cF1Ad846F6b8b260B60CC",
|
||||
@@ -71387,8 +71668,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "309.708054284",
|
||||
"remaining_tokens": "90.291945716"
|
||||
"withdrawn_tokens": "375.44580162",
|
||||
"remaining_tokens": "24.55419838"
|
||||
},
|
||||
{
|
||||
"address": "0x71bA18Cb18d7Cd338CF6E911882E717A8dEf8041",
|
||||
@@ -88571,6 +88852,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "84.673820396",
|
||||
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x26941fa486bf2aa16da2748a2326f87c3491c4f6f939f4329bd1eb3171ce6c3d"
|
||||
},
|
||||
{
|
||||
"amount": "191.520738204",
|
||||
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
|
||||
@@ -88585,8 +88872,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "293.716882292",
|
||||
"remaining_tokens": "106.283117708"
|
||||
"withdrawn_tokens": "378.390702688",
|
||||
"remaining_tokens": "21.609297312"
|
||||
},
|
||||
{
|
||||
"address": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
@@ -88599,6 +88886,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "84.67351598",
|
||||
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x891254ca1ea63a87ea7e14c3a985f58a403f4a0baf9fba86582785da6d2ae806"
|
||||
},
|
||||
{
|
||||
"amount": "191.524581432",
|
||||
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
@@ -88613,8 +88906,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "293.715512428",
|
||||
"remaining_tokens": "106.284487572"
|
||||
"withdrawn_tokens": "378.389028408",
|
||||
"remaining_tokens": "21.610971592"
|
||||
},
|
||||
{
|
||||
"address": "0xB0473C4FeBd722a6d7eE561329d3134030318132",
|
||||
@@ -88971,7 +89264,7 @@
|
||||
"tranche_start": "2021-12-05T00:00:00.000Z",
|
||||
"tranche_end": "2022-06-05T00:00:00.000Z",
|
||||
"total_added": "171288.42",
|
||||
"total_removed": "70140.5995794947989",
|
||||
"total_removed": "71890.5995794947989",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -93196,6 +93489,41 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xcAEbd70D80D5aB92Ae5A2E1c92F479298826548C",
|
||||
"tx": "0x7ffbc82e96bb7c796feb548a7ca48a7430a47dae3f2e9de859e6d45ac31a4e6c"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xac494D6eC2CcA7C9BD1caD0c1D0516F3706c6b7c",
|
||||
"tx": "0x85a5d2a4faa6490fc25e080f09dc4d9b5fb3857d762042ba2dcbc0f04de41e0c"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x4eE1C5ED78143f298ae4E5E17e538a99E8512db7",
|
||||
"tx": "0xcb7b99dc5240cdcaaed2fae06e9fd8172db0387179fe09b5f1e1cefff16c6025"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x01E41ffFBcBC9DDA6E2e9e7291cA8AcCE1AF2091",
|
||||
"tx": "0x0af2b3703c268eed4532409b52b6fae066207adf80f9dd2fc17b7aacc40f2879"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xa9C1CA1E66CB3063352433843521861844A2Fd33",
|
||||
"tx": "0x8fad99b8976fff57a099e59351ae96379ec3041e32ccce144727eea29cfffe59"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x13A01db15250975cE4f7AddF5C20CC1f5056Fc47",
|
||||
"tx": "0xc4bcffd71245b9f2af79e04fd58740f7b4cce50da80a405403c959563d132f2c"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x2A6Ea2aBEf3D7d7A76CBeF3c297eE0eF9a86BBA8",
|
||||
"tx": "0x163b699e8a25073ad1ad45c8a8f9b1348eb0b648d3f4bc6f3f64648e0ad726bf"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x0E3407C9A94effa675471afe7A9D37e687C32141",
|
||||
@@ -95538,10 +95866,17 @@
|
||||
"tx": "0x87b7e454992157e175f824cbd5cbfd068ce890b8603edcb70174bae9b5c4afc8"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x13A01db15250975cE4f7AddF5C20CC1f5056Fc47",
|
||||
"tranche_id": 6,
|
||||
"tx": "0xc4bcffd71245b9f2af79e04fd58740f7b4cce50da80a405403c959563d132f2c"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x7b4127D262acC0Ff3dB2D0521d1F1f3b602243C2",
|
||||
@@ -98054,10 +98389,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x4eE1C5ED78143f298ae4E5E17e538a99E8512db7",
|
||||
"tranche_id": 6,
|
||||
"tx": "0xcb7b99dc5240cdcaaed2fae06e9fd8172db0387179fe09b5f1e1cefff16c6025"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x1dC24e9601e40013a12CB9976a519860c4eDF359",
|
||||
@@ -98099,10 +98441,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x01E41ffFBcBC9DDA6E2e9e7291cA8AcCE1AF2091",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x0af2b3703c268eed4532409b52b6fae066207adf80f9dd2fc17b7aacc40f2879"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x6EE016F4512C910b401B18087BBAC15412dD97cA",
|
||||
@@ -98436,10 +98785,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xac494D6eC2CcA7C9BD1caD0c1D0516F3706c6b7c",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x85a5d2a4faa6490fc25e080f09dc4d9b5fb3857d762042ba2dcbc0f04de41e0c"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xa7137DA63B53138A6DCdC9D9a010F42F951F1113",
|
||||
@@ -98481,10 +98837,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xa9C1CA1E66CB3063352433843521861844A2Fd33",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x8fad99b8976fff57a099e59351ae96379ec3041e32ccce144727eea29cfffe59"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xbCd93996629B0fFA1DfaE74Bb6e107ddB1CE7934",
|
||||
@@ -98511,10 +98874,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0xcAEbd70D80D5aB92Ae5A2E1c92F479298826548C",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x7ffbc82e96bb7c796feb548a7ca48a7430a47dae3f2e9de859e6d45ac31a4e6c"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x64Aac7Cb63317DE77C331686E0b2dAA41303Adf0",
|
||||
@@ -98593,10 +98963,17 @@
|
||||
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x2A6Ea2aBEf3D7d7A76CBeF3c297eE0eF9a86BBA8",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x163b699e8a25073ad1ad45c8a8f9b1348eb0b648d3f4bc6f3f64648e0ad726bf"
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "250"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x5D84C52FEf44a84298422f0F87147F80145e2043",
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
projectId: 'et4snf',
|
||||
defaultCommandTimeout: 10000,
|
||||
viewportWidth: 1440,
|
||||
viewportWidth: 1800,
|
||||
viewportHeight: 900,
|
||||
responseTimeout: 50000,
|
||||
requestTimeout: 20000,
|
||||
|
||||
@@ -7,6 +7,9 @@ const marketTitle = 'accordion-title';
|
||||
const externalLink = 'external-link';
|
||||
const accordionContent = 'accordion-content';
|
||||
const providerName = 'provider-name';
|
||||
const oracleBannerStatus = 'oracle-banner-status';
|
||||
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
|
||||
const oracleFullProfile = 'oracle-full-profile';
|
||||
|
||||
describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -29,7 +32,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
|
||||
it('show oracle banner', () => {
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
cy.getByTestId('oracle-status').should('contain.text', 'COMPROMISED');
|
||||
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
|
||||
cy.getByTestId(oracleBannerDialogTrigger)
|
||||
.should('contain.text', 'Show more')
|
||||
.click();
|
||||
cy.getByTestId(oracleFullProfile).should('exist');
|
||||
});
|
||||
|
||||
it('current fees displayed', () => {
|
||||
@@ -192,7 +199,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
.within(() => {
|
||||
cy.getByTestId('block-explorer-link').contains('Block explorer');
|
||||
cy.getByTestId('github-link').contains('Oracle repository');
|
||||
cy.getByTestId('verified-accounts').contains('0 proofs of ownership');
|
||||
});
|
||||
cy.getByTestId('dialog-close').click();
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
AuctionTrigger,
|
||||
MarketState,
|
||||
MarketTradingMode,
|
||||
} from '@vegaprotocol/types';
|
||||
|
||||
describe('markets selector', { tags: '@smoke' }, () => {
|
||||
const list = 'market-selector-list';
|
||||
const searchInput = 'search-term';
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then((window) => {
|
||||
window.localStorage.setItem('marketId', 'market-1');
|
||||
});
|
||||
cy.mockTradingPage(
|
||||
MarketState.STATE_ACTIVE,
|
||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
cy.wait('@MarketsCandles');
|
||||
});
|
||||
|
||||
it('can toggle the sidebar', () => {
|
||||
cy.getByTestId('market-selector').should('be.visible');
|
||||
cy.getByTestId('sidebar-toggle').click();
|
||||
cy.getByTestId('market-selector').should('not.exist');
|
||||
cy.getByTestId('sidebar-toggle').click();
|
||||
cy.getByTestId('market-selector').should('be.visible');
|
||||
});
|
||||
|
||||
// need function keyword as we need 'this' to access market data
|
||||
it('displays data as expected', () => {
|
||||
// TODO: load data from mocks in. Using alias and wrap intermittently fails
|
||||
const data = [
|
||||
{
|
||||
code: 'AAPL.MF21',
|
||||
name: 'Apple Monthly (30 Jun 2022)',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'BTCUSD.MF21',
|
||||
name: 'ACTIVE MARKET',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'ETHBTC.QM21',
|
||||
name: 'ETHBTC Quarterly (30 Jun 2022)',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'SOLUSD',
|
||||
name: 'SUSPENDED MARKET',
|
||||
markPrice: '84.41',
|
||||
change: '+200.00%',
|
||||
},
|
||||
];
|
||||
cy.getByTestId(list)
|
||||
.find('a')
|
||||
.each((item, i) => {
|
||||
const market = data[i];
|
||||
// 6001-MARK-021
|
||||
expect(item.find('h3').text()).equals(market.code);
|
||||
// 6001-MARK-022
|
||||
expect(item.find('h4').text()).equals(market.name);
|
||||
// 6001-MARK-024
|
||||
expect(item.find('[data-testid="market-item-price"]').text()).equals(
|
||||
market.markPrice
|
||||
);
|
||||
// 6001-MARK-023
|
||||
expect(item.find('[data-testid="market-item-change"]').text()).equals(
|
||||
market.change
|
||||
);
|
||||
// 6001-MARK-025
|
||||
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
// 6001-MARK-27
|
||||
it('can use the filter options', () => {
|
||||
// product type
|
||||
cy.getByTestId('product-Spot').click();
|
||||
cy.getByTestId(list).contains('Spot markets coming soon.');
|
||||
cy.getByTestId('product-Perpetual').click();
|
||||
cy.getByTestId(list).contains('Perpetual markets coming soon.');
|
||||
cy.getByTestId('product-Future').click();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
|
||||
// 6001-MARK-29
|
||||
cy.getByTestId(searchInput).clear().type('btc');
|
||||
cy.getByTestId(list).find('a').should('have.length', 2);
|
||||
cy.getByTestId(list).find('a').eq(0).contains('BTCUSD.MF21');
|
||||
cy.getByTestId(list).find('a').eq(1).contains('ETHBTC.QM21');
|
||||
|
||||
cy.getByTestId(searchInput).clear();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe('Market trading page', () => {
|
||||
// 7002-SORD-001
|
||||
// 7002-SORD-002
|
||||
it('must display market name', () => {
|
||||
cy.getByTestId('popover-trigger').should('not.be.empty');
|
||||
cy.getByTestId('header-title').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('must see market expiry', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
|
||||
const dialogCloseBtn = 'dialog-close';
|
||||
const popoverTrigger = 'popover-trigger';
|
||||
|
||||
describe('markets table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -31,36 +30,6 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('sparkline-svg').should('be.visible');
|
||||
});
|
||||
|
||||
it('renders market list drop down', () => {
|
||||
openMarketDropDown();
|
||||
cy.getByTestId('price').invoke('text').should('not.be.empty');
|
||||
cy.getByTestId('trading-mode-col').should('not.be.empty');
|
||||
cy.getByTestId('taker-fee').should('contain.text', '%');
|
||||
cy.getByTestId('market-volume').should('not.be.empty');
|
||||
cy.getByTestId('market-name').should('not.be.empty');
|
||||
|
||||
cy.getByTestId('trading-mode-col')
|
||||
.contains('Monitoring auction - liquidity')
|
||||
.eq(0)
|
||||
.realHover();
|
||||
cy.get('[data-testid="trading-mode-tooltip"] p').should('have.class', '');
|
||||
cy.get(
|
||||
'[data-testid="market-trading-mode"] [data-testid="item-value"]'
|
||||
).realHover();
|
||||
cy.get('[data-testid="trading-mode-tooltip"] p').should(
|
||||
'have.class',
|
||||
'mb-4'
|
||||
);
|
||||
});
|
||||
|
||||
it('able to select market from dropdown', () => {
|
||||
openMarketDropDown();
|
||||
cy.getByTestId('market-link-market-0').first().should('be.visible').click();
|
||||
cy.contains('ACTIVE MARKET').should('be.visible');
|
||||
cy.url().should('include', '/markets/market-0');
|
||||
cy.getByTestId('popover-trigger').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('able to open and sort full market list - market page', () => {
|
||||
const ExpectedSortedMarkets = [
|
||||
'AAPL.MF21',
|
||||
@@ -253,15 +222,3 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function openMarketDropDown() {
|
||||
cy.getByTestId(dialogCloseBtn).then((button) => {
|
||||
if (button.is(':visible')) {
|
||||
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
}
|
||||
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
|
||||
cy.getByTestId(popoverTrigger).click({ force: true });
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
});
|
||||
|
||||
describe('limit order', () => {
|
||||
before(() => {
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
@@ -98,7 +102,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
'have.text',
|
||||
'Total margin available'
|
||||
);
|
||||
cy.get('.text-neutral-500').should('have.text', '~100,000.01 tDAI');
|
||||
cy.get('.text-neutral-500').should('have.text', '100,000.01 tDAI');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import {
|
||||
accountsQuery,
|
||||
amendGeneralAccountBalance,
|
||||
estimateOrderQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { createOrder } from '../support/create-order';
|
||||
|
||||
@@ -44,13 +40,10 @@ describe(
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
const accounts = accountsQuery();
|
||||
amendGeneralAccountBalance(accounts, 'market-0', '100000000');
|
||||
amendGeneralAccountBalance(accounts, 'market-0', '1');
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Accounts', accounts);
|
||||
});
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
@@ -66,7 +59,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId('dealticket-warning-margin').should(
|
||||
'contain.text',
|
||||
'You may not have enough margin available to open this position. 2,354.72283 tDAI is currently required. You have only 1,000.01 tDAI available.'
|
||||
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||
cy.getByTestId('dialog-content')
|
||||
|
||||
@@ -291,12 +291,58 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
|
||||
.should('have.text', '200.00');
|
||||
});
|
||||
|
||||
it('must see a pegged order - ask', () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
peggedOrder: {
|
||||
__typename: 'PeggedOrder',
|
||||
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
|
||||
offset: '250000',
|
||||
},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'Ask - 2.50 Peg limit');
|
||||
});
|
||||
|
||||
it('must see a pegged order - bid', () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
peggedOrder: {
|
||||
__typename: 'PeggedOrder',
|
||||
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
|
||||
offset: '100',
|
||||
},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'Bid + 0.001 Peg limit');
|
||||
});
|
||||
|
||||
it('must see a pegged order - mid', () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
peggedOrder: {
|
||||
__typename: 'PeggedOrder',
|
||||
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
|
||||
offset: '0.5',
|
||||
},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'Mid + 0.00001 Peg limit');
|
||||
});
|
||||
|
||||
it('for market typy must not see a price for active or parked orders', () => {
|
||||
// 7003-MORD-005
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
status: Schema.OrderStatus.STATUS_PARKED,
|
||||
peggedOrder: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="price"]')
|
||||
@@ -337,6 +383,7 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: {},
|
||||
liquidityProvisionId: '6536',
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
|
||||
import type { Provider, Status } from '@vegaprotocol/oracles';
|
||||
import type { Provider, Status } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
accountsQuery,
|
||||
assetQuery,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
chainIdQuery,
|
||||
chartQuery,
|
||||
depositsQuery,
|
||||
estimateOrderQuery,
|
||||
estimateFeesQuery,
|
||||
marginsQuery,
|
||||
marketCandlesQuery,
|
||||
marketDataQuery,
|
||||
@@ -22,11 +22,14 @@ import {
|
||||
networkParamsQuery,
|
||||
nodeGuardQuery,
|
||||
ordersQuery,
|
||||
estimatePositionQuery,
|
||||
positionsQuery,
|
||||
proposalListQuery,
|
||||
statisticsQuery,
|
||||
tradesQuery,
|
||||
withdrawalsQuery,
|
||||
protocolUpgradeProposalsQuery,
|
||||
blockStatisticsQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/market-list';
|
||||
@@ -157,9 +160,16 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
|
||||
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
|
||||
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
|
||||
aliasGQLQuery(req, 'EstimateFees', estimateFeesQuery());
|
||||
aliasGQLQuery(req, 'EstimatePosition', estimatePositionQuery());
|
||||
aliasGQLQuery(req, 'ProposalsList', proposalListQuery());
|
||||
aliasGQLQuery(req, 'Deposits', depositsQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'ProtocolUpgradeProposals',
|
||||
protocolUpgradeProposalsQuery()
|
||||
);
|
||||
aliasGQLQuery(req, 'BlockStatistics', blockStatisticsQuery());
|
||||
};
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
function ReactMarkdown({ children }) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default ReactMarkdown;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { AssetDropdown } from './asset-dropdown';
|
||||
|
||||
const createAssets = (count = 3) => {
|
||||
return new Array(count).fill(null).map((_, i) => ({
|
||||
id: i.toString(),
|
||||
symbol: 'asset-1',
|
||||
}));
|
||||
};
|
||||
|
||||
describe('AssetDropdown', () => {
|
||||
const assets = createAssets();
|
||||
|
||||
it('renders and selects chosen assets', async () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
render(
|
||||
<AssetDropdown
|
||||
checkedAssets={[]}
|
||||
assets={assets}
|
||||
onSelect={mockOnSelect}
|
||||
onReset={jest.fn()}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const items = screen.getAllByRole('menuitemcheckbox');
|
||||
expect(items).toHaveLength(assets.length);
|
||||
expect(items.map((i) => i.textContent)).toEqual(
|
||||
assets.map((a) => a.symbol)
|
||||
);
|
||||
await userEvent.click(items[0]);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, true);
|
||||
});
|
||||
|
||||
it('unselects already selected assets', async () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
render(
|
||||
<AssetDropdown
|
||||
checkedAssets={[assets[0].id]}
|
||||
assets={assets}
|
||||
onSelect={mockOnSelect}
|
||||
onReset={jest.fn()}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const items = screen.getAllByRole('menuitemcheckbox');
|
||||
await userEvent.click(items[0]);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, false);
|
||||
});
|
||||
|
||||
it('can be reset clearing all assets', async () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
const mockOnReset = jest.fn();
|
||||
render(
|
||||
<AssetDropdown
|
||||
checkedAssets={assets.map((a) => a.id)}
|
||||
assets={assets}
|
||||
onSelect={mockOnSelect}
|
||||
onReset={mockOnReset}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const items = screen.getAllByRole('menuitemcheckbox');
|
||||
// all should be checked
|
||||
items.forEach((item) => {
|
||||
expect(item).toBeChecked();
|
||||
});
|
||||
await userEvent.click(screen.getByText('Reset'));
|
||||
expect(mockOnReset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('doesnt render if no assets provided', async () => {
|
||||
const { container } = render(
|
||||
<AssetDropdown
|
||||
checkedAssets={[]}
|
||||
assets={[]}
|
||||
onSelect={jest.fn()}
|
||||
onReset={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const AssetDropdown = ({
|
||||
assets,
|
||||
checkedAssets,
|
||||
onSelect,
|
||||
onReset,
|
||||
}: {
|
||||
assets: Array<{ id: string; symbol: string }> | undefined;
|
||||
checkedAssets: string[];
|
||||
onSelect: (id: string, checked: boolean) => void;
|
||||
onReset: () => void;
|
||||
}) => {
|
||||
if (!assets?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger data-testid="asset-trigger">
|
||||
<span className="px-1">$</span>
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{assets?.map((a) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={a.id}
|
||||
checked={checkedAssets.includes(a.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (typeof checked === 'boolean') {
|
||||
onSelect(a.id, checked);
|
||||
}
|
||||
}}
|
||||
data-testid={`asset-id-${a.id}`}
|
||||
>
|
||||
{a.symbol}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
+74
-92
@@ -5,13 +5,7 @@ import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
ColumnKind,
|
||||
SelectMarketPopover,
|
||||
} from '../../components/select-market';
|
||||
import type { OnCellClickHandler } from '../../components/select-market';
|
||||
import { Header, HeaderStat } from '../../components/header';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { HeaderStat } from '../../components/header';
|
||||
import { MarketMarkPrice } from '../../components/market-mark-price';
|
||||
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/market-info';
|
||||
import { MarketState } from '../../components/market-state';
|
||||
@@ -19,102 +13,90 @@ import { HeaderStatMarketTradingMode } from '../../components/market-trading-mod
|
||||
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
|
||||
import { MarketState as State } from '@vegaprotocol/types';
|
||||
|
||||
interface TradeMarketHeaderProps {
|
||||
interface HeaderStatsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export const TradeMarketHeader = ({
|
||||
market,
|
||||
onSelect,
|
||||
}: TradeMarketHeaderProps) => {
|
||||
export const HeaderStats = ({ market }: HeaderStatsProps) => {
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
|
||||
const asset = market?.tradableInstrument.instrument.product?.settlementAsset;
|
||||
|
||||
const onCellClick: OnCellClickHandler = (e, kind, value) => {
|
||||
if (value && kind === ColumnKind.Asset) {
|
||||
openAssetDetailsDialog(value, e.target as HTMLElement);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
<SelectMarketPopover
|
||||
marketCode={market?.tradableInstrument.instrument.code || NO_MARKET}
|
||||
marketName={market?.tradableInstrument.instrument.name || NO_MARKET}
|
||||
onSelect={onSelect}
|
||||
onCellClick={onCellClick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HeaderStat
|
||||
heading={t('Expiry')}
|
||||
description={
|
||||
market && (
|
||||
<ExpiryTooltipContent
|
||||
market={market}
|
||||
explorerUrl={VEGA_EXPLORER_URL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
testId="market-expiry"
|
||||
>
|
||||
<ExpiryLabel market={market} />
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Price')} testId="market-price">
|
||||
<MarketMarkPrice
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Change (24h)')} testId="market-change">
|
||||
<Last24hPriceChange
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Volume (24h)')}
|
||||
testId="market-volume"
|
||||
description={t(
|
||||
'The total number of contracts traded in the last 24 hours.'
|
||||
)}
|
||||
>
|
||||
<Last24hVolume
|
||||
marketId={market?.id}
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
marketId={market?.id}
|
||||
initialTradingMode={market?.tradingMode}
|
||||
/>
|
||||
<MarketState market={market} />
|
||||
{asset ? (
|
||||
<HeaderStat
|
||||
heading={t('Settlement asset')}
|
||||
testId="market-settlement-asset"
|
||||
<div className="flex flex-col justify-end lg:pt-4">
|
||||
<div className="xl:flex xl:gap-4 items-end">
|
||||
<div
|
||||
data-testid="header-summary"
|
||||
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
|
||||
>
|
||||
<div>
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
|
||||
}}
|
||||
<HeaderStat
|
||||
heading={t('Expiry')}
|
||||
description={
|
||||
market && (
|
||||
<ExpiryTooltipContent
|
||||
market={market}
|
||||
explorerUrl={VEGA_EXPLORER_URL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
testId="market-expiry"
|
||||
>
|
||||
<ExpiryLabel market={market} />
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Price')} testId="market-price">
|
||||
<MarketMarkPrice
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Change (24h)')} testId="market-change">
|
||||
<Last24hPriceChange
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Volume (24h)')}
|
||||
testId="market-volume"
|
||||
description={t(
|
||||
'The total number of contracts traded in the last 24 hours.'
|
||||
)}
|
||||
>
|
||||
<Last24hVolume
|
||||
marketId={market?.id}
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
marketId={market?.id}
|
||||
initialTradingMode={market?.tradingMode}
|
||||
/>
|
||||
<MarketState market={market} />
|
||||
{asset ? (
|
||||
<HeaderStat
|
||||
heading={t('Settlement asset')}
|
||||
testId="market-settlement-asset"
|
||||
>
|
||||
{asset.symbol}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
) : null}
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market?.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
/>
|
||||
<MarketProposalNotification marketId={market?.id} />
|
||||
</Header>
|
||||
<div>
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{asset.symbol}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
) : null}
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market?.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
/>
|
||||
<MarketProposalNotification marketId={market?.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import { MarketSelectorItem } from './market-selector-item';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type {
|
||||
MarketDataUpdateFieldsFragment,
|
||||
MarketDataUpdateSubscription,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { MarketDataUpdateDocument } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
AuctionTrigger,
|
||||
MarketState,
|
||||
MarketTradingMode,
|
||||
} from '@vegaprotocol/types';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
describe('MarketSelectorItem', () => {
|
||||
const market = createMarketFragment({
|
||||
id: 'market-0',
|
||||
decimalPlaces: 2,
|
||||
// @ts-ignore fragment doesn't contain candles
|
||||
candles: [{ close: '5' }, { close: '10' }],
|
||||
});
|
||||
const marketData: MarketDataUpdateFieldsFragment = {
|
||||
__typename: 'ObservableMarketData',
|
||||
marketId: market.id,
|
||||
auctionEnd: null,
|
||||
auctionStart: null,
|
||||
bestBidPrice: '100',
|
||||
bestBidVolume: '100',
|
||||
bestOfferPrice: '100',
|
||||
bestOfferVolume: '100',
|
||||
bestStaticBidPrice: '100',
|
||||
bestStaticBidVolume: '100',
|
||||
bestStaticOfferPrice: '100',
|
||||
bestStaticOfferVolume: '100',
|
||||
indicativePrice: '100',
|
||||
indicativeVolume: '100',
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketValueProxy: '100',
|
||||
markPrice: '50000',
|
||||
midPrice: '100',
|
||||
staticMidPrice: '100',
|
||||
openInterest: '100',
|
||||
suppliedStake: '1000000',
|
||||
targetStake: '1000000',
|
||||
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
|
||||
priceMonitoringBounds: null,
|
||||
};
|
||||
const mock: MockedResponse<MarketDataUpdateSubscription> = {
|
||||
request: {
|
||||
query: MarketDataUpdateDocument,
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsData: [marketData],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
const renderJsx = () => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<MarketSelectorItem
|
||||
market={market}
|
||||
currentMarketId={market.id}
|
||||
style={{}}
|
||||
onSelect={mockOnSelect}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders market information', async () => {
|
||||
renderJsx();
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
// link renders and is styled
|
||||
expect(link).toHaveAttribute('href', '/markets/' + market.id);
|
||||
|
||||
expect(link).toHaveClass('ring-1');
|
||||
|
||||
expect(screen.getByTestId('market-item-price')).toHaveTextContent('-');
|
||||
|
||||
// candles are loaded immediately
|
||||
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
|
||||
'+100.00%'
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-item-price')).toHaveTextContent(
|
||||
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
|
||||
);
|
||||
});
|
||||
|
||||
await userEvent.click(link);
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(market.id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
priceChangePercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
|
||||
import { useMarketDataUpdateSubscription } from '@vegaprotocol/market-list';
|
||||
import { Sparkline } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const MarketSelectorItem = ({
|
||||
market,
|
||||
style,
|
||||
currentMarketId,
|
||||
onSelect,
|
||||
}: {
|
||||
market: MarketMaybeWithDataAndCandles;
|
||||
style: CSSProperties;
|
||||
currentMarketId?: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
}) => {
|
||||
const wrapperClasses = classNames(
|
||||
'block bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4',
|
||||
'min-h-[120px]',
|
||||
{
|
||||
'ring-1 ring-vega-light-300 dark:ring-vega-dark-300':
|
||||
currentMarketId === market.id,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div style={style} className="my-0.5 px-4">
|
||||
<Link
|
||||
to={`/markets/${market.id}`}
|
||||
className={wrapperClasses}
|
||||
onClick={() => {
|
||||
onSelect && onSelect(market.id);
|
||||
}}
|
||||
>
|
||||
<h3>{market.tradableInstrument.instrument.code}</h3>
|
||||
<h4
|
||||
title={market.tradableInstrument.instrument.name}
|
||||
className="text-sm text-vega-light-300 dark:text-vega-dark-300 text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{market.tradableInstrument.instrument.name}
|
||||
</h4>
|
||||
<MarketData market={market} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
const { data } = useMarketDataUpdateSubscription({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
});
|
||||
|
||||
const marketData = data?.marketsData[0];
|
||||
|
||||
// use market data price if available as this is comes from
|
||||
// the subscription
|
||||
const price = marketData
|
||||
? addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
|
||||
: market.data
|
||||
? addDecimalsFormatNumber(market.data.markPrice, market.decimalPlaces)
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap justify-between items-center mt-1">
|
||||
<div className="w-1/2">
|
||||
<div
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
data-testid="market-item-price"
|
||||
>
|
||||
{price}
|
||||
</div>
|
||||
{market.candles && (
|
||||
<PriceChange candles={market.candles.map((c) => c.close)} />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-1/2 max-w-[120px]">
|
||||
{market.candles ? (
|
||||
<Sparkline
|
||||
width={120}
|
||||
height={20}
|
||||
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PriceChange = ({ candles }: { candles: string[] }) => {
|
||||
const priceChange = candles ? priceChangePercentage(candles) : undefined;
|
||||
const priceChangeClasses = classNames('text-xs', {
|
||||
'text-vega-pink': priceChange && priceChange < 0,
|
||||
'text-vega-green': priceChange && priceChange > 0,
|
||||
});
|
||||
let prefix = '';
|
||||
if (priceChange && priceChange > 0) {
|
||||
prefix = '+';
|
||||
}
|
||||
const formattedChange = formatNumber(Number(priceChange), 2);
|
||||
return (
|
||||
<div className={priceChangeClasses} data-testid="market-item-change">
|
||||
{priceChange ? `${prefix}${formattedChange}%` : '-'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,289 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MarketSelector } from './market-selector';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { SortType } from './sort-dropdown';
|
||||
import { SortTypeMapping } from './sort-dropdown';
|
||||
import { Sort } from './sort-dropdown';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
jest.mock('@vegaprotocol/market-list');
|
||||
const mockUseMarketList = useMarketList as jest.Mock;
|
||||
|
||||
// mock market list items to avoid subscriptions starting
|
||||
jest.mock('./market-selector-item', () => ({
|
||||
MarketSelectorItem: (props: { market: { id: string } }) => (
|
||||
<div data-testid={props.market.id} />
|
||||
),
|
||||
}));
|
||||
|
||||
// without a real DOM autosize won't render with an actual height or width
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return ({
|
||||
children,
|
||||
}: {
|
||||
children: (size: { width: number; height: number }) => ReactNode;
|
||||
}) => <div>{children({ width: 300, height: 1000 })}</div>;
|
||||
});
|
||||
|
||||
describe('MarketSelector', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'a',
|
||||
name: 'a',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore candles get joined outside this type
|
||||
candles: [{ close: '100' }, { close: '200' }],
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 1).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'b',
|
||||
name: 'b',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore candles get joined outside this type
|
||||
candles: [{ close: '100' }, { close: '400' }],
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 2).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'c',
|
||||
name: 'c',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore candles get joined outside this type
|
||||
candles: [{ close: '100' }, { close: '1000' }],
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 0).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'cd',
|
||||
name: 'cd',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore candles get joined outside this type
|
||||
candles: [{ close: '100' }, { close: '300' }],
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 3).toISOString(),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const activeMarkets = markets.filter((m) =>
|
||||
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(m.state)
|
||||
);
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
it('renders only active markets', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
|
||||
activeMarkets.length
|
||||
);
|
||||
expect(screen.getByRole('link')).toHaveTextContent('All markets');
|
||||
});
|
||||
|
||||
it('filters by product type', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('product-Spot'));
|
||||
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
|
||||
expect(screen.getByTestId('no-items')).toHaveTextContent(
|
||||
'Spot markets coming soon.'
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('product-Perpetual'));
|
||||
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
|
||||
expect(screen.getByTestId('no-items')).toHaveTextContent(
|
||||
'Perpetual markets coming soon.'
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('product-Future'));
|
||||
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(
|
||||
activeMarkets.length
|
||||
);
|
||||
expect(screen.queryByTestId('no-items')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters by search term', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByTestId('search-term'), 'zzz');
|
||||
expect(screen.getByTestId('no-items')).toHaveTextContent('No markets');
|
||||
const input = screen.getByTestId('search-term');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'a');
|
||||
expect(input).toHaveValue('a');
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
|
||||
expect(screen.getByTestId('market-0')).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'b');
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
|
||||
expect(screen.getByTestId('market-1')).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'c');
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
|
||||
expect(screen.getByTestId('market-3')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('market-4')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters by asset', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('asset-trigger'));
|
||||
expect(screen.getAllByTestId(/asset-id/)).toHaveLength(3);
|
||||
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
|
||||
expect(screen.getByTestId('market-0')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('market-1')).toBeInTheDocument();
|
||||
|
||||
// reopen asset dropdown and add asset-1
|
||||
await userEvent.click(screen.getByTestId('asset-trigger'));
|
||||
await userEvent.click(screen.getByTestId('asset-id-asset-1'));
|
||||
|
||||
// all markets with asset-0 or asset-1 shown (no market id as market is closed)
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(3);
|
||||
expect(screen.getByTestId('market-0')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('market-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('market-3')).toBeInTheDocument();
|
||||
|
||||
// reopen and uncheck asset-0
|
||||
await userEvent.click(screen.getByTestId('asset-trigger'));
|
||||
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
|
||||
|
||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
|
||||
expect(screen.getByTestId('market-3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sorts by gained', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('sort-trigger'));
|
||||
const options = screen.getAllByTestId(/sort-item/);
|
||||
expect(options.map((o) => o.textContent)).toEqual(
|
||||
Object.entries(Sort)
|
||||
.filter(([key]) => key !== Sort.None)
|
||||
.map(([key]) => SortTypeMapping[key as SortType])
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('sort-item-Gained'));
|
||||
expect(
|
||||
screen
|
||||
.getAllByTestId(/market-\d/)
|
||||
.map((el) => el.getAttribute('data-testid'))
|
||||
).toEqual([markets[3].id, markets[1].id, markets[4].id, markets[0].id]);
|
||||
});
|
||||
|
||||
it('sorts by lost', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('sort-trigger'));
|
||||
await userEvent.click(screen.getByTestId('sort-item-Lost'));
|
||||
expect(
|
||||
screen
|
||||
.getAllByTestId(/market-\d/)
|
||||
.map((el) => el.getAttribute('data-testid'))
|
||||
).toEqual([markets[0].id, markets[4].id, markets[1].id, markets[3].id]);
|
||||
});
|
||||
|
||||
it('sorts by new', async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MarketSelector currentMarketId="market-0" />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('sort-trigger'));
|
||||
await userEvent.click(screen.getByTestId('sort-item-New'));
|
||||
expect(
|
||||
screen
|
||||
.getAllByTestId(/market-\d/)
|
||||
.map((el) => el.getAttribute('data-testid'))
|
||||
).toEqual([markets[3].id, markets[0].id, markets[1].id, markets[4].id]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
Input,
|
||||
TinyScroll,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useMarketSelectorList } from './use-market-selector-list';
|
||||
import type { ProductType } from './product-selector';
|
||||
import { Product, ProductSelector } from './product-selector';
|
||||
import { AssetDropdown } from './asset-dropdown';
|
||||
import type { SortType } from './sort-dropdown';
|
||||
import { Sort, SortDropdown } from './sort-dropdown';
|
||||
import { MarketSelectorItem } from './market-selector-item';
|
||||
|
||||
export type Filter = {
|
||||
searchTerm: string;
|
||||
product: ProductType;
|
||||
sort: SortType;
|
||||
assets: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches market data and filters it given a set of filter properties
|
||||
* defined in Filter
|
||||
*/
|
||||
export const MarketSelector = ({
|
||||
currentMarketId,
|
||||
onSelect,
|
||||
}: {
|
||||
currentMarketId?: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const { markets, data, loading, error } = useMarketSelectorList(filter);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-rows-[min-content_1fr_min-content] h-full"
|
||||
data-testid="market-selector"
|
||||
>
|
||||
<div className="px-4 pt-2 pb-4">
|
||||
<ProductSelector
|
||||
product={filter.product}
|
||||
onSelect={(product) => {
|
||||
setFilter((curr) => ({ ...curr, product }));
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm flex gap-1 items-stretch">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
onChange={(e) =>
|
||||
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
|
||||
}
|
||||
value={filter.searchTerm}
|
||||
type="text"
|
||||
placeholder={t('Search')}
|
||||
data-testid="search-term"
|
||||
className="w-full"
|
||||
appendElement={
|
||||
filter.searchTerm.length ? (
|
||||
<button
|
||||
onClick={() =>
|
||||
setFilter((curr) => ({ ...curr, searchTerm: '' }))
|
||||
}
|
||||
className="text-vega-light-200 dark:text-vega-dark-200"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CROSS} />
|
||||
</button>
|
||||
) : (
|
||||
<span />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<AssetDropdown
|
||||
assets={uniqBy(
|
||||
data?.map(
|
||||
(d) => d.tradableInstrument.instrument.product.settlementAsset
|
||||
),
|
||||
'id'
|
||||
)}
|
||||
checkedAssets={filter.assets}
|
||||
onSelect={(id: string, checked) => {
|
||||
setFilter((curr) => {
|
||||
if (checked) {
|
||||
if (curr.assets.includes(id)) {
|
||||
return curr;
|
||||
} else {
|
||||
return { ...curr, assets: [...curr.assets, id] };
|
||||
}
|
||||
} else {
|
||||
if (curr.assets.includes(id)) {
|
||||
return {
|
||||
...curr,
|
||||
assets: curr.assets.filter((x) => x !== id),
|
||||
};
|
||||
}
|
||||
}
|
||||
return curr;
|
||||
});
|
||||
}}
|
||||
onReset={() => setFilter((curr) => ({ ...curr, assets: [] }))}
|
||||
/>
|
||||
<SortDropdown
|
||||
currentSort={filter.sort}
|
||||
onSelect={(sort) => {
|
||||
setFilter((curr) => {
|
||||
if (curr.sort === sort) {
|
||||
return { ...curr, sort: Sort.None };
|
||||
}
|
||||
return {
|
||||
...curr,
|
||||
sort,
|
||||
};
|
||||
});
|
||||
}}
|
||||
onReset={() => setFilter((curr) => ({ ...curr, sort: Sort.None }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="market-selector-list">
|
||||
<MarketList
|
||||
data={markets}
|
||||
loading={loading}
|
||||
error={error}
|
||||
searchTerm={filter.searchTerm}
|
||||
currentMarketId={currentMarketId}
|
||||
onSelect={onSelect}
|
||||
noItems={
|
||||
filter.product === Product.Perpetual
|
||||
? t('Perpetual markets coming soon.')
|
||||
: filter.product === Product.Spot
|
||||
? t('Spot markets coming soon.')
|
||||
: t('No markets')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 py-2">
|
||||
<span className="inline-block border-b border-black dark:border-white">
|
||||
<Link to={'/markets/all'} className="flex items-center gap-x-2">
|
||||
{t('All markets')}
|
||||
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MarketList = ({
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
currentMarketId,
|
||||
onSelect,
|
||||
noItems,
|
||||
}: {
|
||||
data: MarketMaybeWithDataAndCandles[];
|
||||
error: Error | undefined;
|
||||
loading: boolean;
|
||||
searchTerm: string;
|
||||
currentMarketId?: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
noItems: string;
|
||||
}) => {
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<TinyScroll>
|
||||
<List
|
||||
data={data}
|
||||
loading={loading}
|
||||
width={width}
|
||||
height={height}
|
||||
currentMarketId={currentMarketId}
|
||||
onSelect={onSelect}
|
||||
noItems={noItems}
|
||||
/>
|
||||
</TinyScroll>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
||||
|
||||
const List = ({
|
||||
data,
|
||||
loading,
|
||||
width,
|
||||
height,
|
||||
onSelect,
|
||||
noItems,
|
||||
currentMarketId,
|
||||
}: {
|
||||
data: MarketMaybeWithDataAndCandles[];
|
||||
loading: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
noItems: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
currentMarketId?: string;
|
||||
}) => {
|
||||
const row = ({ index, style }: { index: number; style: CSSProperties }) => {
|
||||
const market = data[index];
|
||||
|
||||
return (
|
||||
<MarketSelectorItem
|
||||
market={market}
|
||||
currentMarketId={currentMarketId}
|
||||
style={style}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!data || loading) {
|
||||
return (
|
||||
<div style={{ width, height }}>
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<div style={{ width, height }} data-testid="no-items">
|
||||
<div className="mb-2 px-4">
|
||||
<div className="text-sm bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg px-4 py-2">
|
||||
{noItems}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
className="virtualized-list"
|
||||
itemCount={data.length}
|
||||
itemSize={130}
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
{row}
|
||||
</FixedSizeList>
|
||||
);
|
||||
};
|
||||
|
||||
const Skeleton = () => {
|
||||
return (
|
||||
<div className="mb-2 px-2">
|
||||
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
|
||||
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
|
||||
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
|
||||
import { useGlobalStore, usePageTitleStore } from '../../stores';
|
||||
import { TradeGrid, TradePanels } from './trade-grid';
|
||||
import { TradeGrid } from './trade-grid';
|
||||
import { TradePanels } from './trade-panels';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
@@ -100,6 +101,7 @@ export const MarketPage = () => {
|
||||
/>
|
||||
);
|
||||
}, [largeScreen, data, onSelect, navigate]);
|
||||
|
||||
if (!data && marketId) {
|
||||
return (
|
||||
<Splash>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Make sure these match the available __typename properties on product
|
||||
export const Product = {
|
||||
Spot: 'Spot',
|
||||
Future: 'Future',
|
||||
Perpetual: 'Perpetual',
|
||||
} as const;
|
||||
|
||||
export type ProductType = keyof typeof Product;
|
||||
|
||||
const ProductTypeMapping: {
|
||||
[key in ProductType]: string;
|
||||
} = {
|
||||
[Product.Spot]: 'Spot',
|
||||
[Product.Future]: 'Futures',
|
||||
[Product.Perpetual]: 'Perpetuals',
|
||||
};
|
||||
|
||||
export const ProductSelector = ({
|
||||
product,
|
||||
onSelect,
|
||||
}: {
|
||||
product: ProductType;
|
||||
onSelect: (product: ProductType) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex gap-3 mb-3">
|
||||
{Object.keys(Product).map((t) => {
|
||||
const classes = classNames('py-1 border-b-2', {
|
||||
'border-vega-yellow text-black dark:text-white': t === product,
|
||||
'border-transparent text-vega-light-300 dark:text-vega-dark-300':
|
||||
t !== product,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => {
|
||||
onSelect(t as ProductType);
|
||||
}}
|
||||
className={classes}
|
||||
data-testid={`product-${t}`}
|
||||
>
|
||||
{ProductTypeMapping[t as ProductType]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Sort = {
|
||||
None: 'None',
|
||||
Gained: 'Gained',
|
||||
Lost: 'Lost',
|
||||
New: 'New',
|
||||
} as const;
|
||||
|
||||
export type SortType = keyof typeof Sort;
|
||||
|
||||
export const SortTypeMapping: {
|
||||
[key in SortType]: string;
|
||||
} = {
|
||||
[Sort.None]: 'None',
|
||||
[Sort.Gained]: 'Top gaining',
|
||||
[Sort.Lost]: 'Top losing',
|
||||
[Sort.New]: 'New markets',
|
||||
};
|
||||
|
||||
export const SortDropdown = ({
|
||||
currentSort,
|
||||
onSelect,
|
||||
onReset,
|
||||
}: {
|
||||
currentSort: SortType;
|
||||
onSelect: (sort: SortType) => void;
|
||||
onReset: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger data-testid="sort-trigger">
|
||||
<VegaIcon name={VegaIconNames.TREND_UP} />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={currentSort}
|
||||
onValueChange={(value) => onSelect(value as SortType)}
|
||||
>
|
||||
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{Object.keys(Sort)
|
||||
.filter((s) => s !== Sort.None)
|
||||
.map((key) => {
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
inset
|
||||
key={key}
|
||||
value={key}
|
||||
data-testid={`sort-item-${key}`}
|
||||
>
|
||||
{SortTypeMapping[key as SortType]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,120 +1,37 @@
|
||||
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
|
||||
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
|
||||
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
||||
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
|
||||
import type { OrderListContainerProps } from '@vegaprotocol/orders';
|
||||
import { FillsContainer } from '@vegaprotocol/fills';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { memo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { memo, useState } from 'react';
|
||||
import type { ReactNode, ComponentProps } from 'react';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
import { OracleBanner } from '../../components/banner';
|
||||
import {
|
||||
Tab,
|
||||
LocalStoragePersistTabs as Tabs,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import { TradeMarketHeader } from './trade-market-header';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { LiquidityContainer } from '../liquidity/liquidity';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { OracleBanner } from '@vegaprotocol/market-info';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { Filter } from '@vegaprotocol/orders';
|
||||
import {
|
||||
usePaneLayout,
|
||||
useScreenDimensions,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Tab,
|
||||
LocalStoragePersistTabs as Tabs,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useMarketClickHandler,
|
||||
useMarketLiquidityClickHandler,
|
||||
} from '../../lib/hooks/use-market-click-handler';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import { HeaderTitle } from '../../components/header';
|
||||
import {
|
||||
ResizableGrid,
|
||||
ResizableGridPanel,
|
||||
} from '../../components/resizable-grid';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
| typeof DepthChartContainer
|
||||
| typeof DealTicketContainer
|
||||
| typeof MarketInfoAccordionContainer
|
||||
| typeof OrderbookContainer
|
||||
| typeof TradesContainer;
|
||||
|
||||
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
|
||||
|
||||
const requiresMarket = (View: MarketDependantView) => {
|
||||
const WrappedComponent = (props: MarketDependantViewProps) =>
|
||||
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
|
||||
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
const TradingViews = {
|
||||
candles: {
|
||||
label: 'Candles',
|
||||
component: requiresMarket(CandlesChartContainer),
|
||||
},
|
||||
depth: {
|
||||
label: 'Depth',
|
||||
component: requiresMarket(DepthChartContainer),
|
||||
},
|
||||
liquidity: {
|
||||
label: 'Liquidity',
|
||||
component: requiresMarket(LiquidityContainer),
|
||||
},
|
||||
ticket: {
|
||||
label: 'Ticket',
|
||||
component: requiresMarket(DealTicketContainer),
|
||||
},
|
||||
info: {
|
||||
label: 'Info',
|
||||
component: requiresMarket(MarketInfoAccordionContainer),
|
||||
},
|
||||
orderbook: {
|
||||
label: 'Orderbook',
|
||||
component: requiresMarket(OrderbookContainer),
|
||||
},
|
||||
trades: {
|
||||
label: 'Trades',
|
||||
component: requiresMarket(TradesContainer),
|
||||
},
|
||||
positions: { label: 'Positions', component: PositionsContainer },
|
||||
activeOrders: {
|
||||
label: 'Active',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Open} />
|
||||
),
|
||||
},
|
||||
closedOrders: {
|
||||
label: 'Closed',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Closed} />
|
||||
),
|
||||
},
|
||||
rejectedOrders: {
|
||||
label: 'Rejected',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Rejected} />
|
||||
),
|
||||
},
|
||||
orders: {
|
||||
label: 'All',
|
||||
component: OrderListContainer,
|
||||
},
|
||||
collateral: { label: 'Collateral', component: AccountsContainer },
|
||||
fills: { label: 'Fills', component: FillsContainer },
|
||||
};
|
||||
|
||||
type TradingView = keyof typeof TradingViews;
|
||||
import { TradingViews } from './trade-views';
|
||||
import { MarketSelector } from './market-selector';
|
||||
import { HeaderStats } from './header-stats';
|
||||
|
||||
interface TradeGridProps {
|
||||
market: Market | null;
|
||||
@@ -410,18 +327,57 @@ const MainGrid = memo(
|
||||
);
|
||||
MainGrid.displayName = 'MainGrid';
|
||||
|
||||
export const TradeGrid = ({
|
||||
market,
|
||||
onSelect,
|
||||
pinnedAsset,
|
||||
}: TradeGridProps) => {
|
||||
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const wrapperClasses = classNames(
|
||||
'h-full grid',
|
||||
'grid-rows-[min-content_min-content_1fr]',
|
||||
'grid-cols-[320px_1fr]'
|
||||
);
|
||||
const paneWrapperClasses = classNames('min-h-0', {
|
||||
'col-span-2 col-start-1': !sidebarOpen,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<div>
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<div className={wrapperClasses}>
|
||||
<div className="border-b border-r border-default">
|
||||
<div className="flex gap-2 justify-between items-center px-4 py-2">
|
||||
<HeaderTitle
|
||||
primaryContent={market?.tradableInstrument.instrument.code}
|
||||
secondaryContent={market?.tradableInstrument.instrument.name}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setSidebarOpen((x) => !x)}
|
||||
className="p-2"
|
||||
data-testid="sidebar-toggle"
|
||||
>
|
||||
<span
|
||||
className={classNames('block', {
|
||||
'rotate-90 translate-x-1': !sidebarOpen,
|
||||
'-rotate-90 -translate-x-1': sidebarOpen,
|
||||
})}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-default min-w-0">
|
||||
<HeaderStats market={market} />
|
||||
</div>
|
||||
<div className="col-span-2 bg-vega-green">
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||
{sidebarOpen && (
|
||||
<div className="border-r border-default min-h-0">
|
||||
<div className="h-full pb-8">
|
||||
<MarketSelector currentMarketId={market?.id} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={paneWrapperClasses}>
|
||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -439,88 +395,3 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onOrderTypeClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({
|
||||
market,
|
||||
onSelect,
|
||||
onClickCollateral,
|
||||
pinnedAsset,
|
||||
}: TradePanelsProps) => {
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
|
||||
|
||||
const [view, setView] = useState<TradingView>('candles');
|
||||
const renderView = () => {
|
||||
const Component = memo<{
|
||||
marketId: string;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onOrderTypeClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}>(TradingViews[view].component);
|
||||
|
||||
if (!Component) {
|
||||
throw new Error(`No component for view: ${view}`);
|
||||
}
|
||||
|
||||
if (!market) return <Splash>{NO_MARKET}</Splash>;
|
||||
|
||||
return (
|
||||
<Component
|
||||
marketId={market?.id}
|
||||
onSelect={onSelect}
|
||||
onClickCollateral={onClickCollateral}
|
||||
pinnedAsset={pinnedAsset}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||
<div>
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width, height }} className="overflow-auto">
|
||||
{renderView()}
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{Object.keys(TradingViews).map((key) => {
|
||||
const isActive = view === key;
|
||||
const className = classNames('p-4 min-w-[100px] capitalize', {
|
||||
'text-black dark:text-vega-yellow': isActive,
|
||||
'bg-neutral-200 dark:bg-neutral-800': isActive,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
data-testid={key}
|
||||
onClick={() => setView(key as TradingView)}
|
||||
className={className}
|
||||
key={key}
|
||||
>
|
||||
{TradingViews[key as keyof typeof TradingViews].label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { OracleBanner } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
useMarketClickHandler,
|
||||
useMarketLiquidityClickHandler,
|
||||
} from '../../lib/hooks/use-market-click-handler';
|
||||
import type { TradingView } from './trade-views';
|
||||
import { TradingViews } from './trade-views';
|
||||
import { memo, useState } from 'react';
|
||||
import {
|
||||
Icon,
|
||||
Splash,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { NO_MARKET } from './constants';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import classNames from 'classnames';
|
||||
import { HeaderStats } from './header-stats';
|
||||
import * as DialogPrimitives from '@radix-ui/react-dialog';
|
||||
import { HeaderTitle } from '../../components/header';
|
||||
import { MarketSelector } from './market-selector';
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onOrderTypeClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({
|
||||
market,
|
||||
onSelect,
|
||||
onClickCollateral,
|
||||
pinnedAsset,
|
||||
}: TradePanelsProps) => {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
|
||||
|
||||
const [view, setView] = useState<TradingView>('candles');
|
||||
const renderView = () => {
|
||||
const Component = memo<{
|
||||
marketId: string;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onOrderTypeClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}>(TradingViews[view].component);
|
||||
|
||||
if (!Component) {
|
||||
throw new Error(`No component for view: ${view}`);
|
||||
}
|
||||
|
||||
if (!market) return <Splash>{NO_MARKET}</Splash>;
|
||||
|
||||
return (
|
||||
<Component
|
||||
marketId={market?.id}
|
||||
onSelect={onSelect}
|
||||
onClickCollateral={onClickCollateral}
|
||||
pinnedAsset={pinnedAsset}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div className="border-b border-default min-w-0">
|
||||
<div className="flex gap-4 items-center px-4 py-2">
|
||||
<HeaderTitle
|
||||
primaryContent={market?.tradableInstrument.instrument.code}
|
||||
secondaryContent={market?.tradableInstrument.instrument.name}
|
||||
/>
|
||||
<button onClick={() => setDrawerOpen((x) => !x)} className="p-2">
|
||||
<span
|
||||
className={classNames('block', {
|
||||
'rotate-90 translate-x-1': !drawerOpen,
|
||||
'-rotate-90 -translate-x-1': drawerOpen,
|
||||
})}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<HeaderStats market={market} />
|
||||
</div>
|
||||
<div>
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width, height }} className="overflow-auto">
|
||||
{renderView()}
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{Object.keys(TradingViews).map((key) => {
|
||||
const isActive = view === key;
|
||||
const className = classNames('p-4 min-w-[100px] capitalize', {
|
||||
'text-black dark:text-vega-yellow': isActive,
|
||||
'bg-neutral-200 dark:bg-neutral-800': isActive,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
data-testid={key}
|
||||
onClick={() => setView(key as TradingView)}
|
||||
className={className}
|
||||
key={key}
|
||||
>
|
||||
{TradingViews[key as keyof typeof TradingViews].label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<DialogPrimitives.Root open={drawerOpen} onOpenChange={setDrawerOpen}>
|
||||
<DialogPrimitives.Portal>
|
||||
<DialogPrimitives.Overlay />
|
||||
<DialogPrimitives.Content
|
||||
className={classNames(
|
||||
'fixed h-full max-w-[500px] w-[90vw] z-10 top-0 left-0 transition-transform',
|
||||
'bg-white dark:bg-black',
|
||||
'border-r border-default'
|
||||
)}
|
||||
>
|
||||
<DialogPrimitives.Close className="absolute top-0 right-0 p-2">
|
||||
<Icon name="cross" />
|
||||
</DialogPrimitives.Close>
|
||||
{drawerOpen && (
|
||||
<MarketSelector onSelect={() => setDrawerOpen(false)} />
|
||||
)}
|
||||
</DialogPrimitives.Content>
|
||||
</DialogPrimitives.Portal>
|
||||
</DialogPrimitives.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
|
||||
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
|
||||
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
||||
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
|
||||
import type { OrderListContainerProps } from '@vegaprotocol/orders';
|
||||
import { FillsContainer } from '@vegaprotocol/fills';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { LiquidityContainer } from '../liquidity/liquidity';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
| typeof DepthChartContainer
|
||||
| typeof DealTicketContainer
|
||||
| typeof MarketInfoAccordionContainer
|
||||
| typeof OrderbookContainer
|
||||
| typeof TradesContainer;
|
||||
|
||||
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
|
||||
|
||||
const requiresMarket = (View: MarketDependantView) => {
|
||||
const WrappedComponent = (props: MarketDependantViewProps) =>
|
||||
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
|
||||
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export type TradingView = keyof typeof TradingViews;
|
||||
|
||||
export const TradingViews = {
|
||||
candles: {
|
||||
label: 'Candles',
|
||||
component: requiresMarket(CandlesChartContainer),
|
||||
},
|
||||
depth: {
|
||||
label: 'Depth',
|
||||
component: requiresMarket(DepthChartContainer),
|
||||
},
|
||||
liquidity: {
|
||||
label: 'Liquidity',
|
||||
component: requiresMarket(LiquidityContainer),
|
||||
},
|
||||
ticket: {
|
||||
label: 'Ticket',
|
||||
component: requiresMarket(DealTicketContainer),
|
||||
},
|
||||
info: {
|
||||
label: 'Info',
|
||||
component: requiresMarket(MarketInfoAccordionContainer),
|
||||
},
|
||||
orderbook: {
|
||||
label: 'Orderbook',
|
||||
component: requiresMarket(OrderbookContainer),
|
||||
},
|
||||
trades: {
|
||||
label: 'Trades',
|
||||
component: requiresMarket(TradesContainer),
|
||||
},
|
||||
positions: { label: 'Positions', component: PositionsContainer },
|
||||
activeOrders: {
|
||||
label: 'Active',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Open} />
|
||||
),
|
||||
},
|
||||
closedOrders: {
|
||||
label: 'Closed',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Closed} />
|
||||
),
|
||||
},
|
||||
rejectedOrders: {
|
||||
label: 'Rejected',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Rejected} />
|
||||
),
|
||||
},
|
||||
orders: {
|
||||
label: 'All',
|
||||
component: OrderListContainer,
|
||||
},
|
||||
collateral: { label: 'Collateral', component: AccountsContainer },
|
||||
fills: { label: 'Fills', component: FillsContainer },
|
||||
};
|
||||
@@ -0,0 +1,473 @@
|
||||
import merge from 'lodash/merge';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useMarketSelectorList } from './use-market-selector-list';
|
||||
import { Product } from './product-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
import type { Filter } from './market-selector';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
jest.mock('@vegaprotocol/market-list');
|
||||
const mockUseMarketList = useMarketList as jest.Mock;
|
||||
|
||||
describe('useMarketSelectorList', () => {
|
||||
const setup = (initialArgs?: Partial<Filter>) => {
|
||||
const defaultArgs: Filter = {
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
};
|
||||
return renderHook((args) => useMarketSelectorList(args), {
|
||||
initialProps: merge(defaultArgs, initialArgs),
|
||||
});
|
||||
};
|
||||
|
||||
it('returns all markets active and suspended markets', () => {
|
||||
const markets = [
|
||||
createMarketFragment({ id: 'market-0' }),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_PENDING,
|
||||
}),
|
||||
];
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
const { result } = setup();
|
||||
const expectedFilteredMarkets = markets.filter((m) =>
|
||||
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(m.state)
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
data: markets,
|
||||
markets: expectedFilteredMarkets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('filters by product', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Spot' as 'Future', // spot isn't in schema yet
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
__typename: 'Perpetual' as 'Future', // spot isn't in schema yet
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
const { result, rerender } = setup();
|
||||
expect(result.current.markets).toEqual([markets[0]]);
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Spot as 'Future',
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[1]]);
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Perpetual as 'Future',
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[2]]);
|
||||
});
|
||||
|
||||
it('filters by asset', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
const { result, rerender } = setup({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: ['asset-0'],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[0], markets[1]]);
|
||||
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: ['asset-0', 'asset-1'],
|
||||
});
|
||||
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[0],
|
||||
markets[1],
|
||||
markets[2],
|
||||
]);
|
||||
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: ['asset-0', 'asset-1', 'asset-2'],
|
||||
});
|
||||
|
||||
// all assets selected
|
||||
expect(result.current.markets).toEqual(markets);
|
||||
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: ['asset-invalid'],
|
||||
});
|
||||
|
||||
expect(result.current.markets).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters by search term', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'abc',
|
||||
name: 'aaa',
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'def',
|
||||
name: 'ggg',
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'defg',
|
||||
name: 'gggh',
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'ggg',
|
||||
name: 'foo',
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
const { result, rerender } = setup({
|
||||
searchTerm: 'abc',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[0]]);
|
||||
rerender({
|
||||
searchTerm: 'def',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[1], markets[2]]);
|
||||
rerender({
|
||||
searchTerm: 'defg',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[2]]);
|
||||
rerender({
|
||||
searchTerm: 'zzz',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([]);
|
||||
|
||||
// by name
|
||||
rerender({
|
||||
searchTerm: 'aaa',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[0]]);
|
||||
rerender({
|
||||
searchTerm: 'ggg',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[1],
|
||||
markets[2],
|
||||
markets[3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts albphabetically', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'd',
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'b',
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'a',
|
||||
},
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'c',
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const { result } = setup({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[2],
|
||||
markets[1],
|
||||
markets[3],
|
||||
markets[0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts by gained', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore actual fragment doesnt contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
close: '100',
|
||||
},
|
||||
{
|
||||
close: '200',
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore actual fragment doesnt contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
close: '100',
|
||||
},
|
||||
{
|
||||
close: '1000',
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore actual fragment doesnt contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
close: '100',
|
||||
},
|
||||
{
|
||||
close: '400',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
const { result, rerender } = setup({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.Gained,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[1],
|
||||
markets[2],
|
||||
markets[0],
|
||||
]);
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.Lost as 'Gained',
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[0],
|
||||
markets[2],
|
||||
markets[1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts by open timestamp', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 3).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 1).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 2).toISOString(),
|
||||
},
|
||||
}),
|
||||
];
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
const { result } = setup({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.New,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[1],
|
||||
markets[2],
|
||||
markets[0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useMemo } from 'react';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
import { priceChangePercentage } from '@vegaprotocol/utils';
|
||||
import type { Filter } from './market-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
|
||||
export const useMarketSelectorList = ({
|
||||
product,
|
||||
assets,
|
||||
sort,
|
||||
searchTerm,
|
||||
}: Filter) => {
|
||||
const { data, loading, error } = useMarketList();
|
||||
|
||||
const markets = useMemo(() => {
|
||||
if (!data?.length) return [];
|
||||
const markets = data
|
||||
// only active
|
||||
.filter((m) => {
|
||||
return [MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(
|
||||
m.state
|
||||
);
|
||||
})
|
||||
// only selected product type
|
||||
.filter((m) => {
|
||||
if (m.tradableInstrument.instrument.product.__typename === product) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter((m) => {
|
||||
if (assets.length === 0) return true;
|
||||
return assets.includes(
|
||||
m.tradableInstrument.instrument.product.settlementAsset.id
|
||||
);
|
||||
})
|
||||
// filter based on search term
|
||||
.filter((m) => {
|
||||
const code = m.tradableInstrument.instrument.code.toLowerCase();
|
||||
const name = m.tradableInstrument.instrument.name.toLowerCase();
|
||||
if (
|
||||
code.includes(searchTerm.toLowerCase()) ||
|
||||
name.includes(searchTerm.toLowerCase())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sort === Sort.None) {
|
||||
return orderBy(markets, ['tradableInstrument.instrument.code'], ['asc']);
|
||||
}
|
||||
|
||||
if (sort === Sort.Gained || sort === Sort.Lost) {
|
||||
const dir = sort === Sort.Gained ? 'desc' : 'asc';
|
||||
return orderBy(
|
||||
markets,
|
||||
[
|
||||
(m) => {
|
||||
if (!m.candles?.length) return 0;
|
||||
return Number(priceChangePercentage(m.candles.map((c) => c.close)));
|
||||
},
|
||||
],
|
||||
[dir]
|
||||
);
|
||||
}
|
||||
|
||||
if (sort === Sort.New) {
|
||||
return orderBy(
|
||||
markets,
|
||||
[(m) => new Date(m.marketTimestamps.open).getTime()],
|
||||
['desc']
|
||||
);
|
||||
}
|
||||
|
||||
return markets;
|
||||
}, [data, product, searchTerm, assets, sort]);
|
||||
|
||||
return { markets, data, loading, error };
|
||||
};
|
||||
@@ -6,8 +6,8 @@ import { MarketState } from '@vegaprotocol/types';
|
||||
import { subDays } from 'date-fns';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/oracles';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
|
||||
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react';
|
||||
import type { Property } from '@vegaprotocol/types';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/oracles';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
|
||||
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
|
||||
import type { SettlementPriceCellProps } from './settlement-price-cell';
|
||||
import { SettlementPriceCell } from './settlement-price-cell';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DApp, EXPLORER_ORACLE, useLinks } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useOracleSpecBindingData } from '@vegaprotocol/oracles';
|
||||
import { useOracleSpecBindingData } from '@vegaprotocol/market-info';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Icon, NavigationLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { COG } from '@blueprintjs/icons/src/generated/iconNames';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
|
||||
export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
|
||||
return (
|
||||
@@ -9,7 +9,7 @@ export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
|
||||
{withMobile ? (
|
||||
t('Settings')
|
||||
) : (
|
||||
<Icon name={COG} className="!align-middle" />
|
||||
<Icon name={IconNames.COG} className="!align-middle" />
|
||||
)}
|
||||
</NavigationLink>
|
||||
);
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './announcement-banner';
|
||||
export * from './oracle-banner';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useMarketOracle } from '@vegaprotocol/market-info';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const oracleStatuses = {
|
||||
UNKNOWN: t(
|
||||
"This public key's proofs have not been verified yet, or no proofs have been provided yet."
|
||||
),
|
||||
GOOD: t("This public key's proofs have been verified."),
|
||||
SUSPICIOUS: t(
|
||||
'This public key is suspected to be acting in bad faith, pending investigation.'
|
||||
),
|
||||
MALICIOUS: t('This public key has been observed acting in bad faith.'),
|
||||
RETIRED: t('This public key is no longer in use.'),
|
||||
COMPROMISED: t(
|
||||
'This public key is no longer in the control of its original owners.'
|
||||
),
|
||||
};
|
||||
|
||||
export const OracleBanner = ({ marketId }: { marketId: string }) => {
|
||||
const oracle = useMarketOracle(marketId);
|
||||
if (!oracle || oracle.status === 'GOOD') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<NotificationBanner intent={Intent.Danger}>
|
||||
<div>
|
||||
Oracle status for this market is{' '}
|
||||
<span data-testId="oracle-status">{oracle.status}</span>.{' '}
|
||||
{oracleStatuses[oracle.status]}
|
||||
</div>
|
||||
{oracle.status_reason ? (
|
||||
<ReactMarkdown
|
||||
className="react-markdown-container"
|
||||
skipHtml={true}
|
||||
disallowedElements={['img']}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
{oracle.status_reason}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export const HeaderStat = ({
|
||||
testId?: string;
|
||||
}) => {
|
||||
const itemClass =
|
||||
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default text-neutral-500 dark:text-neutral-400';
|
||||
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default first:border-none text-neutral-500 dark:text-neutral-400';
|
||||
const itemHeading = 'text-black dark:text-white';
|
||||
|
||||
return (
|
||||
@@ -72,7 +72,7 @@ export const HeaderTitle = ({
|
||||
secondaryContent: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-left">
|
||||
<div className="text-left" data-testid="header-title">
|
||||
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap leading-4">
|
||||
{primaryContent}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { Navbar } from './navbar';
|
||||
|
||||
describe('Navbar', () => {
|
||||
const pubKey = 'pubKey';
|
||||
it('should be properly rendered', () => {
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Navbar theme="dark" />
|
||||
</VegaWalletContext.Provider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('Markets')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('Trading')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Markets page route should not match empty market page', () => {
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter initialEntries={['/markets/all']}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Navbar theme="dark" />
|
||||
</VegaWalletContext.Provider>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('Markets')).toHaveClass('active');
|
||||
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
|
||||
});
|
||||
});
|
||||
@@ -75,7 +75,7 @@ export const Navbar = ({
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink data-testid="Trading" to={tradingPath}>
|
||||
<NavigationLink data-testid="Trading" to={tradingPath} end>
|
||||
{t('Trading')}
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './select-market-columns';
|
||||
export * from './select-market-table';
|
||||
export * from './select-market';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { PriceCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
|
||||
import { PriceCell } from '@vegaprotocol/datagrid';
|
||||
import { Link as UILink, Sparkline, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import isNil from 'lodash/isNil';
|
||||
import type { CandleClose } from '@vegaprotocol/types';
|
||||
@@ -341,187 +341,3 @@ export const columns = (
|
||||
];
|
||||
return selectMarketColumns;
|
||||
};
|
||||
|
||||
export const columnsPositionMarkets = (
|
||||
market: MarketMaybeWithDataAndCandles,
|
||||
onSelect: (id: string, metaKey?: boolean) => void,
|
||||
inViewRoot?: RefObject<HTMLElement>,
|
||||
openVolume?: string,
|
||||
onCellClick?: OnCellClickHandler
|
||||
) => {
|
||||
const candlesClose = market.candles
|
||||
?.map((candle) => candle?.close)
|
||||
.filter((c: string | undefined): c is CandleClose => !isNil(c));
|
||||
const candleLow = market.candles && calcCandleLow(market.candles);
|
||||
const candleHigh = market.candles && calcCandleHigh(market.candles);
|
||||
const candleVolume = market.candles && calcCandleVolume(market.candles);
|
||||
const selectMarketColumns: Column[] = [
|
||||
{
|
||||
kind: ColumnKind.Market,
|
||||
value: (
|
||||
<Link
|
||||
to={Links[Routes.MARKET](market.id)}
|
||||
data-testid={`market-link-${market.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelect(market.id, e.metaKey || e.ctrlKey);
|
||||
}}
|
||||
>
|
||||
<UILink>{market.tradableInstrument.instrument.code}</UILink>
|
||||
</Link>
|
||||
),
|
||||
className: cellClassNames,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.ProductType,
|
||||
value: market.tradableInstrument.instrument.product.__typename,
|
||||
className: `py-2 first:text-left hidden sm:table-cell`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.LastPrice,
|
||||
value: (
|
||||
<MarketMarkPrice
|
||||
marketId={market.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
inViewRoot={inViewRoot}
|
||||
initialValue={market.data?.markPrice}
|
||||
asPriceCell
|
||||
/>
|
||||
),
|
||||
className: cellClassNames,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Change24,
|
||||
value: (
|
||||
<Last24hPriceChange
|
||||
marketId={market.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
inViewRoot={inViewRoot}
|
||||
initialValue={candlesClose}
|
||||
/>
|
||||
),
|
||||
className: cellClassNames,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Sparkline,
|
||||
value: candlesClose && (
|
||||
<Sparkline
|
||||
width={100}
|
||||
height={20}
|
||||
muted={false}
|
||||
data={candlesClose.map((c: string) => Number(c))}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} hidden lg:table-cell`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Asset,
|
||||
value: (
|
||||
<button
|
||||
data-dialog-trigger
|
||||
className="inline underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!onCellClick) return;
|
||||
onCellClick(
|
||||
e,
|
||||
ColumnKind.Asset,
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||
);
|
||||
}}
|
||||
>
|
||||
{market.tradableInstrument.instrument.product.settlementAsset.symbol}
|
||||
</button>
|
||||
),
|
||||
className: `${cellClassNames} hidden sm:table-cell`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.High24,
|
||||
value: candleHigh ? (
|
||||
<PriceCell
|
||||
value={Number(candleHigh)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
candleHigh.toString(),
|
||||
market.decimalPlaces,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
className: `${cellClassNames} hidden xl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Low24,
|
||||
value: candleLow ? (
|
||||
<PriceCell
|
||||
value={Number(candleLow)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
candleLow.toString(),
|
||||
market.decimalPlaces,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
className: `${cellClassNames} hidden xl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Volume,
|
||||
value: (
|
||||
<Last24hVolume
|
||||
marketId={market.id}
|
||||
inViewRoot={inViewRoot}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
initialValue={candleVolume}
|
||||
formatDecimals={2}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} hidden lg:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
dataTestId: 'market-volume',
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.TradingMode,
|
||||
value: (
|
||||
<MarketTradingMode
|
||||
marketId={market?.id}
|
||||
inViewRoot={inViewRoot}
|
||||
initialTradingMode={market.tradingMode}
|
||||
initialTrigger={market.data?.trigger}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} hidden lg:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
dataTestId: 'trading-mode-col',
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Fee,
|
||||
value: <FeesCell feeFactors={market.fees.factors} />,
|
||||
className: `${cellClassNames} hidden xl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Position,
|
||||
value: (
|
||||
<p className={signedNumberCssClass(openVolume || '')}>
|
||||
{openVolume &&
|
||||
addDecimalsFormatNumber(openVolume, market.positionDecimalPlaces)}
|
||||
</p>
|
||||
),
|
||||
className: `${cellClassNames} hidden xxl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
];
|
||||
return selectMarketColumns;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { columnHeaders } from './select-market-columns';
|
||||
import classNames from 'classnames';
|
||||
import type { Column } from './select-market-columns';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const SelectMarketTableHeader = ({
|
||||
detailed = false,
|
||||
@@ -62,23 +61,3 @@ export const SelectMarketTableRow = ({
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectMarketTableRowSplash = ({
|
||||
children,
|
||||
colSpan,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
colSpan: number;
|
||||
}) => {
|
||||
return (
|
||||
<tr className={`relative`}>
|
||||
<td
|
||||
className="text-center p-10 pt-14 text-xs"
|
||||
key="splash"
|
||||
colSpan={colSpan}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
import { SelectAllMarketsTableBody } from './select-market';
|
||||
|
||||
import type {
|
||||
MarketMaybeWithCandles,
|
||||
MarketMaybeWithData,
|
||||
MarketData,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
|
||||
|
||||
type PartialMarket = Partial<
|
||||
Omit<Market, 'data'> & { data: Partial<MarketData> }
|
||||
>;
|
||||
|
||||
const MARKET_A: PartialMarket = {
|
||||
__typename: 'Market',
|
||||
id: '1',
|
||||
decimalPlaces: 2,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
id: '1',
|
||||
code: 'ABCDEF',
|
||||
name: 'ABCDEF 1-Day',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'ABCDEF',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-ABC',
|
||||
name: '',
|
||||
decimals: 2,
|
||||
symbol: 'ABC',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
},
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
tradingTerminationProperty: 'trading-termination-property',
|
||||
settlementDataProperty: 'settlement-data-property',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
__typename: 'InstrumentMetadata',
|
||||
tags: ['ABC', 'DEF'],
|
||||
},
|
||||
},
|
||||
},
|
||||
fees: {
|
||||
__typename: 'Fees',
|
||||
factors: {
|
||||
__typename: 'FeeFactors',
|
||||
infrastructureFee: '0.01',
|
||||
liquidityFee: '0.01',
|
||||
makerFee: '0.01',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '1',
|
||||
},
|
||||
markPrice: '90',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
|
||||
marketState: Schema.MarketState.STATE_PENDING,
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
indicativeVolume: '1000',
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
high: '100',
|
||||
low: '10',
|
||||
open: '10',
|
||||
close: '80',
|
||||
volume: '1000',
|
||||
periodStart: '2022-11-01T15:49:00Z',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
high: '10',
|
||||
low: '1',
|
||||
open: '1',
|
||||
close: '100',
|
||||
volume: '1000',
|
||||
periodStart: '2022-11-01T15:50:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const MARKET_B: PartialMarket = {
|
||||
__typename: 'Market',
|
||||
id: '2',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 0,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
id: '2',
|
||||
code: 'XYZ',
|
||||
name: 'XYZ 1-Day',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'XYZ',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-XYZ',
|
||||
name: 'asset-XYZ',
|
||||
decimals: 2,
|
||||
symbol: 'XYZ',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
},
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
tradingTerminationProperty: 'trading-termination-property',
|
||||
settlementDataProperty: 'settlement-data-property',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
__typename: 'InstrumentMetadata',
|
||||
tags: ['XYZ'],
|
||||
},
|
||||
},
|
||||
},
|
||||
fees: {
|
||||
__typename: 'Fees',
|
||||
factors: {
|
||||
__typename: 'FeeFactors',
|
||||
infrastructureFee: '0.01',
|
||||
liquidityFee: '0.01',
|
||||
makerFee: '0.01',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '2',
|
||||
},
|
||||
markPrice: '123.123',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
|
||||
marketState: Schema.MarketState.STATE_PENDING,
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
indicativeVolume: '2000',
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
high: '100',
|
||||
low: '10',
|
||||
open: '10',
|
||||
close: '80',
|
||||
volume: '1000',
|
||||
periodStart: '2022-11-01T15:49:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('SelectMarket', () => {
|
||||
const table = document.createElement('table');
|
||||
|
||||
it('should render the SelectAllMarketsTableBody', () => {
|
||||
const onSelect = jest.fn();
|
||||
const onCellClick = jest.fn();
|
||||
const { container } = render(
|
||||
<MemoryRouter>
|
||||
<SelectAllMarketsTableBody
|
||||
markets={[MARKET_A as Market, MARKET_B as Market]}
|
||||
onCellClick={onCellClick}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
{ wrapper: MockedProvider, container: document.body.appendChild(table) }
|
||||
);
|
||||
expect(screen.getByText('ABCDEF')).toBeTruthy(); // name
|
||||
expect(screen.getByText('25.00%')).toBeTruthy(); // price change
|
||||
expect(container).toHaveTextContent(/1,000/); // volume
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
|
||||
expect(onSelect).toHaveBeenCalledWith('1', false);
|
||||
});
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
import { positionsDataProvider } from '@vegaprotocol/positions';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ExternalLink, Icon, Loader, Popover } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
columnHeaders,
|
||||
columnHeadersPositionMarkets,
|
||||
columns,
|
||||
columnsPositionMarkets,
|
||||
} from './select-market-columns';
|
||||
import {
|
||||
SelectMarketTableHeader,
|
||||
SelectMarketTableRow,
|
||||
SelectMarketTableRowSplash,
|
||||
} from './select-market-table';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
|
||||
import type { PositionFieldsFragment } from '@vegaprotocol/positions';
|
||||
import type { Column, OnCellClickHandler } from './select-market-columns';
|
||||
import {
|
||||
DApp,
|
||||
TOKEN_NEW_MARKET_PROPOSAL,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { HeaderTitle } from '../header';
|
||||
|
||||
export const SelectAllMarketsTableBody = ({
|
||||
markets,
|
||||
positions,
|
||||
onSelect,
|
||||
onCellClick,
|
||||
inViewRoot,
|
||||
headers = columnHeaders,
|
||||
tableColumns = (market) => columns(market, onSelect, onCellClick, inViewRoot),
|
||||
}: {
|
||||
markets?: MarketMaybeWithDataAndCandles[] | null;
|
||||
positions?: PositionFieldsFragment[];
|
||||
title?: string;
|
||||
onSelect: (id: string, metaKey?: boolean) => void;
|
||||
onCellClick: OnCellClickHandler;
|
||||
headers?: Column[];
|
||||
tableColumns?: (
|
||||
market: MarketMaybeWithDataAndCandles,
|
||||
inViewRoot?: RefObject<HTMLDivElement>,
|
||||
openVolume?: string
|
||||
) => Column[];
|
||||
inViewRoot?: RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
if (!markets) return null;
|
||||
return (
|
||||
<>
|
||||
<thead className="bg-neutral-100 dark:bg-neutral-800">
|
||||
<SelectMarketTableHeader detailed={true} headers={headers} />
|
||||
</thead>
|
||||
{/* Border styles required to create space between tbody elements margin/padding don't work */}
|
||||
<tbody className="border-b-[10px] border-transparent">
|
||||
{markets.length > 0 ? (
|
||||
markets?.map((market, i) => (
|
||||
<SelectMarketTableRow
|
||||
marketId={market.id}
|
||||
key={i}
|
||||
detailed
|
||||
onSelect={onSelect}
|
||||
columns={tableColumns(
|
||||
market,
|
||||
inViewRoot,
|
||||
positions &&
|
||||
positions.find((p) => p.market.id === market.id)?.openVolume
|
||||
)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SelectMarketTableRowSplash colSpan={12}>
|
||||
{t('No markets ')}
|
||||
<ExternalLink href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}>
|
||||
{t('Propose a new market')}
|
||||
</ExternalLink>
|
||||
</SelectMarketTableRowSplash>
|
||||
)}
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectMarketPopover = ({
|
||||
marketCode,
|
||||
marketName,
|
||||
onSelect,
|
||||
onCellClick,
|
||||
}: {
|
||||
marketCode: string;
|
||||
marketName: string;
|
||||
onSelect: (id: string, metaKey?: boolean) => void;
|
||||
onCellClick: OnCellClickHandler;
|
||||
}) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [open, setOpen] = useState(false);
|
||||
const inViewRoot = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
data,
|
||||
loading: marketsLoading,
|
||||
reload: marketListReload,
|
||||
} = useMarketList();
|
||||
const {
|
||||
data: positions,
|
||||
loading: positionsLoading,
|
||||
reload,
|
||||
} = useDataProvider({
|
||||
dataProvider: positionsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const onSelectMarket = useCallback(
|
||||
(marketId: string, metaKey?: boolean) => {
|
||||
onSelect(marketId, metaKey);
|
||||
setOpen(false);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
const iconClass = open ? 'rotate-180' : '';
|
||||
const markets = useMemo(
|
||||
() =>
|
||||
data?.filter((market) =>
|
||||
positions?.find((node) => node.market.id === market.id)
|
||||
),
|
||||
[data, positions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reload();
|
||||
marketListReload();
|
||||
}
|
||||
}, [open, marketListReload, reload]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onChange={setOpen}
|
||||
trigger={
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderTitle
|
||||
primaryContent={marketCode}
|
||||
secondaryContent={marketName}
|
||||
/>
|
||||
<Icon name="chevron-down" className={iconClass} size={6} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="w-[90vw] max-h-[80vh] overflow-y-auto"
|
||||
data-testid="select-market-list"
|
||||
ref={inViewRoot}
|
||||
>
|
||||
{marketsLoading || (pubKey && positionsLoading) ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Loader size="small" />
|
||||
{t('Loading market data')}
|
||||
</div>
|
||||
) : (
|
||||
<table className="relative text-sm w-full whitespace-nowrap">
|
||||
{pubKey && (positions?.length ?? 0) && (markets?.length ?? 0) ? (
|
||||
<>
|
||||
<TableTitle>{t('My markets')}</TableTitle>
|
||||
<SelectAllMarketsTableBody
|
||||
inViewRoot={inViewRoot}
|
||||
markets={markets}
|
||||
positions={positions || undefined}
|
||||
onSelect={onSelectMarket}
|
||||
onCellClick={onCellClick}
|
||||
headers={columnHeadersPositionMarkets}
|
||||
tableColumns={(market, inViewRoot, openVolume) =>
|
||||
columnsPositionMarkets(
|
||||
market,
|
||||
onSelectMarket,
|
||||
inViewRoot,
|
||||
openVolume,
|
||||
onCellClick
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<TableTitle>{t('All markets')}</TableTitle>
|
||||
<SelectAllMarketsTableBody
|
||||
inViewRoot={inViewRoot}
|
||||
markets={data}
|
||||
onSelect={onSelectMarket}
|
||||
onCellClick={onCellClick}
|
||||
/>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const TableTitle = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="font-normal text-left">
|
||||
<h3 className="text-lg">{children}</h3>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
@@ -81,7 +81,7 @@ function AppBody({ Component }: AppProps) {
|
||||
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[repeat(3,min-content),1fr]'
|
||||
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -93,7 +93,7 @@ function AppBody({ Component }: AppProps) {
|
||||
<Title />
|
||||
<div className={gridClasses}>
|
||||
<AnnouncementBanner />
|
||||
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
|
||||
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
|
||||
<div data-testid="banners">
|
||||
<ProtocolUpgradeProposalNotification
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
|
||||
@@ -127,3 +127,24 @@ html [data-theme='light'] {
|
||||
.ag-theme-balham .ag-row.no-hover:hover {
|
||||
background: var(--ag-background-color);
|
||||
}
|
||||
|
||||
.virtualized-list {
|
||||
/* Works on Firefox */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #999 #333;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
.virtualized-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.virtualized-list::-webkit-scrollbar-thumb {
|
||||
width: 6px;
|
||||
background-color: #333;
|
||||
}
|
||||
.virtualized-list::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||
EXPOSE 80
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY ./dist-result/ /usr/share/nginx/html/
|
||||
@@ -1,29 +0,0 @@
|
||||
# Build container
|
||||
ARG NODE_VERSION
|
||||
FROM --platform=amd64 node:${NODE_VERSION}-alpine3.16 as build
|
||||
WORKDIR /app
|
||||
# Argument to allow building of different apps
|
||||
ARG APP
|
||||
ARG ENV_NAME=""
|
||||
RUN apk add --update --no-cache \
|
||||
python3 \
|
||||
make \
|
||||
gcc \
|
||||
g++
|
||||
COPY . ./
|
||||
RUN yarn --network-timeout 100000 --pure-lockfile
|
||||
# work around for different build process in trading
|
||||
RUN sh docker/docker-build.sh
|
||||
|
||||
# Server environment
|
||||
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
|
||||
# this is to ensure that we run always same version of alpine to make sure ipfs is indempotent
|
||||
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||
# configuration of system
|
||||
EXPOSE 80
|
||||
# Copy dist
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build /app/dist/apps/${APP}/* /usr/share/nginx/html
|
||||
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > /ipfs-hash; apk del go-ipfs
|
||||
@@ -27,5 +27,5 @@ RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build /app/dist/apps/${APP}/* /usr/share/nginx/html
|
||||
RUN apk add --no-cache go-ipfs==0.16.0-r6 \
|
||||
&& ipfs init \
|
||||
&& echo "$(ipfs add -rwQ /usr/share/nginx/html)" > /ipfs-hash \
|
||||
&& echo "$(ipfs add -rQ /usr/share/nginx/html)" > /ipfs-hash \
|
||||
&& echo "ipfs hash of this build: $(cat /ipfs-hash)"
|
||||
|
||||
@@ -5,6 +5,6 @@ RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY ./dist-result/ /usr/share/nginx/html/
|
||||
RUN apk add --no-cache go-ipfs==0.16.0-r6 \
|
||||
&& ipfs init \
|
||||
&& echo "$(ipfs add -rwQ /usr/share/nginx/html)" > /ipfs-hash \
|
||||
&& echo "$(ipfs add -rQ /usr/share/nginx/html)" > /ipfs-hash \
|
||||
&& echo "ipfs hash of this build: $(cat /ipfs-hash)"
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
onClickDeposit && onClickDeposit(data.asset.id);
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="flex gap-2">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.DEPOSIT}
|
||||
size={16}
|
||||
@@ -331,7 +331,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
onClickWithdraw && onClickWithdraw(data.asset.id)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<span className="flex gap-2">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.WITHDRAW}
|
||||
size={16}
|
||||
@@ -347,7 +347,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
setRow(data);
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="flex gap-2">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.BREAKDOWN}
|
||||
size={16}
|
||||
|
||||
@@ -33,9 +33,9 @@ export const useAccountBalance = (assetId?: string) => {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
accountBalance,
|
||||
accountDecimals,
|
||||
accountBalance: pubKey ? accountBalance : '',
|
||||
accountDecimals: pubKey ? accountDecimals : null,
|
||||
}),
|
||||
[accountBalance, accountDecimals]
|
||||
[accountBalance, accountDecimals, pubKey]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,9 +32,9 @@ export const useMarketAccountBalance = (marketId: string) => {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
accountBalance,
|
||||
accountDecimals,
|
||||
accountBalance: pubKey ? accountBalance : '',
|
||||
accountDecimals: pubKey ? accountDecimals : null,
|
||||
}),
|
||||
[accountBalance, accountDecimals]
|
||||
[accountBalance, accountDecimals, pubKey]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,16 +12,19 @@ export * from '../fills/src/lib/fills.mock';
|
||||
export * from '../proposals/src/lib/proposals-data-provider/proposals.mock';
|
||||
export * from '../ledger/src/lib/ledger-entries.mock';
|
||||
export * from '../market-depth/src/lib/market-depth.mock';
|
||||
export * from '../market-info/src/components/market-info/market-info.mock';
|
||||
export * from '../market-info/src/lib/components/market-info/market-info.mock';
|
||||
export * from '../market-list/src/lib/market-candles.mock';
|
||||
export * from '../market-list/src/lib/market-data.mock';
|
||||
export * from '../market-list/src/lib/markets-candles.mock';
|
||||
export * from '../market-list/src/lib/markets-data.mock';
|
||||
export * from '../market-list/src/lib/markets.mock';
|
||||
export * from '../oracles/src/lib/oracle-spec-data-connection.mock';
|
||||
export * from '../market-info/src/lib/oracle-spec-data-connection.mock';
|
||||
export * from '../orders/src/lib/components/order-data-provider/orders.mock';
|
||||
export * from '../positions/src/lib/positions.mock';
|
||||
export * from '../network-parameters/src/network-params.mock';
|
||||
export * from '../wallet/src/connect-dialog/chain-id.mock';
|
||||
export * from '../positions/src/lib/estimate-position.mock';
|
||||
export * from '../trades/src/lib/trades.mock';
|
||||
export * from '../withdraws/src/lib/withdrawal.mock';
|
||||
export * from '../proposals/src/lib/protocol-upgrade-proposals/protocol-statistics-proposals.mock';
|
||||
export * from '../proposals/src/lib/protocol-upgrade-proposals/block-statistics.mock';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { gql } from 'graphql-request';
|
||||
import { selfDelegate } from '../capsule/self-delegate';
|
||||
import { requestGQL, setGraphQLEndpoint } from '../capsule/request';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -19,112 +21,143 @@ export const addValidatorsSelfDelegate = () => {
|
||||
vegaUrl: Cypress.env('VEGA_URL'),
|
||||
faucetUrl: Cypress.env('FAUCET_URL'),
|
||||
};
|
||||
setGraphQLEndpoint(config.vegaUrl);
|
||||
cy.wrap(getStakedByOperator()).as('selfStakeAmount');
|
||||
cy.get('@selfStakeAmount').then((selfStakeAmount) => {
|
||||
if (String(selfStakeAmount) == '0') {
|
||||
// Get node wallet recovery phrases
|
||||
cy.exec('vegacapsule nodes ls --home-path ~/.vegacapsule/testnet/')
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const obj = JSON.parse(result);
|
||||
console.log(obj);
|
||||
cy.writeFile(
|
||||
'./src/fixtures/wallet/node0RecoveryPhrase',
|
||||
obj['testnet-nodeset-validators-0-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletRecoveryPhrase
|
||||
);
|
||||
cy.writeFile(
|
||||
'./src/fixtures/wallet/node1RecoveryPhrase',
|
||||
obj['testnet-nodeset-validators-1-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletRecoveryPhrase
|
||||
);
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-0-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletPublicKey
|
||||
).as('node0PubKey');
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-1-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletPublicKey
|
||||
).as('node1PubKey');
|
||||
|
||||
// Get node wallet recovery phrases
|
||||
cy.exec('vegacapsule nodes ls --home-path ~/.vegacapsule/testnet/')
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const obj = JSON.parse(result);
|
||||
console.log(obj);
|
||||
cy.writeFile(
|
||||
'./src/fixtures/wallet/node0RecoveryPhrase',
|
||||
obj['testnet-nodeset-validators-0-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletRecoveryPhrase
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-0-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletID
|
||||
).as('node0Id');
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-1-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletID
|
||||
).as('node1Id');
|
||||
});
|
||||
|
||||
// Import node wallets
|
||||
cy.exec(
|
||||
'vega wallet import -w node0_wallet --recovery-phrase-file ./src/fixtures/wallet/node0RecoveryPhrase -p ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
);
|
||||
cy.writeFile(
|
||||
'./src/fixtures/wallet/node1RecoveryPhrase',
|
||||
obj['testnet-nodeset-validators-1-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletRecoveryPhrase
|
||||
cy.exec(
|
||||
'vega wallet import -w node1_wallet --recovery-phrase-file ./src/fixtures/wallet/node1RecoveryPhrase -p ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
);
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-0-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletPublicKey
|
||||
).as('node0PubKey');
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-1-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletPublicKey
|
||||
).as('node1PubKey');
|
||||
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-0-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletID
|
||||
).as('node0Id');
|
||||
cy.wrap(
|
||||
obj['testnet-nodeset-validators-1-validator'].Vega.NodeWalletInfo
|
||||
.VegaWalletID
|
||||
).as('node1Id');
|
||||
});
|
||||
// Initialise api token
|
||||
cy.exec(
|
||||
'vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file ./src/fixtures/wallet/passphrase'
|
||||
);
|
||||
|
||||
// Import node wallets
|
||||
cy.exec(
|
||||
'vega wallet import -w node0_wallet --recovery-phrase-file ./src/fixtures/wallet/node0RecoveryPhrase -p ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
);
|
||||
cy.exec(
|
||||
'vega wallet import -w node1_wallet --recovery-phrase-file ./src/fixtures/wallet/node1RecoveryPhrase -p ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
);
|
||||
// Generate api tokens for wallets
|
||||
cy.exec(
|
||||
'vega wallet api-token generate --wallet-name node0_wallet --tokens-passphrase-file ./src/fixtures/wallet/passphrase --wallet-passphrase-file ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
)
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const apiToken = result.match('[a-zA-Z0-9]{64}');
|
||||
if (apiToken) {
|
||||
cy.wrap(apiToken[0]).as('node0ApiToken');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialise api token
|
||||
cy.exec(
|
||||
'vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file ./src/fixtures/wallet/passphrase'
|
||||
);
|
||||
cy.exec(
|
||||
'vega wallet api-token generate --wallet-name node1_wallet --tokens-passphrase-file ./src/fixtures/wallet/passphrase --wallet-passphrase-file ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
)
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const apiToken = result.match('[a-zA-Z0-9]{64}');
|
||||
if (apiToken) {
|
||||
cy.wrap(apiToken[0]).as('node1ApiToken');
|
||||
}
|
||||
});
|
||||
|
||||
// Generate api tokens for wallets
|
||||
cy.exec(
|
||||
'vega wallet api-token generate --wallet-name node0_wallet --tokens-passphrase-file ./src/fixtures/wallet/passphrase --wallet-passphrase-file ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
)
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const apiToken = result.match('[a-zA-Z0-9]{64}');
|
||||
if (apiToken) {
|
||||
cy.wrap(apiToken[0]).as('node0ApiToken');
|
||||
}
|
||||
});
|
||||
cy.updateCapsuleMultiSig();
|
||||
cy.highlight('Validators self-delegating');
|
||||
|
||||
cy.exec(
|
||||
'vega wallet api-token generate --wallet-name node1_wallet --tokens-passphrase-file ./src/fixtures/wallet/passphrase --wallet-passphrase-file ./src/fixtures/wallet/passphrase --home ~/.vegacapsule/testnet/wallet'
|
||||
)
|
||||
.its('stdout')
|
||||
.then((result) => {
|
||||
const apiToken = result.match('[a-zA-Z0-9]{64}');
|
||||
if (apiToken) {
|
||||
cy.wrap(apiToken[0]).as('node1ApiToken');
|
||||
}
|
||||
});
|
||||
|
||||
cy.updateCapsuleMultiSig();
|
||||
cy.highlight('Validators self-delegating');
|
||||
|
||||
// Self delegating Node 0 wallet
|
||||
cy.get('@node0PubKey').then((node0PubKey) => {
|
||||
cy.get('@node0ApiToken').then((node0ApiToken) => {
|
||||
cy.get('@node0Id').then((node0Id) => {
|
||||
cy.wrap(
|
||||
selfDelegate(
|
||||
config,
|
||||
String(node0PubKey),
|
||||
String(node0ApiToken),
|
||||
String(node0Id)
|
||||
),
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
// Self delegating Node 1 wallet
|
||||
cy.get('@node1PubKey').then((node1PubKey) => {
|
||||
cy.get('@node1ApiToken').then((node1ApiToken) => {
|
||||
cy.get('@node1Id').then((node1Id) => {
|
||||
cy.wrap(
|
||||
selfDelegate(
|
||||
config,
|
||||
String(node1PubKey),
|
||||
String(node1ApiToken),
|
||||
String(node1Id)
|
||||
),
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
// Self delegating Node 0 wallet
|
||||
cy.get('@node0PubKey').then((node0PubKey) => {
|
||||
cy.get('@node0ApiToken').then((node0ApiToken) => {
|
||||
cy.get('@node0Id').then((node0Id) => {
|
||||
cy.wrap(
|
||||
selfDelegate(
|
||||
config,
|
||||
String(node0PubKey),
|
||||
String(node0ApiToken),
|
||||
String(node0Id)
|
||||
),
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
// Self delegating Node 1 wallet
|
||||
cy.get('@node1PubKey').then((node1PubKey) => {
|
||||
cy.get('@node1ApiToken').then((node1ApiToken) => {
|
||||
cy.get('@node1Id').then((node1Id) => {
|
||||
cy.wrap(
|
||||
selfDelegate(
|
||||
config,
|
||||
String(node1PubKey),
|
||||
String(node1ApiToken),
|
||||
String(node1Id)
|
||||
),
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async function getStakedByOperator() {
|
||||
const query = gql`
|
||||
query ExplorerNodes {
|
||||
nodesConnection {
|
||||
edges {
|
||||
node {
|
||||
stakedByOperator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const res = await requestGQL<{
|
||||
nodesConnection: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
stakedByOperator: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
}>(query);
|
||||
|
||||
return res.nodesConnection.edges[0].node.stakedByOperator;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,12 @@ const hasOperationName = (
|
||||
operationName: string
|
||||
) => {
|
||||
const { body } = req;
|
||||
return 'operationName' in body && body.operationName === operationName;
|
||||
return (
|
||||
typeof body === 'object' &&
|
||||
body !== null &&
|
||||
'operationName' in body &&
|
||||
body.operationName === operationName
|
||||
);
|
||||
};
|
||||
|
||||
export function addMockGQLCommand() {
|
||||
|
||||
@@ -20,8 +20,12 @@ const mockSocketServer = Cypress.env('VEGA_URL')
|
||||
: null;
|
||||
|
||||
// DO NOT REMOVE: PASSTHROUGH for walletconnect
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const relayServer = new Server('wss://relay.walletconnect.com', {
|
||||
new Server('wss://relay.walletconnect.com', {
|
||||
mock: false,
|
||||
});
|
||||
|
||||
// DO NOT REMOVE: PASSTHROUGH for hot module reload
|
||||
new Server('ws://localhost:4200/_next/webpack-hmr', {
|
||||
mock: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
priceChange,
|
||||
priceChangePercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { memo, forwardRef } from 'react';
|
||||
@@ -13,29 +15,6 @@ export interface PriceChangeCellProps {
|
||||
decimalPlaces?: number;
|
||||
}
|
||||
|
||||
export const priceChangePercentage = (candles: string[]) => {
|
||||
const change = priceChange(candles);
|
||||
if (change && candles && candles.length > 0) {
|
||||
const yesterdayLastPrice = candles[0] && BigInt(candles[0]);
|
||||
if (yesterdayLastPrice) {
|
||||
return new BigNumber(change.toString())
|
||||
.dividedBy(new BigNumber(yesterdayLastPrice.toString()))
|
||||
.multipliedBy(100)
|
||||
.toNumber();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const priceChange = (candles: string[]) => {
|
||||
return candles &&
|
||||
candles[candles.length - 1] !== undefined &&
|
||||
candles[0] !== undefined
|
||||
? BigInt(candles[candles.length - 1] ?? 0) - BigInt(candles[0] ?? 0)
|
||||
: 0;
|
||||
};
|
||||
|
||||
export const PriceChangeCell = memo(
|
||||
forwardRef<HTMLSpanElement, PriceChangeCellProps>(
|
||||
({ candles, decimalPlaces }: PriceChangeCellProps, ref) => {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
function ReactMarkdown({ children }) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default ReactMarkdown;
|
||||
@@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Icon, Tooltip, TrafficLight } from '@vegaprotocol/ui-toolkit';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import * as constants from '../constants';
|
||||
|
||||
interface DealTicketEstimatesProps {
|
||||
quoteName?: string;
|
||||
price?: string;
|
||||
estCloseOut?: string;
|
||||
estMargin?: string;
|
||||
fees?: string;
|
||||
notionalSize?: string;
|
||||
size?: string;
|
||||
slippage?: string;
|
||||
}
|
||||
|
||||
export const DealTicketEstimates = ({
|
||||
price,
|
||||
quoteName,
|
||||
estCloseOut,
|
||||
estMargin,
|
||||
fees,
|
||||
notionalSize,
|
||||
size,
|
||||
slippage,
|
||||
}: DealTicketEstimatesProps) => (
|
||||
<dl className="text-black dark:text-white">
|
||||
{size && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle>{t('Contracts')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={size}
|
||||
description={constants.CONTRACTS_MARGIN_TOOLTIP_TEXT}
|
||||
id="contracts_tooltip_trigger"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{price && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle>{t('Est. Price')}</DataTitle>
|
||||
<dd>{price}</dd>
|
||||
</div>
|
||||
)}
|
||||
{notionalSize && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Position Size')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={notionalSize}
|
||||
description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName || '')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{fees && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Fees')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={fees}
|
||||
description={constants.EST_FEES_TOOLTIP_TEXT}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{estMargin && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={estMargin}
|
||||
description={constants.EST_MARGIN_TOOLTIP_TEXT(quoteName || '')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{estCloseOut && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={estCloseOut}
|
||||
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT(quoteName || '')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{slippage && (
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
|
||||
<ValueTooltipRow description={constants.EST_SLIPPAGE}>
|
||||
<TrafficLight value={parseFloat(slippage)} q1={1} q2={5}>
|
||||
{slippage}%
|
||||
</TrafficLight>
|
||||
</ValueTooltipRow>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
);
|
||||
|
||||
interface DataTitleProps {
|
||||
children: ReactNode;
|
||||
quoteName?: string;
|
||||
}
|
||||
|
||||
export const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
|
||||
<dt>
|
||||
{children}
|
||||
{quoteName && <small> ({quoteName})</small>}
|
||||
</dt>
|
||||
);
|
||||
|
||||
interface ValueTooltipProps {
|
||||
value?: string;
|
||||
children?: ReactNode;
|
||||
description: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const ValueTooltipRow = ({
|
||||
value,
|
||||
children,
|
||||
description,
|
||||
id,
|
||||
}: ValueTooltipProps) => (
|
||||
<dd className="flex gap-x-2 items-center">
|
||||
{value || children}
|
||||
<Tooltip align="center" description={description}>
|
||||
<div className="cursor-help" id={id || ''} tabIndex={-1}>
|
||||
<Icon
|
||||
name={IconNames.ISSUE}
|
||||
className="block rotate-180"
|
||||
ariaLabel={description}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</dd>
|
||||
);
|
||||
@@ -1,24 +1,8 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
getFeeDetailsValues,
|
||||
useFeeDealTicketDetails,
|
||||
} from '../../hooks/use-fee-deal-ticket-details';
|
||||
|
||||
interface DealTicketFeeDetailsProps {
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
currentInitialMargin?: string;
|
||||
currentMaintenanceMargin?: string;
|
||||
estimatedInitialMargin: string;
|
||||
estimatedTotalInitialMargin: string;
|
||||
marginAccountBalance: string;
|
||||
generalAccountBalance: string;
|
||||
}
|
||||
import { getFeeDetailsValues } from '../../hooks/use-fee-deal-ticket-details';
|
||||
import type { FeeDetails } from '../../hooks/use-fee-deal-ticket-details';
|
||||
|
||||
export interface DealTicketFeeDetailProps {
|
||||
label: string;
|
||||
@@ -45,17 +29,8 @@ export const DealTicketFeeDetail = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DealTicketFeeDetails = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
...args
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const feeDetails = useFeeDealTicketDetails(order, market, marketData);
|
||||
const details = getFeeDetailsValues({
|
||||
...feeDetails,
|
||||
...args,
|
||||
});
|
||||
export const DealTicketFeeDetails = (props: FeeDetails) => {
|
||||
const details = getFeeDetailsValues(props);
|
||||
return (
|
||||
<div>
|
||||
{details.map(({ label, value, labelDescription, symbol, indent }) => (
|
||||
|
||||
@@ -25,6 +25,16 @@ import {
|
||||
TinyScroll,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import {
|
||||
useEstimatePositionQuery,
|
||||
useOpenVolume,
|
||||
} from '@vegaprotocol/positions';
|
||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { useEstimateFees } from '../../hooks/use-fee-deal-ticket-details';
|
||||
import { getDerivedPrice } from '../../utils/get-price';
|
||||
import type { OrderInfo } from '@vegaprotocol/types';
|
||||
|
||||
import {
|
||||
validateExpiration,
|
||||
validateMarketState,
|
||||
@@ -34,7 +44,6 @@ import {
|
||||
} from '../../utils';
|
||||
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||
import { SummaryValidationType } from '../../constants';
|
||||
import { useInitialMargin } from '../../hooks/use-initial-margin';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
import {
|
||||
@@ -104,7 +113,67 @@ export const DealTicket = ({
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
|
||||
const { margin, totalMargin } = useInitialMargin(market.id, normalizedOrder);
|
||||
const price = useMemo(() => {
|
||||
return normalizedOrder && getDerivedPrice(normalizedOrder, marketData);
|
||||
}, [normalizedOrder, marketData]);
|
||||
|
||||
const notionalSize = useMemo(() => {
|
||||
if (price && normalizedOrder?.size) {
|
||||
return removeDecimal(
|
||||
toBigNum(
|
||||
normalizedOrder.size,
|
||||
market.positionDecimalPlaces
|
||||
).multipliedBy(toBigNum(price, market.decimalPlaces)),
|
||||
asset.decimals
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
price,
|
||||
normalizedOrder?.size,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
asset.decimals,
|
||||
]);
|
||||
|
||||
const feeEstimate = useEstimateFees(
|
||||
normalizedOrder && { ...normalizedOrder, price }
|
||||
);
|
||||
const { data: activeOrders } = useDataProvider({
|
||||
dataProvider: activeOrdersProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
|
||||
const orders = activeOrders
|
||||
? activeOrders.map<OrderInfo>(({ node: order }) => ({
|
||||
isMarketOrder: order.type === OrderType.TYPE_MARKET,
|
||||
price: order.price,
|
||||
remaining: order.remaining,
|
||||
side: order.side,
|
||||
}))
|
||||
: [];
|
||||
if (normalizedOrder) {
|
||||
orders.push({
|
||||
isMarketOrder: normalizedOrder.type === OrderType.TYPE_MARKET,
|
||||
price: normalizedOrder.price ?? '0',
|
||||
remaining: normalizedOrder.size,
|
||||
side: normalizedOrder.side,
|
||||
});
|
||||
}
|
||||
const { data: positionEstimate } = useEstimatePositionQuery({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
openVolume,
|
||||
orders,
|
||||
collateralAvailable:
|
||||
marginAccountBalance || generalAccountBalance ? balance : undefined,
|
||||
},
|
||||
skip: !normalizedOrder,
|
||||
});
|
||||
|
||||
const assetSymbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
@@ -401,7 +470,10 @@ export const DealTicket = ({
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
margin={totalMargin}
|
||||
margin={
|
||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
||||
'0'
|
||||
}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onClickCollateral={onClickCollateral}
|
||||
@@ -413,15 +485,15 @@ export const DealTicket = ({
|
||||
}
|
||||
/>
|
||||
<DealTicketFeeDetails
|
||||
order={normalizedOrder}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
estimatedInitialMargin={margin}
|
||||
estimatedTotalInitialMargin={totalMargin}
|
||||
currentInitialMargin={currentMargins?.initialLevel}
|
||||
currentMaintenanceMargin={currentMargins?.maintenanceLevel}
|
||||
feeEstimate={feeEstimate}
|
||||
notionalSize={notionalSize}
|
||||
assetSymbol={assetSymbol}
|
||||
marginAccountBalance={marginAccountBalance}
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
positionEstimate={positionEstimate?.estimatePosition}
|
||||
market={market}
|
||||
currentInitialMargin={currentMargins?.initialLevel}
|
||||
currentMaintenanceMargin={currentMargins?.maintenanceLevel}
|
||||
/>
|
||||
</form>
|
||||
</TinyScroll>
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from './deal-ticket-limit-amount';
|
||||
export * from './deal-ticket-market-amount';
|
||||
export * from './deal-ticket';
|
||||
export * from './expiry-selector';
|
||||
export * from './market-selector';
|
||||
export * from './side-selector';
|
||||
export * from './time-in-force-selector';
|
||||
export * from './type-selector';
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import * as DialogPrimitives from '@radix-ui/react-dialog';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ButtonLink,
|
||||
Icon,
|
||||
Input,
|
||||
Loader,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useScreenDimensions,
|
||||
useOutsideClick,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
|
||||
interface Props {
|
||||
market: Market;
|
||||
setMarket: (marketId: string) => void;
|
||||
ItemRenderer?: React.FC<{
|
||||
market: Market;
|
||||
isMobile?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
function escapeRegExp(str: string) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export const MarketSelector = ({ market, setMarket, ItemRenderer }: Props) => {
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const contRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const arrowButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [results, setResults] = useState<Market[]>([]);
|
||||
const [showPane, setShowPane] = useState(false);
|
||||
const [lookup, setLookup] = useState(
|
||||
market.tradableInstrument.instrument.name || ''
|
||||
);
|
||||
const [dialogContent, setDialogContent] = useState<React.ReactNode | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketsProvider,
|
||||
variables: undefined,
|
||||
skipUpdates: true,
|
||||
});
|
||||
|
||||
const outsideClickCb = useCallback(() => {
|
||||
if (!isMobile) {
|
||||
setShowPane(false);
|
||||
}
|
||||
}, [setShowPane, isMobile]);
|
||||
|
||||
useOutsideClick({ refs: [contRef, arrowButtonRef], func: outsideClickCb });
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
setLookup(value);
|
||||
setShowPane(true);
|
||||
},
|
||||
[setLookup, setShowPane]
|
||||
);
|
||||
|
||||
const handleMarketSelect = useCallback(
|
||||
(market: {
|
||||
id: string;
|
||||
tradableInstrument: { instrument: { name: string } };
|
||||
}) => {
|
||||
setLookup(market.tradableInstrument.instrument.name);
|
||||
setShowPane(false);
|
||||
setMarket(market.id);
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
[setLookup, setShowPane, setMarket, inputRef]
|
||||
);
|
||||
|
||||
const handleItemKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent, market: Market, index: number) => {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
if (index < results.length - 1) {
|
||||
(contRef.current?.children[index + 1] as HTMLDivElement).focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
if (!index) {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.setSelectionRange(
|
||||
inputRef.current?.value.length,
|
||||
inputRef.current?.value.length
|
||||
);
|
||||
return;
|
||||
}
|
||||
(contRef.current?.children[index - 1] as HTMLDivElement).focus();
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
handleMarketSelect(market);
|
||||
break;
|
||||
default:
|
||||
setShowPane(false);
|
||||
setLookup(market.tradableInstrument.instrument.name);
|
||||
}
|
||||
},
|
||||
[results, handleMarketSelect]
|
||||
);
|
||||
|
||||
const handleInputKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
(contRef.current?.children[0] as HTMLDivElement)?.focus();
|
||||
}
|
||||
},
|
||||
[contRef]
|
||||
);
|
||||
|
||||
const handleOnBlur = useCallback(() => {
|
||||
if (!lookup && !showPane) {
|
||||
setLookup(market.tradableInstrument.instrument.name);
|
||||
}
|
||||
}, [market, lookup, showPane, setLookup]);
|
||||
|
||||
const openPane = useCallback(() => {
|
||||
setShowPane(!showPane);
|
||||
inputRef.current?.focus();
|
||||
}, [showPane, setShowPane, inputRef]);
|
||||
|
||||
const handleDialogOnchange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
setShowPane(isOpen);
|
||||
if (!isOpen) {
|
||||
setLookup(lookup || market.tradableInstrument.instrument.name);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
},
|
||||
[
|
||||
setShowPane,
|
||||
lookup,
|
||||
setLookup,
|
||||
market.tradableInstrument.instrument.name,
|
||||
inputRef,
|
||||
]
|
||||
);
|
||||
|
||||
const selectorContent = useMemo(() => {
|
||||
return (
|
||||
<div className="relative flex flex-col">
|
||||
<div className="relative w-full min-h-[30px]">
|
||||
<Input
|
||||
className="h-[30px] w-[calc(100%-20px)] border-none outline-none"
|
||||
ref={inputRef}
|
||||
tabIndex={0}
|
||||
value={lookup}
|
||||
placeholder={t('Search')}
|
||||
onChange={handleOnChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleOnBlur}
|
||||
/>
|
||||
<span className="absolute self-end top-0 right-0 z-10">
|
||||
<ButtonLink
|
||||
onClick={openPane}
|
||||
ref={arrowButtonRef}
|
||||
data-testid="arrow-button"
|
||||
>
|
||||
<Icon
|
||||
name={IconNames.ARROW_DOWN}
|
||||
className={classNames('fill-current transition-transform', {
|
||||
'rotate-180': showPane,
|
||||
})}
|
||||
/>
|
||||
</ButtonLink>
|
||||
</span>
|
||||
</div>
|
||||
<hr className="mb-2" />
|
||||
<div
|
||||
className={classNames(
|
||||
'md:absolute z-20 flex flex-col top-[30px] md:drop-shadow-md md:border md:border-black md:dark:border-white bg-white dark:bg-black text-black dark:text-white min-w-full md:max-h-[200px] overflow-y-auto',
|
||||
showPane ? 'block' : 'hidden'
|
||||
)}
|
||||
data-testid="market-pane"
|
||||
>
|
||||
{loading && (
|
||||
<div className="p-4">
|
||||
<Loader size="small" />
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<Splash>{t(`Something went wrong: ${error.message}`)}</Splash>
|
||||
)}
|
||||
<div ref={contRef} className="w-full">
|
||||
{results.map((market, i) => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={market.id}
|
||||
className="bg-white dark:bg-black cursor-pointer px-4 py-2"
|
||||
onClick={() => handleMarketSelect(market)}
|
||||
onKeyDown={(e) => handleItemKeyDown(e, market, i)}
|
||||
>
|
||||
{ItemRenderer ? (
|
||||
<ItemRenderer market={market} />
|
||||
) : (
|
||||
market.tradableInstrument.instrument.name
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
ItemRenderer,
|
||||
error,
|
||||
handleInputKeyDown,
|
||||
handleItemKeyDown,
|
||||
handleMarketSelect,
|
||||
handleOnBlur,
|
||||
handleOnChange,
|
||||
loading,
|
||||
lookup,
|
||||
openPane,
|
||||
results,
|
||||
showPane,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setResults(
|
||||
data?.filter(
|
||||
(item) =>
|
||||
item.state === Schema.MarketState.STATE_ACTIVE &&
|
||||
item.tradableInstrument.instrument.name.match(
|
||||
new RegExp(escapeRegExp(lookup), 'i')
|
||||
)
|
||||
) || []
|
||||
);
|
||||
}, [data, lookup]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, [inputRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPane && isMobile) {
|
||||
setDialogContent(selectorContent);
|
||||
inputRef.current?.focus();
|
||||
window.scrollTo(0, 0);
|
||||
} else {
|
||||
setDialogContent(null);
|
||||
}
|
||||
}, [selectorContent, showPane, isMobile, setDialogContent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!dialogContent && selectorContent}
|
||||
<MarketDrawer
|
||||
open={Boolean(dialogContent)}
|
||||
onChange={handleDialogOnchange}
|
||||
>
|
||||
{dialogContent}
|
||||
</MarketDrawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface MarketDrawerProps {
|
||||
children: ReactNode;
|
||||
open: boolean;
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const MarketDrawer = ({
|
||||
children,
|
||||
open,
|
||||
onChange,
|
||||
}: MarketDrawerProps) => {
|
||||
const contentClasses = classNames(
|
||||
// Positions the modal in the center of screen
|
||||
'z-20 fixed p-8 inset-x-1/2 dark:text-white w-screen',
|
||||
// Need to apply background and text colors again as content is rendered in a portal
|
||||
'dark:bg-black bg-white',
|
||||
'left-[0px] top-[99px] h-[calc(100%-99px)] overflow-y-auto'
|
||||
);
|
||||
return (
|
||||
<DialogPrimitives.Root open={open} onOpenChange={(x) => onChange?.(x)}>
|
||||
<DialogPrimitives.Portal>
|
||||
<DialogPrimitives.Overlay
|
||||
className="fixed inset-0 bg-black/50 z-10"
|
||||
data-testid="dialog-overlay"
|
||||
/>
|
||||
<DialogPrimitives.Content className={contentClasses}>
|
||||
<DialogPrimitives.Close
|
||||
className="p-2 absolute top-2 right-2"
|
||||
data-testid="dialog-close"
|
||||
>
|
||||
<Icon name="cross" />
|
||||
</DialogPrimitives.Close>
|
||||
<div className="flex gap-4 max-w-full">
|
||||
<div data-testid="dialog-content" className="flex-1">
|
||||
<h1 className="text-xl uppercase mb-4" data-testid="dialog-title">
|
||||
{t('Select market')}
|
||||
</h1>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPrimitives.Content>
|
||||
</DialogPrimitives.Portal>
|
||||
</DialogPrimitives.Root>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './deal-ticket';
|
||||
export * from './deal-ticket-validation';
|
||||
export * from './trading-mode-tooltip';
|
||||
export * from './deal-ticket-estimates';
|
||||
|
||||
@@ -59,6 +59,10 @@ export const EST_FEES_TOOLTIP_TEXT = t(
|
||||
'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.'
|
||||
);
|
||||
|
||||
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
|
||||
'This is a approximation to the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
|
||||
);
|
||||
|
||||
export const EST_SLIPPAGE = t(
|
||||
'When you execute a trade on Vega, the price obtained in the market may differ from the best available price displayed at the time of placing the trade. The estimated slippage shows the difference between the best available price and the estimated execution price, determined by market liquidity and your chosen order size.'
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user