Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b216467009 | ||
|
|
03ec5aac5b | ||
|
|
34400be18a | ||
|
|
d4a0a4a42c | ||
|
|
fabf643ce7 | ||
|
|
45e210092c | ||
|
|
1dc3b3fd05 | ||
|
|
0a80e3e39d | ||
|
|
7b8d9dc94b | ||
|
|
c0320c2468 | ||
|
|
11173d6614 | ||
|
|
e3324331e2 | ||
|
|
0a6e3cb6f5 | ||
|
|
68ede90609 | ||
|
|
09778a5d3f | ||
|
|
59993e3ea6 | ||
|
|
54161a6dba | ||
|
|
26f524ea07 | ||
|
|
db13e6f21d | ||
|
|
4a679813ba | ||
|
|
a4279d5b5e | ||
|
|
1a274a67c3 | ||
|
|
1e9ff31fd7 |
@@ -20,7 +20,7 @@ jobs:
|
||||
project: ${{ fromJSON(inputs.projects) }}
|
||||
name: ${{ matrix.project }}
|
||||
runs-on: self-hosted-runner
|
||||
timeout-minutes: 40
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
# Checks if skip cache was requested
|
||||
- name: Set skip-nx-cache flag
|
||||
@@ -98,4 +98,4 @@ jobs:
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: test-report-${{ matrix.project }}
|
||||
path: frontend-monorepo/apps/trading-e2e/cypress/reports
|
||||
path: frontend-monorepo/apps/${{ matrix.project }}/cypress/reports
|
||||
|
||||
@@ -191,37 +191,36 @@ jobs:
|
||||
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
|
||||
https://api.fleek.co/graphql
|
||||
|
||||
- name: Checkout vega.xyz
|
||||
- name: Update vega.trading DNS to redirect to the new console
|
||||
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
|
||||
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
|
||||
|
||||
# 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
|
||||
# Generate console URL
|
||||
new_console_url_type=ipfs
|
||||
# new_console_url_type=ipns
|
||||
new_console_url_domain=cf-ipfs.com
|
||||
# new_console_url_domain=dweb.link
|
||||
new_console_url="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
|
||||
echo "new_console_url=${new_console_url}"
|
||||
|
||||
# Update record in DNSimple
|
||||
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
|
||||
dnsimple_account_id=84895
|
||||
dnsimple_zone_name=vega.trading
|
||||
dnsimple_record_id=44409591
|
||||
# see: https://dnsimple.com/a/84895/domains/vega.trading/records/44409591/edit
|
||||
|
||||
curl -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X PATCH \
|
||||
-d "{
|
||||
\"content\": \"${new_console_url}\"
|
||||
}" \
|
||||
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
|
||||
|
||||
+1
-1
@@ -49,4 +49,4 @@ cypress.env.json
|
||||
.next
|
||||
|
||||
#cypress
|
||||
/apps/trading-e2e/cypress/reports/
|
||||
/apps/**/cypress/reports/
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { defineConfig } = require('cypress');
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: 'et4snf',
|
||||
reporter: '../../node_modules/cypress-mochawesome-reporter',
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
require('cypress-mochawesome-reporter/plugin')(on);
|
||||
require('@cypress/grep/src/plugin')(config);
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
|
||||
import '@vegaprotocol/cypress';
|
||||
import './common.functions.js';
|
||||
import 'cypress-mochawesome-reporter/register';
|
||||
import registerCypressGrep from '@cypress/grep';
|
||||
registerCypressGrep();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Header } from './header';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
NetworkSwitcher: () => (
|
||||
<div data-testid="network-switcher">NetworkSwitcher</div>
|
||||
),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { defineConfig } = require('cypress');
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: 'et4snf',
|
||||
reporter: '../../node_modules/cypress-mochawesome-reporter',
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
require('cypress-mochawesome-reporter/plugin')(on);
|
||||
require('@cypress/grep/src/plugin')(config);
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -63,7 +63,6 @@ context(
|
||||
});
|
||||
|
||||
it('should have option to go to last and newest page', function () {
|
||||
waitForBeginningOfEpoch();
|
||||
cy.getByTestId('goto-last-page').click();
|
||||
cy.getByTestId('epoch-total-rewards-table')
|
||||
.last()
|
||||
|
||||
@@ -8,6 +8,7 @@ import './wallet-eth.functions.ts';
|
||||
import './wallet-teardown.functions.ts';
|
||||
import './wallet-vega.functions.ts';
|
||||
import './proposal.functions.ts';
|
||||
import 'cypress-mochawesome-reporter/register';
|
||||
import registerCypressGrep from '@cypress/grep';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
} from './components/telemetry-dialog/telemetry-dialog';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isPartyNotFoundError } from './lib/party';
|
||||
|
||||
const cache: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
@@ -211,24 +212,36 @@ const AppContainer = () => {
|
||||
enabled: true,
|
||||
environment: VEGA_ENV,
|
||||
release: GIT_COMMIT_HASH,
|
||||
beforeSend(event) {
|
||||
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 && isPartyNotFoundError({ message: error })) ||
|
||||
(errorIsObject && isPartyNotFoundError(error))
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
const updatedRequest =
|
||||
requestUrl && requestUrl.includes('/test?')
|
||||
requestUrl && requestUrl.includes('/claim?')
|
||||
? { ...event.request, url: removeQueryParams(requestUrl) }
|
||||
: event.request;
|
||||
|
||||
const updatedTransaction =
|
||||
transaction && transaction.includes('/test?')
|
||||
transaction && transaction.includes('/claim?')
|
||||
? removeQueryParams(transaction)
|
||||
: transaction;
|
||||
|
||||
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
|
||||
if (
|
||||
breadcrumb.type === 'navigation' &&
|
||||
breadcrumb.data?.to?.includes('/test?')
|
||||
breadcrumb.data?.to?.includes('/claim?')
|
||||
) {
|
||||
return {
|
||||
...breadcrumb,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExternalLinks } from '@vegaprotocol/utils';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
|
||||
export const DownloadWalletPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
WalletDelegationFieldsFragment,
|
||||
} from './__generated__/Delegations';
|
||||
import { DelegationsDocument } from './__generated__/Delegations';
|
||||
import { isPartyNotFoundError } from '../../lib/party';
|
||||
|
||||
export const usePollForDelegations = () => {
|
||||
const { token: vegaToken } = useContracts();
|
||||
@@ -75,6 +76,7 @@ export const usePollForDelegations = () => {
|
||||
})
|
||||
.then((res) => {
|
||||
if (!mounted) return;
|
||||
|
||||
const canonisedDelegations = removePaginationWrapper(
|
||||
res.data.party?.delegationsConnection?.edges
|
||||
);
|
||||
@@ -203,6 +205,15 @@ export const usePollForDelegations = () => {
|
||||
setDelegatedNodes(delegatedAmounts);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// if party isn't found, dont log to Sentry, just clear state as, user
|
||||
// will not have any delagations or accounts
|
||||
if (isPartyNotFoundError(err)) {
|
||||
setDelegations([]);
|
||||
setAccounts([]);
|
||||
setDelegatedNodes([]);
|
||||
setCurrentStakeAvailable(new BigNumber(0));
|
||||
return;
|
||||
}
|
||||
Sentry.captureException(err);
|
||||
// If query fails stop interval. Its almost certain that the query
|
||||
// will just continue to fail
|
||||
|
||||
@@ -594,7 +594,9 @@
|
||||
"numberOfAgainstVotes": "Number of votes against",
|
||||
"yesPercentage": "Yes percentage",
|
||||
"noPercentage": "No percentage",
|
||||
"proposalTerms": "Proposal terms",
|
||||
"proposalJson": "Full proposal JSON",
|
||||
"proposalDetails": "Proposal details",
|
||||
"proposalDescription": "Description",
|
||||
"currentlySetTo": "Currently expected to ",
|
||||
"currently": "currently",
|
||||
"finalOutcomeMayDiffer": "Final outcome may differ",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const PARTY_NOT_FOUND = 'failed to get party for ID';
|
||||
|
||||
export const isPartyNotFoundError = (error: { message: string }) => {
|
||||
if (error.message.includes(PARTY_NOT_FOUND)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -12,7 +12,8 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
||||
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
|
||||
import Routes from '../routes';
|
||||
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
|
||||
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { ProposalDescription } from './proposal-description';
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import classnames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
|
||||
export const ProposalDescription = ({
|
||||
description,
|
||||
}: {
|
||||
description: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDescription, setShowDescription] = useState(false);
|
||||
const showDescriptionIconClasses = classnames('mb-4', {
|
||||
'rotate-180': showDescription,
|
||||
});
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-description">
|
||||
<button
|
||||
onClick={() => setShowDescription(!showDescription)}
|
||||
data-testid="proposal-description-toggle"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SubHeading title={t('proposalDescription')} />
|
||||
<div className={showDescriptionIconClasses}>
|
||||
<Icon name="chevron-down" size={8} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{showDescription && (
|
||||
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
|
||||
<div className="p-2">
|
||||
<ReactMarkdown
|
||||
className="react-markdown-container"
|
||||
/* Prevents HTML embedded in the description from rendering */
|
||||
skipHtml={true}
|
||||
/* Stops users embedding images which could be used for tracking */
|
||||
disallowedElements={['img']}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</RoundedWrapper>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
-22
@@ -206,28 +206,6 @@ describe('Proposal header', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders Freeform proposal - long rationale (105 chars) - details', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
id: 'long',
|
||||
rationale: {
|
||||
title: '0x0',
|
||||
description:
|
||||
'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.',
|
||||
},
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
}),
|
||||
false
|
||||
);
|
||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
||||
/Class aptent/
|
||||
);
|
||||
});
|
||||
|
||||
// Remove once proposals have rationale and re-enable above tests
|
||||
it('Renders Freeform proposal - id for title', () => {
|
||||
renderComponent(
|
||||
|
||||
+1
-24
@@ -5,7 +5,6 @@ import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
@@ -23,12 +22,7 @@ export const ProposalHeader = ({
|
||||
let details: ReactNode;
|
||||
let proposalType = '';
|
||||
|
||||
let title = proposal?.rationale.title.trim();
|
||||
let description = proposal?.rationale.description.trim();
|
||||
if (title?.length === 0 && description && description.length > 0) {
|
||||
title = description;
|
||||
description = '';
|
||||
}
|
||||
const title = proposal?.rationale.title.trim();
|
||||
|
||||
const titleContent = shorten(title ?? '', 100);
|
||||
|
||||
@@ -145,23 +139,6 @@ export const ProposalHeader = ({
|
||||
{details}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{description && !isListItem && (
|
||||
<div data-testid="proposal-description">
|
||||
{/*<div className="uppercase mr-2">{t('ProposalDescription')}:</div>*/}
|
||||
<SubHeading title={t('ProposalDescription')} />
|
||||
<ReactMarkdown
|
||||
className="react-markdown-container"
|
||||
/* Prevents HTML embedded in the description from rendering */
|
||||
skipHtml={true}
|
||||
/* Stops users embedding images which could be used for tracking */
|
||||
disallowedElements={['img']}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { ProposalJson } from './proposal-json';
|
||||
+11
-11
@@ -1,15 +1,15 @@
|
||||
import classnames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
export const ProposalTermsJson = ({
|
||||
terms,
|
||||
export const ProposalJson = ({
|
||||
proposal,
|
||||
}: {
|
||||
terms: PartialDeep<Schema.ProposalTerms>;
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
@@ -18,20 +18,20 @@ export const ProposalTermsJson = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
<section data-testid="proposal-json">
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
data-testid="proposal-terms-toggle"
|
||||
data-testid="proposal-json-toggle"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SubHeading title={t('proposalTerms')} />
|
||||
<SubHeading title={t('proposalJson')} />
|
||||
<div className={showDetailsIconClasses}>
|
||||
<Icon name="chevron-down" size={8} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{showDetails && <SyntaxHighlighter data={terms} />}
|
||||
{showDetails && <SyntaxHighlighter data={proposal} />}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ProposalTermsJson } from './proposal-terms-json';
|
||||
@@ -0,0 +1 @@
|
||||
export { ProposalTerms } from './proposal-terms';
|
||||
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
RoundedWrapper,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface ProposalTermsProps {
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const getParsedValue = (value: unknown) => {
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
// Check if value is a number string - if so use bignumber to maintain precision
|
||||
if (/^\d+(\.\d+)?$/.test(value)) {
|
||||
return new BigNumber(value).toString();
|
||||
} else {
|
||||
// This would convert, for example, a JSON object ('{"key":"value"}')
|
||||
// into an actual JS object ({key: "value"})
|
||||
return JSON.parse(value);
|
||||
}
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const RenderKeyValue = ({
|
||||
title,
|
||||
value,
|
||||
}: {
|
||||
title: string;
|
||||
value: string | number;
|
||||
}) => (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{title}
|
||||
{value}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
);
|
||||
|
||||
const RenderArray = ({ title, array }: { title: string; array: unknown[] }) => {
|
||||
if (array.every((item) => typeof item === 'string')) {
|
||||
return <RenderKeyValue title={title} value={array.join(', ')} />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2">{title}</div>
|
||||
{array.map((item, index) => (
|
||||
<div key={index}>
|
||||
<ProposalTermsRenderer data={item as Record<string, unknown>} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Working with 'unknown' type as a proposal's terms can be in many shapes
|
||||
const RenderTerm = ({ title, value }: { title: string; value: unknown }) => {
|
||||
const parsedValue = getParsedValue(value);
|
||||
|
||||
if (parsedValue === null || typeof parsedValue === 'boolean') {
|
||||
return <RenderKeyValue title={title} value={String(parsedValue)} />;
|
||||
} else if (
|
||||
typeof parsedValue === 'string' ||
|
||||
typeof parsedValue === 'number'
|
||||
) {
|
||||
return <RenderKeyValue title={title} value={parsedValue} />;
|
||||
} else if (typeof parsedValue === 'object') {
|
||||
if (Array.isArray(parsedValue)) {
|
||||
return <RenderArray title={title} array={parsedValue} />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div className="my-2">{title}</div>
|
||||
<ProposalTermsRenderer
|
||||
data={parsedValue as Record<string, unknown>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (parsedValue === undefined) {
|
||||
return <span>{'undefined'}</span>;
|
||||
} else {
|
||||
return <span>{String(parsedValue)}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const ProposalTermsRenderer = ({ data }: ProposalTermsProps) => {
|
||||
return (
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
{Object.keys(data)
|
||||
.filter(
|
||||
(key) =>
|
||||
!['__typename', 'closingDatetime', 'enactmentDatetime'].includes(
|
||||
key
|
||||
)
|
||||
)
|
||||
.map((key, index) => (
|
||||
<div key={index}>
|
||||
<RenderTerm title={key} value={data[key]} />
|
||||
</div>
|
||||
))}
|
||||
</RoundedWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProposalTerms = ({ data }: ProposalTermsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showTerms, setShowTerms] = useState(false);
|
||||
const showTermsIconClasses = classnames('mb-4', {
|
||||
'rotate-180': showTerms,
|
||||
});
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-terms">
|
||||
<button
|
||||
onClick={() => setShowTerms(!showTerms)}
|
||||
data-testid="proposal-terms-toggle"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SubHeading title={t('proposalDetails')} />
|
||||
<div className={showTermsIconClasses}>
|
||||
<Icon name="chevron-down" size={8} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{showTerms && <ProposalTermsRenderer data={data} />}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -25,8 +25,11 @@ jest.mock('../proposal-detail-header/proposal-header', () => ({
|
||||
jest.mock('../proposal-change-table', () => ({
|
||||
ProposalChangeTable: () => <div data-testid="proposal-change-table"></div>,
|
||||
}));
|
||||
jest.mock('../proposal-terms-json', () => ({
|
||||
ProposalTermsJson: () => <div data-testid="proposal-terms-json"></div>,
|
||||
jest.mock('../proposal-json', () => ({
|
||||
ProposalJson: () => <div data-testid="proposal-json"></div>,
|
||||
}));
|
||||
jest.mock('../proposal-terms/proposal-terms', () => ({
|
||||
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
|
||||
}));
|
||||
jest.mock('../proposal-votes-table', () => ({
|
||||
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
|
||||
@@ -49,7 +52,8 @@ it('renders each section', async () => {
|
||||
render(<Proposal proposal={proposal as ProposalQuery['proposal']} />);
|
||||
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-terms-json')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
|
||||
|
||||
@@ -6,8 +6,10 @@ import { AsyncRenderer, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalDescription } from '../proposal-description';
|
||||
import { ProposalChangeTable } from '../proposal-change-table';
|
||||
import { ProposalTermsJson } from '../proposal-terms-json';
|
||||
import { ProposalJson } from '../proposal-json';
|
||||
import { ProposalTerms } from '../proposal-terms';
|
||||
import { ProposalVotesTable } from '../proposal-votes-table';
|
||||
import { VoteDetails } from '../vote-details';
|
||||
import { ListAsset } from '../list-asset';
|
||||
@@ -20,7 +22,7 @@ export enum ProposalType {
|
||||
PROPOSAL_NETWORK_PARAMETER = 'PROPOSAL_NETWORK_PARAMETER',
|
||||
PROPOSAL_FREEFORM = 'PROPOSAL_FREEFORM',
|
||||
}
|
||||
interface ProposalProps {
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}
|
||||
|
||||
@@ -78,9 +80,11 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<ProposalHeader proposal={proposal} isListItem={false} />
|
||||
|
||||
<div className="my-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
proposal.terms.change.source.__typename === 'ERC20' &&
|
||||
proposal.id ? (
|
||||
@@ -90,7 +94,20 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mb-12">
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalDescription description={proposal.rationale.description} />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalTerms data={proposal.terms} />
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<ProposalJson proposal={proposal} />
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
@@ -102,10 +119,10 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
||||
/>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
||||
</div>
|
||||
<ProposalTermsJson terms={proposal.terms} />
|
||||
</section>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
|
||||
@@ -8,12 +8,11 @@ import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import Routes from '../../../routes';
|
||||
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createDocsLinks, ExternalLinks } from '@vegaprotocol/utils';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
||||
@@ -36,7 +35,6 @@ export const ProposalsList = ({
|
||||
protocolUpgradeProposals,
|
||||
lastBlockHeight,
|
||||
}: ProposalsListProps) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const [filterString, setFilterString] = useState('');
|
||||
const sortedProposals = proposals.reduce(
|
||||
@@ -83,9 +81,9 @@ export const ProposalsList = ({
|
||||
marginBottom={false}
|
||||
title={t('pageTitleProposals')}
|
||||
/>
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<div className="xs:justify-self-end" data-testid="new-proposal-link">
|
||||
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}>
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
||||
<Button variant="primary" size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{t('NewProposal')}
|
||||
|
||||
+4
-3
@@ -2,9 +2,10 @@ import { render, screen } from '@testing-library/react';
|
||||
import { ProposalFormTerms } from './proposal-form-terms';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
DocsLinks: {
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
|
||||
},
|
||||
}));
|
||||
|
||||
const renderComponent = () => {
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
ExternalLink,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import type { UseFormRegisterReturn } from 'react-hook-form';
|
||||
|
||||
interface ProposalFormTermsProps {
|
||||
@@ -22,23 +21,20 @@ export const ProposalFormTerms = ({
|
||||
labelOverride,
|
||||
docsLink,
|
||||
}: ProposalFormTermsProps) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormGroup
|
||||
label={labelOverride || t('ProposalTerms')}
|
||||
labelFor="proposal-terms"
|
||||
>
|
||||
{docsLink && VEGA_DOCS_URL && (
|
||||
{docsLink && DocsLinks && (
|
||||
<div className="mt-[-4px] mb-2 text-sm font-light">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${docsLink}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${docsLink}`}
|
||||
target="_blank"
|
||||
>
|
||||
{`${createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}${docsLink}`}
|
||||
{`${DocsLinks.PROPOSALS_GUIDE}${docsLink}`}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
DocsLinks: {
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
|
||||
},
|
||||
}));
|
||||
|
||||
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useProposalSubmit,
|
||||
deadlineToRoundedHours,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormDescription,
|
||||
ProposalFormSubheader,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { ProposalMinRequirements } from '../../components/shared';
|
||||
import { AsyncRenderer, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -42,7 +41,7 @@ export const ProposeFreeform = () => {
|
||||
NetworkParams.governance_proposal_freeform_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -111,17 +110,13 @@ export const ProposeFreeform = () => {
|
||||
userAction={ProposalUserAction.CREATE}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
|
||||
target="_blank"
|
||||
>{`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}</ExternalLink>
|
||||
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
+4
-3
@@ -10,9 +10,10 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
DocsLinks: {
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
|
||||
},
|
||||
}));
|
||||
|
||||
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
|
||||
+6
-13
@@ -1,10 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
createDocsLinks,
|
||||
suitableForSyntaxHighlighter,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { suitableForSyntaxHighlighter } from '@vegaprotocol/utils';
|
||||
import { useNetworkParams } from '@vegaprotocol/network-parameters';
|
||||
import {
|
||||
getClosingTimestamp,
|
||||
@@ -12,7 +9,7 @@ import {
|
||||
useProposalSubmit,
|
||||
doesValueEquateToParam,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
ProposalFormDescription,
|
||||
ProposalFormSubheader,
|
||||
@@ -84,7 +81,7 @@ export const ProposeNetworkParameter = () => {
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams();
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -175,17 +172,13 @@ export const ProposeNetworkParameter = () => {
|
||||
userAction={ProposalUserAction.CREATE}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
|
||||
target="_blank"
|
||||
>{`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}</ExternalLink>
|
||||
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
DocsLinks: {
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
|
||||
},
|
||||
}));
|
||||
|
||||
const newAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
useProposalSubmit,
|
||||
doesValueEquateToParam,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import { validateJson } from '@vegaprotocol/utils';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -53,7 +53,7 @@ export const ProposeNewAsset = () => {
|
||||
NetworkParams.governance_proposal_asset_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -142,17 +142,13 @@ export const ProposeNewAsset = () => {
|
||||
userAction={ProposalUserAction.CREATE}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
|
||||
target="_blank"
|
||||
>{`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}</ExternalLink>
|
||||
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
DocsLinks: {
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
|
||||
},
|
||||
}));
|
||||
|
||||
const newMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
useProposalSubmit,
|
||||
doesValueEquateToParam,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import { validateJson } from '@vegaprotocol/utils';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -51,7 +51,7 @@ export const ProposeNewMarket = () => {
|
||||
NetworkParams.governance_proposal_market_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -132,17 +132,13 @@ export const ProposeNewMarket = () => {
|
||||
userAction={ProposalUserAction.CREATE}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
|
||||
target="_blank"
|
||||
>{`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}</ExternalLink>
|
||||
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ import Routes from '../../routes';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
|
||||
export const Propose = () => {
|
||||
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -15,14 +14,11 @@ export const Propose = () => {
|
||||
<section className="pb-6">
|
||||
<Heading title={t('NewProposal')} />
|
||||
<div className="text-sm">
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p>
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
|
||||
target="_blank"
|
||||
>
|
||||
{createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE} target="_blank">
|
||||
{DocsLinks.PROPOSALS_GUIDE}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
InputError,
|
||||
TextArea,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
|
||||
import { validateJson } from '@vegaprotocol/utils';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -39,7 +39,7 @@ export const ProposeRaw = () => {
|
||||
NetworkParams.governance_proposal_freeform_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -79,14 +79,11 @@ export const ProposeRaw = () => {
|
||||
spamProtectionMin={params.spam_protection_proposal_min_tokens}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
|
||||
target="_blank"
|
||||
>
|
||||
{createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE} target="_blank">
|
||||
{DocsLinks.PROPOSALS_GUIDE}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
+4
-3
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: () => ({
|
||||
VEGA_DOCS_URL: 'https://docs.vega.xyz',
|
||||
}),
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
DocsLinks: {
|
||||
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
|
||||
},
|
||||
}));
|
||||
|
||||
const updateAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
useProposalSubmit,
|
||||
doesValueEquateToParam,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import { validateJson } from '@vegaprotocol/utils';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -51,7 +51,7 @@ export const ProposeUpdateAsset = () => {
|
||||
NetworkParams.governance_proposal_updateAsset_minProposerBalance,
|
||||
NetworkParams.spam_protection_proposal_min_tokens,
|
||||
]);
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -132,17 +132,13 @@ export const ProposeUpdateAsset = () => {
|
||||
userAction={ProposalUserAction.CREATE}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
|
||||
target="_blank"
|
||||
>{`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}</ExternalLink>
|
||||
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
+6
-10
@@ -7,8 +7,8 @@ import {
|
||||
getEnactmentTimestamp,
|
||||
useProposalSubmit,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import { validateJson } from '@vegaprotocol/utils';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -98,7 +98,7 @@ export const ProposeUpdateMarket = () => {
|
||||
const [selectedMarket, setSelectedMarket] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
@@ -181,17 +181,13 @@ export const ProposeUpdateMarket = () => {
|
||||
userAction={ProposalUserAction.CREATE}
|
||||
/>
|
||||
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<p className="text-sm" data-testid="proposal-docs-link">
|
||||
<span className="mr-1">{t('ProposalTermsText')}</span>
|
||||
<ExternalLink
|
||||
href={`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}
|
||||
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
|
||||
target="_blank"
|
||||
>{`${
|
||||
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
|
||||
}${DOCS_LINK}`}</ExternalLink>
|
||||
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
+4
-2
@@ -25,7 +25,7 @@ const mockData = {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
ACCOUNT_TYPE_FEES_LIQUIDITY: {
|
||||
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
@@ -55,7 +55,9 @@ describe('EpochIndividualRewardsTable', () => {
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
|
||||
).toBeInTheDocument();
|
||||
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
|
||||
).toBeInTheDocument();
|
||||
|
||||
+11
-11
@@ -24,7 +24,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
};
|
||||
|
||||
const reward3: RewardFieldsFragment = {
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '200',
|
||||
percentageOfTotal: '0.2',
|
||||
receivedAt: new Date(),
|
||||
@@ -34,7 +34,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
};
|
||||
|
||||
const reward4: RewardFieldsFragment = {
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '100',
|
||||
percentageOfTotal: '0.1',
|
||||
receivedAt: new Date(),
|
||||
@@ -44,7 +44,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
};
|
||||
|
||||
const reward5: RewardFieldsFragment = {
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '150',
|
||||
percentageOfTotal: '0.15',
|
||||
receivedAt: new Date(),
|
||||
@@ -105,7 +105,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
@@ -172,7 +172,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '200',
|
||||
percentageOfTotal: '0.2',
|
||||
},
|
||||
@@ -202,7 +202,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
@@ -237,7 +237,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '100',
|
||||
percentageOfTotal: '0.1',
|
||||
},
|
||||
@@ -285,7 +285,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '150',
|
||||
percentageOfTotal: '0.15',
|
||||
},
|
||||
@@ -320,7 +320,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '200',
|
||||
percentageOfTotal: '0.2',
|
||||
},
|
||||
@@ -350,7 +350,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
@@ -395,7 +395,7 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '100',
|
||||
percentageOfTotal: '0.1',
|
||||
},
|
||||
|
||||
+4
-2
@@ -29,7 +29,7 @@ const rewardsList = [
|
||||
amount: '0',
|
||||
},
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '0',
|
||||
},
|
||||
{
|
||||
@@ -78,7 +78,9 @@ describe('EpochTotalRewardsTable', () => {
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
|
||||
).toBeInTheDocument();
|
||||
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
|
||||
).toBeInTheDocument();
|
||||
|
||||
+21
-15
@@ -162,9 +162,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '0',
|
||||
},
|
||||
],
|
||||
@@ -227,7 +228,7 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
node: {
|
||||
epoch: 2,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '5',
|
||||
},
|
||||
},
|
||||
@@ -287,9 +288,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '0',
|
||||
},
|
||||
],
|
||||
@@ -351,9 +353,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '5',
|
||||
},
|
||||
],
|
||||
@@ -416,7 +419,7 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
node: {
|
||||
epoch: 2,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '6',
|
||||
},
|
||||
},
|
||||
@@ -424,7 +427,7 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
node: {
|
||||
epoch: 2,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '27',
|
||||
},
|
||||
},
|
||||
@@ -497,9 +500,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '33',
|
||||
},
|
||||
],
|
||||
@@ -561,9 +565,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '0',
|
||||
},
|
||||
],
|
||||
@@ -637,9 +642,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
},
|
||||
],
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
rewardType:
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
amount: '0',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
import {
|
||||
useNetworkParams,
|
||||
NetworkParams,
|
||||
@@ -21,7 +20,7 @@ import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||
import { Heading, SubHeading } from '../../../components/heading';
|
||||
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
|
||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
|
||||
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
|
||||
|
||||
@@ -29,7 +28,6 @@ type RewardsView = 'total' | 'individual';
|
||||
|
||||
export const RewardsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const [toggleRewardsView, setToggleRewardsView] =
|
||||
useState<RewardsView>('total');
|
||||
@@ -68,9 +66,9 @@ export const RewardsPage = () => {
|
||||
<Heading title={t('pageTitleRewards')} />
|
||||
<p className="mb-12">
|
||||
{t('rewardsIntro')}{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).REWARDS_GUIDE}
|
||||
href={DocsLinks.REWARDS_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="rewards-guide-link"
|
||||
className="text-white"
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const RowAccountTypes = {
|
||||
columnTitle: 'rewardsColPriceMakingHeader',
|
||||
description: 'rewardsColPriceMakingTooltip',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
columnTitle: 'rewardsColLiquidityProvisionHeader',
|
||||
description: 'rewardsColLiquidityProvisionTooltip',
|
||||
},
|
||||
|
||||
@@ -229,7 +229,7 @@ const redirects = [
|
||||
const routerConfig = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
element: <LazyHome name="Token" />,
|
||||
element: <LazyHome name="Home" />,
|
||||
},
|
||||
{
|
||||
path: Routes.PROPOSALS,
|
||||
@@ -291,7 +291,7 @@ const routerConfig = [
|
||||
// Not lazy as loaded when a user first hits the site
|
||||
children: [
|
||||
{
|
||||
element: <Home name="Home" />,
|
||||
element: <Home name="Token" />,
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
@@ -325,8 +325,11 @@ const routerConfig = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: 'associate', element: <LazyStakingAssociate /> },
|
||||
{ path: 'disassociate', element: <LazyStakingDisassociate /> },
|
||||
{ path: 'associate', element: <LazyStakingAssociate name="Associate" /> },
|
||||
{
|
||||
path: 'disassociate',
|
||||
element: <LazyStakingDisassociate name="Disassociate" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import { StakingWalletsContainer } from './components/staking-wallets-container/staking-wallets-container';
|
||||
import { StakingWalletsContainer } from './components/staking-wallets-container';
|
||||
import { AssociatePage } from './associate-page';
|
||||
import { AssociatePageNoVega } from './associate-page-no-vega';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '../../index';
|
||||
|
||||
export const AssociateContainer = () => {
|
||||
export const AssociateContainer = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { config } = useEthereumConfig();
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jest.mock('@vegaprotocol/wallet', () => ({
|
||||
}));
|
||||
|
||||
const renderComponent = () => {
|
||||
return render(<Disassociate />);
|
||||
return render(<Disassociate name="Disassociate" />);
|
||||
};
|
||||
|
||||
describe('Disassociate', () => {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { useWeb3React } from '@web3-react/core';
|
||||
import { EthConnectPrompt } from '../../../components/eth-connect-prompt';
|
||||
import { DisassociatePage } from './components/disassociate-page';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '../../index';
|
||||
|
||||
export const DisassociateContainer = () => {
|
||||
export const DisassociateContainer = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { account } = useWeb3React();
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EpochData } from './epoch-data';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import React from 'react';
|
||||
|
||||
export const Staking = () => {
|
||||
const { t } = useTranslation();
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -16,9 +14,9 @@ export const Staking = () => {
|
||||
<section>
|
||||
<p className="mb-12">
|
||||
{t('stakingIntro')}{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||
href={DocsLinks.STAKING_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="staking-guide-link"
|
||||
className="text-white"
|
||||
|
||||
+69
-58
@@ -131,20 +131,25 @@ export const ConsensusValidatorsTable = ({
|
||||
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
|
||||
const thirdOfTotalStake = useMemo(
|
||||
() => new BigNumber(totalStake).dividedBy(3),
|
||||
[totalStake]
|
||||
);
|
||||
|
||||
const nodes = useMemo(() => {
|
||||
if (!data) return [];
|
||||
let canonisedNodes = data
|
||||
.sort((a, b) => {
|
||||
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
|
||||
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
|
||||
return bVotingPower.minus(aVotingPower).toNumber();
|
||||
const aStakedTotal = new BigNumber(a.stakedTotal);
|
||||
const bStakedTotal = new BigNumber(b.stakedTotal);
|
||||
return bStakedTotal.minus(aStakedTotal).toNumber();
|
||||
})
|
||||
.map((node, index) => {
|
||||
const votingPowerRanking = index + 1;
|
||||
const stakedTotalRanking = index + 1;
|
||||
|
||||
return {
|
||||
...node,
|
||||
votingPowerRanking,
|
||||
stakedTotalRanking,
|
||||
};
|
||||
})
|
||||
.map(
|
||||
@@ -157,7 +162,7 @@ export const ConsensusValidatorsTable = ({
|
||||
stakedTotal,
|
||||
rankingScore: { stakeScore, votingPower },
|
||||
pendingStake,
|
||||
votingPowerRanking,
|
||||
stakedTotalRanking,
|
||||
stakedByUser,
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
@@ -175,7 +180,7 @@ export const ConsensusValidatorsTable = ({
|
||||
|
||||
return {
|
||||
id,
|
||||
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
|
||||
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
|
||||
[ValidatorFields.VALIDATOR]: {
|
||||
avatarUrl: logo,
|
||||
name,
|
||||
@@ -236,16 +241,12 @@ export const ConsensusValidatorsTable = ({
|
||||
return canonisedNodes;
|
||||
}
|
||||
|
||||
// The point of identifying and hiding the group that could halt the network
|
||||
// is that we assume the top 1/3 of stake is held by considerably less than
|
||||
// 1/3 of the validators and we really want people not to stake any more to
|
||||
// that group, because we want to make it require as many difference
|
||||
// validators to collude as possible to halt the network, so we hide them.
|
||||
const { topThird, remaining } = canonisedNodes.reduce(
|
||||
(acc, node) => {
|
||||
if (acc.cumulativeScore < 100 / 3) {
|
||||
acc.cumulativeScore += parseFloat(
|
||||
node[ValidatorFields.NORMALISED_VOTING_POWER]
|
||||
if (acc.cumulativeScore.isLessThan(thirdOfTotalStake)) {
|
||||
const prev = acc.cumulativeScore;
|
||||
acc.cumulativeScore = prev.plus(
|
||||
new BigNumber(node[ValidatorFields.STAKE])
|
||||
);
|
||||
acc.topThird.push(node);
|
||||
return acc;
|
||||
@@ -253,61 +254,70 @@ export const ConsensusValidatorsTable = ({
|
||||
acc.remaining.push(node);
|
||||
return acc;
|
||||
},
|
||||
{ topThird: [], remaining: [], cumulativeScore: 0 } as {
|
||||
{ topThird: [], remaining: [], cumulativeScore: new BigNumber(0) } as {
|
||||
topThird: CanonisedConsensusNodeProps[];
|
||||
remaining: CanonisedConsensusNodeProps[];
|
||||
cumulativeScore: number;
|
||||
cumulativeScore: BigNumber;
|
||||
}
|
||||
);
|
||||
|
||||
// We need to combine the top third of validators into a single node, this
|
||||
// way the combined values can be passed to AG grid so that the combined cell's
|
||||
// values are correct for ordering.
|
||||
const combinedTopThird = topThird.reduce((acc, node) => {
|
||||
const {
|
||||
[ValidatorFields.STAKE]: stake,
|
||||
[ValidatorFields.STAKE_SHARE]: stakeShare,
|
||||
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
|
||||
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties,
|
||||
} = node;
|
||||
const combinedTopThird = topThird.reduce(
|
||||
(acc, node) => {
|
||||
const {
|
||||
[ValidatorFields.STAKE]: stake,
|
||||
[ValidatorFields.STAKE_SHARE]: stakeShare,
|
||||
[ValidatorFields.PENDING_STAKE]: pendingStake,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
|
||||
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties,
|
||||
} = node;
|
||||
|
||||
const {
|
||||
[ValidatorFields.STAKE]: accStake,
|
||||
[ValidatorFields.STAKE_SHARE]: accStakeShare,
|
||||
[ValidatorFields.PENDING_STAKE]: accPendingStake,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: accNormalisedVotingPower,
|
||||
[ValidatorFields.TOTAL_PENALTIES]: accTotalPenalties,
|
||||
} = acc;
|
||||
const {
|
||||
[ValidatorFields.STAKE]: accStake,
|
||||
[ValidatorFields.STAKE_SHARE]: accStakeShare,
|
||||
[ValidatorFields.PENDING_STAKE]: accPendingStake,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: accNormalisedVotingPower,
|
||||
[ValidatorFields.TOTAL_PENALTIES]: accTotalPenalties,
|
||||
} = acc;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[ValidatorFields.STAKE]: toBigNum(accStake, decimals)
|
||||
.plus(toBigNum(stake, decimals))
|
||||
.toString(),
|
||||
[ValidatorFields.STAKE_SHARE]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accStakeShare)).plus(
|
||||
new BigNumber(parseFloat(stakeShare))
|
||||
return {
|
||||
...acc,
|
||||
[ValidatorFields.STAKE]: new BigNumber(accStake)
|
||||
.plus(new BigNumber(stake))
|
||||
.toString(),
|
||||
[ValidatorFields.STAKE_SHARE]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accStakeShare)).plus(
|
||||
new BigNumber(parseFloat(stakeShare))
|
||||
),
|
||||
2
|
||||
),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.PENDING_STAKE]: toBigNum(accPendingStake, decimals)
|
||||
.plus(toBigNum(pendingStake, decimals))
|
||||
.toString(),
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accNormalisedVotingPower)).plus(
|
||||
new BigNumber(parseFloat(normalisedVotingPower))
|
||||
[ValidatorFields.PENDING_STAKE]: toBigNum(accPendingStake, decimals)
|
||||
.plus(toBigNum(pendingStake, decimals))
|
||||
.toString(),
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accNormalisedVotingPower)).plus(
|
||||
new BigNumber(parseFloat(normalisedVotingPower))
|
||||
),
|
||||
2
|
||||
),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accTotalPenalties)).plus(
|
||||
new BigNumber(parseFloat(totalPenalties))
|
||||
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
|
||||
new BigNumber(parseFloat(accTotalPenalties)).plus(
|
||||
new BigNumber(parseFloat(totalPenalties))
|
||||
),
|
||||
2
|
||||
),
|
||||
2
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
},
|
||||
{
|
||||
[ValidatorFields.STAKE]: '0',
|
||||
[ValidatorFields.STAKE_SHARE]: '0',
|
||||
[ValidatorFields.PENDING_STAKE]: '0',
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: '0',
|
||||
[ValidatorFields.TOTAL_PENALTIES]: '0',
|
||||
}
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -322,6 +332,7 @@ export const ConsensusValidatorsTable = ({
|
||||
decimals,
|
||||
hideTopThird,
|
||||
previousEpochData,
|
||||
thirdOfTotalStake,
|
||||
totalStake,
|
||||
validatorsView,
|
||||
]);
|
||||
@@ -352,6 +363,7 @@ export const ConsensusValidatorsTable = ({
|
||||
headerTooltip: t('StakeDescription').toString(),
|
||||
cellRenderer: TotalStakeRenderer,
|
||||
width: 120,
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.PENDING_STAKE,
|
||||
@@ -373,7 +385,6 @@ export const ConsensusValidatorsTable = ({
|
||||
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
|
||||
cellRenderer: VotingPowerRenderer,
|
||||
width: 200,
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.TOTAL_PENALTIES,
|
||||
|
||||
+9
-9
@@ -57,16 +57,16 @@ export const StandbyPendingValidatorsTable = ({
|
||||
|
||||
return data
|
||||
.sort((a, b) => {
|
||||
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
|
||||
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
|
||||
return bVotingPower.minus(aVotingPower).toNumber();
|
||||
const aStakedTotal = new BigNumber(a.stakedTotal);
|
||||
const bStakedTotal = new BigNumber(b.stakedTotal);
|
||||
return bStakedTotal.minus(aStakedTotal).toNumber();
|
||||
})
|
||||
.map((node, index) => {
|
||||
const votingPowerRanking = index + 1;
|
||||
const stakedTotalRanking = index + 1;
|
||||
|
||||
return {
|
||||
...node,
|
||||
votingPowerRanking,
|
||||
stakedTotalRanking,
|
||||
};
|
||||
})
|
||||
.map(
|
||||
@@ -79,7 +79,7 @@ export const StandbyPendingValidatorsTable = ({
|
||||
stakedTotal,
|
||||
rankingScore: { stakeScore },
|
||||
pendingStake,
|
||||
votingPowerRanking,
|
||||
stakedTotalRanking,
|
||||
stakedByUser,
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
@@ -125,7 +125,7 @@ export const StandbyPendingValidatorsTable = ({
|
||||
|
||||
return {
|
||||
id,
|
||||
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
|
||||
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
|
||||
[ValidatorFields.VALIDATOR]: {
|
||||
avatarUrl,
|
||||
name,
|
||||
@@ -166,7 +166,7 @@ export const StandbyPendingValidatorsTable = ({
|
||||
: undefined,
|
||||
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
|
||||
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
|
||||
? formatNumberPercentage(new BigNumber(userStakeShare))
|
||||
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
@@ -210,6 +210,7 @@ export const StandbyPendingValidatorsTable = ({
|
||||
headerTooltip: t('StakeDescription').toString(),
|
||||
cellRenderer: TotalStakeRenderer,
|
||||
width: 120,
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.PENDING_STAKE,
|
||||
@@ -233,7 +234,6 @@ export const StandbyPendingValidatorsTable = ({
|
||||
}),
|
||||
cellRenderer: StakeNeededForPromotionRenderer,
|
||||
width: 210,
|
||||
sort: 'asc',
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.TOTAL_PENALTIES,
|
||||
|
||||
@@ -2,12 +2,8 @@ import { useMemo, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
createDocsLinks,
|
||||
removePaginationWrapper,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/utils';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { Link as UTLink, Toggle } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { formatNumber } from '../../../../lib/format-number';
|
||||
@@ -42,7 +38,6 @@ export const ValidatorTables = ({
|
||||
previousEpochData,
|
||||
}: ValidatorsTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
@@ -213,12 +208,12 @@ export const ValidatorTables = ({
|
||||
<>
|
||||
<SubHeading title={t('status-pending')} />
|
||||
<p>
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<>
|
||||
<span>{t('pendingDescription1')} </span>
|
||||
<span>
|
||||
<UTLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||
href={DocsLinks.STAKING_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="validator-forum-link"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ENV } from '../../../config';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
@@ -10,6 +9,7 @@ import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { StakingQuery } from '../__generated__/Staking';
|
||||
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
|
||||
import { isPartyNotFoundError } from '../../../lib/party';
|
||||
|
||||
// TODO should only request a single node. When migrating from deprecated APIs we should address this.
|
||||
|
||||
@@ -39,21 +39,9 @@ export const NodeContainer = ({
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
errorPolicy: 'all',
|
||||
});
|
||||
|
||||
const [isRefetching, setIsRefetching] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (error && error.message.includes('failed to get party for ID')) {
|
||||
setIsRefetching(true);
|
||||
// The API errors if there is a pubkey, but it hasn't interacted with the
|
||||
// chain before. In that case, retry the query with empty pubKey
|
||||
refetch({
|
||||
partyId: '',
|
||||
}).finally(() => setIsRefetching(false));
|
||||
}
|
||||
}, [error, refetch, delegationsPagination]);
|
||||
|
||||
const { data: previousEpochData } = usePreviousEpochQuery({
|
||||
variables: {
|
||||
epochId: (Number(data?.epoch.id) - 1).toString(),
|
||||
@@ -63,7 +51,7 @@ export const NodeContainer = ({
|
||||
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
if (error && !isRefetching) {
|
||||
if (error && !isPartyNotFoundError(error)) {
|
||||
return (
|
||||
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
|
||||
<pre>
|
||||
@@ -75,7 +63,7 @@ export const NodeContainer = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || isRefetching) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { createDocsLinks, ExternalLinks, toBigNum } from '@vegaprotocol/utils';
|
||||
import {
|
||||
useEnvironment,
|
||||
DocsLinks,
|
||||
ExternalLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
Link as UTLink,
|
||||
@@ -64,7 +68,7 @@ export const ValidatorTable = ({
|
||||
stakedTotal,
|
||||
previousEpochData,
|
||||
}: ValidatorTableProps) => {
|
||||
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
appState: { decimals },
|
||||
@@ -90,9 +94,9 @@ export const ValidatorTable = ({
|
||||
<>
|
||||
<p className="mb-12">
|
||||
{t('validatorFormIntro')}{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
|
||||
href={DocsLinks.STAKING_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="validator-table-staking-guide-link"
|
||||
className="text-white"
|
||||
|
||||
@@ -60,9 +60,14 @@ export const getOverstakingPenalty = (
|
||||
}
|
||||
|
||||
return formatNumberPercentage(
|
||||
new BigNumber(1)
|
||||
.minus(new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore)))
|
||||
.times(100),
|
||||
BigNumber.max(
|
||||
new BigNumber(1)
|
||||
.minus(
|
||||
new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore))
|
||||
)
|
||||
.times(100),
|
||||
new BigNumber(0)
|
||||
),
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Heading, SubHeading } from '../../components/heading';
|
||||
import { ExternalLinks, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '..';
|
||||
|
||||
@@ -36,12 +36,54 @@
|
||||
"tranche_id": 58,
|
||||
"tranche_start": "2023-05-11T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-11T00:00:00.000Z",
|
||||
"total_added": "0",
|
||||
"total_added": "11570",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "0",
|
||||
"deposits": [],
|
||||
"locked_amount": "9048.146919802867877",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "11447",
|
||||
"user": "0x73F1955867a9b19ADFEa2cc3f425839607126ec7",
|
||||
"tx": "0xd31ed81d798f6690586935dbc6e722b3d2a3b6a049be4565dc7a4894084f0a05"
|
||||
},
|
||||
{
|
||||
"amount": "123",
|
||||
"user": "0x6A228Fffda4D1B3C773b9ce38DA43592581b36FC",
|
||||
"tx": "0xd6e8129942a80bb2a611c07347ed0f0917690e67cbd99cba1922b5c4aed2f179"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"users": []
|
||||
"users": [
|
||||
{
|
||||
"address": "0x73F1955867a9b19ADFEa2cc3f425839607126ec7",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "11447",
|
||||
"user": "0x73F1955867a9b19ADFEa2cc3f425839607126ec7",
|
||||
"tranche_id": 58,
|
||||
"tx": "0xd31ed81d798f6690586935dbc6e722b3d2a3b6a049be4565dc7a4894084f0a05"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "11447",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "11447"
|
||||
},
|
||||
{
|
||||
"address": "0x6A228Fffda4D1B3C773b9ce38DA43592581b36FC",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "123",
|
||||
"user": "0x6A228Fffda4D1B3C773b9ce38DA43592581b36FC",
|
||||
"tranche_id": 58,
|
||||
"tx": "0xd6e8129942a80bb2a611c07347ed0f0917690e67cbd99cba1922b5c4aed2f179"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "123",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "123"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tranche_id": 57,
|
||||
@@ -81,8 +123,8 @@
|
||||
"tranche_start": "2023-04-20T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-20T00:00:00.000Z",
|
||||
"total_added": "19242.125",
|
||||
"total_removed": "2249.511113406525",
|
||||
"locked_amount": "2401.96952025462936594125",
|
||||
"total_removed": "2667.900599530005",
|
||||
"locked_amount": "1438.72745037615633127375",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "188",
|
||||
@@ -276,6 +318,16 @@
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tx": "0x156ef84c345adecdf7b1c55756b1f4326716d1b0191ce1a208f614fc807ce033"
|
||||
},
|
||||
{
|
||||
"amount": "219.675555560475",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tx": "0x682c6453a6c681312a81e15f244e4885c0ceaab35c7dc041c02d536ff42ca62f"
|
||||
},
|
||||
{
|
||||
"amount": "198.713930563005",
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
"tx": "0x9cdddebcfa4f7e189e435c3a0b5f69ecc6240d59b4d9f959739458f610d8fa95"
|
||||
},
|
||||
{
|
||||
"amount": "202.093666077975",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
@@ -355,6 +407,12 @@
|
||||
"tranche_id": 56,
|
||||
"tx": "0x156ef84c345adecdf7b1c55756b1f4326716d1b0191ce1a208f614fc807ce033"
|
||||
},
|
||||
{
|
||||
"amount": "219.675555560475",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
"tranche_id": 56,
|
||||
"tx": "0x682c6453a6c681312a81e15f244e4885c0ceaab35c7dc041c02d536ff42ca62f"
|
||||
},
|
||||
{
|
||||
"amount": "202.093666077975",
|
||||
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
|
||||
@@ -381,8 +439,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "1207.5",
|
||||
"withdrawn_tokens": "866.872728582",
|
||||
"remaining_tokens": "340.627271418"
|
||||
"withdrawn_tokens": "1086.548284142475",
|
||||
"remaining_tokens": "120.951715857525"
|
||||
},
|
||||
{
|
||||
"address": "0x33Ce1D9E53AFb7367E34749517C086405a651a95",
|
||||
@@ -422,6 +480,12 @@
|
||||
"tranche_id": 56,
|
||||
"tx": "0x47f5bf2c758c5270dd1b6519ac649ddabef8199f8a0bae319e36f8ac5c9c142e"
|
||||
},
|
||||
{
|
||||
"amount": "198.713930563005",
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
"tranche_id": 56,
|
||||
"tx": "0x9cdddebcfa4f7e189e435c3a0b5f69ecc6240d59b4d9f959739458f610d8fa95"
|
||||
},
|
||||
{
|
||||
"amount": "195.89040769089",
|
||||
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
|
||||
@@ -430,8 +494,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "914.25",
|
||||
"withdrawn_tokens": "624.076855605525",
|
||||
"remaining_tokens": "290.173144394475"
|
||||
"withdrawn_tokens": "822.79078616853",
|
||||
"remaining_tokens": "91.45921383147"
|
||||
},
|
||||
{
|
||||
"address": "0x9573BDF7FfC5519912d293e4D1f750eab2E471E7",
|
||||
@@ -942,8 +1006,8 @@
|
||||
"tranche_id": 54,
|
||||
"tranche_start": "2023-04-06T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-06T00:00:00.000Z",
|
||||
"total_added": "14610",
|
||||
"total_removed": "6918.45090157707",
|
||||
"total_added": "14658",
|
||||
"total_removed": "7275.45090157707",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -951,6 +1015,11 @@
|
||||
"user": "0x05659B08a8079E003eE37F26c29e52532728c034",
|
||||
"tx": "0x83cb92910e0725b82b3459e025260c448f931224f7cb00a376351dbd75ae2733"
|
||||
},
|
||||
{
|
||||
"amount": "48",
|
||||
"user": "0x2a65Ae527C6Ff4665e048B0E0883c486A7BA4DBc",
|
||||
"tx": "0x6d708d9b252c3ef0eb41c1bb5f412ae4a644ba3483bbdb329d403728c8257794"
|
||||
},
|
||||
{
|
||||
"amount": "111",
|
||||
"user": "0x90Cf7B9958C3BbBCddAb52Ecc408535D4f3C4241",
|
||||
@@ -1163,6 +1232,16 @@
|
||||
"user": "0x6324334064d1d807940191C658d925DA7d323b12",
|
||||
"tx": "0xd4d9926de15e6dc4992c47e9fbb9d25bf873d7e331524994bb6139a2953ea96a"
|
||||
},
|
||||
{
|
||||
"amount": "309",
|
||||
"user": "0xf9c6C5F942B94Db9dedC550956865cf99D743959",
|
||||
"tx": "0x4fa5d02d920d90835694cbab1e41a66d317b2bc35860d07a5ccd70d12182116b"
|
||||
},
|
||||
{
|
||||
"amount": "48",
|
||||
"user": "0x2a65Ae527C6Ff4665e048B0E0883c486A7BA4DBc",
|
||||
"tx": "0x54ac8f0ed6570e6ce5fc437f15744be4be5c716c08fa7cf58d23ed93ca29dc84"
|
||||
},
|
||||
{
|
||||
"amount": "106.53500000286",
|
||||
"user": "0x6F32AA5A6198329c16e438512F992a0548C856f9",
|
||||
@@ -1232,6 +1311,28 @@
|
||||
"withdrawn_tokens": "90",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x2a65Ae527C6Ff4665e048B0E0883c486A7BA4DBc",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "48",
|
||||
"user": "0x2a65Ae527C6Ff4665e048B0E0883c486A7BA4DBc",
|
||||
"tranche_id": 54,
|
||||
"tx": "0x6d708d9b252c3ef0eb41c1bb5f412ae4a644ba3483bbdb329d403728c8257794"
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "48",
|
||||
"user": "0x2a65Ae527C6Ff4665e048B0E0883c486A7BA4DBc",
|
||||
"tranche_id": 54,
|
||||
"tx": "0x54ac8f0ed6570e6ce5fc437f15744be4be5c716c08fa7cf58d23ed93ca29dc84"
|
||||
}
|
||||
],
|
||||
"total_tokens": "48",
|
||||
"withdrawn_tokens": "48",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x90Cf7B9958C3BbBCddAb52Ecc408535D4f3C4241",
|
||||
"deposits": [
|
||||
@@ -1794,10 +1895,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",
|
||||
@@ -5011,7 +5119,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "48140.1269815871359888489",
|
||||
"locked_amount": "47783.5436471553443054371",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -5077,7 +5185,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "216.275564713064575",
|
||||
"locked_amount": "195.64684447496955",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -5110,7 +5218,7 @@
|
||||
"tranche_end": "2023-11-01T00:00:00.000Z",
|
||||
"total_added": "15000.000000000000015",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "13756.37454710145001375637454710145",
|
||||
"locked_amount": "13633.947576992754013633947576992754",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1.5e-14",
|
||||
@@ -5218,7 +5326,7 @@
|
||||
"tranche_end": "2023-09-01T00:00:00.000Z",
|
||||
"total_added": "17500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "10247.473203502416",
|
||||
"locked_amount": "10104.641738375604",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12500",
|
||||
@@ -5485,7 +5593,7 @@
|
||||
"tranche_end": "2023-08-01T00:00:00.000Z",
|
||||
"total_added": "37500",
|
||||
"total_removed": "18500.56159815",
|
||||
"locked_amount": "15900.17840699815875",
|
||||
"locked_amount": "15589.038040976061",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -5926,7 +6034,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "48096.20517293948430816",
|
||||
"locked_amount": "47739.947175933310891677",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -5959,7 +6067,7 @@
|
||||
"tranche_end": "2024-04-01T00:00:00.000Z",
|
||||
"total_added": "54144.7663",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "47449.8785431340723146965",
|
||||
"locked_amount": "47227.71179229529789203532",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "54144.7663",
|
||||
@@ -5992,7 +6100,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "18821.995357686454604",
|
||||
"locked_amount": "18564.43137366818766",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -6185,7 +6293,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1695.1350837138507",
|
||||
"locked_amount": "1674.562880517504",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -7254,7 +7362,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "1709370.7872515768348",
|
||||
"locked_amount": "88996.7980509664211246026",
|
||||
"locked_amount": "81015.0778870224568958077",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -12775,10 +12883,15 @@
|
||||
"tranche_id": 11,
|
||||
"tranche_start": "2021-09-03T00:00:00.000Z",
|
||||
"tranche_end": "2022-09-03T00:00:00.000Z",
|
||||
"total_added": "57278.000000000000000003",
|
||||
"total_added": "57398.000000000000000003",
|
||||
"total_removed": "50196.35141572991",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "120",
|
||||
"user": "0x05783022615f76de7E887aed81307aE39206Aa95",
|
||||
"tx": "0x388bd7537d91f3a919522ae885ce8dd9ddf415e3df3d5de687c1d5653790fcfd"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xfb269c54dC4b43Fe98EdacE28Ce0a36A03B375D8",
|
||||
@@ -24478,6 +24591,21 @@
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"address": "0x05783022615f76de7E887aed81307aE39206Aa95",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "120",
|
||||
"user": "0x05783022615f76de7E887aed81307aE39206Aa95",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x388bd7537d91f3a919522ae885ce8dd9ddf415e3df3d5de687c1d5653790fcfd"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "120",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "120"
|
||||
},
|
||||
{
|
||||
"address": "0xfb269c54dC4b43Fe98EdacE28Ce0a36A03B375D8",
|
||||
"deposits": [
|
||||
@@ -41126,7 +41254,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "715655.108029600523393",
|
||||
"locked_amount": "161258.417939562670358424702",
|
||||
"locked_amount": "148993.2929873881043215713685",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -42518,8 +42646,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "885240.49984321970795352",
|
||||
"locked_amount": "5871499.5818745175664760343676677342903488",
|
||||
"total_removed": "886751.39142845377669352",
|
||||
"locked_amount": "5828008.23629039446427521587970555856528786",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -43058,6 +43186,16 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xb048cb2dcdfc30508770a3a112ddbacd63210f740cc887559660522bd506612c"
|
||||
},
|
||||
{
|
||||
"amount": "1007.907542",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tx": "0x6a0d503d78cb1fd2291064c2d876cb8d2d91ad87c23ea6ff0b87b6443f8b434e"
|
||||
},
|
||||
{
|
||||
"amount": "502.98404323406874",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0x34f5294e12b0fcbfe0be234ce712cfbc52d4d75b159ca4e55610eb75efe43286"
|
||||
},
|
||||
{
|
||||
"amount": "10150.87581603206683",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -47046,6 +47184,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0xc8835acd2e148eeaacde2a0df0ecad19c22717c7c51b207fdeac9a1af3f98c88"
|
||||
},
|
||||
{
|
||||
"amount": "502.98404323406874",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x34f5294e12b0fcbfe0be234ce712cfbc52d4d75b159ca4e55610eb75efe43286"
|
||||
},
|
||||
{
|
||||
"amount": "10150.87581603206683",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -47336,8 +47480,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "150551.801",
|
||||
"withdrawn_tokens": "94629.10697606348661",
|
||||
"remaining_tokens": "55922.69402393651339"
|
||||
"withdrawn_tokens": "95132.09101929755535",
|
||||
"remaining_tokens": "55419.70998070244465"
|
||||
},
|
||||
{
|
||||
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
|
||||
@@ -47563,6 +47707,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0xdedcbf90e8f19c2a732ba2da663f59b5a19ad94800ddca6a3575019f19e9ca20"
|
||||
},
|
||||
{
|
||||
"amount": "1007.907542",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x6a0d503d78cb1fd2291064c2d876cb8d2d91ad87c23ea6ff0b87b6443f8b434e"
|
||||
},
|
||||
{
|
||||
"amount": "1784.722222",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
@@ -47823,8 +47973,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200000",
|
||||
"withdrawn_tokens": "125368.96374",
|
||||
"remaining_tokens": "74631.03626"
|
||||
"withdrawn_tokens": "126376.871282",
|
||||
"remaining_tokens": "73623.128718"
|
||||
},
|
||||
{
|
||||
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
|
||||
@@ -49206,7 +49356,7 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "6246751.954791069035514289",
|
||||
"total_removed": "6411995.942894415700913537",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -49421,6 +49571,36 @@
|
||||
"user": "0xfc3b2D0b548d3edBb512CeE0Bb79Fb7FaD50AaF3",
|
||||
"tx": "0x5c4a7a8fbab9c7c5899835a06397ea9f9dfbc434376dd0515482b8ba83944d67"
|
||||
},
|
||||
{
|
||||
"amount": "52738.10729529675679353",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
"tx": "0x6e58fe35d24a3b99b2dbf1b967952645c6025c4bf40cece1fb80f5c323d1b16e"
|
||||
},
|
||||
{
|
||||
"amount": "60704.453275555138884",
|
||||
"user": "0x47FbE34Fd93416c63d35544dEE6c6f442Db8513d",
|
||||
"tx": "0x21a962bdd6ba4dd4fc1229d2cf4801c0c8a7b54bb3be80881a15e2b3acdbf148"
|
||||
},
|
||||
{
|
||||
"amount": "15176.2482394768129956",
|
||||
"user": "0xcc2cf726A84e71301f9079FC177BA946aa98A694",
|
||||
"tx": "0xacccd728b8095f4a66cd6b5126f589270f32f643086d7f3e6191eab13444e69f"
|
||||
},
|
||||
{
|
||||
"amount": "12208.093275125918557286",
|
||||
"user": "0x85E14d90C63f70421a098AC26a5aCaB3854a1ee8",
|
||||
"tx": "0xcc751c50c669ee8908fb382e3712f6a3df7832acb43c8f8707351214cdc23892"
|
||||
},
|
||||
{
|
||||
"amount": "12208.430575675159834467",
|
||||
"user": "0x534200f54E753D998F9C2984B5971e4DB490e56F",
|
||||
"tx": "0x24c682a7c0ee0f12ce81e921c0b2dfe8dd5e5a7d8ddbe5a7e5ebf61a908ca58b"
|
||||
},
|
||||
{
|
||||
"amount": "12208.655442216878334365",
|
||||
"user": "0x35022e6c85B50F4904C7BC7e692A9F3bbEE8D2CE",
|
||||
"tx": "0xa4ff302beb1dbc78161fcfd4bc457af7a75d5c759d88c4067b961d3cc48db4f3"
|
||||
},
|
||||
{
|
||||
"amount": "1257.7360413991436375",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -55802,6 +55982,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "12208.093275125918557286",
|
||||
"user": "0x85E14d90C63f70421a098AC26a5aCaB3854a1ee8",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xcc751c50c669ee8908fb382e3712f6a3df7832acb43c8f8707351214cdc23892"
|
||||
},
|
||||
{
|
||||
"amount": "61458.259458207381442714",
|
||||
"user": "0x85E14d90C63f70421a098AC26a5aCaB3854a1ee8",
|
||||
@@ -55810,8 +55996,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "73666.3527333333",
|
||||
"withdrawn_tokens": "61458.259458207381442714",
|
||||
"remaining_tokens": "12208.093275125918557286"
|
||||
"withdrawn_tokens": "73666.3527333333",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xcc2cf726A84e71301f9079FC177BA946aa98A694",
|
||||
@@ -55824,6 +56010,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "15176.2482394768129956",
|
||||
"user": "0xcc2cf726A84e71301f9079FC177BA946aa98A694",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xacccd728b8095f4a66cd6b5126f589270f32f643086d7f3e6191eab13444e69f"
|
||||
},
|
||||
{
|
||||
"amount": "3671.528996246291985",
|
||||
"user": "0xcc2cf726A84e71301f9079FC177BA946aa98A694",
|
||||
@@ -55874,8 +56066,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "66299.71746",
|
||||
"withdrawn_tokens": "51123.4692205231870044",
|
||||
"remaining_tokens": "15176.2482394768129956"
|
||||
"withdrawn_tokens": "66299.71746",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
|
||||
@@ -56396,6 +56588,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "60704.453275555138884",
|
||||
"user": "0x47FbE34Fd93416c63d35544dEE6c6f442Db8513d",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x21a962bdd6ba4dd4fc1229d2cf4801c0c8a7b54bb3be80881a15e2b3acdbf148"
|
||||
},
|
||||
{
|
||||
"amount": "14686.9255071873432384",
|
||||
"user": "0x47FbE34Fd93416c63d35544dEE6c6f442Db8513d",
|
||||
@@ -56446,8 +56644,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "265198.86984",
|
||||
"withdrawn_tokens": "204494.416564444861116",
|
||||
"remaining_tokens": "60704.453275555138884"
|
||||
"withdrawn_tokens": "265198.86984",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x35022e6c85B50F4904C7BC7e692A9F3bbEE8D2CE",
|
||||
@@ -56460,6 +56658,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "12208.655442216878334365",
|
||||
"user": "0x35022e6c85B50F4904C7BC7e692A9F3bbEE8D2CE",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xa4ff302beb1dbc78161fcfd4bc457af7a75d5c759d88c4067b961d3cc48db4f3"
|
||||
},
|
||||
{
|
||||
"amount": "61457.697291116421665635",
|
||||
"user": "0x35022e6c85B50F4904C7BC7e692A9F3bbEE8D2CE",
|
||||
@@ -56468,8 +56672,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "73666.3527333333",
|
||||
"withdrawn_tokens": "61457.697291116421665635",
|
||||
"remaining_tokens": "12208.655442216878334365"
|
||||
"withdrawn_tokens": "73666.3527333333",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xC62BC272187e0Fd48ed126B99ABD6e7943707E64",
|
||||
@@ -56564,6 +56768,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "12208.430575675159834467",
|
||||
"user": "0x534200f54E753D998F9C2984B5971e4DB490e56F",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x24c682a7c0ee0f12ce81e921c0b2dfe8dd5e5a7d8ddbe5a7e5ebf61a908ca58b"
|
||||
},
|
||||
{
|
||||
"amount": "61457.922157658140165533",
|
||||
"user": "0x534200f54E753D998F9C2984B5971e4DB490e56F",
|
||||
@@ -56572,8 +56782,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "73666.3527333333",
|
||||
"withdrawn_tokens": "61457.922157658140165533",
|
||||
"remaining_tokens": "12208.430575675159834467"
|
||||
"withdrawn_tokens": "73666.3527333333",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x4020DC404D9470E0F08D2E7A8d6eBc54035CF42C",
|
||||
@@ -56669,6 +56879,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "52738.10729529675679353",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x6e58fe35d24a3b99b2dbf1b967952645c6025c4bf40cece1fb80f5c323d1b16e"
|
||||
},
|
||||
{
|
||||
"amount": "12758.73908063512839093",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
@@ -56713,8 +56929,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "230391.5181735",
|
||||
"withdrawn_tokens": "177653.41087820324320647",
|
||||
"remaining_tokens": "52738.10729529675679353"
|
||||
"withdrawn_tokens": "230391.5181735",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xC6b2864fbCD2A8dEC8DEfD1D421bC1E450C370AD",
|
||||
@@ -57133,7 +57349,7 @@
|
||||
"tranche_start": "2021-10-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "3492469.400132244009296769",
|
||||
"total_removed": "3521250.583356139543129269",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -57283,6 +57499,11 @@
|
||||
"user": "0x759C9ABABA492500c4c730bEB568B5b851Dec2c7",
|
||||
"tx": "0xd4191eb657ad26e08faddde326514247ae4a88e8aefcc7592590f368010c4046"
|
||||
},
|
||||
{
|
||||
"amount": "28781.1832238955338325",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
"tx": "0xf8179ed797f62e95dccb30c6efcbfa272d1e52efbcb94d04bfeb33312744755c"
|
||||
},
|
||||
{
|
||||
"amount": "58898.87608135458795",
|
||||
"user": "0x83b1a48376E045D26420200345414e6b93066396",
|
||||
@@ -58152,6 +58373,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "28781.1832238955338325",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
"tranche_id": 4,
|
||||
"tx": "0xf8179ed797f62e95dccb30c6efcbfa272d1e52efbcb94d04bfeb33312744755c"
|
||||
},
|
||||
{
|
||||
"amount": "9162.0421945609117395",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
@@ -58196,8 +58423,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "165749.29365",
|
||||
"withdrawn_tokens": "136968.1104261044661675",
|
||||
"remaining_tokens": "28781.1832238955338325"
|
||||
"withdrawn_tokens": "165749.29365",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x72e7BB93E73b2885a22CA29c34759361399a5C0e",
|
||||
@@ -59349,8 +59576,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "44796.7287294743416",
|
||||
"locked_amount": "25552.318114939110235126642617948",
|
||||
"total_removed": "45451.7934047643416",
|
||||
"locked_amount": "23608.838955823785031981498325736",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@@ -65989,6 +66216,41 @@
|
||||
"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": "69.499619482",
|
||||
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
|
||||
"tx": "0xfee87c40eeb3d6681eaa844b65ddb52756356c3612e2c2c8606b96f4965bc195"
|
||||
},
|
||||
{
|
||||
"amount": "36.82039574",
|
||||
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
|
||||
"tx": "0xebbec73307d5d44428f791ad28622e6a075fa9a02d627f028db45a1042af473a"
|
||||
},
|
||||
{
|
||||
"amount": "182.662252664",
|
||||
"user": "0xb1169C6daAc76bAcaf0D8f87641Fc38fbabe569F",
|
||||
@@ -70975,10 +71237,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",
|
||||
@@ -71035,10 +71310,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",
|
||||
@@ -80070,6 +80352,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "36.82039574",
|
||||
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xebbec73307d5d44428f791ad28622e6a075fa9a02d627f028db45a1042af473a"
|
||||
},
|
||||
{
|
||||
"amount": "26.662404872",
|
||||
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
|
||||
@@ -80174,8 +80462,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "342.891159308",
|
||||
"remaining_tokens": "57.108840692"
|
||||
"withdrawn_tokens": "379.711555048",
|
||||
"remaining_tokens": "20.288444952"
|
||||
},
|
||||
{
|
||||
"address": "0x5c90765F50629570738fEe7b7FA82ae118f81Ed1",
|
||||
@@ -86623,6 +86911,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "69.499619482",
|
||||
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xfee87c40eeb3d6681eaa844b65ddb52756356c3612e2c2c8606b96f4965bc195"
|
||||
},
|
||||
{
|
||||
"amount": "26.330974124",
|
||||
"user": "0x8c951C54F9cd08Cf81F770248e835D7A4F491a46",
|
||||
@@ -86643,8 +86937,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "120.251972348",
|
||||
"remaining_tokens": "79.748027652"
|
||||
"withdrawn_tokens": "189.75159183",
|
||||
"remaining_tokens": "10.24840817"
|
||||
},
|
||||
{
|
||||
"address": "0x3437f6ED844013852650851D769ae7939B5e0aec",
|
||||
@@ -88773,6 +89067,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "84.673820396",
|
||||
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x26941fa486bf2aa16da2748a2326f87c3491c4f6f939f4329bd1eb3171ce6c3d"
|
||||
},
|
||||
{
|
||||
"amount": "191.520738204",
|
||||
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
|
||||
@@ -88787,8 +89087,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "293.716882292",
|
||||
"remaining_tokens": "106.283117708"
|
||||
"withdrawn_tokens": "378.390702688",
|
||||
"remaining_tokens": "21.609297312"
|
||||
},
|
||||
{
|
||||
"address": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
@@ -88801,6 +89101,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "84.67351598",
|
||||
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x891254ca1ea63a87ea7e14c3a985f58a403f4a0baf9fba86582785da6d2ae806"
|
||||
},
|
||||
{
|
||||
"amount": "191.524581432",
|
||||
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
|
||||
@@ -88815,8 +89121,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "293.715512428",
|
||||
"remaining_tokens": "106.284487572"
|
||||
"withdrawn_tokens": "378.389028408",
|
||||
"remaining_tokens": "21.610971592"
|
||||
},
|
||||
{
|
||||
"address": "0xB0473C4FeBd722a6d7eE561329d3134030318132",
|
||||
@@ -89173,7 +89479,7 @@
|
||||
"tranche_start": "2021-12-05T00:00:00.000Z",
|
||||
"tranche_end": "2022-06-05T00:00:00.000Z",
|
||||
"total_added": "171288.42",
|
||||
"total_removed": "71890.5995794947989",
|
||||
"total_removed": "72069.2365096947989",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -93433,6 +93739,11 @@
|
||||
"user": "0x2A6Ea2aBEf3D7d7A76CBeF3c297eE0eF9a86BBA8",
|
||||
"tx": "0x163b699e8a25073ad1ad45c8a8f9b1348eb0b648d3f4bc6f3f64648e0ad726bf"
|
||||
},
|
||||
{
|
||||
"amount": "178.6369302",
|
||||
"user": "0xC3d15bD936DDCb45b87BFF3Ff13445284a6c9d43",
|
||||
"tx": "0x7381b77de572d356ae185a760ff839dbdc85cb97bf408b110f7779bb095ea293"
|
||||
},
|
||||
{
|
||||
"amount": "250",
|
||||
"user": "0x0E3407C9A94effa675471afe7A9D37e687C32141",
|
||||
@@ -109929,6 +110240,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "178.6369302",
|
||||
"user": "0xC3d15bD936DDCb45b87BFF3Ff13445284a6c9d43",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x7381b77de572d356ae185a760ff839dbdc85cb97bf408b110f7779bb095ea293"
|
||||
},
|
||||
{
|
||||
"amount": "71.3630698",
|
||||
"user": "0xC3d15bD936DDCb45b87BFF3Ff13445284a6c9d43",
|
||||
@@ -109937,8 +110254,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "71.3630698",
|
||||
"remaining_tokens": "178.6369302"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x4A13d4dC5e06ACdA81C011D55a7DaAc332bC5Dbf",
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
projectId: 'et4snf',
|
||||
defaultCommandTimeout: 10000,
|
||||
viewportWidth: 1440,
|
||||
viewportWidth: 1800,
|
||||
viewportHeight: 900,
|
||||
responseTimeout: 50000,
|
||||
requestTimeout: 20000,
|
||||
|
||||
@@ -199,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');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}]`)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { tooltipMapping } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
createDocsLinks,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -39,7 +38,7 @@ import { Link, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
|
||||
const enum LiquidityTabs {
|
||||
Active = 'active',
|
||||
@@ -147,7 +146,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
|
||||
const symbol =
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.market_liquidity_stakeToCcyVolume,
|
||||
@@ -216,10 +214,12 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Learn more')}>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
|
||||
{DocsLinks ? (
|
||||
<ExternalLink href={DocsLinks.LIQUIDITY}>
|
||||
{t('Providing liquidity')}
|
||||
</ExternalLink>
|
||||
) : (
|
||||
(null as React.ReactNode)
|
||||
)}
|
||||
</HeaderStat>
|
||||
</Header>
|
||||
|
||||
@@ -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 '@vegaprotocol/market-info';
|
||||
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 };
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -19,11 +19,10 @@ import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
createDocsLinks,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
|
||||
interface Props {
|
||||
marketId?: string;
|
||||
@@ -47,8 +46,6 @@ export const MarketLiquiditySupplied = ({
|
||||
params.market_liquidity_targetstake_triggering_ratio
|
||||
);
|
||||
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId: marketId || '',
|
||||
@@ -131,11 +128,8 @@ export const MarketLiquiditySupplied = ({
|
||||
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
|
||||
{t('View liquidity provision table')}
|
||||
</Link>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}
|
||||
className="mt-2"
|
||||
>
|
||||
{DocsLinks && (
|
||||
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
|
||||
{t('Learn about providing liquidity')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
TOKEN_GOVERNANCE,
|
||||
useEnvironment,
|
||||
useLinks,
|
||||
DocsLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
import { SettingsButton } from '../../client-pages/settings';
|
||||
import {
|
||||
ProtocolUpgradeCountdown,
|
||||
@@ -34,7 +34,7 @@ export const Navbar = ({
|
||||
}: {
|
||||
theme: ComponentProps<typeof Navigation>['theme'];
|
||||
}) => {
|
||||
const { VEGA_DOCS_URL, GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||
const { GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
const { marketId } = useGlobalStore((store) => ({
|
||||
marketId: store.marketId,
|
||||
@@ -92,15 +92,13 @@ export const Navbar = ({
|
||||
{t('Governance')}
|
||||
</NavExternalLink>
|
||||
</NavigationItem>
|
||||
{VEGA_DOCS_URL && GITHUB_FEEDBACK_URL && (
|
||||
{DocsLinks?.NEW_TO_VEGA && GITHUB_FEEDBACK_URL && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger>{t('Resources')}</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
<NavigationList>
|
||||
<NavigationItem>
|
||||
<NavExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).NEW_TO_VEGA}
|
||||
>
|
||||
<NavExternalLink href={DocsLinks.NEW_TO_VEGA}>
|
||||
{t('Docs')}
|
||||
</NavExternalLink>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
import { RISK_ACCEPTED_KEY } from '../constants';
|
||||
import { TelemetryApproval } from './telemetry-approval';
|
||||
import {
|
||||
DOCS_VEGA_WALLET,
|
||||
GET_VEGA_WALLET_URL,
|
||||
Networks,
|
||||
useDocsLink,
|
||||
useEnvironment,
|
||||
DocsLinks,
|
||||
ExternalLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
|
||||
@@ -53,8 +52,6 @@ export const RiskNoticeDialog = ({ onClose, network }: Props) => {
|
||||
|
||||
const TestnetContent = ({ network }: { network: Networks }) => {
|
||||
const { GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||
const docsLink = useDocsLink();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4">
|
||||
@@ -69,16 +66,16 @@ const TestnetContent = ({ network }: { network: Networks }) => {
|
||||
[network]
|
||||
)}
|
||||
</p>
|
||||
{GITHUB_FEEDBACK_URL && GET_VEGA_WALLET_URL && docsLink && (
|
||||
{GITHUB_FEEDBACK_URL && DocsLinks && (
|
||||
<ul className="list-disc pl-4">
|
||||
<li className="mb-1">
|
||||
<Link href={GET_VEGA_WALLET_URL} target="_blank">
|
||||
<Link href={ExternalLinks.VEGA_WALLET_URL} target="_blank">
|
||||
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-1">
|
||||
<Link href={docsLink(DOCS_VEGA_WALLET)} target="_blank">
|
||||
<Link href={DocsLinks.VEGA_WALLET_TOOLS_URL} target="_blank">
|
||||
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</Link>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
BLOG,
|
||||
DApp,
|
||||
Networks,
|
||||
TOKEN_NEW_MARKET_PROPOSAL,
|
||||
TOKEN_PROPOSALS,
|
||||
useEnvironment,
|
||||
useLinks,
|
||||
ExternalLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { ProposedMarkets } from './proposed-markets';
|
||||
|
||||
@@ -52,7 +52,7 @@ export const WelcomeNoticeDialog = () => {
|
||||
</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink target="_blank" href={BLOG}>
|
||||
<ExternalLink target="_blank" href={ExternalLinks.BLOG}>
|
||||
{t('Read about the mainnet launch')}
|
||||
</ExternalLink>
|
||||
</li>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { useMemo } from 'react';
|
||||
import { parseISO, isValid, isAfter } from 'date-fns';
|
||||
import classNames from 'classnames';
|
||||
import { useProposalOfMarketQuery } from '@vegaprotocol/proposals';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { createDocsLinks, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { ExternalLink, SimpleGrid } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -23,7 +23,6 @@ export const TradingModeTooltip = ({
|
||||
skip,
|
||||
skipGrid,
|
||||
}: TradingModeTooltipProps) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const { data: marketData } = useStaticMarketData(marketId, skip);
|
||||
const { marketTradingMode, trigger } = marketData || {};
|
||||
@@ -102,9 +101,9 @@ export const TradingModeTooltip = ({
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).AUCTION_TYPE_OPENING}
|
||||
href={DocsLinks.AUCTION_TYPE_OPENING}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
@@ -126,12 +125,9 @@ export const TradingModeTooltip = ({
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)}
|
||||
</span>
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_LIQUIDITY_MONITORING
|
||||
}
|
||||
href={DocsLinks.AUCTION_TYPE_LIQUIDITY_MONITORING}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
@@ -151,12 +147,9 @@ export const TradingModeTooltip = ({
|
||||
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
|
||||
)}
|
||||
</span>
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_LIQUIDITY_MONITORING
|
||||
}
|
||||
href={DocsLinks.AUCTION_TYPE_LIQUIDITY_MONITORING}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
@@ -174,12 +167,9 @@ export const TradingModeTooltip = ({
|
||||
<span>
|
||||
{t('This market is in auction due to high price volatility.')}
|
||||
</span>{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
{DocsLinks && (
|
||||
<ExternalLink
|
||||
href={
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_PRICE_MONITORING
|
||||
}
|
||||
href={DocsLinks.AUCTION_TYPE_PRICE_MONITORING}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user