Compare commits

..
92 changed files with 1867 additions and 2869 deletions
@@ -9,11 +9,6 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -32,16 +27,14 @@ jobs:
- name: resolve ipfs hashes for release
run: |
echo "Tag name: ${{ github.event.release.tag_name }}"
echo "Name: ${{ github.event.release.name }}"
echo "Description: ${{ github.event.release.body }}"
echo "Tag: ${{ github.event.release.tag_name }}"
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
echo "Commit: $commit"
until docker pull vegaprotocol/trading:$commit; do
until docker pull vegaprotocol/trading:${{ github.event.release.tag_name }}; do
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
docker run --rm vegaprotocol/trading:${{ github.event.release.tag_name }} cat /ipfs-hash > ipfs-hash
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"
+2
View File
@@ -6,6 +6,8 @@ on:
- release/*
- develop
- main
tags:
- v*
pull_request:
types:
- opened
+65 -56
View File
@@ -31,7 +31,6 @@ jobs:
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
@@ -40,7 +39,7 @@ jobs:
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -85,6 +84,8 @@ jobs:
fi
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
envName="mainnet"
elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then
envName="mainnet"
fi
if [[ "${envName}" = "mainnet" ]]; then
@@ -113,7 +114,9 @@ jobs:
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
flags="--env=${{ env.ENV_NAME }}"
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
fi
if [ "${{ matrix.app }}" = "trading" ]; then
@@ -158,7 +161,7 @@ jobs:
uses: docker/build-push-action@v3
continue-on-error: true
id: ghcr-push
if: ${{ github.event_name == 'pull_request' }}
if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -173,7 +176,7 @@ jobs:
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -182,8 +185,8 @@ jobs:
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
vegaprotocol/${{ matrix.app }}:${{ github.ref_name }}
vegaprotocol/${{ matrix.app }}:mainnet
- name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3
@@ -209,14 +212,13 @@ jobs:
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
vegaprotocol/${{ matrix.app }}:${{ github.ref_name }}
vegaprotocol/${{ matrix.app }}:mainnet
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') && ( matrix.app != 'trading' || (matrix.app == 'trading' && !endsWith(github.ref, 'main') ) ) }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -234,38 +236,22 @@ jobs:
number: ${{ github.event.number }}
- name: Trigger fleek deployment
# release to ipfs happens only on mainnet (represented by main branch) for trading
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
run: |
if echo ${{ github.ref }} | grep -q main; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
elif echo ${{ github.ref }} | grep -q release/testnet; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fi
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
@@ -273,12 +259,11 @@ jobs:
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update interstitial page to point to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
- name: Update console.vega.xyz DNS to redirect to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
# set CID
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"
@@ -286,28 +271,52 @@ jobs:
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
ls -al ipfs-redirect
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
(
cd ipfs-redirect
# configure git
git status
cat .git/config
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
# update CID files
if echo ${{ github.ref }} | grep -q main; then
echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt
elif echo ${{ github.ref }} | grep -q release/testnet; then
echo $new_hash > cidv0-fairground.txt
echo $new_cid > cidv1-fairground.txt
git add cidv0-fairground.txt cidv1-fairground.txt
fi
# create commit
branch_name="update-hash-${{ github.ref }}"
git checkout -b "$branch_name"
commit_msg="Automated hash update from ${{ github.ref }}"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "main"
git push -u origin "$branch_name"
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
)
# # 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=console.vega.xyz
# dnsimple_record_id=44409591
# # see: https://dnsimple.com/a/84895/domains/console.vega.xyz/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
View File
@@ -5,7 +5,7 @@ NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
+1 -1
View File
@@ -70,7 +70,7 @@
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.71.4/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
@@ -1,13 +1,29 @@
import WS from 'jest-websocket-mock';
import { render, screen } from '@testing-library/react';
import useWebSocket from 'react-use-websocket';
import {
render,
screen,
fireEvent,
act,
waitFor,
} from '@testing-library/react';
import { TendermintWebsocketContext } from '../../contexts/websocket/tendermint-websocket-context';
import { BlocksRefetch } from './blocks-refetch';
const BlocksRefetchInWebsocketProvider = ({
callback,
mocketLocation,
}: {
callback: () => null;
mocketLocation: string;
}) => {
return <BlocksRefetch refetch={callback} />;
const contextShape = useWebSocket(mocketLocation);
return (
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
<BlocksRefetch refetch={callback} />
</TendermintWebsocketContext.Provider>
);
};
describe('Blocks refetch', () => {
@@ -16,8 +32,111 @@ describe('Blocks refetch', () => {
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(<BlocksRefetchInWebsocketProvider callback={() => null} />);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
expect(screen.getByTestId('refresh')).toBeInTheDocument();
mocket.close();
});
it('should initiate callback when the button is clicked', async () => {
const mocketLocation = 'wss:localhost:3003';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
const callback = jest.fn();
render(
<BlocksRefetchInWebsocketProvider
callback={callback}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
const button = screen.getByTestId('refresh');
act(() => {
fireEvent.click(button);
});
expect(callback.mock.calls.length).toEqual(1);
mocket.close();
});
it('should show new blocks as websocket is correctly updated', async () => {
const mocketLocation = 'wss:localhost:3004';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
// Ensuring we send an ID equal to the one the client subscribed with.
await waitFor(() => expect(mocket.messages.length).toEqual(1));
// @ts-ignore id on messages
const id = mocket.messages[0].id;
const newBlockMessage = {
id,
result: {
query: "tm.event = 'NewBlock'",
},
};
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
act(() => {
mocket.send(newBlockMessage);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('1 new blocks');
act(() => {
mocket.send(newBlockMessage);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('2 new blocks');
mocket.close();
});
it('will not show new blocks if websocket has wrong ID', async () => {
const mocketLocation = 'wss:localhost:3005';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
// Ensuring we send an ID equal to the one the client subscribed with.
await waitFor(() => expect(mocket.messages.length).toEqual(1));
const newBlockMessageBadId = {
id: 'blahblahblah',
result: {
query: "tm.event = 'NewBlock'",
},
};
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
act(() => {
mocket.send(newBlockMessageBadId);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
mocket.close();
});
});
@@ -1,19 +1,36 @@
import { useState, useEffect } from 'react';
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
import { t } from '@vegaprotocol/i18n';
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
interface BlocksRefetchProps {
refetch: () => void;
}
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
const [blocksToLoad, setBlocksToLoad] = useState<number>(0);
const { messages } = useTendermintWebsocket({
query: "tm.event = 'NewBlock'",
});
useEffect(() => {
if (messages.length > 0) {
setBlocksToLoad((prev) => prev + 1);
}
}, [messages]);
const refresh = () => {
refetch();
setBlocksToLoad(0);
};
return (
<Button onClick={refresh} data-testid="refresh" size="xs">
<Icon name="refresh" className="!align-baseline mr-2" size={3} />
{t('Load new')}
</Button>
<div className="mb-4">
<span data-testid="new-blocks">{blocksToLoad} new blocks - </span>
<ButtonLink onClick={refresh} data-testid="refresh">
{t('refresh to see latest')}
</ButtonLink>
</div>
);
};
@@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment>) => {
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
@@ -1,24 +0,0 @@
import { t } from '@vegaprotocol/i18n';
export interface FilterLabelProps {
filters: Set<string>;
}
/**
* Renders the list (currently limited to 1) of filters set by the
* Transaction Filter
*/
export function FilterLabel({ filters }: FilterLabelProps) {
if (!filters || filters.size !== 1) {
return <span className="uppercase">{t('Filter')}</span>;
}
return (
<div>
<span className="uppercase">{t('Filters')}:</span>&nbsp;
<code className="bg-vega-light-150 px-2 rounded-md capitalize">
{Array.from(filters)[0]}
</code>
</div>
);
}
@@ -1,164 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuSubContent,
Icon,
Button,
} from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction } from 'react';
import { FilterLabel } from './tx-filter-label';
// All possible transaction types. Should be generated.
export type FilterOption =
| 'Amend LiquidityProvision Order'
| 'Amend Order'
| 'Batch Market Instructions'
| 'Cancel LiquidityProvision Order'
| 'Cancel Order'
| 'Cancel Transfer Funds'
| 'Chain Event'
| 'Delegate'
| 'Ethereum Key Rotate Submission'
| 'Issue Signatures'
| 'Key Rotate Submission'
| 'Liquidity Provision Order'
| 'Node Signature'
| 'Node Vote'
| 'Proposal'
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Submit Oracle Data'
| 'Submit Order'
| 'Transfer Funds'
| 'Undelegate'
| 'Validator Heartbeat'
| 'Vote on Proposal'
| 'Withdraw';
// Alphabetised list of transaction types to appear at the top level
export const PrimaryFilterOptions: FilterOption[] = [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Cancel Transfer Funds',
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
'Undelegate',
'Vote on Proposal',
'Withdraw',
];
// Alphabetised list of transaction types to nest under a 'More...' submenu
export const SecondaryFilterOptions: FilterOption[] = [
'Chain Event',
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
];
export const AllFilterOptions: FilterOption[] = [
...PrimaryFilterOptions,
...SecondaryFilterOptions,
];
export interface TxFilterProps {
filters: Set<FilterOption>;
setFilters: Dispatch<SetStateAction<Set<FilterOption>>>;
}
/**
* Renders a structured dropdown menu of all of the available transaction
* types. It allows a user to select one transaction type to view. Later
* it will support multiple selection, but until the API supports that it is
* one or all.
* @param filters null or Set of tranaction types
* @param setFilters A function to update the filters prop
* @returns
*/
export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
return (
<DropdownMenu
modal={false}
trigger={
<DropdownMenuTrigger className="ml-2">
<Button size="xs">
<FilterLabel filters={filters} />
</Button>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{filters.size > 1 ? null : (
<>
<DropdownMenuCheckboxItem
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
>
{t('Clear filters')} <Icon name="cross" />
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
</>
)}
{PrimaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={() => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{t('More Types')}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{SecondaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/i18n';
@@ -69,17 +69,6 @@ export const TxsInfiniteList = ({
}: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm'].includes(screenSize);
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const hasMountedRef = useRef(false);
useEffect(() => {
if (hasMountedRef.current) {
if (infiniteLoaderRef.current) {
infiniteLoaderRef.current.resetloadMoreItemsCache(true);
}
}
hasMountedRef.current = true;
}, [loadMoreTxs]);
if (!txs) {
if (!areTxsLoading) {
@@ -121,7 +110,6 @@ export const TxsInfiniteList = ({
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
ref={infiniteLoaderRef}
>
{({ onItemsRendered, ref }) => (
<List
+1 -9
View File
@@ -33,7 +33,7 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
// Hacky fix for param as array
let urlAsString = url.toString();
if (filters) {
urlAsString += '&' + filters.replace(' ', '%20');
urlAsString += '&' + filters;
}
return urlAsString;
@@ -65,14 +65,6 @@ export const useTxsData = ({ limit, filters }: IUseTxsData) => {
}
}, [setTxsState, data]);
useEffect(() => {
setTxsState((prev) => ({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
}));
}, [filters]);
const loadTxs = useCallback(() => {
return refetch({
limit: limit,
@@ -5,43 +5,17 @@ import { TxsInfiniteList } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import { useState } from 'react';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
const BE_TXS_PER_REQUEST = 15;
const BE_TXS_PER_REQUEST = 20;
export const TxsList = () => {
useDocumentTitle(['Transactions']);
return (
<section className="md:p-2 lg:p-4 xl:p-6 relative">
<RouteTitle>{t('Transactions')}</RouteTitle>
<TxsListFiltered />
</section>
);
};
export const TxsListFiltered = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions));
const f =
filters && filters.size === 1
? `filters[cmd.type]=${Array.from(filters)[0]}`
: '';
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({
limit: BE_TXS_PER_REQUEST,
filters: f,
});
useTxsData({ limit: BE_TXS_PER_REQUEST });
return (
<>
<menu className="mb-2">
<BlocksRefetch refetch={refreshTxs} />
<TxsFilter filters={filters} setFilters={setFilters} />
</menu>
<section className="md:p-2 lg:p-4 xl:p-6">
<RouteTitle>{t('Transactions')}</RouteTitle>
<BlocksRefetch refetch={refreshTxs} />
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
@@ -50,6 +24,6 @@ export const TxsListFiltered = () => {
error={error}
className="mb-28"
/>
</>
</section>
);
};
+421 -428
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -49,7 +49,7 @@ module.exports = defineConfig({
vegaTokenContractAddress: '0xF41bD86d462D36b997C0bbb4D97a0a3382f205B7',
vegaTokenAddress: '0x67175Da1D5e966e40D11c4B2519392B2058373de',
txTimeout: { timeout: 70000 },
epochTimeout: { timeout: 10000 },
epochTimeout: { timeout: 6000 },
blockConfirmations: 3,
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
@@ -14,16 +14,9 @@ import {
submitUniqueRawProposal,
voteForProposal,
} from '../../../../governance-e2e/src/support/governance.functions';
import {
ensureSpecifiedUnstakedTokensAreAssociated,
stakingPageAssociateTokens,
stakingPageDisassociateAllTokens,
} from '../../../../governance-e2e/src/support/staking.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../../../governance-e2e/src/support/staking.functions';
import { ethereumWalletConnect } from '../../../../governance-e2e/src/support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletSetSpecifiedApprovalAmount,
} from '../../support/wallet-functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../../../governance-e2e/src/support/wallet-teardown.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
import { formatDateWithLocalTimezone } from '@vegaprotocol/utils';
@@ -51,7 +44,7 @@ describe(
before('connect wallets and set approval limit', function () {
cy.visit('/');
ethereumWalletConnect();
// cy.associateTokensToVegaWallet('1');
cy.associateTokensToVegaWallet('1');
});
beforeEach('visit proposals tab', function () {
@@ -304,33 +297,5 @@ describe(
.and('be.visible');
});
});
it('Able to vote for proposal twice by switching public key', function () {
ensureSpecifiedUnstakedTokensAreAssociated('1');
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
voteForProposal('for');
cy.contains('You voted: For').should('be.visible');
ethereumWalletConnect();
switchVegaWalletPubKey();
stakingPageAssociateTokens('2');
navigateTo(navigation.proposals);
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
cy.getByTestId('you-voted').should('not.exist');
voteForProposal('against');
cy.contains('You voted: Against').should('be.visible');
switchVegaWalletPubKey();
cy.get(proposalVoteProgressForTokens).should('contain.text', '1.00');
// Checking vote status for different public keys is displayed correctly
cy.contains('You voted: For').should('be.visible');
});
switchVegaWalletPubKey();
stakingPageDisassociateAllTokens();
});
}
);
@@ -16,7 +16,7 @@ import {
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const closedProposals = '[data-testid="closed-proposals"]';
@@ -32,7 +32,7 @@ import {
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
@@ -23,11 +23,10 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const openProposals = '[data-testid="open-proposals"]';
@@ -206,33 +205,29 @@ context(
});
// 3003-PMAN-001
it(
'Able to submit valid new market proposal',
{ tags: '@smoke' },
function () {
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
it('Able to submit valid new market proposal', function () {
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
});
});
cy.get(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
});
});
}
);
});
it('Unable to submit new market proposal with missing/invalid fields', function () {
const errorMsg =
@@ -270,7 +265,8 @@ context(
// Will fail if run after 'Able to submit update market proposal and vote for proposal'
// 3002-PROP-022
it('Unable to submit update market proposal without equity-like share in the market', function () {
switchVegaWalletPubKey();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click(); // switch to second wallet pub key
stakingPageAssociateTokens('1');
goToMakeNewProposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal - rejected');
@@ -300,7 +296,8 @@ context(
closeDialog();
ethereumWalletConnect();
stakingPageDisassociateAllTokens();
switchVegaWalletPubKey();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
});
// 3002-PROP-020
@@ -21,7 +21,7 @@ import {
} from '../../support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalListItem = 'proposals-list-item';
const openProposals = '[data-testid="open-proposals"]';
@@ -14,7 +14,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
depositAsset,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de';
const vegaWalletUnstakedBalance =
@@ -25,7 +25,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
@@ -54,7 +54,7 @@ context(
'Staking Tab - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
// 1002-STKE-002, 1002-STKE-032
// 2001-STKE-002, 2001-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
ethereumWalletConnect();
@@ -63,7 +63,7 @@ context(
vegaWalletSetSpecifiedApprovalAmount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
describe.skip('Eth wallet - contains VEGA tokens', function () {
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
@@ -77,41 +77,25 @@ context(
}
);
// 1002-STKE-035 1002-STKE-036
it('Unable to stake against a validator with less than minimum and more than associated amount', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
cy.getByTestId(stakeAddStakeRadioButton, epochTimeout).click({
force: true,
});
cy.getByTestId(stakeTokenAmountInputBox).type('0.001');
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
cy.getByTestId(stakeTokenAmountInputBox).clear().type('4');
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
});
it('Able to stake against a validator - using vega from wallet', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
// 1002-STKE-031
// 2001-STKE-031
clickOnValidatorFromList(0);
// 1002-STKE-033, 1002-STKE-034, 1002-STKE-037
// 2001-STKE-033, 2001-STKE-034, 2001-STKE-037
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
// 1002-STKE-039
// 2001-STKE-039
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0); // 1002-STKE-016 1002-STKE-038
verifyThisEpochValue(2.0); // 1002-STKE-013
verifyNextEpochValue(2.0); // 2001-STKE-016 2001-STKE-038
verifyThisEpochValue(2.0); // 2001-STKE-013
closeStakingDialog();
navigateTo(navigation.validators);
// 2002-SINC-007 1002-STKE-015 1002-STKE-017 1002-STKE-052
// 2002-SINC-007
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
});
@@ -234,7 +218,7 @@ context(
});
});
// 1002-STKE-041 1002-STKE-053
// 2001-STKE-041
it(
'Able to remove part of a stake against a validator',
{ tags: '@smoke' },
@@ -247,11 +231,11 @@ context(
verifyUnstakedBalance(1.0);
closeStakingDialog();
navigateTo(navigation.validators);
// 1002-STKE-040
// 2001-STKE-040
clickOnValidatorFromList(0);
// 1002-STKE-044, 1002-STKE-048
// 2001-STKE-044, 2001-STKE-048
stakingValidatorPageRemoveStake('1');
// 1002-STKE-049
// 2001-STKE-049
verifyNextEpochValue(2.0);
verifyUnstakedBalance(2.0);
verifyStakedBalance(2.0);
@@ -271,7 +255,7 @@ context(
}
);
// 1002-STKE-045
// 2001-STKE-045
it('Able to remove a full stake against a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
@@ -319,7 +303,6 @@ context(
.and('be.visible');
});
// 1002-STKE-046 1002-STKE-047
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
@@ -422,7 +405,7 @@ context(
});
it('Associating wallet tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 1002-STKE-004
// 2001-STKE-004
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -436,7 +419,7 @@ context(
});
it('Associating vesting contract tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 1002-STKE-004
// 2001-STKE-004
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -450,7 +433,7 @@ context(
});
it('Associating vesting contract tokens - when wallet tokens already staked - auto stakes tokens to staked validator', function () {
// 1002-STKE-004
// 2001-STKE-004
stakingPageAssociateTokens('3', { type: 'wallet' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -464,7 +447,7 @@ context(
});
it('Associating tokens - with multiple validators already staked - auto stakes to staked validators - abiding by existing stake ratio', function () {
// 1002-STKE-004
// 2001-STKE-004
stakingPageAssociateTokens('6');
verifyUnstakedBalance(6.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -517,7 +500,7 @@ context(
}
function verifyThisEpochValue(amount: number) {
cy.getByTestId('stake-this-epoch', epochTimeout) // 1002-STKE-013
cy.getByTestId('stake-this-epoch', epochTimeout) // 2001-STKE-013
.contains(amount, epochTimeout)
.should('be.visible');
}
@@ -14,12 +14,11 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletAssociate,
vegaWalletDisassociate,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-functions';
} from '../../support/wallet-teardown.functions';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
@@ -65,38 +64,31 @@ context(
}
);
it(
'Able to associate tokens - from wallet',
{ tags: '@smoke' },
function () {
//1004-ASSO-003
//1004-ASSO-005
//1004-ASSO-009
//1004-ASSO-030
//1004-ASSO-012
//1004-ASSO-013
//1004-ASSO-014
//1004-ASSO-015
//1004-ASSO-030
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
2.0
);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
}
);
it('Able to associate tokens - from wallet', function () {
//1004-ASSO-003
//1004-ASSO-005
//1004-ASSO-009
//1004-ASSO-030
//1004-ASSO-012
//1004-ASSO-013
//1004-ASSO-014
//1004-ASSO-015
//1004-ASSO-030
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
});
it('Able to disassociate all associated tokens - manually', function () {
// 1004-ASSO-025
@@ -302,7 +294,8 @@ context(
Cypress.env('vegaWalletPublicKey')
);
switchVegaWalletPubKey();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
cy.get(connectedVegaKey).should(
'have.text',
Cypress.env('vegaWalletPublicKey2')
@@ -5,7 +5,7 @@ import {
waitForSpinner,
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { depositAsset } from '../../support/wallet-functions';
import { depositAsset } from '../../support/wallet-teardown.functions';
const withdraw = 'withdraw';
const withdrawalForm = 'withdraw-form';
@@ -55,10 +55,6 @@ context(
navigateTo(navigation.withdraw);
cy.connectVegaWallet();
ethereumWalletConnect();
cy.getByTestId('currency-title', txTimeout).should(
'contain.text',
usdtName
);
});
it('Able to open withdrawal form with vega wallet connected', function () {
@@ -96,75 +92,71 @@ context(
});
});
it(
'Able to withdraw asset: -eth wallet connected -withdraw funds button',
{ tags: '@smoke' },
function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
it('Able to withdraw asset: -eth wallet connected -withdraw funds button', function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
});
// assert withdrawal request
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(toastCompleteWithdrawal).click();
cy.getByTestId(toastClose).click();
});
// assert withdrawal request
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
cy.getByTestId(toastCompleteWithdrawal).click();
cy.getByTestId(toastClose).click();
});
// withdrawal complete
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
});
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '120.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
cy.get(tableTxHash)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/tx/');
});
}
);
// withdrawal complete
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
});
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '120.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
cy.get(tableTxHash)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/tx/');
});
});
it('Able to withdraw asset: -eth wallet not connected', function () {
const ethWalletAddress = Cypress.env('ethWalletPublicKey');
@@ -11,7 +11,7 @@ import {
goToMakeNewProposal,
governanceProposalType,
} from '../../support/governance.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey2');
const vegaPubkeyTruncated = Cypress.env('vegaWalletPublicKey2Short');
@@ -5,10 +5,7 @@ import {
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { clickOnValidatorFromList } from '../../support/staking.functions';
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
@@ -40,213 +37,206 @@ context('Validators Page - verify elements on page', function () {
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
describe('description section', function () {
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
it('Should have Staking Guide link visible', function () {
// 1002-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
// 1002-STKE-032
it('Should have button to connect vega wallet in validator page', function () {
clickOnValidatorFromList(0);
cy.getByTestId('connect-to-vega-wallet-btn').should('be.visible');
cy.visit('/validators');
});
});
// 1002-STKE-020 1002-STKE-021 1002-STKE-022 1002-STKE-023 1002-STKE-024
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 1002-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should be able to see validator stake tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('total-stake').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 60.00%');
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 1002-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 1002-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 1002-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 1002-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 1002-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink).should('not.be.empty').and('have.attr', 'href');
});
// TODO validators missing url for more information about them 1002-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 1002-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 1002-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 1002-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 2001-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
});
}
);
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should be able to see validator stake tooltip', function () {
cy.getByTestId('total-stake').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 60.00%');
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 2001-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 2001-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 2001-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 2001-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 2001-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink)
.should('not.be.empty')
.and('have.attr', 'href');
});
// TODO validators missing url for more information about them 2001-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 2001-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 2001-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 2001-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
});
});
@@ -1,9 +1,7 @@
import { truncateByChars } from '@vegaprotocol/utils';
import { waitForSpinner } from '../../support/common.functions';
import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
} from '../../support/wallet-functions';
import { vegaWalletTeardown } from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
@@ -339,7 +337,6 @@ context(
.parent()
.siblings()
.invoke('text')
.should('have.length.at.least', 4)
.then(parseFloat)
.should('be.gte', parseFloat(expectedAmount));
+2 -1
View File
@@ -5,7 +5,8 @@ import './common.functions.ts';
import './staking.functions.ts';
import './governance.functions.ts';
import './wallet-eth.functions.ts';
import './wallet-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';
@@ -1,5 +1,5 @@
import { closeDialog } from './common.functions';
import { vegaWalletTeardown } from './wallet-functions';
import { vegaWalletTeardown } from './wallet-teardown.functions';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
@@ -173,27 +173,3 @@ export async function vegaWalletDisassociate(amount: string) {
amount = amount + '0'.repeat(18);
stakingBridgeContract.remove_stake(amount, vegaWalletPubKey);
}
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
export function switchVegaWalletPubKey() {
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
}
@@ -0,0 +1,18 @@
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
@@ -596,8 +596,6 @@
"noPercentage": "No percentage",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"marketSpecification": "Market specification",
"viewMarketJson": "View market JSON",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
@@ -731,11 +729,7 @@
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
"Hours": "hours",
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "The proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline": "The proposal will fail if enactment deadline is below the minimum",
"ProposalWillFailIfVotingIsBelowTheMinimumDeadline": "The proposal will fail if voting deadline is below the minimum",
"ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline": "The proposal will fail if enactment deadline is above the maximum",
"ProposalWillFailIfVotingIsAboveTheMaximumDeadline": "The proposal will fail if voting deadline is above the maximum",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline",
"SelectAMarketToChange": "Select a market to change",
"MarketName": "Market name",
"MarketCode": "Market code",
@@ -1,6 +0,0 @@
import classnames from 'classnames';
export const collapsibleToggleStyles = (toggleState: boolean) =>
classnames('mb-4 transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});
@@ -1,9 +1,9 @@
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';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
export const ProposalDescription = ({
description,
@@ -12,6 +12,9 @@ export const ProposalDescription = ({
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
@@ -21,7 +24,7 @@ export const ProposalDescription = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={collapsibleToggleStyles(showDescription)}>
<div className={showDescriptionIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1,8 +1,8 @@
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 { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -13,6 +13,9 @@ export const ProposalJson = ({
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<section data-testid="proposal-json">
@@ -22,7 +25,7 @@ export const ProposalJson = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalJson')} />
<div className={collapsibleToggleStyles(showDetails)}>
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1 +0,0 @@
export * from './proposal-market-data';
@@ -1,222 +0,0 @@
import isEqual from 'lodash/isEqual';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import { create } from 'zustand';
type MarketDataDialogState = {
isOpen: boolean;
open: () => void;
close: () => void;
};
export const useMarketDataDialogStore = create<MarketDataDialogState>(
(set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
})
);
export const ProposalMarketData = ({
marketData,
}: {
marketData: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
return null;
}
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
return (
<section className="relative" data-testid="proposal-market-data">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-market-data-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('marketSpecification')} />
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && (
<>
<div className="float-right">
<Button onClick={open} data-testid="view-market-json">
{t('viewMarketJson')}
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
</>
)}
<Dialog
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
<button className="bg-vega-dark-100 rounded-sm py-2 px-3 mb-4 text-white">
<span>
<Icon name="duplicate" />
</span>
<span className="ml-2">Copy</span>
</button>
</CopyWithTooltip>
<SyntaxHighlighter data={marketData} />
</Dialog>
</section>
);
};
@@ -1,3 +1,4 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -12,7 +13,6 @@ import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -57,6 +57,10 @@ export const ProposalVotesTable = ({
? t('byTokenVote')
: t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<>
<button
@@ -65,7 +69,7 @@ export const ProposalVotesTable = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('voteBreakdown')} />
<div className={collapsibleToggleStyles(showDetails)}>
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -29,6 +29,9 @@ jest.mock('../proposal-change-table', () => ({
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>,
}));
@@ -71,6 +74,7 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).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();
@@ -1,22 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, Icon, 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 { 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';
import { Link } from 'react-router-dom';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import React from 'react';
import { useTranslation } from 'react-i18next';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -28,16 +28,11 @@ export enum ProposalType {
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
export const Proposal = ({
proposal,
restData,
newMarketData,
}: ProposalProps) => {
export const Proposal = ({ proposal, restData }: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
@@ -102,58 +97,51 @@ export const Proposal = ({
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={
proposal.terms.change.source.withdrawThreshold
}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
<div className="mb-4">
<ProposalMarketData marketData={newMarketData} />
<ProposalTerms data={proposal.terms} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</div>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</section>
</AsyncRenderer>
@@ -224,47 +224,7 @@ describe('Proposal form vote, validation and enactment deadline', () => {
expect(
screen.getByTestId('enactment-before-voting-deadline')
).toHaveTextContent(
'The proposal will fail if enactment is earlier than the voting deadline'
);
});
it('displays error text if the vote deadline is set earlier than the minimum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('voting-less-than-min')).toHaveTextContent(
'The proposal will fail if voting deadline is below the minimum'
);
});
it('displays error text if the vote deadline is set later than the maximum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('voting-greater-than-max')).toHaveTextContent(
'The proposal will fail if voting deadline is above the maximum'
);
});
it('displays error text if the enactment deadline is set earlier than the minimum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('enactment-less-than-min')).toHaveTextContent(
'The proposal will fail if enactment deadline is below the minimum'
);
});
it('displays error text if the enactment deadline is set later than the maximum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('enactment-greater-than-max')).toHaveTextContent(
'The proposal will fail if enactment deadline is above the maximum'
'Proposal will fail if enactment is earlier than the voting deadline'
);
});
});
@@ -220,41 +220,21 @@ const EnactmentForm = ({
<span data-testid="enactment-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.enactment)}
</span>
{deadlines.enactment && (
<>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
{deadlines.enactment < minEnactmentHours && (
<span
data-testid="enactment-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.enactment > maxEnactmentHours && (
<span
data-testid="enactment-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline')}
</span>
)}
</>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment && deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
</p>
)}
@@ -520,33 +500,13 @@ export function ProposalFormVoteAndEnactmentDeadline({
<span data-testid="voting-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.vote)}
</span>
{deadlines.vote && (
<>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.vote < minVoteHours && (
<span
data-testid="voting-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.vote > maxVoteHours && (
<span
data-testid="voting-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsAboveTheMaximumDeadline')}
</span>
)}
</>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
</p>
)}
@@ -1,10 +1,11 @@
import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { FinalizedVote } from '@vegaprotocol/proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
export enum VoteState {
NotCast = 'NotCast',
@@ -44,23 +45,21 @@ export function useUserVote(
);
useEffect(() => {
if (finalizedVote?.vote.value && finalizedVote.pubKey === pubKey) {
if (finalizedVote?.vote.value) {
setUserVote(finalizedVote);
} else if (data?.party?.votesConnection?.edges && pubKey) {
const vote = removePaginationWrapper(
data?.party?.votesConnection?.edges
).find(({ proposalId: pId }) => proposalId === pId);
if (vote) {
setUserVote({ ...vote, pubKey });
}
} else if (data?.party?.votesConnection?.edges) {
// This sets the vote (if any) when the user first loads the page
setUserVote(
removePaginationWrapper(data?.party?.votesConnection?.edges).find(
({ proposalId: pId }) => proposalId === pId
)
);
}
}, [
finalizedVote?.vote.value,
data?.party?.votesConnection?.edges,
finalizedVote,
proposalId,
pubKey,
]);
// If user vote changes update the vote state
@@ -95,8 +94,6 @@ export function useUserVote(
return {
voteState,
userVote,
voteDatetime: userVote?.vote?.datetime
? new Date(userVote.vote.datetime)
: null,
voteDatetime: userVote ? new Date(userVote.vote.datetime) : null,
};
}
@@ -7,8 +7,6 @@ import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@@ -22,36 +20,15 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
data: newMarketData,
loading: newMarketLoading,
error: newMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
useEffect(() => {
const interval = setInterval(refetch, 2000);
const interval = setInterval(refetch, 1000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
>
<AsyncRenderer loading={loading} error={error} data={data}>
{data?.proposal ? (
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
/>
<Proposal proposal={data.proposal} restData={restData} />
) : (
<ProposalNotFound />
)}
@@ -48,22 +48,15 @@ export const EpochIndividualRewards = ({
return removePaginationWrapper(data.party.rewardsConnection.edges);
}, [data]);
const epochRewardSummaries = useMemo(() => {
if (!data?.epochRewardSummaries) return [];
return removePaginationWrapper(data.epochRewardSummaries.edges);
}, [data]);
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList({
rewards,
epochId,
epochRewardSummaries,
page,
size: EPOCHS_PAGE_SIZE,
});
}, [data?.party, epochId, epochRewardSummaries, page, rewards]);
}, [data?.party, epochId, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
@@ -65,11 +65,7 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an empty array if no rewards are provided', () => {
expect(
generateEpochIndividualRewardsList({
rewards: [],
epochId: 1,
epochRewardSummaries: [],
})
generateEpochIndividualRewardsList({ rewards: [], epochId: 1 })
).toEqual([
{
epoch: 1,
@@ -82,7 +78,6 @@ describe('generateEpochIndividualRewardsList', () => {
const result = generateEpochIndividualRewardsList({
rewards: [rewardWrongType],
epochId: 1,
epochRewardSummaries: [],
});
expect(result).toEqual([
@@ -97,15 +92,6 @@ describe('generateEpochIndividualRewardsList', () => {
const result = generateEpochIndividualRewardsList({
rewards: [reward1],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '100000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
@@ -148,11 +134,7 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
epochRewardSummaries: [],
});
const result1 = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1);
@@ -161,7 +143,6 @@ describe('generateEpochIndividualRewardsList', () => {
const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result2[0].epoch).toEqual(2);
@@ -170,11 +151,7 @@ describe('generateEpochIndividualRewardsList', () => {
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 1,
epochRewardSummaries: [],
});
const result = generateEpochIndividualRewardsList({ rewards, epochId: 1 });
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
@@ -182,11 +159,7 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
epochRewardSummaries: [],
});
const result = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result).toEqual([
{
@@ -300,7 +273,6 @@ describe('generateEpochIndividualRewardsList', () => {
const resultPageOne = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
epochRewardSummaries: [],
page: 1,
size: 2,
});
@@ -414,7 +386,6 @@ describe('generateEpochIndividualRewardsList', () => {
const resultPageTwo = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
epochRewardSummaries: [],
page: 2,
size: 2,
});
@@ -458,69 +429,4 @@ describe('generateEpochIndividualRewardsList', () => {
},
]);
});
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
const result = generateEpochIndividualRewardsList({
rewards: [
// reward1 is 100 usd, which is 10% of the total rewards amount
reward1,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '1' },
},
],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '1000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
epoch: 1,
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '300',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '300',
percentageOfTotal: '30',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
});
});
});
@@ -1,9 +1,6 @@
import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type {
EpochRewardSummaryFieldsFragment,
RewardFieldsFragment,
} from '../home/__generated__/Rewards';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
@@ -35,13 +32,11 @@ const emptyRowAccountTypes = accountTypes.map((type) => [
export const generateEpochIndividualRewardsList = ({
rewards,
epochId,
epochRewardSummaries,
page = 1,
size = 10,
}: {
rewards: RewardFieldsFragment[];
epochId: number;
epochRewardSummaries: EpochRewardSummaryFieldsFragment[];
page?: number;
size?: number;
}) => {
@@ -59,7 +54,6 @@ export const generateEpochIndividualRewardsList = ({
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const assetId = reward.asset.id;
const assetDecimals = reward.asset.decimals;
const rewardType = reward.rewardType;
const amount = reward.amount;
@@ -76,14 +70,6 @@ export const generateEpochIndividualRewardsList = ({
const epoch = acc.get(epochId);
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
const matchingTotalRewardAmount = epochRewardSummaries.find(
(summary) =>
summary.epoch === Number(epochId) &&
summary.assetId === assetId &&
summary.rewardType === rewardType
)?.amount;
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
@@ -100,24 +86,22 @@ export const generateEpochIndividualRewardsList = ({
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
} else {
const previousAmount = asset.rewardTypes[rewardType]?.amount;
const newAmount = previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount;
const previousPercentageOfTotal =
asset.rewardTypes[rewardType]?.percentageOfTotal;
asset.rewardTypes[rewardType] = {
amount: newAmount,
percentageOfTotal: matchingTotalRewardAmount
? new BigNumber(newAmount)
.dividedBy(matchingTotalRewardAmount)
.multipliedBy(100)
amount: previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount,
percentageOfTotal: previousPercentageOfTotal
? new BigNumber(previousPercentageOfTotal)
.plus(percentageOfTotal)
.toString()
: // this should never be reached, if there's an individual reward there should
// always be a reward total from the api too, but set it as a fallback just in case
percentageOfTotal,
: percentageOfTotal,
};
}
// totalAmount is the sum of all individual rewardTypes amounts
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
(sum, rewardType) => {
return new BigNumber(sum).plus(rewardType.amount).toString();
@@ -22,13 +22,6 @@ fragment DelegationFields on Delegation {
epoch
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
epoch
assetId
amount
rewardType
}
query Rewards(
$partyId: ID!
$fromEpoch: Int
@@ -57,16 +50,13 @@ query Rewards(
}
}
}
epochRewardSummaries(
filter: { fromEpoch: $fromEpoch, toEpoch: $toEpoch }
pagination: $rewardsPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
epoch
assetId
amount
rewardType
}
query EpochAssetsRewards(
@@ -7,8 +7,6 @@ export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.Ac
export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
@@ -18,7 +16,9 @@ export type RewardsQueryVariables = Types.Exact<{
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type EpochAssetsRewardsQueryVariables = Types.Exact<{
epochRewardSummariesFilter?: Types.InputMaybe<Types.RewardSummaryFilter>;
@@ -102,20 +102,9 @@ export const RewardsDocument = gql`
}
}
}
epochRewardSummaries(
filter: {fromEpoch: $fromEpoch, toEpoch: $toEpoch}
pagination: $rewardsPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
}
${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}
${EpochRewardSummaryFieldsFragmentDoc}`;
${DelegationFieldsFragmentDoc}`;
/**
* __useRewardsQuery__
@@ -1,8 +1,10 @@
const dialogContent = 'dialog-content';
const nodeHealth = 'node-health';
const statusIncidentsLink = 'footer [data-testid=external-link]';
describe('home', { tags: '@regression' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
@@ -74,14 +76,23 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/');
});
// 0006-NETW-002
// 0006-NETW-003
// 0006-NETW-011
it('switch to fairground network and check status & incidents link', () => {
// 0006-NETW-002
// 0006-NETW-003
cy.getByTestId('navigation')
.find('[data-testid="network-switcher"]')
.should('have.text', 'Custom')
.click();
cy.getByTestId('network-item').contains('Fairground testnet');
cy.getByTestId('network-item').contains('Fairground testnet').click();
cy.get('[aria-haspopup="menu"]').should('contain.text', 'Fairground');
cy.url().should('include', 'fairground.wtf');
cy.contains('Continue').click();
cy.get(statusIncidentsLink)
.children('span')
.should('have.text', 'Mainnet status & incidents');
cy.get(statusIncidentsLink)
.should('have.attr', 'href')
.and('contain', 'https://blog.vega.xyz/tagged/vega-incident-reports');
});
});
});
@@ -81,7 +81,6 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelector)
.first()
.find(colAverageEntryValuation)
@@ -166,7 +165,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
});
it('can see liquidity supplied', () => {
// 5002-LIQP-008
//// 5002-LIQP-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
@@ -238,7 +237,6 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelectorLiquidityActive)
.first()
.find(colAverageEntryValuation)
@@ -270,7 +268,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
});
it('renders liquidity inactive table correctly', () => {
// 5002-LIQP-012
//// 5002-LIQP-012
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
@@ -128,7 +128,6 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.wrap(btn).click();
});
}
cy.contains('Loading...').should('not.exist');
});
// 7001-COLL-010
it('sorting by asset', () => {
@@ -147,17 +146,12 @@ describe('accounts', { tags: '@smoke' }, () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.00',
'1,000.01',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00001',
'1,000.00002',
'1,000.01',
];
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
@@ -194,24 +188,19 @@ describe('accounts', { tags: '@smoke' }, () => {
);
});
it('sorting by available', () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.00',
'1,000.01',
'1,000.00',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00',
'1,000.00001',
'1,000.00002',
];
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'available',
'total',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
@@ -1,10 +1,6 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -16,9 +12,8 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -28,11 +23,6 @@ describe(
});
it('should show an error if your balance is zero', () => {
const accounts = accountsQuery();
amendMarginAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
// 7002-SORD-003
@@ -50,9 +40,8 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -5,10 +5,6 @@ import {
toggleMarket,
} from '../support/deal-ticket';
const tooltipContent = 'tooltip-content';
const reduceOnly = 'reduce-only';
const postOnly = 'post-only';
describe('time in force validation', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
@@ -126,18 +122,13 @@ describe('time in force validation', { tags: '@smoke' }, () => {
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId(postOnly).should('be.disabled');
cy.getByTestId(reduceOnly).should('be.enabled');
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
});
@@ -153,33 +144,14 @@ describe('time in force validation', { tags: '@smoke' }, () => {
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId(postOnly).should('be.enabled');
cy.getByTestId(reduceOnly).should('be.disabled');
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
});
});
it(`can see explanation of what post only and reduce only is/does`, function () {
// 7003-SORD-058
cy.get('[for="post-only"]').should('have.text', 'Post only').realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.`
);
cy.get('[for="reduce-only"]')
.should('have.text', 'Reduce only')
.realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".`
);
});
});
});
@@ -1,79 +1,32 @@
const colHeader = '.ag-header-cell-text';
const colIdPrice = '[col-id=price]';
const colIdSize = '[col-id=size]';
const colIdCreatedAt = '[col-id=createdAt]';
const tradesTab = 'Trades';
const tradesTable = 'tab-trades';
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
});
describe('trades', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.getByTestId(tradesTab).click();
});
const colIdPrice = 'price';
const colIdSize = 'size';
const colIdCreatedAt = 'createdAt';
it('show trades', () => {
// 6005-THIS-001
// 6005-THIS-002
cy.getByTestId(tradesTab).should('be.visible');
cy.getByTestId(tradesTable).should('be.visible');
cy.getByTestId(tradesTable).should('not.be.empty');
});
it('renders trades', () => {
cy.getByTestId('Trades').click();
cy.getByTestId('tab-trades').should('be.visible');
it('show trades prices', () => {
// 6005-THIS-003
cy.get(`${colIdPrice} ${colHeader}`).first().should('have.text', 'Price');
cy.get(colIdPrice).each(($tradePrice) => {
cy.get(`[col-id=${colIdPrice}]`).each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
});
it('show trades sizes', () => {
// 6005-THIS-004
cy.get(`${colIdSize} ${colHeader}`).first().should('have.text', 'Size');
cy.get(colIdSize).each(($tradeSize) => {
cy.get(`[col-id=${colIdSize}]`).each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
});
it('show trades date and time', () => {
// 6005-THIS-005
cy.get(`${colIdCreatedAt} ${colHeader}`).should('have.text', 'Created at');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get(colIdCreatedAt).each(($tradeDateTime, index) => {
cy.get(`[col-id=${colIdCreatedAt}]`).each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
});
it('trades are sorted descending by datetime', () => {
// 6005-THIS-006
const dateTimes: Date[] = [];
cy.get(colIdCreatedAt)
.each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
dateTimes.push(new Date($tradeDateTime.text()));
}
})
.then(() => {
expect(dateTimes).to.deep.equal(
dateTimes.sort((a, b) => b.getTime() - a.getTime())
);
});
});
it('copy price to deal ticket form', () => {
// 6005-THIS-007
cy.get(colIdPrice).last().click();
cy.getByTestId('order-price').should('have.value', '171.16898');
});
});
@@ -83,8 +83,6 @@ export const LiquidityContainer = ({
const assetDecimalPlaces =
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const quantum =
market?.tradableInstrument.instrument.product.settlementAsset.quantum || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
@@ -100,7 +98,6 @@ export const LiquidityContainer = ({
rowData={data}
symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces}
quantum={quantum}
stakeToCcyVolume={stakeToCcyVolume}
overlayNoRowsTemplate={error ? error.message : t('No data')}
/>
@@ -49,7 +49,7 @@ const MarketBottomPanel = memo(
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'bottom' });
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
return 'xxxl' === screenSize ? (
<ResizableGrid
@@ -39,7 +39,7 @@ export const TradePanels = ({
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
@@ -22,7 +22,7 @@ export const DepositsContainer = () => {
<div className="h-full">
<div className="h-full relative">
<DepositsTable
rowData={data}
rowData={data || []}
ref={gridRef}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
@@ -34,7 +34,7 @@ export const Portfolio = () => {
}, [updateTitle]);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
@@ -49,9 +49,6 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
Statistics: {
merge: true,
},
Account: {
keyFields: false,
fields: {
@@ -83,6 +80,12 @@ const cacheConfig: InMemoryCacheConfig = {
ERC20: {
keyFields: ['contractAddress'],
},
PositionUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
Party: {
keyFields: false,
},
@@ -92,16 +95,8 @@ const cacheConfig: InMemoryCacheConfig = {
Fees: {
keyFields: false,
},
// The folling types are cached by the data provider and not by apollo
PositionUpdate: {
keyFields: false,
},
TradeUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
// Don't cache order update as this subscription result gets merged into the main order cache
// We don't need to write these to the cache at all
OrderUpdate: {
keyFields: false,
},
@@ -125,19 +125,14 @@ export const MarketLiquiditySupplied = ({
</KeyValueTableRow>
</KeyValueTable>
<br />
<div className="flex flex-col gap-2">
<Link
href={`/#/liquidity/${marketId}`}
data-testid="view-liquidity-link"
>
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
</div>
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
{showMessage && (
<p className="mt-4">
{t(
@@ -20,8 +20,20 @@ export const useMarketClickHandler = (replace = false) => {
);
};
export const useMarketLiquidityClickHandler = () => {
return useCallback((selectedId: string, metaKey?: boolean) => {
window.open(`/#/liquidity/${selectedId}`, metaKey ? '_blank' : '_self');
}, []);
export const useMarketLiquidityClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isLiquidityPage = pathname.match(/^\/liquidity\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.LIQUIDITY](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isLiquidityPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isLiquidityPage]
);
};
@@ -94,7 +94,6 @@ export const accountsOnlyDataProvider = makeDataProvider<
update,
getData,
getDelta,
fetchPolicy: 'no-cache',
});
export interface AccountFields extends Account {
@@ -72,6 +72,7 @@ export const AccountBreakdownDialog = memo(
onClose: () => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
console.log('render');
return (
<Dialog
size="medium"
+13 -47
View File
@@ -151,53 +151,19 @@ export const amendGeneralAccountBalance = (
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const marginAccount = accounts.party?.accountsConnection?.edges?.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const edges = accounts.party.accountsConnection.edges.map((edge) =>
edge?.node.asset.id === marginAccount.node.asset.id && !edge?.node.market
? { ...edge, node: { ...edge.node, balance } }
: edge
if (accounts.party?.accountsConnection?.edges) {
const marginAccount = accounts.party.accountsConnection.edges.find(
(edge) => edge?.node.market?.id === marketId
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
if (marginAccount) {
const generalAccount = accounts.party.accountsConnection.edges.find(
(edge) =>
edge?.node.asset.id === marginAccount.node.asset.id &&
!edge?.node.market
);
if (generalAccount) {
generalAccount.node.balance = balance;
}
}
}
return accounts;
};
export const amendMarginAccountBalance = (
accounts: AccountsQuery,
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const edges = accounts.party?.accountsConnection?.edges?.map((edge) =>
edge?.node.market?.id === marketId
? { ...edge, node: { ...edge?.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
};
-8
View File
@@ -290,14 +290,6 @@ export const TransferFee = ({
decimals?: number;
}) => {
if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null;
}
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
+14 -22
View File
@@ -44,35 +44,27 @@ export const checkSorting = (
orderTabDesc: string[]
) => {
checkSortChange(orderTabDefault, column);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).last().click();
});
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabAsc, column);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
cy.get('.ag-center-cols-container')
.last()
.within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
});
cy.get('.ag-center-cols-container').within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
});
});
});
};
type Edges = { node: unknown }[];
+4 -6
View File
@@ -24,13 +24,11 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
TField
>;
export type VegaValueGetterParams<TRow> = Omit<
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
ValueGetterParams,
'data' | 'node'
> & {
data?: TRow;
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
};
TRow,
TField
>;
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
RowHelper<ICellRendererParams, TRow, TField>,
@@ -1,6 +1,6 @@
import { formatRange, formatValue } from './range';
import { formatRange, formatValue } from './deal-ticket-fee-details';
describe('formatValue', () => {
describe('formatRange, formatValue', () => {
it.each([
{ v: 123000, d: 5, o: '1.23' },
{ v: 123000, d: 3, o: '123.00' },
@@ -21,12 +21,12 @@ describe('formatValue', () => {
{ v: 123000, d: 3, o: '123.00', q: '0.1' },
{ v: 123000, d: 1, o: '12,300.00', q: '0.1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' },
{ v: 123001, d: 2, o: '1,230.01', q: '100' },
{ v: 123001, d: 2, o: '1,230', q: '100' },
{ v: 123001, d: 2, o: '1,230.01', q: '0.1' },
{
v: '123456789123456789',
d: 10,
o: '12,345,678.91234568',
o: '12,345,678.9123457',
q: '0.00003846',
},
])(
@@ -35,16 +35,9 @@ describe('formatValue', () => {
expect(formatValue(v.toString(), d, q)).toStrictEqual(o);
}
);
});
describe('formatRange', () => {
it.each([
{
min: 123000,
max: 12300011111,
d: 5,
o: '1.23 - 123,000.11111',
q: '0.1',
},
{ min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' },
{
min: 123000,
max: 12300011111,
@@ -63,7 +56,7 @@ describe('formatRange', () => {
min: 123001000,
max: 12300011111,
d: 2,
o: '1,230,010.00 - 123,000,111.11',
o: '1,230,010 - 123,000,111',
q: '100',
},
])(
@@ -11,7 +11,11 @@ import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
isNumeric,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -27,6 +31,32 @@ import {
const emptyValue = '-';
export const formatValue = (
value: string | number | null | undefined,
formatDecimals: number,
quantum?: string
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) return addDecimalsFormatNumber(value, formatDecimals);
return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
formatDecimals: number,
quantum?: string
) => {
const minFormatted = formatValue(min, formatDecimals, quantum);
const maxFormatted = formatValue(max, formatDecimals, quantum);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};
export interface DealTicketFeeDetailPros {
label: string;
value?: string | null | undefined;
@@ -113,7 +143,6 @@ export const DealTicketFeeDetails = ({
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
@@ -203,6 +232,7 @@ export const DealTicketFeeDetails = ({
}
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateFormatted;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
@@ -228,10 +258,18 @@ export const DealTicketFeeDetails = ({
liquidationEstimateWorstCaseIncludingSellOrders
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
// The estimate order query API gives us the liquidation price in formatted by asset decimals.
// We need to calculate it with asset decimals, but display it with market decimals precision until the API changes.
liquidationPriceEstimate = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals
);
liquidationPriceEstimateFormatted = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
@@ -241,8 +279,7 @@ export const DealTicketFeeDetails = ({
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
undefined,
market.decimalPlaces
quantum
);
}
@@ -251,16 +288,14 @@ export const DealTicketFeeDetails = ({
[]
);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<div>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
value={formatValue(notionalSize, assetDecimals)}
formattedValue={formatValue(notionalSize, assetDecimals, quantum)}
symbol={assetSymbol}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol)}
/>
<DealTicketFeeDetail
label={t('Fees')}
@@ -342,8 +377,8 @@ export const DealTicketFeeDetails = ({
<DealTicketFeeDetail
label={t('Liquidation price estimate')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
formattedValue={liquidationPriceEstimateFormatted}
symbol={assetSymbol}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
/>
{partyId && (
@@ -195,7 +195,7 @@ export const DealTicket = ({
}
const hasNoBalance =
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
!generalAccountBalance || !BigInt(generalAccountBalance);
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
@@ -219,7 +219,6 @@ export const DealTicket = ({
marketState,
marketTradingMode,
generalAccountBalance,
marginAccountBalance,
pubKey,
setError,
clearErrors,
+2
View File
@@ -6,6 +6,7 @@ import {
truncateByChars,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
@@ -24,6 +25,7 @@ export const DepositsTable = forwardRef<
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No deposits')}
defaultColDef={{ resizable: true }}
style={{ width: '100%', height: '100%' }}
suppressCellFocus={true}
+1 -1
View File
@@ -48,7 +48,7 @@ const mapFillUpdateToFill = (
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: sellerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
};
};
+112 -154
View File
@@ -1,22 +1,20 @@
import { forwardRef, useMemo } from 'react';
import { forwardRef } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import type {
VegaValueFormatterParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type {
ColDef,
ITooltipParams,
ValueFormatterParams,
} from 'ag-grid-community';
import { AgGridColumn } from 'ag-grid-react';
import type { ValueFormatterParams } from 'ag-grid-community';
import BigNumber from 'bignumber.js';
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
@@ -37,152 +35,21 @@ export interface LiquidityTableProps
symbol?: string;
assetDecimalPlaces?: number;
stakeToCcyVolume: string | null;
quantum?: string | number;
}
export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
(
{ symbol = '', assetDecimalPlaces, stakeToCcyVolume, quantum, ...props },
ref
) => {
const colDefs = useMemo(() => {
const assetDecimalsFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`;
};
const assetDecimalsQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const defs: ColDef[] = [
{
headerName: t('Party'),
field: 'party.id',
headerTooltip: t(
'The public key of the party making this commitment.'
),
},
{
headerName: t(`Commitment (${symbol})`),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t(`Share`),
field: 'equityLikeShare',
type: 'rightAligned',
headerTooltip: t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
),
valueFormatter: percentageFormatter,
},
{
headerName: t('Proposed fee'),
headerTooltip: t(
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
},
{
headerName: t('Market valuation at entry'),
field: 'averageEntryValuation',
type: 'rightAligned',
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
minWidth: 160,
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Supplied'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
},
},
{
headerName: t('Created'),
headerTooltip: t(
'The date and time this liquidity provision was created.'
),
field: 'createdAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
{
headerName: t('Updated'),
headerTooltip: t(
'The date and time this liquidity provision was last updated.'
),
field: 'updatedAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
];
return defs;
}, [assetDecimalPlaces, quantum, stakeToCcyVolume, symbol]);
({ symbol = '', assetDecimalPlaces, stakeToCcyVolume, ...props }, ref) => {
const assetDecimalsFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0, 5)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0, 5)}`;
};
return (
<AgGrid
@@ -199,8 +66,99 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
}}
storeKey="liquidityProvisionTable"
{...props}
columnDefs={colDefs}
></AgGrid>
>
<AgGridColumn
headerName={t('Party')}
field="party.id"
headerTooltip={t(
'The public key of the party making this commitment.'
)}
/>
<AgGridColumn
headerName={t(`Commitment (${symbol})`)}
field="commitmentAmount"
type="rightAligned"
headerTooltip={t(
'The amount committed to the market by this liquidity provider.'
)}
valueFormatter={assetDecimalsFormatter}
/>
<AgGridColumn
headerName={t('Share')}
field="equityLikeShare"
type="rightAligned"
headerTooltip={t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
)}
valueFormatter={percentageFormatter}
/>
<AgGridColumn
headerName={t('Proposed fee')}
headerTooltip={t(
'The fee percentage (per trade) proposed by each liquidity provider.'
)}
field="fee"
type="rightAligned"
valueFormatter={percentageFormatter}
/>
<AgGridColumn
headerName={t('Market valuation at entry')}
field="averageEntryValuation"
type="rightAligned"
headerTooltip={t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
)}
minWidth={160}
valueFormatter={assetDecimalsFormatter}
/>
<AgGridColumn
headerName={t('Obligation')}
field="commitmentAmount"
type="rightAligned"
headerTooltip={t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_siskas network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
)}
valueFormatter={stakeToCcyVolumeFormatter}
/>
<AgGridColumn
headerName={t('Supplied')}
headerTooltip={t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
)}
field="balance"
type="rightAligned"
valueFormatter={stakeToCcyVolumeFormatter}
/>
<AgGridColumn
headerName={t('Status')}
headerTooltip={t('The current status of this liquidity provision.')}
field="status"
valueFormatter={({
value,
}: VegaValueFormatterParams<LiquidityProvisionData, 'status'>) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[value];
}}
/>
<AgGridColumn
headerName={t('Created')}
headerTooltip={t(
'The date and time this liquidity provision was created.'
)}
field="createdAt"
type="rightAligned"
valueFormatter={dateValueFormatter}
/>
<AgGridColumn
headerName={t('Updated')}
headerTooltip={t(
'The last time this liquidity provision was updated.'
)}
field="updatedAt"
type="rightAligned"
valueFormatter={dateValueFormatter}
/>
</AgGrid>
);
}
);
@@ -76,7 +76,9 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
@@ -100,7 +102,12 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
@@ -127,7 +134,9 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
@@ -1,60 +0,0 @@
import { useEstimatePositionQuery } from './__generated__/Positions';
import { formatRange } from '@vegaprotocol/utils';
export const LiquidationPrice = ({
marketId,
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
}) => {
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
marketId,
openVolume,
collateralAvailable,
},
fetchPolicy: 'no-cache',
skip: !openVolume || openVolume === '0',
});
const data = currentData || previousData;
let value = '-';
if (data) {
const bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
const worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
value =
bestCase && worstCase && BigInt(bestCase) < BigInt(worstCase)
? formatRange(
bestCase,
worstCase,
decimalPlaces,
undefined,
formatDecimals,
value
)
: formatRange(
worstCase,
bestCase,
decimalPlaces,
undefined,
formatDecimals,
value
);
}
return <span data-testid="liquidation-price">{value}</span>;
};
@@ -34,7 +34,6 @@ export interface Position {
averageEntryPrice: string;
currentLeverage: number | undefined;
decimals: number;
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
@@ -74,7 +73,6 @@ export const getMetrics = (
decimals,
id: assetId,
symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
@@ -116,7 +114,6 @@ export const getMetrics = (
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
@@ -200,7 +197,6 @@ const positionsDataProvider = makeDataProvider<
getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
subscriptionData.positions,
getSubscriptionVariables,
fetchPolicy: 'no-cache',
});
const positionDataProvider = makeDerivedDataProvider<
@@ -7,12 +7,6 @@ import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => (
<span data-testid="liquidation-price">liquidation price</span>
),
}));
const singleRow: Position = {
partyId: 'partyId',
assetId: 'asset-id',
@@ -20,7 +14,6 @@ const singleRow: Position = {
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
@@ -55,7 +48,7 @@ it('render correct columns', async () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
expect(headers).toHaveLength(11);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
@@ -63,7 +56,6 @@ it('render correct columns', async () => {
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
@@ -151,20 +143,12 @@ it('displays mark price', async () => {
expect(cells[3].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
expect(cells[6].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
@@ -172,7 +156,7 @@ it('displays allocated margin', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
const cell = cells[7];
expect(cell.textContent).toEqual('123,456.00');
});
@@ -181,7 +165,7 @@ it('displays realised and unrealised PNL', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[10].textContent).toEqual('4.56');
expect(cells[9].textContent).toEqual('4.56');
});
it('displays close button', async () => {
@@ -198,7 +182,7 @@ it('displays close button', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
expect(cells[11].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
@@ -214,7 +198,7 @@ it('do not display close button if openVolume is zero', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
expect(cells[11].textContent).toEqual('');
});
describe('PNLCell', () => {
+325 -336
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { forwardRef, useMemo } from 'react';
import { forwardRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community';
import type { CellRendererSelectorResult } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
VegaValueGetterParams,
@@ -33,6 +33,7 @@ import {
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
@@ -42,7 +43,6 @@ import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -121,344 +121,333 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
MarketNameCell,
}}
{...props}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
? {
headerName: t('Vega key'),
field: 'partyId',
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)
?.name) ||
data?.partyId,
minWidth: 190,
}
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
minWidth: 190,
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(
data.notional,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 80,
},
{
headerName: t('Open volume'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
cellRenderer: OpenVolumeCell,
minWidth: 100,
},
{
headerName: t('Mark price'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(
data.markPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
>
{multipleKeys ? (
<AgGridColumn
headerName={t('Vega key')}
field="partyId"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'partyId'>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)?.name) ||
data?.partyId
}
minWidth={190}
/>
) : null}
<AgGridColumn
headerName={t('Market')}
field="marketName"
cellRenderer="MarketNameCell"
cellRendererParams={{ idPath: 'marketId', onMarketClick }}
minWidth={190}
/>
<AgGridColumn
headerName={t('Notional')}
headerTooltip={t('Mark price x open volume.')}
field="notional"
type="rightAligned"
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'notional'>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
}}
minWidth={80}
/>
<AgGridColumn
headerName={t('Open volume')}
field="openVolume"
type="rightAligned"
cellClass="font-mono text-right"
cellClassRules={signedNumberCssClassRules}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'openVolume'>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>):
| string
| undefined => {
return data?.openVolume === undefined
? undefined
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
},
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
}}
cellRenderer={OpenVolumeCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Mark price')}
field="markPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'markPrice'>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return undefined;
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="assetSymbol"
colId="asset"
minWidth={100}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(data.assetId, e.target as HTMLElement);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? ''
: formatNumber(value.toString(), 1),
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Margin allocated'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
minWidth: 100,
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
},
headerTooltip: t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Unrealised PNL'),
field: 'unrealisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Updated'),
field: 'updatedAt',
type: 'rightAligned',
filter: DateRangeFilter,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}
return getDateTimeFormat().format(new Date(value));
},
minWidth: 150,
},
onClose && !isReadOnly
? {
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Leverage')}
field="currentLeverage"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? undefined
: formatNumber(value.toString(), 1)
}
minWidth={100}
/>
)}
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Margin allocated')}
field="marginAccountBalance"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}}
minWidth={100}
/>
)}
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
}}
headerTooltip={t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
field="unrealisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? undefined
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals)
}
headerTooltip={t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Updated')}
field="updatedAt"
type="rightAligned"
filter={DateRangeFilter}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return value;
}
return getDateTimeFormat().format(new Date(value));
}}
minWidth={150}
/>
{onClose && !isReadOnly ? (
<AgGridColumn
{...COL_DEFS.actions}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
}}
minWidth={90}
maxWidth={90}
/>
) : null}
</AgGrid>
);
}
);
@@ -1,2 +1 @@
export * from './use-vote-submit';
export * from './__generated__/VoteSubsciption';
@@ -5,16 +5,15 @@ import { useVoteEvent } from './use-vote-event';
import type { VoteValue } from '@vegaprotocol/types';
import type { VoteEventFieldsFragment } from './__generated__/VoteSubsciption';
export type FinalizedVote = VoteEventFieldsFragment & { pubKey: string };
export type FinalizedVote = VoteEventFieldsFragment;
export const useVoteSubmit = () => {
const { pubKey } = useVegaWallet();
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
const waitForVoteEvent = useVoteEvent(transaction);
const [finalizedVote, setFinalizedVote] = useState<FinalizedVote | null>(
null
);
const [finalizedVote, setFinalizedVote] =
useState<VoteEventFieldsFragment | null>(null);
const submit = useCallback(
async (voteValue: VoteValue, proposalId: string | null) => {
@@ -34,7 +33,7 @@ export const useVoteSubmit = () => {
if (res) {
waitForVoteEvent(proposalId, pubKey, (v) => {
setFinalizedVote({ ...v, pubKey });
setFinalizedVote(v);
setComplete();
});
}
@@ -99,7 +99,6 @@ export const tradesProvider = makeDataProvider<
append,
first: MAX_TRADES,
},
fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ marketId }) => ({ marketId }),
});
+8 -8
View File
@@ -53,10 +53,10 @@ export const tradesUpdateSubscription = (
const trades: TradeFieldsFragment[] = [
{
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
@@ -77,10 +77,10 @@ const trades: TradeFieldsFragment[] = [
__typename: 'Trade',
},
{
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/ui-toolkit",
"version": "0.12.5"
"version": "0.12.3"
}
@@ -1,6 +1,6 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import type { ComponentProps, ReactNode } from 'react';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
import { VegaIcon, VegaIconNames } from '../icon';
import { Icon } from '../icon';
@@ -161,49 +161,6 @@ export const DropdownMenuSeparator = forwardRef<
/>
));
/**
* Container element for submenus
*/
export const DropdownMenuSub = (
subProps: ComponentProps<typeof DropdownMenuPrimitive.Sub>
) => <DropdownMenuPrimitive.Sub {...subProps} />;
/**
* Container within a DropdownMenuSub specifically for the content
*/
export const DropdownMenuSubContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...subContentProps }, forwardedRef) => (
<DropdownMenuPrimitive.SubContent
ref={forwardedRef}
className={classNames('bg-vega-light-150 dark:bg-vega-dark-150', className)}
{...subContentProps}
/>
));
/**
* Equivalent to trigger, but for triggering sub menus
*/
export const DropdownMenuSubTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>
>(({ className, ...subTriggerProps }, forwardedRef) => (
<DropdownMenuPrimitive.SubTrigger
className={classNames(className, itemClass)}
ref={forwardedRef}
{...subTriggerProps}
/>
));
/**
* Portal to ensure menu portions are rendered outwith where they appear in the
* DOM.
*/
export const DropdownMenuPortal = (
portalProps: ComponentProps<typeof DropdownMenuPrimitive.Portal>
) => <DropdownMenuPrimitive.Portal {...portalProps} />;
/**
* Wraps a regular DropdownMenuItem with copy to clip board functionality
*/
@@ -1,8 +1,5 @@
import classNames from 'classnames';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { BigNumber } from 'bignumber.js';
import { getIntentBackground, Intent } from '../../utils/intent';
@@ -77,17 +74,13 @@ const Level = ({
.multipliedBy(100)
.toNumber();
const formattedFee = fee
? formatNumberPercentage(new BigNumber(fee).times(100), 2)
: '-';
const tooltipContent = (
<div className="text-vega-dark-100 dark:text-vega-light-200">
<>
<div className="mt-1.5 inline-flex">
<Indicator variant={intent} />
</div>
<span>
{formattedFee} {t('Fee')}
{fee}% {t('Fee')}
</span>
<div className="flex flex-col">
<span>
@@ -95,7 +88,7 @@ const Level = ({
{addDecimalsFormatNumber(commitmentAmount, decimals)}
</span>
</div>
</div>
</>
);
return (
@@ -186,7 +179,7 @@ export const HealthBar = ({
opacity={opacity}
fee={fee}
prevLevel={prevLevel}
decimals={decimals}
decimals={0}
intent={intent}
key={'healthbar-segment-' + index}
/>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@vegaprotocol/utils",
"version": "0.0.5",
"version": "0.0.4",
"type": "commonjs"
}
-1
View File
@@ -1,5 +1,4 @@
export * from './date';
export * from './number';
export * from './range';
export * from './size';
export * from './strings';
+73 -80
View File
@@ -29,21 +29,14 @@ describe('number utils', () => {
{ v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 },
{ v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 1 },
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
o: '12,345,678.9123457',
q: '0.00003846',
},
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '1',
},
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
@@ -77,80 +70,80 @@ describe('number utils', () => {
])('formats given number correctly', ({ v, d, o }) => {
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
});
});
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000'] },
{ v: undefined, d: 3, o: ['0', '000'] },
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
{
v: new BigNumber(30000),
d: undefined,
o: ['30,000', '00'],
},
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
expect(toNumberParts(v, d)).toStrictEqual(o);
});
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000'] },
{ v: undefined, d: 3, o: ['0', '000'] },
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
{
v: new BigNumber(30000),
d: undefined,
o: ['30,000', '00'],
},
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
expect(toNumberParts(v, d)).toStrictEqual(o);
});
});
describe('isNumeric', () => {
it.each([
{ i: null, o: false },
{ i: undefined, o: false },
{ i: 1, o: true },
{ i: '1', o: true },
{ i: '-1', o: true },
{ i: 0.1, o: true },
{ i: '.1', o: true },
{ i: '-.1', o: true },
{ i: 123, o: true },
{ i: -123, o: true },
{ i: '123', o: true },
{ i: '123.01', o: true },
{ i: '-123.01', o: true },
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
{ i: new BigNumber(123), o: true },
{ i: Infinity, o: false },
{ i: NaN, o: false },
])(
'returns correct results',
({
i,
o,
}: {
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);
}
);
});
describe('isNumeric', () => {
it.each([
{ i: null, o: false },
{ i: undefined, o: false },
{ i: 1, o: true },
{ i: '1', o: true },
{ i: '-1', o: true },
{ i: 0.1, o: true },
{ i: '.1', o: true },
{ i: '-.1', o: true },
{ i: 123, o: true },
{ i: -123, o: true },
{ i: '123', o: true },
{ i: '123.01', o: true },
{ i: '-123.01', o: true },
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
{ i: new BigNumber(123), o: true },
{ i: Infinity, o: false },
{ i: NaN, o: false },
])(
'returns correct results',
({
i,
o,
}: {
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);
}
);
});
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
});
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
});
});
+1 -2
View File
@@ -98,8 +98,7 @@ export const addDecimalsFormatNumberQuantum = (
if (isNaN(Number(quantum))) {
return addDecimalsFormatNumber(rawValue, decimalPlaces);
}
const quantumValue = addDecimal(quantum, decimalPlaces);
const numberDP = Math.max(0, Math.log10(100 / Number(quantumValue)));
const numberDP = Math.max(0, Math.log10(100 / Number(quantum)));
return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP));
};
-38
View File
@@ -1,38 +0,0 @@
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
isNumeric,
} from './number';
export const formatValue = (
value: string | number | null | undefined,
decimalPlaces: number,
quantum?: string,
formatDecimals?: number,
emptyValue = '-'
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) {
return addDecimalsFormatNumber(value, decimalPlaces, formatDecimals);
}
return addDecimalsFormatNumberQuantum(value, decimalPlaces, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
decimalPlaces: number,
quantum?: string,
formatDecimals?: number,
emptyValue = '-'
) => {
const minFormatted = formatValue(min, decimalPlaces, quantum, formatDecimals);
const maxFormatted = formatValue(max, decimalPlaces, quantum, formatDecimals);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};