Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd0b29c251 | ||
|
|
6471be323d | ||
|
|
3e9aeb2f2f | ||
|
|
e83a7915ad | ||
|
|
d2b2d16c9c | ||
|
|
16b2360da7 | ||
|
|
1adbc2dbd7 |
@@ -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"
|
||||
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
- release/*
|
||||
- develop
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
@@ -134,35 +136,22 @@ jobs:
|
||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||
fi
|
||||
projects="$(echo $projects_e2e | sed 's|-e2e||g')"
|
||||
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
|
||||
if echo "$affected" | grep -q multisig-signer; then
|
||||
echo "Tools are affected"
|
||||
# tools are only applicable to check previews or deploy from develop to mainnet
|
||||
if echo "$affected" | grep -q multisig-signer; then
|
||||
echo "Tools are affected"
|
||||
# tools are only applicable to check previews or deploy from develop to mainnet
|
||||
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
|
||||
echo "Deploying tools on preview"
|
||||
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
|
||||
projects+=' "multisig-signer" '
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
if echo "$affected" | grep -q multisig-signer; then
|
||||
echo "Tools are affected"
|
||||
# tools are only applicable to check previews or deploy from develop to mainnet
|
||||
if [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
echo "Deploying tools on s3"
|
||||
projects+=' "multisig-signer" '
|
||||
fi
|
||||
if echo "$affected" | grep -q static; then
|
||||
echo "static is affected"
|
||||
echo "Deploying static on s3"
|
||||
projects+=' "static" '
|
||||
fi
|
||||
if echo "$affected" | grep -q ui-toolkit; then
|
||||
echo "ui-toolkit is affected"
|
||||
echo "Deploying ui-toolkit on s3"
|
||||
projects+=' "ui-toolkit" '
|
||||
fi
|
||||
fi
|
||||
|
||||
projects_e2e=${projects_e2e%?}
|
||||
projects_e2e=[${projects_e2e// /,}]
|
||||
projects=${projects%?}
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
|
||||
@@ -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') }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
# registry: registry.hub.docker.com
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -75,16 +74,10 @@ jobs:
|
||||
envName="mainnet"
|
||||
bucketName="tools.vega.xyz"
|
||||
fi
|
||||
if [[ "${{ matrix.app }}" = "static" ]]; then
|
||||
envName="mainnet"
|
||||
bucketName="static.vega.xyz"
|
||||
fi
|
||||
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
|
||||
envName="mainnet"
|
||||
bucketName="ui.vega.rocks"
|
||||
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,15 +106,14 @@ 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
|
||||
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
|
||||
DIST_LOCATION=dist/apps/trading/exported
|
||||
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
|
||||
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
|
||||
DIST_LOCATION=dist/storybook/ui-toolkit
|
||||
else
|
||||
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
|
||||
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
||||
@@ -156,9 +148,7 @@ jobs:
|
||||
|
||||
- name: Publish dist as docker image (ghcr)
|
||||
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
|
||||
@@ -171,9 +161,7 @@ jobs:
|
||||
|
||||
- name: Publish dist as docker image (docker hub)
|
||||
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') }}
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
context: .
|
||||
file: docker/node-outside-docker.Dockerfile
|
||||
@@ -182,41 +170,13 @@ jobs:
|
||||
APP=${{ matrix.app }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||
vegaprotocol/${{ matrix.app }}:mainnet
|
||||
|
||||
- name: Publish dist as docker image (ghcr - retry)
|
||||
uses: docker/build-push-action@v3
|
||||
if: ${{ steps.ghcr-push.outcome == 'failure' }}
|
||||
with:
|
||||
context: .
|
||||
file: docker/node-outside-docker.Dockerfile
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Publish dist as docker image (docker hub - retry)
|
||||
uses: docker/build-push-action@v3
|
||||
if: ${{ steps.dockerhub-push.outcome == 'failure' }}
|
||||
with:
|
||||
context: .
|
||||
file: docker/node-outside-docker.Dockerfile
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||
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') ) ) }}
|
||||
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,8 +194,7 @@ 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') }}
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
# display info about app
|
||||
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
|
||||
@@ -250,7 +209,7 @@ jobs:
|
||||
https://api.fleek.co/graphql
|
||||
|
||||
- name: Check out ipfs-redirect
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'vegaprotocol/ipfs-redirect'
|
||||
@@ -259,7 +218,7 @@ jobs:
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
|
||||
- name: Update console.vega.xyz DNS to redirect to the new console
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -295,3 +254,27 @@ jobs:
|
||||
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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
<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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,7 @@ context(
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
});
|
||||
|
||||
describe.skip('Eth wallet - contains VEGA tokens', function () {
|
||||
describe('Eth wallet - contains VEGA tokens', function () {
|
||||
beforeEach(
|
||||
'teardown wallet & drill into a specific validator',
|
||||
function () {
|
||||
@@ -381,8 +381,6 @@ context(
|
||||
|
||||
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
cy.reload();
|
||||
ethereumWalletConnect();
|
||||
stakingPageAssociateTokens('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
|
||||
@@ -69,10 +69,7 @@ context(
|
||||
it('Unable to submit withdrawal with invalid fields', function () {
|
||||
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.get('select').select(usdtSelectValue, { force: true });
|
||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||
cy.getByTestId(submitWithdrawalButton).click();
|
||||
cy.getByTestId(formValidationError).should('have.length', 1);
|
||||
@@ -96,10 +93,7 @@ context(
|
||||
// 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.get('select').select(usdtSelectValue, { force: true });
|
||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||
cy.getByTestId(withdrawalThreshold).should(
|
||||
'have.text',
|
||||
@@ -165,10 +159,7 @@ context(
|
||||
// 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.get('select').select(usdtSelectValue, { force: true });
|
||||
cy.getByTestId(ethAddressInput).should('be.empty');
|
||||
cy.getByTestId(amountInput).click().type('110');
|
||||
cy.getByTestId(submitWithdrawalButton).click();
|
||||
@@ -228,10 +219,7 @@ context(
|
||||
it('Should be able to see withdrawal details from toast', function () {
|
||||
cy.getByTestId(withdraw).click();
|
||||
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
|
||||
cy.getByTestId('select-asset').click();
|
||||
cy.get('select')
|
||||
.select(usdtSelectValue, { force: true })
|
||||
.should('have.value', usdtSelectValue);
|
||||
cy.get('select').select(usdtSelectValue, { force: true });
|
||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||
cy.getByTestId(withdrawalThreshold).should(
|
||||
'have.text',
|
||||
@@ -286,10 +274,7 @@ context(
|
||||
cy.connectPublicKey(vegaWalletPubKey);
|
||||
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.get('select').select(usdtSelectValue, { force: true });
|
||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||
cy.getByTestId(amountInput).click().type('100');
|
||||
cy.pause();
|
||||
|
||||
@@ -824,7 +824,5 @@
|
||||
"disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App.",
|
||||
"multisigContractLink": "Ethereum Multisig Contract",
|
||||
"multisigContractIncorrect": "is incorrectly configured. Validator and delegator rewards will be penalised until this is resolved.",
|
||||
"learnMore": "Learn more",
|
||||
"AllValidators": "All validators",
|
||||
"AllProposals": "All proposals"
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { Proposal } from './proposal';
|
||||
@@ -42,35 +41,19 @@ jest.mock('../list-asset', () => ({
|
||||
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
|
||||
}));
|
||||
|
||||
const renderComponent = (proposal: ProposalQuery['proposal']) => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Proposal
|
||||
restData={{}}
|
||||
proposal={proposal as ProposalQuery['proposal']}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('Renders with data-testid', async () => {
|
||||
const proposal = generateProposal();
|
||||
renderComponent(proposal);
|
||||
|
||||
render(
|
||||
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
|
||||
);
|
||||
expect(await screen.findByTestId('proposal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders with a link back to "all proposals"', async () => {
|
||||
const proposal = generateProposal();
|
||||
renderComponent(proposal);
|
||||
|
||||
expect(await screen.findByTestId('all-proposals-link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders each section', async () => {
|
||||
const proposal = generateProposal();
|
||||
renderComponent(proposal);
|
||||
|
||||
render(
|
||||
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
|
||||
);
|
||||
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
|
||||
@@ -97,7 +80,8 @@ it('renders whitelist section if proposal is new asset and source is erc20', asy
|
||||
},
|
||||
},
|
||||
});
|
||||
renderComponent(proposal);
|
||||
|
||||
render(
|
||||
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
|
||||
);
|
||||
expect(screen.getByTestId('proposal-list-asset')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
@@ -13,10 +13,6 @@ 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 React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export enum ProposalType {
|
||||
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
|
||||
@@ -33,7 +29,6 @@ export interface ProposalProps {
|
||||
}
|
||||
|
||||
export const Proposal = ({ proposal, restData }: ProposalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { params, loading, error } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
|
||||
@@ -86,15 +81,6 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
|
||||
return (
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
data-testid="all-proposals-link"
|
||||
>
|
||||
<Icon name={'chevron-left'} />
|
||||
<Link className="underline" to={Routes.PROPOSALS}>
|
||||
{t('AllProposals')}
|
||||
</Link>
|
||||
</div>
|
||||
<ProposalHeader proposal={proposal} isListItem={false} />
|
||||
|
||||
<div className="my-10">
|
||||
@@ -116,8 +102,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
|
||||
</div>
|
||||
|
||||
{proposal.terms.change.__typename !== 'NewMarket' &&
|
||||
proposal.terms.change.__typename !== 'UpdateMarket' &&
|
||||
proposal.terms.change.__typename !== 'NewFreeform' && (
|
||||
proposal.terms.change.__typename !== 'UpdateMarket' && (
|
||||
<div className="mb-4">
|
||||
<ProposalTerms data={proposal.terms} />
|
||||
</div>
|
||||
|
||||
+1
-8
@@ -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) => {
|
||||
|
||||
+4
-98
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+11
-27
@@ -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__
|
||||
|
||||
@@ -107,7 +107,7 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name={'chevron-left'} />
|
||||
<Link className="underline" to={Routes.VALIDATORS}>
|
||||
{t('AllValidators')}
|
||||
{t('All validators')}
|
||||
</Link>
|
||||
</div>
|
||||
<Heading
|
||||
|
||||
@@ -7,30 +7,31 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
getFeeLevels,
|
||||
sumLiquidityCommitted,
|
||||
marketLiquidityDataProvider,
|
||||
lpAggregatedDataProvider,
|
||||
} from '@vegaprotocol/liquidity';
|
||||
import { marketWithDataProvider } from '@vegaprotocol/markets';
|
||||
import type { MarketWithData } from '@vegaprotocol/markets';
|
||||
import type { MarketLpQuery } from '@vegaprotocol/liquidity';
|
||||
|
||||
import { Market } from './market';
|
||||
import { Header } from './header';
|
||||
import { LPProvidersGrid } from './providers';
|
||||
|
||||
const formatMarket = (market: MarketWithData) => {
|
||||
const formatMarket = (data: MarketLpQuery) => {
|
||||
return {
|
||||
name: market?.tradableInstrument.instrument.name,
|
||||
name: data?.market?.tradableInstrument.instrument.name,
|
||||
symbol:
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||
data?.market?.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol,
|
||||
settlementAsset:
|
||||
market?.tradableInstrument.instrument.product.settlementAsset,
|
||||
targetStake: market?.data?.targetStake,
|
||||
tradingMode: market?.data?.marketTradingMode,
|
||||
trigger: market?.data?.trigger,
|
||||
data?.market?.tradableInstrument.instrument.product.settlementAsset,
|
||||
targetStake: data?.market?.data?.targetStake,
|
||||
tradingMode: data?.market?.data?.marketTradingMode,
|
||||
trigger: data?.market?.data?.trigger,
|
||||
};
|
||||
};
|
||||
|
||||
export const lpDataProvider = makeDerivedDataProvider(
|
||||
[marketWithDataProvider, lpAggregatedDataProvider],
|
||||
[marketLiquidityDataProvider, lpAggregatedDataProvider],
|
||||
([market, lpAggregatedData]) => ({
|
||||
market: { ...formatMarket(market) },
|
||||
liquidityProviders: lpAggregatedData || [],
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# Static
|
||||
|
||||
A static CDN for Vega assets: `static.vega.xyz`
|
||||
|
||||
prepare assets by running: `yarn nx build static`
|
||||
A static CDN for Vega assets
|
||||
|
||||
@@ -26,27 +26,27 @@
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:first-child {
|
||||
animation-delay: -0.1s;
|
||||
animation-delay: -50ms;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(2) {
|
||||
animation-delay: 0.3s;
|
||||
animation-delay: 0.2s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(3) {
|
||||
animation-delay: -0.45s;
|
||||
animation-delay: -0.6s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(4) {
|
||||
animation-delay: 1s;
|
||||
animation-delay: 0.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(5) {
|
||||
animation-delay: -0.75s;
|
||||
animation-delay: -0.5s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(6) {
|
||||
animation-delay: 0.9s;
|
||||
animation-delay: 0.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(7) {
|
||||
@@ -54,11 +54,11 @@
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(8) {
|
||||
animation-delay: 1.6s;
|
||||
animation-delay: 2s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(9) {
|
||||
animation-delay: -0.45s;
|
||||
animation-delay: -0.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(10) {
|
||||
@@ -66,7 +66,7 @@
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(11) {
|
||||
animation-delay: -2.75s;
|
||||
animation-delay: -0.55s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(12) {
|
||||
@@ -74,57 +74,21 @@
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(13) {
|
||||
animation-delay: -1.95s;
|
||||
animation-delay: -0.65s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(14) {
|
||||
animation-delay: 2.8s;
|
||||
animation-delay: 0.7s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(15) {
|
||||
animation-delay: -0.75s;
|
||||
animation-delay: -3.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(16) {
|
||||
animation-delay: 4s;
|
||||
animation-delay: 1.6s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(17) {
|
||||
animation-delay: -0.85s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(18) {
|
||||
animation-delay: 1.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(19) {
|
||||
animation-delay: -1.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(20) {
|
||||
animation-delay: 5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(21) {
|
||||
animation-delay: -5.25s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(22) {
|
||||
animation-delay: 4.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(23) {
|
||||
animation-delay: -5.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(24) {
|
||||
animation-delay: 4.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(25) {
|
||||
animation-delay: -5s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
animation: flickering 0.4s linear infinite alternate;
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: black;
|
||||
}
|
||||
.pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@for $i from 0 through 25 {
|
||||
.loader-item:nth-child(#{$i}) {
|
||||
@if $i % 2 == 0 {
|
||||
animation-delay: #{$i * 50 * random(5)}ms;
|
||||
animation-direction: reverse;
|
||||
} @else {
|
||||
animation-delay: #{$i * -50 * random(5)}ms;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
.loader-item {
|
||||
animation: flickering 0.4s linear alternate infinite;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
html.dark {
|
||||
.pre-loader {
|
||||
.loader-item {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// #region consts
|
||||
const assetColId = '[col-id="asset.symbol"]';
|
||||
const asset = 'asset';
|
||||
const assetDetailsDialog = 'dialog-content';
|
||||
const assetRow = 'key-value-table-row';
|
||||
const contractAddress = '7_value';
|
||||
@@ -108,7 +108,7 @@ beforeEach(() => {
|
||||
|
||||
const visitPortfolioAndClickAsset = (assetName: string) => {
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get(assetColId).contains(assetName).click();
|
||||
cy.getByTestId(asset).contains(assetName).click();
|
||||
};
|
||||
|
||||
const testTooltip = (index: number, testId: string, tooltip: string) => {
|
||||
|
||||
@@ -450,17 +450,3 @@ describe('Closed markets', { tags: '@smoke' }, () => {
|
||||
.should('have.text', 'View on Explorer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Closed markets"]').click();
|
||||
});
|
||||
|
||||
it('can see no markets message', () => {
|
||||
// 6001-MARK-034
|
||||
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import type { MarketsQuery } from '@vegaprotocol/markets';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const rowSelector =
|
||||
'[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row';
|
||||
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
|
||||
|
||||
describe('markets all table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -63,7 +60,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
// 6001-MARK-035
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colInstrumentCode)
|
||||
.find('[col-id="tradableInstrument.instrument.code"]')
|
||||
.should('have.text', 'SOLUSD');
|
||||
|
||||
// 6001-MARK-036
|
||||
@@ -158,7 +155,6 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('able to open and sort full market list - market page', () => {
|
||||
// 6001-MARK-064
|
||||
const ExpectedSortedMarkets = [
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
@@ -171,38 +167,8 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
|
||||
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
|
||||
cy.get(`[row-index=${i}]`)
|
||||
.find(colInstrumentCode)
|
||||
.find('[col-id="tradableInstrument.instrument.code"]')
|
||||
.should('have.text', ExpectedSortedMarkets[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('can drag and drop columns', () => {
|
||||
// 6001-MARK-065
|
||||
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
|
||||
cy.get(colInstrumentCode)
|
||||
.realMouseDown()
|
||||
.realMouseMove(700, 15)
|
||||
.realMouseUp();
|
||||
cy.get(colInstrumentCode).should(($element) => {
|
||||
const attributeValue = $element.attr('aria-colindex');
|
||||
expect(attributeValue).not.to.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
const markets: MarketsQuery = {};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Markets', markets);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
});
|
||||
|
||||
it('can see no markets message', () => {
|
||||
// 6001-MARK-048
|
||||
cy.getByTestId('tab-all-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
|
||||
const accordionContent = 'accordion-content';
|
||||
const blockExplorerLink = 'block-explorer-link';
|
||||
const dialogClose = 'dialog-close';
|
||||
const dialogContent = 'dialog-content';
|
||||
const externalLink = 'external-link';
|
||||
const githubLink = 'github-link';
|
||||
const liquidityLink = 'view-liquidity-link';
|
||||
const marketInfoBtn = 'Info';
|
||||
const marketTitle = 'accordion-title';
|
||||
const providerName = 'provider-name';
|
||||
const row = 'key-value-table-row';
|
||||
const verifiedProofs = 'verified-proofs';
|
||||
const marketTitle = 'accordion-title';
|
||||
const externalLink = 'external-link';
|
||||
const accordionContent = 'accordion-content';
|
||||
const providerName = 'provider-name';
|
||||
const oracleBannerStatus = 'oracle-banner-status';
|
||||
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
|
||||
const oracleFullProfile = 'oracle-full-profile';
|
||||
|
||||
describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -20,7 +17,12 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
before(() => {
|
||||
cy.mockTradingPage(MarketState.STATE_ACTIVE);
|
||||
cy.mockTradingPage(
|
||||
MarketState.STATE_ACTIVE,
|
||||
undefined,
|
||||
undefined,
|
||||
'COMPROMISED'
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
@@ -28,8 +30,16 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
cy.wait('@MarketInfo');
|
||||
});
|
||||
|
||||
it('show oracle banner', () => {
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
|
||||
cy.getByTestId(oracleBannerDialogTrigger)
|
||||
.should('contain.text', 'Show more')
|
||||
.click();
|
||||
cy.getByTestId(oracleFullProfile).should('exist');
|
||||
});
|
||||
|
||||
it('current fees displayed', () => {
|
||||
// 6002-MDET-101
|
||||
cy.getByTestId(marketTitle).contains('Current fees').click();
|
||||
validateMarketDataRow(0, 'Maker Fee', '0.02%');
|
||||
validateMarketDataRow(1, 'Infrastructure Fee', '0.05%');
|
||||
@@ -38,7 +48,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('market price', () => {
|
||||
// 6002-MDET-102
|
||||
cy.getByTestId(marketTitle).contains('Market price').click();
|
||||
validateMarketDataRow(0, 'Mark Price', '46,126.90058');
|
||||
validateMarketDataRow(1, 'Best Bid Price', '44,126.90058 ');
|
||||
@@ -47,7 +56,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('market volume displayed', () => {
|
||||
// 6002-MDET-103
|
||||
cy.getByTestId(marketTitle).contains('Market volume').click();
|
||||
validateMarketDataRow(1, 'Open Interest', '-');
|
||||
validateMarketDataRow(2, 'Best Bid Volume', '1');
|
||||
@@ -57,13 +65,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('insurance pool displayed', () => {
|
||||
// 6002-MDET-104
|
||||
cy.getByTestId(marketTitle).contains('Insurance pool').click();
|
||||
validateMarketDataRow(0, 'Balance', '0');
|
||||
});
|
||||
|
||||
it('key details displayed', () => {
|
||||
// 6002-MDET-201
|
||||
cy.getByTestId(marketTitle).contains('Key details').click();
|
||||
|
||||
validateMarketDataRow(0, 'Name', 'BTCUSD Monthly (30 Jun 2022)');
|
||||
@@ -79,7 +85,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('instrument displayed', () => {
|
||||
// 6002-MDET-202
|
||||
cy.getByTestId(marketTitle).contains('Instrument').click();
|
||||
|
||||
validateMarketDataRow(0, 'Market Name', 'BTCUSD Monthly (30 Jun 2022)');
|
||||
@@ -88,30 +93,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
validateMarketDataRow(3, 'Quote Name', 'BTC');
|
||||
});
|
||||
|
||||
it('oracle displayed', () => {
|
||||
// 6002-MDET-203
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId(providerName)
|
||||
.and('contain', 'Another oracle');
|
||||
|
||||
cy.getByTestId(providerName).should('be.visible').click();
|
||||
cy.getByTestId(dialogContent)
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.getByTestId(blockExplorerLink).contains('Block explorer');
|
||||
cy.getByTestId(githubLink).contains('Oracle repository');
|
||||
});
|
||||
cy.getByTestId(dialogClose).click();
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId(verifiedProofs)
|
||||
.and('contain', '1');
|
||||
});
|
||||
|
||||
it('settlement asset displayed', () => {
|
||||
// 6002-MDET-206
|
||||
cy.getByTestId(marketTitle).contains('Settlement asset').click();
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT');
|
||||
@@ -130,12 +112,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
);
|
||||
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
|
||||
validateMarketDataRow(9, 'Lifetime limit', '1,230');
|
||||
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
|
||||
validateMarketDataRow(11, 'Global reward pool account balance', '0.00002');
|
||||
});
|
||||
|
||||
it('metadata displayed', () => {
|
||||
// 6002-MDET-207
|
||||
cy.getByTestId(marketTitle).contains('Metadata').click();
|
||||
|
||||
validateMarketDataRow(0, 'Formerly', '076BB86A5AA41E3E');
|
||||
@@ -146,21 +125,18 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('risk model displayed', () => {
|
||||
// 6002-MDET-208
|
||||
cy.getByTestId(marketTitle).contains('Risk model').click();
|
||||
validateMarketDataRow(0, 'Tau', '0.0001140771161');
|
||||
validateMarketDataRow(1, 'Risk Aversion Parameter', '0.01');
|
||||
});
|
||||
|
||||
it('risk parameters displayed', () => {
|
||||
// 6002-MDET-209
|
||||
cy.getByTestId(marketTitle).contains('Risk parameters').click();
|
||||
validateMarketDataRow(0, 'R', '0.016');
|
||||
validateMarketDataRow(1, 'Sigma', '0.3');
|
||||
});
|
||||
|
||||
it('risk factors displayed', () => {
|
||||
// 6002-MDET-210
|
||||
cy.getByTestId(marketTitle).contains('Risk factors').click();
|
||||
|
||||
validateMarketDataRow(0, 'Short', '0.008571790367285281');
|
||||
@@ -168,7 +144,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('price monitoring bounds displayed', () => {
|
||||
// 6002-MDET-211
|
||||
cy.getByTestId(marketTitle).contains('Price monitoring bounds 1').click();
|
||||
cy.get('p.col-span-1').contains('99.99999% probability price bounds');
|
||||
cy.get('p.col-span-1').contains('Within 43,200 seconds');
|
||||
@@ -177,7 +152,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('liquidity monitoring parameters displayed', () => {
|
||||
// 6002-MDET-212
|
||||
cy.getByTestId(marketTitle)
|
||||
.contains('Liquidity monitoring parameters')
|
||||
.click();
|
||||
@@ -188,7 +162,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('liquidity displayed', () => {
|
||||
// 6002-MDET-213
|
||||
cy.getByTestId(marketTitle)
|
||||
.contains(/Liquidity(?! m)/)
|
||||
.click();
|
||||
@@ -196,14 +169,14 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
validateMarketDataRow(0, 'Target Stake', '10.00 tBTC');
|
||||
validateMarketDataRow(1, 'Supplied Stake', '0.01 tBTC');
|
||||
validateMarketDataRow(2, 'Market Value Proxy', '20.00 tBTC');
|
||||
cy.getByTestId(liquidityLink).should(
|
||||
|
||||
cy.getByTestId('view-liquidity-link').should(
|
||||
'have.text',
|
||||
'View liquidity provision table'
|
||||
);
|
||||
});
|
||||
|
||||
it('liquidity price range displayed', () => {
|
||||
// 6002-MDET-214
|
||||
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
|
||||
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
|
||||
@@ -211,8 +184,29 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
validateMarketDataRow(2, 'Highest Price', '47,049.438 BTC');
|
||||
});
|
||||
|
||||
it('oracle displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId(providerName)
|
||||
.and('contain', 'Another oracle');
|
||||
|
||||
cy.getByTestId(providerName).should('be.visible').click();
|
||||
|
||||
cy.getByTestId('dialog-content')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.getByTestId('block-explorer-link').contains('Block explorer');
|
||||
cy.getByTestId('github-link').contains('Oracle repository');
|
||||
});
|
||||
cy.getByTestId('dialog-close').click();
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId('verified-proofs')
|
||||
.and('contain', '1');
|
||||
});
|
||||
|
||||
it('proposal displayed', () => {
|
||||
// 6002-MDET-301
|
||||
cy.getByTestId(marketTitle).contains('Proposal').click();
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
import { checkSorting } from '@vegaprotocol/cypress';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const liquidityTab = 'Liquidity';
|
||||
const rowSelector =
|
||||
'[data-testid="tab-liquidity"] .ag-center-cols-container .ag-row';
|
||||
const rowSelectorLiquidityActive =
|
||||
'[data-testid="tab-active"] .ag-center-cols-container .ag-row';
|
||||
const rowSelectorLiquidityInactive =
|
||||
'[data-testid="tab-inactive"] .ag-center-cols-container .ag-row';
|
||||
const marketSummaryBlock = 'header-summary';
|
||||
const itemValue = 'item-value';
|
||||
const itemHeader = 'item-header';
|
||||
const colCommitmentAmount = '[col-id="commitmentAmount"]';
|
||||
const colAverageEntryValuation = '[col-id="averageEntryValuation"]';
|
||||
const colEquityLikeShare = '[col-id="equityLikeShare"]';
|
||||
const colFee = '[col-id="fee"]';
|
||||
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
|
||||
const colBalance = '[col-id="balance"]';
|
||||
const colStatus = '[col-id="status"]';
|
||||
const colCreatedAt = '[col-id="createdAt"] button';
|
||||
const colUpdatedAt = '[col-id="updatedAt"] button';
|
||||
|
||||
const headers = [
|
||||
'Party',
|
||||
'Commitment (tDAI)',
|
||||
'Share',
|
||||
'Proposed fee',
|
||||
'Market valuation at entry',
|
||||
'Obligation',
|
||||
'Supplied',
|
||||
'Status',
|
||||
'Created',
|
||||
'Updated',
|
||||
];
|
||||
|
||||
describe('liquidity table - trading', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockSubscription();
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@MarketData');
|
||||
cy.getByTestId(liquidityTab).click();
|
||||
cy.wait('@LiquidityProvisions');
|
||||
});
|
||||
|
||||
it('can see table headers', () => {
|
||||
// 5002-LIQP-001
|
||||
cy.getByTestId('tab-liquidity').within(($headers) => {
|
||||
cy.wrap($headers)
|
||||
.get('.ag-header-cell-text')
|
||||
.each(($header, i) => {
|
||||
cy.wrap($header).should('have.text', headers[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders liquidity table correctly', () => {
|
||||
// 5002-LIQP-002
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colCommitmentAmount)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colEquityLikeShare)
|
||||
.should('have.text', '100.00%');
|
||||
|
||||
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colAverageEntryValuation)
|
||||
.should('have.text', '685,852.93692');
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colCommitmentAmount_1)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colBalance)
|
||||
.scrollIntoView()
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelector).first().find(colStatus).should('have.text', 'Active');
|
||||
|
||||
cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty');
|
||||
cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty');
|
||||
});
|
||||
it.skip('liquidity status column should be sorted properly', () => {
|
||||
// 5002-LIQP-003
|
||||
const liquidityColDefault = ['Active', 'Pending'];
|
||||
const liquidityColAsc = ['Active', 'Pending'];
|
||||
const liquidityColDesc = ['Pending', 'Active'];
|
||||
checkSorting(
|
||||
'status',
|
||||
liquidityColDefault,
|
||||
liquidityColAsc,
|
||||
liquidityColDesc
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockSubscription();
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.visit('/#/liquidity/market-0');
|
||||
cy.wait('@LiquidityProvisions');
|
||||
});
|
||||
|
||||
it('can see header title', () => {
|
||||
// 5002-LIQP-004
|
||||
// 5002-LIQP-005
|
||||
cy.getByTestId('header-title')
|
||||
.should('contain.text', 'BTCUSD.MF21 liquidity provision')
|
||||
.and('contain.text', 'Go to trading');
|
||||
});
|
||||
|
||||
it('can see target stake', () => {
|
||||
// 5002-LIQP-006
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId('target-stake').within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Target stake');
|
||||
cy.getByTestId(itemValue).should('have.text', '10.00 tDAI').realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId('tooltip-content').should(
|
||||
'contain.text',
|
||||
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
|
||||
);
|
||||
});
|
||||
|
||||
it('can see supplied stake', () => {
|
||||
// 5002-LIQP-007
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId('supplied-stake').within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Supplied stake');
|
||||
cy.getByTestId(itemValue).should('have.text', '0.01 tDAI').realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId('tooltip-content').should(
|
||||
'contain.text',
|
||||
'The current amount of liquidity supplied for this market.'
|
||||
);
|
||||
});
|
||||
|
||||
it('can see liquidity supplied', () => {
|
||||
//// 5002-LIQP-008
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId('liquidity-supplied').within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
|
||||
cy.getByTestId('indicator').should('be.visible');
|
||||
cy.getByTestId(itemValue).should('have.text', '0.10%').realHover();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can see market id', () => {
|
||||
// 5002-LIQP-009
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId('liquidity-market-id').within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Market ID');
|
||||
cy.getByTestId(itemValue).should('have.text', 'market-0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can see market id', () => {
|
||||
// 5002-LIQP-010
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId('liquidity-learn-more').within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Learn more');
|
||||
cy.getByTestId(itemValue).should('have.text', 'Providing liquidity');
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
'https://docs.vega.xyz/testnet/concepts/liquidity/provision'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('liquidity table view', { tags: '@smoke' }, () => {
|
||||
it('can see table headers', () => {
|
||||
cy.getByTestId('tab-active').within(($headers) => {
|
||||
cy.wrap($headers)
|
||||
.get('.ag-header-cell-text')
|
||||
.each(($header, i) => {
|
||||
cy.wrap($header).should('have.text', headers[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders liquidity active table correctly', () => {
|
||||
// 5002-LIQP-011
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colCommitmentAmount)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colEquityLikeShare)
|
||||
.should('have.text', '100.00%');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colFee)
|
||||
.should('have.text', '0.09%');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colAverageEntryValuation)
|
||||
.should('have.text', '685,852.93692');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colCommitmentAmount_1)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colBalance)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colStatus)
|
||||
.should('have.text', 'Active');
|
||||
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colCreatedAt)
|
||||
.should('not.be.empty');
|
||||
cy.get(rowSelectorLiquidityActive)
|
||||
.first()
|
||||
.find(colUpdatedAt)
|
||||
.should('not.be.empty');
|
||||
});
|
||||
|
||||
it('renders liquidity inactive table correctly', () => {
|
||||
//// 5002-LIQP-012
|
||||
cy.getByTestId('Inactive').click();
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find('[col-id="party.id"]')
|
||||
.should(
|
||||
'have.text',
|
||||
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
|
||||
);
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colCommitmentAmount)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colEquityLikeShare)
|
||||
.should('have.text', '100.00%');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colFee)
|
||||
.should('have.text', '0.40%');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colAverageEntryValuation)
|
||||
.should('have.text', '685,852.93692');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colCommitmentAmount_1)
|
||||
.should('have.text', '4,000.00');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colBalance)
|
||||
.should('have.text', '2,000.00');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colStatus)
|
||||
.should('have.text', 'Pending');
|
||||
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colCreatedAt)
|
||||
.should('not.be.empty');
|
||||
cy.get(rowSelectorLiquidityInactive)
|
||||
.first()
|
||||
.find(colUpdatedAt)
|
||||
.should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,6 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
cy.wait('@MarketsCandles');
|
||||
});
|
||||
|
||||
// 6001-MARK-066
|
||||
it('can toggle the sidebar', () => {
|
||||
cy.getByTestId('market-selector').should('be.visible');
|
||||
cy.getByTestId('sidebar-toggle').click();
|
||||
@@ -68,7 +67,6 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
.each((item, i) => {
|
||||
const market = data[i];
|
||||
// 6001-MARK-021
|
||||
// 6001-MARK-022
|
||||
expect(item.find('h3').text()).equals(market.code);
|
||||
expect(
|
||||
item.find('[data-testid="market-selector-data-row"]').eq(0).text()
|
||||
@@ -86,19 +84,8 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can see all markets link', () => {
|
||||
// 6001-MARK-026
|
||||
cy.getByTestId('market-selector').within(() => {
|
||||
cy.getByTestId('all-markets-link')
|
||||
.should('be.visible')
|
||||
.and('have.text', 'All markets')
|
||||
.and('have.attr', 'href')
|
||||
.and('contain', '#/markets/all');
|
||||
});
|
||||
});
|
||||
|
||||
// 6001-MARK-27
|
||||
it('can use the filter options', () => {
|
||||
// 6001-MARK-027
|
||||
// product type
|
||||
cy.getByTestId('product-Spot').click();
|
||||
cy.getByTestId(list).contains('Spot markets coming soon.');
|
||||
@@ -107,7 +94,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('product-Future').click();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
|
||||
// 6001-MARK-029
|
||||
// 6001-MARK-29
|
||||
cy.getByTestId(searchInput).clear().type('btc');
|
||||
cy.getByTestId(list).find('a').should('have.length', 2);
|
||||
cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21');
|
||||
@@ -116,29 +103,4 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(searchInput).clear();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
});
|
||||
|
||||
it('can sort by by top gaining and top losing market', () => {
|
||||
// 6001-MARK-030
|
||||
// 6001-MARK-031
|
||||
// 6001-MARK-032
|
||||
// 6001-MARK-033
|
||||
cy.getByTestId(' sort-trigger').click();
|
||||
cy.getByTestId('sort-item-Gained')
|
||||
.contains('Top gaining')
|
||||
.should('be.visible');
|
||||
cy.getByTestId('sort-item-Lost')
|
||||
.contains('Top losing')
|
||||
.should('be.visible');
|
||||
cy.getByTestId('sort-item-New')
|
||||
.contains('New markets')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('can filter by settlement asset', () => {
|
||||
// 6001-MARK-028
|
||||
cy.getByTestId('asset-trigger').click();
|
||||
cy.getByTestId('asset-id-asset-3').contains('tBTC').click();
|
||||
cy.getByTestId(list).find('a').should('have.length', 1);
|
||||
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const expirtyTooltip = 'expiry-tooltip';
|
||||
const externalLink = 'external-link';
|
||||
const itemHeader = 'item-header';
|
||||
const itemValue = 'item-value';
|
||||
const link = 'link';
|
||||
const liquidityLink = 'view-liquidity-link';
|
||||
const liquiditySupplied = 'liquidity-supplied';
|
||||
const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip';
|
||||
const marketChange = 'market-change';
|
||||
const marketExpiry = 'market-expiry';
|
||||
const marketMode = 'market-trading-mode';
|
||||
const marketName = 'header-title';
|
||||
const marketPrice = 'market-price';
|
||||
const marketSettlement = 'market-settlement-asset';
|
||||
const marketState = 'market-state';
|
||||
const marketSummaryBlock = 'header-summary';
|
||||
const marketExpiry = 'market-expiry';
|
||||
const marketPrice = 'market-price';
|
||||
const marketChange = 'market-change';
|
||||
const marketVolume = 'market-volume';
|
||||
const marketMode = 'market-trading-mode';
|
||||
const marketState = 'market-state';
|
||||
const marketSettlement = 'market-settlement-asset';
|
||||
const percentageValue = 'price-change-percentage';
|
||||
const priceChangeValue = 'price-change';
|
||||
const tradingModeTooltip = 'trading-mode-tooltip';
|
||||
const itemHeader = 'item-header';
|
||||
const itemValue = 'item-value';
|
||||
|
||||
describe('Market trading page', () => {
|
||||
before(() => {
|
||||
@@ -39,12 +31,10 @@ describe('Market trading page', () => {
|
||||
// 7002-SORD-001
|
||||
// 7002-SORD-002
|
||||
it('must display market name', () => {
|
||||
// 6002-MDET-001
|
||||
cy.getByTestId(marketName).should('not.be.empty');
|
||||
cy.getByTestId('header-title').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('must see market expiry', () => {
|
||||
// 6002-MDET-002
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketExpiry).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Expiry');
|
||||
@@ -54,7 +44,6 @@ describe('Market trading page', () => {
|
||||
});
|
||||
|
||||
it('must see market price', () => {
|
||||
// 6002-MDET-003
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketPrice).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Price');
|
||||
@@ -64,7 +53,6 @@ describe('Market trading page', () => {
|
||||
});
|
||||
|
||||
it('must see market change', () => {
|
||||
// 6002-MDET-004
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketChange).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Change (24h)');
|
||||
@@ -75,7 +63,6 @@ describe('Market trading page', () => {
|
||||
});
|
||||
|
||||
it('must see market volume', () => {
|
||||
// 6002-MDET-005
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketVolume).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)');
|
||||
@@ -85,21 +72,16 @@ describe('Market trading page', () => {
|
||||
});
|
||||
|
||||
it('must see market mode', () => {
|
||||
// 6002-MDET-006
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketMode).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
|
||||
cy.getByTestId(itemValue).should(
|
||||
'have.text',
|
||||
'Monitoring auction - liquidity (target not met)'
|
||||
);
|
||||
cy.getByTestId(itemValue).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('must see market status', () => {
|
||||
// 6002-MDET-007
|
||||
// 7002-SORD-061
|
||||
it('must see market state', () => {
|
||||
//7002-SORD-061
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketState).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Status');
|
||||
@@ -109,7 +91,6 @@ describe('Market trading page', () => {
|
||||
});
|
||||
|
||||
it('must see market settlement', () => {
|
||||
// 6002-MDET-008
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketSettlement).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Settlement asset');
|
||||
@@ -117,12 +98,15 @@ describe('Market trading page', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
it('must see market liquidity supplied', () => {
|
||||
// 6002-MDET-009
|
||||
|
||||
it('must see market mode', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(liquiditySupplied).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
|
||||
cy.getByTestId(itemValue).should('not.be.empty');
|
||||
cy.getByTestId(marketMode).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
|
||||
cy.getByTestId(itemValue).should(
|
||||
'have.text',
|
||||
'Monitoring auction - liquidity (target not met)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -137,14 +121,14 @@ describe('Market trading page', () => {
|
||||
.realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(expirtyTooltip)
|
||||
cy.getByTestId('expiry-tooltip')
|
||||
.eq(0)
|
||||
.should(
|
||||
'contain.text',
|
||||
'This market expires when triggered by its oracle, not on a set date.'
|
||||
)
|
||||
.within(() => {
|
||||
cy.getByTestId(link)
|
||||
cy.getByTestId('link')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', Cypress.env('EXPLORER_URL'));
|
||||
});
|
||||
@@ -170,14 +154,15 @@ describe('Market trading page', () => {
|
||||
.realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(tradingModeTooltip)
|
||||
|
||||
cy.getByTestId('trading-mode-tooltip')
|
||||
.should(
|
||||
'contain.text',
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.getByTestId(externalLink)
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', Cypress.env('TRADING_MODE_LINK'));
|
||||
|
||||
@@ -189,27 +174,5 @@ describe('Market trading page', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should see liquidity supplied tooltip', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(liquiditySupplied).within(() => {
|
||||
cy.getByTestId(itemValue).realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(liquiditySuppliedTooltip)
|
||||
.should('contain.text', 'Supplied stake')
|
||||
.and('contain.text', 'Target stake')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId(liquidityLink).should(
|
||||
'have.text',
|
||||
'View liquidity provision table'
|
||||
);
|
||||
cy.getByTestId(externalLink).should(
|
||||
'have.text',
|
||||
'Learn about providing liquidity'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
|
||||
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
|
||||
import { checkSorting } from '@vegaprotocol/cypress';
|
||||
|
||||
const rowSelector =
|
||||
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
|
||||
const colMarketId = '[col-id="market"]';
|
||||
|
||||
describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage().then(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('can see table headers', () => {
|
||||
@@ -35,7 +35,10 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
|
||||
it('renders markets correctly', () => {
|
||||
// 6001-MARK-049
|
||||
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="market"]')
|
||||
.should('have.text', 'ETHUSD');
|
||||
|
||||
// 6001-MARK-050
|
||||
cy.get(rowSelector)
|
||||
@@ -116,7 +119,6 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
);
|
||||
});
|
||||
it('proposed markets tab should be sorted properly', () => {
|
||||
// 6001-MARK-062
|
||||
cy.get('[data-testid="Proposed markets"]').click({ force: true });
|
||||
const marketColDefault = [
|
||||
'ETHUSD',
|
||||
@@ -194,31 +196,4 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
];
|
||||
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
|
||||
});
|
||||
|
||||
it('can drag and drop columns', () => {
|
||||
// 6001-MARK-063
|
||||
cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp();
|
||||
cy.get(colMarketId).should(($element) => {
|
||||
const attributeValue = $element.attr('aria-colindex');
|
||||
expect(attributeValue).not.to.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
const proposal: ProposalsListQuery = {};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ProposalsList', proposal);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
});
|
||||
|
||||
it('can see no markets message', () => {
|
||||
// 6001-MARK-061
|
||||
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
|
||||
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
|
||||
const oracleBannerStatus = 'oracle-banner-status';
|
||||
const oracleFullProfile = 'oracle-full-profile';
|
||||
|
||||
describe('oracle information', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage(
|
||||
MarketState.STATE_ACTIVE,
|
||||
undefined,
|
||||
undefined,
|
||||
'COMPROMISED'
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('show oracle banner', () => {
|
||||
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
|
||||
cy.getByTestId(oracleBannerDialogTrigger)
|
||||
.should('contain.text', 'Show more')
|
||||
.click();
|
||||
cy.getByTestId(oracleFullProfile).should('exist');
|
||||
});
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
const orderbookTab = 'Orderbook';
|
||||
const orderbookTable = 'tab-orderbook';
|
||||
const askPrice = 'price-9894585';
|
||||
const bidPrice = 'price-9889001';
|
||||
const askVolume = 'ask-vol-9894585';
|
||||
const bidVolume = 'bid-vol-9889001';
|
||||
const askCumulative = 'cumulative-vol-9894585';
|
||||
const bidCumulative = 'cumulative-vol-9889001';
|
||||
const midPrice = 'middle-mark-price-4612690000';
|
||||
const priceResolution = 'resolution';
|
||||
const dealTicketPrice = 'order-price';
|
||||
const resPrice = 'price-990';
|
||||
|
||||
describe('order book', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
});
|
||||
|
||||
it('show order book', () => {
|
||||
// 6003-ORDB-001
|
||||
// 6003-ORDB-002
|
||||
cy.getByTestId(orderbookTab).click();
|
||||
cy.getByTestId(orderbookTable).should('be.visible');
|
||||
cy.getByTestId(orderbookTable).should('not.be.empty');
|
||||
});
|
||||
|
||||
it('show orders prices', () => {
|
||||
// 6003-ORDB-003
|
||||
cy.getByTestId(askPrice).should('have.text', '98.94585');
|
||||
cy.getByTestId(bidPrice).should('have.text', '98.89001');
|
||||
});
|
||||
|
||||
it('show prices volumes', () => {
|
||||
// 6003-ORDB-004
|
||||
cy.getByTestId(askVolume).should('have.text', '1');
|
||||
cy.getByTestId(bidVolume).should('have.text', '1');
|
||||
});
|
||||
|
||||
it('show prices cumulative volumes', () => {
|
||||
// 6003-ORDB-005
|
||||
cy.getByTestId(askCumulative).should('have.text', '39');
|
||||
cy.getByTestId(bidCumulative).should('have.text', '7');
|
||||
});
|
||||
|
||||
it('show mid price', () => {
|
||||
// 6003-ORDB-006
|
||||
cy.getByTestId(midPrice).should('have.text', '46,126.90');
|
||||
});
|
||||
|
||||
it('sort prices descending', () => {
|
||||
// 6003-ORDB-007
|
||||
const prices: number[] = [];
|
||||
cy.getByTestId(orderbookTable).within(() => {
|
||||
cy.get('[data-testid*=price]')
|
||||
.each(($el) => {
|
||||
prices.push(Number($el.text()));
|
||||
})
|
||||
.then(() => {
|
||||
expect(prices).to.deep.equal(prices.sort((a, b) => b - a));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('copy price to deal ticket form', () => {
|
||||
// 6003-ORDB-009
|
||||
cy.getByTestId(askPrice).click();
|
||||
cy.getByTestId(dealTicketPrice).should('have.value', '98.94585');
|
||||
});
|
||||
|
||||
it('change price resolution', () => {
|
||||
// 6003-ORDB-008
|
||||
const resolutions = [
|
||||
'0.00000',
|
||||
'0.0000',
|
||||
'0.000',
|
||||
'0.00',
|
||||
'0.0',
|
||||
'0',
|
||||
'10',
|
||||
'100',
|
||||
'1,000',
|
||||
'10,000',
|
||||
];
|
||||
cy.getByTestId(priceResolution)
|
||||
.find('option')
|
||||
.each(($el, index) => {
|
||||
expect($el.text()).to.equal(resolutions[index]);
|
||||
});
|
||||
|
||||
cy.getByTestId(priceResolution).select('0.0');
|
||||
cy.getByTestId(resPrice).should('have.text', '99.0');
|
||||
cy.getByTestId(askPrice).should('not.exist');
|
||||
cy.getByTestId(bidPrice).should('not.exist');
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,6 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
// 7001-COLL-006
|
||||
// 7001-COLL-007
|
||||
// 1003-TRAN-001
|
||||
// 7001-COLL-012
|
||||
|
||||
const tradingAccountRowId = '[row-id="t-0"]';
|
||||
cy.getByTestId('Collateral').click();
|
||||
@@ -35,23 +34,28 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="used"]')
|
||||
.should('have.text', '1.01' + '1.00%');
|
||||
.should('have.text', '1.010.00%');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="available"]')
|
||||
.should('have.text', '100.00');
|
||||
.should('have.text', '100,000.00');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="total"]')
|
||||
.should('have.text', '101.01');
|
||||
.should('have.text', '100,001.01');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="accounts-actions"]')
|
||||
.should('have.text', '');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="total"]')
|
||||
.should('have.text', '100,001.01');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.get('[col-id="accounts-actions"]')
|
||||
.find('[data-testid="dropdown-menu"]')
|
||||
@@ -97,7 +101,7 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
'Liquidity provision fee reward account balance',
|
||||
'Market proposer reward account balance',
|
||||
];
|
||||
cy.get('[col-id="asset.symbol"]').contains('tEURO').click();
|
||||
cy.getByTestId('asset').contains('tEURO').click();
|
||||
cy.get('[data-testid$="_label"]').should('have.length', 16);
|
||||
cy.get('[data-testid$="_label"]').each((element, index) => {
|
||||
cy.wrap(element).should('have.text', titles[index]);
|
||||
@@ -108,8 +112,8 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
|
||||
it('should open usage breakdown dialog when clicked on used', () => {
|
||||
// 7001-COLL-009
|
||||
cy.get('[col-id="used"]').contains('1.01').click();
|
||||
const headers = ['Market', 'Account type', 'Balance', 'Margin health'];
|
||||
cy.getByTestId('breakdown').contains('1.01').click();
|
||||
const headers = ['Market', 'Account type', 'Balance'];
|
||||
cy.getByTestId('usage-breakdown').within(($headers) => {
|
||||
cy.wrap($headers)
|
||||
.get('.ag-header-cell-text')
|
||||
@@ -129,12 +133,11 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
// 7001-COLL-010
|
||||
it('sorting by asset', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
const marketsSortedDefault = ['tBTC', 'tEURO', 'tDAI', 'tBTC'];
|
||||
const marketsSortedAsc = ['tBTC', 'tBTC', 'tDAI', 'tEURO'];
|
||||
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
|
||||
const marketsSortedDesc = ['tEURO', 'tDAI', 'tBTC', 'tBTC'];
|
||||
checkSorting(
|
||||
'asset.symbol',
|
||||
marketsSortedDefault,
|
||||
@@ -146,14 +149,23 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
it('sorting by total', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
const marketsSortedDefault = [
|
||||
'1,000.00',
|
||||
'1,000.00002',
|
||||
'1,000.01',
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
'1,000.00002',
|
||||
'1,000.01',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'1,000.01',
|
||||
'1,000.00002',
|
||||
'1,000.00001',
|
||||
'1,000.00',
|
||||
];
|
||||
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
|
||||
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
|
||||
|
||||
checkSorting(
|
||||
'total',
|
||||
marketsSortedDefault,
|
||||
@@ -164,22 +176,24 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
|
||||
it('sorting by used', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
// concat actual value with percentage value
|
||||
// as cypress will pick up the entire cell contes
|
||||
// textContent
|
||||
const marketsSortedDefault = [
|
||||
'0.00' + '0.00%',
|
||||
'0.01' + '0.00%',
|
||||
'0.00' + '0.00%',
|
||||
'0.00' + '0.00%',
|
||||
'0.000.00%',
|
||||
'0.010.00%',
|
||||
'0.000.00%',
|
||||
'0.000.00%',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'0.00' + '0.00%',
|
||||
'0.00' + '0.00%',
|
||||
'0.00' + '0.00%',
|
||||
'0.01' + '0.00%',
|
||||
'0.000.00%',
|
||||
'0.000.00%',
|
||||
'0.000.00%',
|
||||
'0.010.00%',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'0.010.00%',
|
||||
'0.000.00%',
|
||||
'0.000.00%',
|
||||
'0.000.00%',
|
||||
];
|
||||
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
|
||||
checkSorting(
|
||||
'used',
|
||||
marketsSortedDefault,
|
||||
@@ -191,13 +205,23 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
it('sorting by total', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
const marketsSortedDefault = [
|
||||
'1,000.00',
|
||||
'1,000.00002',
|
||||
'1,000.01',
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'1,000.00',
|
||||
'1,000.00001',
|
||||
'1,000.00002',
|
||||
'1,000.01',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'1,000.01',
|
||||
'1,000.00002',
|
||||
'1,000.00001',
|
||||
'1,000.00',
|
||||
];
|
||||
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
|
||||
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
|
||||
|
||||
checkSorting(
|
||||
'total',
|
||||
|
||||
@@ -81,7 +81,7 @@ describe(
|
||||
cy.visit('/#/markets/market-0');
|
||||
});
|
||||
it('must display that market is not accepting orders', function () {
|
||||
cy.getByTestId('deal-ticket-error-message-summary').should(
|
||||
cy.getByTestId('dealticket-error-message-summary').should(
|
||||
'have.text',
|
||||
`This market is ${marketState
|
||||
.split('_')
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
|
||||
cy.getByTestId('deal-ticket-error-message-expiry').should(
|
||||
cy.getByTestId('dealticket-error-message-expiry').should(
|
||||
'have.text',
|
||||
'The expiry date that you have entered appears to be in the past'
|
||||
);
|
||||
@@ -57,7 +57,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||
cy.getByTestId('deal-ticket-error-message-price-limit').should(
|
||||
cy.getByTestId('dealticket-error-message-price-limit').should(
|
||||
'have.text',
|
||||
'Price accepts up to 5 decimal places'
|
||||
);
|
||||
@@ -79,7 +79,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(orderSizeField).clear().type('1.234');
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('deal-ticket-error-message-size-market').should(
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size must be whole numbers for this market'
|
||||
);
|
||||
@@ -88,7 +88,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
it('must warn if order size is set to 0', function () {
|
||||
cy.getByTestId(orderSizeField).clear().type('0');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('deal-ticket-error-message-size-market').should(
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size cannot be lower than 1'
|
||||
);
|
||||
@@ -96,29 +96,15 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
|
||||
it('must have total margin available', () => {
|
||||
// 7001-COLL-011
|
||||
cy.getByTestId('deal-ticket-fee-total-margin-available').within(() => {
|
||||
cy.get('[data-state="closed"]').should(
|
||||
'have.text',
|
||||
'Total margin available100.01 tDAI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('must have current margin allocation', () => {
|
||||
cy.getByTestId('deal-ticket-fee-current-margin-allocation').within(() => {
|
||||
cy.get('[data-state="closed"]:first').should(
|
||||
'have.text',
|
||||
'Current margin allocation'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should open usage breakdown dialog when clicked on current margin allocation', () => {
|
||||
cy.getByTestId('deal-ticket-fee-current-margin-allocation').within(() => {
|
||||
cy.get('button').click();
|
||||
});
|
||||
cy.getByTestId('usage-breakdown').should('exist');
|
||||
cy.getByTestId('dialog-close').click();
|
||||
cy.getByTestId('tab-ticket')
|
||||
.find('.text-xs')
|
||||
.eq(5)
|
||||
.within(() => {
|
||||
cy.get('[data-state="closed"]').should(
|
||||
'have.text',
|
||||
'Total margin available100,000.01 tDAI'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,15 +23,10 @@ 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
|
||||
cy.getByTestId('deal-ticket-error-message-zero-balance').should(
|
||||
cy.getByTestId('dealticket-error-message-zero-balance').should(
|
||||
'have.text',
|
||||
'You need ' +
|
||||
'tDAI' +
|
||||
@@ -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);
|
||||
});
|
||||
@@ -65,11 +54,11 @@ describe(
|
||||
// 7002-SORD-003
|
||||
|
||||
// warning should show immediately
|
||||
cy.getByTestId('deal-ticket-warning-margin').should(
|
||||
cy.getByTestId('dealticket-warning-margin').should(
|
||||
'contain.text',
|
||||
'You may not have enough margin available to open this position'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-warning-margin').should(
|
||||
cy.getByTestId('dealticket-warning-margin').should(
|
||||
'contain.text',
|
||||
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId('deal-ticket-error-message-type').should(
|
||||
cy.getByTestId('dealticket-error-message-type').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
|
||||
);
|
||||
@@ -48,7 +48,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
cy.getByTestId(orderPriceField).clear().type('0.1');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('deal-ticket-warning-auction').should(
|
||||
cy.getByTestId('dealticket-warning-auction').should(
|
||||
'have.text',
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
);
|
||||
@@ -60,7 +60,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
TIFlist.filter((item) => item.code === 'FOK')[0].value
|
||||
);
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('deal-ticket-error-message-tif').should(
|
||||
cy.getByTestId('dealticket-error-message-tif').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
|
||||
);
|
||||
|
||||
@@ -3,18 +3,6 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { marketsDataQuery } from '@vegaprotocol/mock';
|
||||
import { positionsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
// #region consts
|
||||
const closePosition = 'close-position';
|
||||
const dialogCloseX = 'dialog-close';
|
||||
const dialogContent = 'dialog-content';
|
||||
const dropDownMenu = 'dropdown-menu';
|
||||
const marketActionsContent = 'market-actions-content';
|
||||
const positions = 'Positions';
|
||||
const tabPositions = 'tab-positions';
|
||||
const toastContent = 'toast-content';
|
||||
const tooltipContent = 'tooltip-content';
|
||||
// #endregion
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
@@ -23,9 +11,8 @@ beforeEach(() => {
|
||||
|
||||
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
|
||||
it('renders positions on trading page', () => {
|
||||
visitAndClickPositions();
|
||||
// 7004-POSI-001
|
||||
// 7004-POSI-002
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('Positions').click();
|
||||
validatePositionsDisplayed();
|
||||
});
|
||||
|
||||
@@ -48,306 +35,174 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
|
||||
}
|
||||
aliasGQLQuery(req, 'Positions', positions);
|
||||
});
|
||||
visitAndClickPositions();
|
||||
// 7004-POSI-001
|
||||
// 7004-POSI-002
|
||||
cy.visit('/#/portfolio');
|
||||
cy.getByTestId('Positions').click();
|
||||
validatePositionsDisplayed(true);
|
||||
});
|
||||
|
||||
it('Close my position', () => {
|
||||
visitAndClickPositions();
|
||||
cy.getByTestId(closePosition).first().click();
|
||||
// 7004-POSI-010
|
||||
cy.getByTestId(toastContent).should(
|
||||
'contain.text',
|
||||
'Awaiting confirmation'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('positions', { tags: '@regression', testIsolation: true }, () => {
|
||||
it('rows should be displayed despite errors', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['marketsConnection', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
describe('renders position among some graphql errors', () => {
|
||||
it('rows should be displayed despite errors', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['marketsConnection', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const marketData = marketsDataQuery();
|
||||
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||
const replace =
|
||||
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||
return { ...market, node: { ...market.node, data: replace } };
|
||||
});
|
||||
const overrides = {
|
||||
...marketData,
|
||||
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
const emptyCells = [
|
||||
'notional',
|
||||
'markPrice',
|
||||
'currentLeverage',
|
||||
'averageEntryPrice',
|
||||
];
|
||||
cy.getByTestId(tabPositions)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(
|
||||
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
|
||||
)
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
emptyCells.forEach((cell) => {
|
||||
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
|
||||
];
|
||||
const marketData = marketsDataQuery();
|
||||
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||
const replace =
|
||||
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||
return { ...market, node: { ...market.node, data: replace } };
|
||||
});
|
||||
const overrides = {
|
||||
...marketData,
|
||||
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
const emptyCells = [
|
||||
'notional',
|
||||
'markPrice',
|
||||
'currentLeverage',
|
||||
'averageEntryPrice',
|
||||
];
|
||||
cy.getByTestId('tab-positions')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(
|
||||
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
|
||||
)
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
emptyCells.forEach((cell) => {
|
||||
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('error message should be displayed', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['assets', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
});
|
||||
});
|
||||
it('error message should be displayed', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['assets', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const overrides = {
|
||||
marketsConnection: { edges: [] },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(tabPositions).contains('no market data');
|
||||
});
|
||||
|
||||
it('sorting by Market', () => {
|
||||
visitAndClickPositions();
|
||||
const marketsSortedDefault = [
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'SUSPENDED MARKET',
|
||||
'ETHBTC Quarterly (30 Jun 2022)',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ACTIVE MARKET',
|
||||
];
|
||||
cy.getByTestId(positions).click();
|
||||
// 7004-POSI-003
|
||||
checkSorting(
|
||||
'marketName',
|
||||
marketsSortedDefault,
|
||||
marketsSortedAsc,
|
||||
marketsSortedDesc
|
||||
);
|
||||
});
|
||||
|
||||
it('Resize column', () => {
|
||||
let elementWidth: number;
|
||||
visitAndClickPositions();
|
||||
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.find('.ag-header-cell-resize')
|
||||
.realMouseDown()
|
||||
.realMouseMove(250, 0)
|
||||
.realMouseUp();
|
||||
});
|
||||
|
||||
// 7004-POSI-006
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.invoke('width')
|
||||
.should('be.greaterThan', 250);
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.invoke('width')
|
||||
.then((width) => {
|
||||
elementWidth = width as number;
|
||||
})
|
||||
.then(() => {
|
||||
let localStorageCopy: Record<string, string>;
|
||||
cy.window().then((win) => {
|
||||
localStorageCopy = { ...win.localStorage };
|
||||
});
|
||||
|
||||
cy.reload();
|
||||
cy.window().then((win) => {
|
||||
Object.keys(localStorageCopy).forEach((key) => {
|
||||
win.localStorage.setItem(key, localStorageCopy[key]);
|
||||
});
|
||||
});
|
||||
|
||||
// 7004-POSI-012
|
||||
cy.get('[col-id="marketName"]')
|
||||
.invoke('width')
|
||||
.should('equal', elementWidth);
|
||||
];
|
||||
const overrides = {
|
||||
marketsConnection: { edges: [] },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
});
|
||||
|
||||
it('Scroll horizontally', () => {
|
||||
visitAndClickPositions();
|
||||
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.find('.ag-header-cell-resize')
|
||||
.realMouseDown()
|
||||
.realMouseMove(400, 0)
|
||||
.realMouseUp();
|
||||
});
|
||||
cy.get('[col-id="marketName"]').should('be.visible');
|
||||
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
|
||||
deltaX: 500,
|
||||
});
|
||||
// 7004-POSI-004
|
||||
cy.get('[col-id="updatedAt"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('Drag and drop columns', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
|
||||
cy.get('[col-id="marketName"]')
|
||||
.realMouseDown()
|
||||
.realMouseMove(700, 15)
|
||||
.realMouseUp();
|
||||
|
||||
// 7004-POSI-005
|
||||
cy.get('[col-id="marketName"]').should(($element) => {
|
||||
const attributeValue = $element.attr('aria-colindex');
|
||||
expect(attributeValue).not.to.equal('1');
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.get('[data-testid="tab-positions"]').contains('no market data');
|
||||
});
|
||||
});
|
||||
|
||||
it('I can see warnings', () => {
|
||||
visitAndClickPositions();
|
||||
|
||||
cy.get('[col-id="openVolume"]').within(() => {
|
||||
cy.get('[aria-label="warning-sign icon"]')
|
||||
.should('be.visible')
|
||||
.realHover();
|
||||
});
|
||||
// 7004-POSI-011
|
||||
cy.getByTestId(tooltipContent).should('be.visible');
|
||||
});
|
||||
|
||||
it('Positive and Negative color change', () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(positions).click();
|
||||
// 7004-POSI-007
|
||||
cy.get('.ag-center-cols-container').within(() => {
|
||||
assertPNLColor(
|
||||
'[col-id="realisedPNL"]',
|
||||
'text-vega-green',
|
||||
'text-vega-pink'
|
||||
describe('sorting by ag-grid columns should work well', () => {
|
||||
it('sorting by Market', () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
const marketsSortedDefault = [
|
||||
'ACTIVE MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'SUSPENDED MARKET',
|
||||
];
|
||||
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
|
||||
const marketsSortedDesc = [
|
||||
'SUSPENDED MARKET',
|
||||
'Apple Monthly (30 Jun 2022)',
|
||||
'ACTIVE MARKET',
|
||||
];
|
||||
cy.getByTestId('Positions').click();
|
||||
checkSorting(
|
||||
'marketName',
|
||||
marketsSortedDefault,
|
||||
marketsSortedAsc,
|
||||
marketsSortedDesc
|
||||
);
|
||||
});
|
||||
cy.get('.ag-center-cols-container').within(() => {
|
||||
assertPNLColor(
|
||||
'[col-id="unrealisedPNL"]',
|
||||
'text-vega-green',
|
||||
'text-vega-pink'
|
||||
it('sorting by notional', () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
const marketsSortedDefault = [
|
||||
'276,761.40348',
|
||||
'46,126.90058',
|
||||
'1,688.20',
|
||||
];
|
||||
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
|
||||
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
|
||||
cy.getByTestId('Positions').click();
|
||||
checkSorting(
|
||||
'notional',
|
||||
marketsSortedDefault,
|
||||
marketsSortedAsc,
|
||||
marketsSortedDesc
|
||||
);
|
||||
});
|
||||
cy.get('.ag-center-cols-container').within(() => {
|
||||
assertPNLColor(
|
||||
'[col-id="openVolume"]',
|
||||
'text-vega-green',
|
||||
'text-vega-pink'
|
||||
it('sorting by unrealisedPNL', () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
|
||||
const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
|
||||
const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
|
||||
cy.getByTestId('Positions').click();
|
||||
checkSorting(
|
||||
'unrealisedPNL',
|
||||
marketsSortedDefault,
|
||||
marketsSortedAsc,
|
||||
marketsSortedDesc
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('View settlement asset', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('[col-id="asset"]').within(() => {
|
||||
cy.get('button[type="button"]').first().click();
|
||||
});
|
||||
// 7004-POSI-008
|
||||
cy.getByTestId(dialogContent).should('be.visible');
|
||||
cy.getByTestId(dialogCloseX).click();
|
||||
cy.getByTestId(dropDownMenu).first().click();
|
||||
cy.getByTestId(marketActionsContent).click();
|
||||
// 7004-POSI-009
|
||||
cy.getByTestId(dialogContent).should('be.visible');
|
||||
});
|
||||
});
|
||||
function validatePositionsDisplayed(multiKey = false) {
|
||||
cy.getByTestId('tab-positions').should('be.visible');
|
||||
cy.getByTestId('tab-positions')
|
||||
.get('.ag-center-cols-container .ag-row')
|
||||
.first()
|
||||
.within(() => {
|
||||
function validatePositionsDisplayed(multiKey = false) {
|
||||
cy.getByTestId('tab-positions').should('be.visible');
|
||||
cy.getByTestId('tab-positions').within(() => {
|
||||
cy.get('[col-id="marketName"]')
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
.each(($marketSymbol) => {
|
||||
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get('[col-id="openVolume"]').should('not.be.empty');
|
||||
cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
|
||||
($openVolume) => {
|
||||
cy.wrap($openVolume).invoke('text').should('not.be.empty');
|
||||
}
|
||||
);
|
||||
|
||||
// includes average entry price, mark price, realised PNL & leverage
|
||||
cy.getByTestId('flash-cell').should('not.be.empty');
|
||||
cy.getByTestId('flash-cell').each(($prices) => {
|
||||
cy.wrap($prices).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
if (!multiKey) {
|
||||
cy.get('[col-id="currentLeverage"]').should('contain.text', '2,767.3');
|
||||
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
|
||||
cy.get('[col-id="marginAccountBalance"]') // margin allocated
|
||||
.should('contain.text', '0.01');
|
||||
}
|
||||
|
||||
cy.get('[col-id="unrealisedPNL"]').should('not.be.empty');
|
||||
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
|
||||
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
|
||||
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
|
||||
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
|
||||
|
||||
cy.get('.ag-header-row [col-id="notional"]')
|
||||
.should('contain.text', 'Notional')
|
||||
.realHover();
|
||||
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
|
||||
});
|
||||
|
||||
cy.get('.ag-header-row [col-id="notional"]')
|
||||
.should('contain.text', 'Notional')
|
||||
.realHover();
|
||||
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
|
||||
|
||||
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
|
||||
}
|
||||
function assertPNLColor(
|
||||
pnlSelector: string,
|
||||
positiveClass: string,
|
||||
negativeClass: string
|
||||
) {
|
||||
cy.get(pnlSelector).each(($el) => {
|
||||
const value = parseFloat($el.text());
|
||||
|
||||
if (value > 0) {
|
||||
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
|
||||
} else if (value < 0) {
|
||||
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
|
||||
} else if (value == 0) {
|
||||
cy.wrap($el)
|
||||
.invoke('attr', 'class')
|
||||
.should('not.contain', negativeClass, positiveClass);
|
||||
} else {
|
||||
throw new Error('Unexpected value');
|
||||
}
|
||||
});
|
||||
}
|
||||
function visitAndClickPositions() {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(positions).click();
|
||||
}
|
||||
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
protocolUpgradeProposalsQuery,
|
||||
blockStatisticsQuery,
|
||||
networkParamQuery,
|
||||
liquidityProvisionsQuery,
|
||||
liquidityProviderFeeShareQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
|
||||
@@ -160,12 +158,6 @@ const mockTradingPage = (
|
||||
);
|
||||
aliasGQLQuery(req, 'Trades', tradesQuery());
|
||||
aliasGQLQuery(req, 'Chart', chartQuery());
|
||||
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'LiquidityProviderFeeShare',
|
||||
liquidityProviderFeeShareQuery
|
||||
);
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
|
||||
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
|
||||
|
||||
@@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
|
||||
# TAG name of the current app version - TODO: bump to the latest upon release
|
||||
NX_APP_VERSION=v0.20.18-core-0.71.8
|
||||
NX_APP_VERSION=v0.20.16-core-0.71.5
|
||||
|
||||
@@ -150,7 +150,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
<HeaderStat
|
||||
heading={t('Target stake')}
|
||||
description={tooltipMapping['targetStake']}
|
||||
testId="target-stake"
|
||||
>
|
||||
<div>
|
||||
{targetStake
|
||||
@@ -164,7 +163,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
<HeaderStat
|
||||
heading={t('Supplied stake')}
|
||||
description={tooltipMapping['suppliedStake']}
|
||||
testId="supplied-stake"
|
||||
>
|
||||
<div>
|
||||
{suppliedStake
|
||||
@@ -180,10 +178,10 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
|
||||
{formatNumberPercentage(percentage, 2)}
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<HeaderStat heading={t('Market ID')}>
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Learn more')} testId="liquidity-learn-more">
|
||||
<HeaderStat heading={t('Learn more')}>
|
||||
{DocsLinks ? (
|
||||
<ExternalLink href={DocsLinks.LIQUIDITY}>
|
||||
{t('Providing liquidity')}
|
||||
|
||||
@@ -151,11 +151,7 @@ export const MarketSelector = ({
|
||||
</div>
|
||||
<div className="px-4 py-2">
|
||||
<span className="inline-block border-b border-black dark:border-white">
|
||||
<Link
|
||||
to={'/markets/all'}
|
||||
data-testid="all-markets-link"
|
||||
className="flex items-center gap-x-2"
|
||||
>
|
||||
<Link to={'/markets/all'} className="flex items-center gap-x-2">
|
||||
{t('All markets')}
|
||||
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
|
||||
</Link>
|
||||
|
||||
@@ -143,7 +143,7 @@ const MarketBottomPanel = memo(
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.collateral.component
|
||||
pinnedAsset={pinnedAsset}
|
||||
onMarketClick={onMarketClick}
|
||||
noBottomPlaceholder
|
||||
hideButtons
|
||||
storeKey="marketCollateral"
|
||||
/>
|
||||
@@ -224,7 +224,6 @@ const MarketBottomPanel = memo(
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.collateral.component
|
||||
pinnedAsset={pinnedAsset}
|
||||
onMarketClick={onMarketClick}
|
||||
hideButtons
|
||||
storeKey="marketCollateral"
|
||||
/>
|
||||
@@ -250,7 +249,6 @@ const MainGrid = memo(
|
||||
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
||||
id: 'middle-1',
|
||||
});
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
|
||||
return (
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
@@ -269,7 +267,6 @@ const MainGrid = memo(
|
||||
<Tab id="ticket" name={t('Ticket')}>
|
||||
<TradingViews.ticket.component
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
@@ -92,10 +92,7 @@ export const Portfolio = () => {
|
||||
<Tabs storageKey="console-portfolio-bottom">
|
||||
<Tab id="collateral" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<AccountsContainer
|
||||
storeKey="portfolioCollateral"
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
<AccountsContainer storeKey="portfolioCollateral" />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="deposits" name={t('Deposits')}>
|
||||
|
||||
@@ -12,13 +12,13 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||
export const AccountsContainer = ({
|
||||
pinnedAsset,
|
||||
hideButtons,
|
||||
noBottomPlaceholder,
|
||||
storeKey,
|
||||
onMarketClick,
|
||||
}: {
|
||||
pinnedAsset?: PinnedAsset;
|
||||
hideButtons?: boolean;
|
||||
noBottomPlaceholder?: boolean;
|
||||
storeKey?: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
@@ -48,9 +48,9 @@ export const AccountsContainer = ({
|
||||
onClickAsset={onClickAsset}
|
||||
onClickWithdraw={openWithdrawalDialog}
|
||||
onClickDeposit={openDepositDialog}
|
||||
onMarketClick={onMarketClick}
|
||||
isReadOnly={isReadOnly}
|
||||
pinnedAsset={pinnedAsset}
|
||||
noBottomPlaceholder={noBottomPlaceholder}
|
||||
storeKey={storeKey}
|
||||
/>
|
||||
{!isReadOnly && !hideButtons && (
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@ export const MarketLiquiditySupplied = ({
|
||||
AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS;
|
||||
|
||||
const description = marketId ? (
|
||||
<section data-testid="liquidity-supplied-tooltip">
|
||||
<section>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('Supplied stake')}</span>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
fragment MarginFields on MarginLevels {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
asset {
|
||||
id
|
||||
}
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
query Margins($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
marginsConnection {
|
||||
edges {
|
||||
node {
|
||||
...MarginFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription MarginsSubscription($partyId: ID!) {
|
||||
margins(partyId: $partyId) {
|
||||
marketId
|
||||
asset
|
||||
partyId
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type MarginsQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
||||
|
||||
export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } };
|
||||
|
||||
export const MarginFieldsFragmentDoc = gql`
|
||||
fragment MarginFields on MarginLevels {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
asset {
|
||||
id
|
||||
}
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const MarginsDocument = gql`
|
||||
query Margins($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
marginsConnection {
|
||||
edges {
|
||||
node {
|
||||
...MarginFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${MarginFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useMarginsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMarginsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMarginsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMarginsQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMarginsQuery(baseOptions: Apollo.QueryHookOptions<MarginsQuery, MarginsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MarginsQuery, MarginsQueryVariables>(MarginsDocument, options);
|
||||
}
|
||||
export function useMarginsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarginsQuery, MarginsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MarginsQuery, MarginsQueryVariables>(MarginsDocument, options);
|
||||
}
|
||||
export type MarginsQueryHookResult = ReturnType<typeof useMarginsQuery>;
|
||||
export type MarginsLazyQueryHookResult = ReturnType<typeof useMarginsLazyQuery>;
|
||||
export type MarginsQueryResult = Apollo.QueryResult<MarginsQuery, MarginsQueryVariables>;
|
||||
export const MarginsSubscriptionDocument = gql`
|
||||
subscription MarginsSubscription($partyId: ID!) {
|
||||
margins(partyId: $partyId) {
|
||||
marketId
|
||||
asset
|
||||
partyId
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMarginsSubscriptionSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useMarginsSubscriptionSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMarginsSubscriptionSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMarginsSubscriptionSubscription({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMarginsSubscriptionSubscription(baseOptions: Apollo.SubscriptionHookOptions<MarginsSubscriptionSubscription, MarginsSubscriptionSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<MarginsSubscriptionSubscription, MarginsSubscriptionSubscriptionVariables>(MarginsSubscriptionDocument, options);
|
||||
}
|
||||
export type MarginsSubscriptionSubscriptionHookResult = ReturnType<typeof useMarginsSubscriptionSubscription>;
|
||||
export type MarginsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<MarginsSubscriptionSubscription>;
|
||||
@@ -94,7 +94,6 @@ export const accountsOnlyDataProvider = makeDataProvider<
|
||||
update,
|
||||
getData,
|
||||
getDelta,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
export interface AccountFields extends Account {
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
import { useRef, memo, useState, useCallback } from 'react';
|
||||
import { useRef, memo, useCallback, useState } from 'react';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import {
|
||||
aggregatedAccountsDataProvider,
|
||||
aggregatedAccountDataProvider,
|
||||
} from './accounts-data-provider';
|
||||
import type { PinnedAsset } from './accounts-table';
|
||||
import { AccountTable } from './accounts-table';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import BreakdownTable from './breakdown-table';
|
||||
|
||||
const AccountBreakdown = ({
|
||||
assetId,
|
||||
partyId,
|
||||
onMarketClick,
|
||||
}: {
|
||||
assetId: string;
|
||||
partyId: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}) => {
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: aggregatedAccountDataProvider,
|
||||
variables: { partyId, assetId },
|
||||
update: ({ data }) => {
|
||||
if (gridRef.current?.api && data?.breakdown) {
|
||||
gridRef.current?.api.setRowData(data?.breakdown);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-[35vh] w-full m-auto flex flex-col"
|
||||
@@ -50,59 +42,19 @@ const AccountBreakdown = ({
|
||||
])}
|
||||
</p>
|
||||
)}
|
||||
<BreakdownTable
|
||||
ref={gridRef}
|
||||
data={data?.breakdown || null}
|
||||
domLayout="autoHeight"
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
<BreakdownTable data={data?.breakdown || null} domLayout="autoHeight" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountBreakdownDialog = memo(
|
||||
({
|
||||
assetId,
|
||||
partyId,
|
||||
onClose,
|
||||
onMarketClick,
|
||||
}: {
|
||||
assetId?: string;
|
||||
partyId: string;
|
||||
onClose: () => void;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}) => {
|
||||
console.log('render');
|
||||
return (
|
||||
<Dialog
|
||||
size="medium"
|
||||
open={Boolean(assetId)}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{assetId && (
|
||||
<AccountBreakdown
|
||||
assetId={assetId}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface AccountManagerProps {
|
||||
partyId: string;
|
||||
onClickAsset: (assetId: string) => void;
|
||||
onClickWithdraw?: (assetId?: string) => void;
|
||||
onClickDeposit?: (assetId?: string) => void;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
isReadOnly: boolean;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
noBottomPlaceholder?: boolean;
|
||||
storeKey?: string;
|
||||
}
|
||||
|
||||
@@ -113,26 +65,49 @@ export const AccountManager = ({
|
||||
partyId,
|
||||
isReadOnly,
|
||||
pinnedAsset,
|
||||
noBottomPlaceholder,
|
||||
storeKey,
|
||||
onMarketClick,
|
||||
}: AccountManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
|
||||
const update = useCallback(
|
||||
({ data }: { data: AccountFields[] | null }) => {
|
||||
if (!data || !gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
const pinnedAssetRowData =
|
||||
pinnedAsset && data.find((d) => d.asset.id === pinnedAsset.id);
|
||||
|
||||
if (pinnedAssetRowData) {
|
||||
const pinnedTopRow = gridRef.current.api.getPinnedTopRow(0);
|
||||
if (
|
||||
pinnedTopRow?.data?.balance === '0' &&
|
||||
pinnedAssetRowData.balance !== '0'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!isEqual(pinnedTopRow?.data, pinnedAssetRowData)) {
|
||||
gridRef.current.api.setPinnedTopRowData([pinnedAssetRowData]);
|
||||
}
|
||||
}
|
||||
gridRef.current.api.setRowData(
|
||||
pinnedAssetRowData
|
||||
? data?.filter((d) => d !== pinnedAssetRowData)
|
||||
: data
|
||||
);
|
||||
return true;
|
||||
},
|
||||
[gridRef, pinnedAsset]
|
||||
);
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
variables: { partyId },
|
||||
update,
|
||||
});
|
||||
const bottomPlaceholderProps = useBottomPlaceholder({
|
||||
gridRef,
|
||||
disabled: noBottomPlaceholder,
|
||||
});
|
||||
|
||||
const onMarketClickInternal = useCallback(
|
||||
(...args: Parameters<NonNullable<typeof onMarketClick>>) => {
|
||||
setBreakdownAssetId(undefined);
|
||||
if (onMarketClick) {
|
||||
onMarketClick(...args);
|
||||
}
|
||||
},
|
||||
[onMarketClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
@@ -146,14 +121,22 @@ export const AccountManager = ({
|
||||
isReadOnly={isReadOnly}
|
||||
pinnedAsset={pinnedAsset}
|
||||
storeKey={storeKey}
|
||||
{...bottomPlaceholderProps}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No accounts')}
|
||||
/>
|
||||
<AccountBreakdownDialog
|
||||
assetId={breakdownAssetId}
|
||||
partyId={partyId}
|
||||
onClose={useCallback(() => setBreakdownAssetId(undefined), [])}
|
||||
onMarketClick={onMarketClick ? onMarketClickInternal : undefined}
|
||||
/>
|
||||
<Dialog
|
||||
size="medium"
|
||||
open={Boolean(breakdownAssetId)}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setBreakdownAssetId(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{breakdownAssetId && (
|
||||
<AccountBreakdown assetId={breakdownAssetId} partyId={partyId} />
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { forwardRef, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
isNumeric,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
@@ -11,15 +10,21 @@ import type {
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { COL_DEFS } from '@vegaprotocol/datagrid';
|
||||
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ButtonLink,
|
||||
Button,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type {
|
||||
IDatasource,
|
||||
IGetRowsParams,
|
||||
RowNode,
|
||||
RowHeightParams,
|
||||
ColDef,
|
||||
} from 'ag-grid-community';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
@@ -30,16 +35,29 @@ import classNames from 'classnames';
|
||||
import { AccountsActionsDropdown } from './accounts-actions-dropdown';
|
||||
|
||||
const colorClass = (percentageUsed: number, neutral = false) => {
|
||||
return classNames('text-right', {
|
||||
return classNames({
|
||||
'text-neutral-500 dark:text-neutral-400': percentageUsed < 75 && !neutral,
|
||||
'text-vega-orange': percentageUsed >= 75 && percentageUsed < 90,
|
||||
'text-vega-pink': percentageUsed >= 90,
|
||||
});
|
||||
};
|
||||
|
||||
export const percentageValue = (part: string, total: string) => {
|
||||
total = !total || total === '0' ? '1' : total;
|
||||
return new BigNumber(part).dividedBy(total).multipliedBy(100).toNumber();
|
||||
export const percentageValue = (part?: string, total?: string) =>
|
||||
new BigNumber(part || 0)
|
||||
.dividedBy(total || 1)
|
||||
.multipliedBy(100)
|
||||
.toNumber();
|
||||
|
||||
const formatWithAssetDecimals = (
|
||||
data: AccountFields | undefined,
|
||||
value: string | undefined
|
||||
) => {
|
||||
return (
|
||||
data &&
|
||||
data.asset &&
|
||||
isNumeric(value) &&
|
||||
addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
);
|
||||
};
|
||||
|
||||
export const accountValuesComparator = (
|
||||
@@ -63,10 +81,15 @@ export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
|
||||
successCallback(rowsThisBlock: AccountFields[], lastRow?: number): void;
|
||||
}
|
||||
|
||||
export interface Datasource extends IDatasource {
|
||||
getRows(params: GetRowsParams): void;
|
||||
}
|
||||
|
||||
export type PinnedAsset = Pick<Asset, 'symbol' | 'name' | 'id' | 'decimals'>;
|
||||
|
||||
export interface AccountTableProps extends AgGridReactProps {
|
||||
rowData?: AccountFields[] | null;
|
||||
datasource?: Datasource;
|
||||
onClickAsset: (assetId: string) => void;
|
||||
onClickWithdraw?: (assetId: string) => void;
|
||||
onClickDeposit?: (assetId: string) => void;
|
||||
@@ -125,55 +148,83 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
|
||||
const showDepositButton = pinnedAsset?.balance === '0';
|
||||
|
||||
const colDefs = useMemo(() => {
|
||||
const defs: ColDef[] = [
|
||||
{
|
||||
headerName: t('Asset'),
|
||||
field: 'asset.symbol',
|
||||
headerTooltip: t(
|
||||
return (
|
||||
<AgGrid
|
||||
{...props}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No accounts')}
|
||||
getRowId={({
|
||||
data,
|
||||
}: {
|
||||
data: AccountFields & { isLastPlaceholder?: boolean; id?: string };
|
||||
}) => (data.isLastPlaceholder && data.id ? data.id : data.asset.id)}
|
||||
ref={ref}
|
||||
tooltipShowDelay={500}
|
||||
rowData={rowData?.filter(
|
||||
(data) => data.asset.id !== props.pinnedAsset?.id
|
||||
)}
|
||||
defaultColDef={{
|
||||
resizable: true,
|
||||
tooltipComponent: TooltipCellComponent,
|
||||
sortable: true,
|
||||
comparator: accountValuesComparator,
|
||||
}}
|
||||
getRowHeight={getPinnedAssetRowHeight}
|
||||
pinnedTopRowData={pinnedAsset ? [pinnedAsset] : undefined}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Asset')}
|
||||
field="asset.symbol"
|
||||
headerTooltip={t(
|
||||
'Asset is the collateral that is deposited into the Vega protocol.'
|
||||
),
|
||||
cellClass: 'underline',
|
||||
onCellClicked: ({ data }) => {
|
||||
if (data) {
|
||||
onClickAsset(data.asset.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Used'),
|
||||
type: 'rightAligned',
|
||||
field: 'used',
|
||||
headerTooltip: t(
|
||||
)}
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="asset"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
onClickAsset(data.asset.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</ButtonLink>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Used')}
|
||||
type="rightAligned"
|
||||
field="used"
|
||||
headerTooltip={t(
|
||||
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
|
||||
),
|
||||
tooltipValueGetter: ({ value, data }) => {
|
||||
if (!value || !data) return null;
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
},
|
||||
onCellClicked: ({ data }) => {
|
||||
if (!data || !onClickBreakdown) return;
|
||||
onClickBreakdown(data.asset.id);
|
||||
},
|
||||
cellRenderer: ({
|
||||
)}
|
||||
cellRenderer={({
|
||||
data,
|
||||
value,
|
||||
}: VegaICellRendererParams<AccountFields, 'used'>) => {
|
||||
if (!value || !data) return '-';
|
||||
if (!data) return null;
|
||||
const percentageUsed = percentageValue(value, data.total);
|
||||
const valueFormatted = addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
);
|
||||
const valueFormatted = formatWithAssetDecimals(data, value);
|
||||
|
||||
return data.breakdown ? (
|
||||
<>
|
||||
<span className="underline">{valueFormatted}</span>
|
||||
<ButtonLink
|
||||
data-testid="breakdown"
|
||||
onClick={() => {
|
||||
onClickBreakdown && onClickBreakdown(data.asset.id);
|
||||
}}
|
||||
>
|
||||
<span>{valueFormatted}</span>
|
||||
</ButtonLink>
|
||||
<span
|
||||
className={classNames(
|
||||
colorClass(percentageUsed),
|
||||
'ml-1 inline-block w-14'
|
||||
'ml-2 inline-block w-14'
|
||||
)}
|
||||
>
|
||||
{percentageUsed.toFixed(2)}%
|
||||
@@ -181,77 +232,59 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="underline">{valueFormatted}</span>
|
||||
<span className="ml-2 inline-block w-14 text-vega-light-200 dark:text-vega-dark-200">
|
||||
{t('0.00%')}'
|
||||
<span>{valueFormatted}</span>
|
||||
<span className="ml-2 inline-block w-14 text-neutral-500 dark:text-neutral-400">
|
||||
0.00%
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Available'),
|
||||
field: 'available',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t(
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Available')}
|
||||
field="available"
|
||||
type="rightAligned"
|
||||
headerTooltip={t(
|
||||
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
|
||||
),
|
||||
tooltipValueGetter: ({ value, data }) => {
|
||||
if (!value || !data) return null;
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
},
|
||||
cellClass: ({ data }) => {
|
||||
)}
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields, 'available'>) => {
|
||||
const percentageUsed = percentageValue(data?.used, data?.total);
|
||||
return colorClass(percentageUsed, true);
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
|
||||
if (!value || !data) return '-';
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
headerName: t('Total'),
|
||||
type: 'rightAligned',
|
||||
field: 'total',
|
||||
headerTooltip: t(
|
||||
'The total amount of each asset on this key. Includes used and available collateral.'
|
||||
),
|
||||
tooltipValueGetter: ({ value, data }) => {
|
||||
if (!value || !data) return null;
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
value,
|
||||
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
|
||||
if (!data || !value) return '-';
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
return (
|
||||
<span className={colorClass(percentageUsed, true)}>
|
||||
{formatWithAssetDecimals(data, value)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'accounts-actions',
|
||||
field: 'asset.id',
|
||||
...COL_DEFS.actions,
|
||||
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
|
||||
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
|
||||
cellRenderer: ({
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Total')}
|
||||
type="rightAligned"
|
||||
field="total"
|
||||
headerTooltip={t(
|
||||
'The total amount of each asset on this key. Includes used and available collateral.'
|
||||
)}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'total'>) =>
|
||||
formatWithAssetDecimals(data, data?.total)
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="accounts-actions"
|
||||
field="asset.id"
|
||||
{...COL_DEFS.actions}
|
||||
minWidth={showDepositButton ? 130 : COL_DEFS.actions.minWidth}
|
||||
maxWidth={showDepositButton ? 130 : COL_DEFS.actions.maxWidth}
|
||||
cellRenderer={({
|
||||
value: assetId,
|
||||
node,
|
||||
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
|
||||
if (!assetId) return null;
|
||||
if (node.rowPinned && node.data?.total === '0') {
|
||||
if (node.rowPinned && node.data?.balance === '0') {
|
||||
return (
|
||||
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
|
||||
<Button
|
||||
@@ -286,42 +319,9 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return defs;
|
||||
}, [
|
||||
onClickAsset,
|
||||
onClickBreakdown,
|
||||
onClickDeposit,
|
||||
onClickWithdraw,
|
||||
props.isReadOnly,
|
||||
showDepositButton,
|
||||
]);
|
||||
|
||||
const data = rowData?.filter(
|
||||
(data) => data.asset.id !== props.pinnedAsset?.id
|
||||
);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
{...props}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No accounts')}
|
||||
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
|
||||
ref={ref}
|
||||
tooltipShowDelay={500}
|
||||
rowData={data}
|
||||
defaultColDef={{
|
||||
resizable: true,
|
||||
tooltipComponent: TooltipCellComponent,
|
||||
sortable: true,
|
||||
comparator: accountValuesComparator,
|
||||
}}
|
||||
columnDefs={colDefs}
|
||||
getRowHeight={getPinnedAssetRowHeight}
|
||||
pinnedTopRowData={pinnedAsset ? [pinnedAsset] : undefined}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -35,7 +35,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
balance: '100000000',
|
||||
market: null,
|
||||
asset: {
|
||||
// tEURO
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
},
|
||||
@@ -45,7 +44,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100000000',
|
||||
asset: {
|
||||
// tDAI
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id-2',
|
||||
},
|
||||
@@ -59,7 +57,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
id: 'market-2',
|
||||
},
|
||||
asset: {
|
||||
// tEURO
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
},
|
||||
@@ -73,7 +70,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
id: 'market-0',
|
||||
},
|
||||
asset: {
|
||||
// AST0
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
},
|
||||
@@ -87,7 +83,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
id: 'market-3',
|
||||
},
|
||||
asset: {
|
||||
// AST0
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
},
|
||||
@@ -95,10 +90,9 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '10000000',
|
||||
balance: '10000000000',
|
||||
market: null,
|
||||
asset: {
|
||||
// AST0
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
},
|
||||
@@ -110,7 +104,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
balance: '100000001',
|
||||
market: null,
|
||||
asset: {
|
||||
// tBTC (sepolia)
|
||||
__typename: 'Asset',
|
||||
id: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
|
||||
},
|
||||
@@ -121,7 +114,6 @@ export const accountFields: AccountFieldsFragment[] = [
|
||||
balance: '100000002',
|
||||
market: null,
|
||||
asset: {
|
||||
// tBTC (test)
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
},
|
||||
@@ -151,53 +143,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,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,14 +4,6 @@ import * as Types from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
|
||||
const marginHealthChartTestId = 'margin-health-chart';
|
||||
|
||||
jest.mock('./margin-health-chart', () => ({
|
||||
MarginHealthChart: () => {
|
||||
return <div data-testid={marginHealthChartTestId}></div>;
|
||||
},
|
||||
}));
|
||||
|
||||
const singleRow = {
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
@@ -45,10 +37,10 @@ describe('BreakdownTable', () => {
|
||||
render(<BreakdownTable data={singleRowData} />);
|
||||
});
|
||||
const headers = await screen.findAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(4);
|
||||
expect(headers).toHaveLength(3);
|
||||
expect(
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual(['Market', 'Account type', 'Balance', 'Margin health']);
|
||||
).toEqual(['Market', 'Account type', 'Balance']);
|
||||
});
|
||||
|
||||
it('should apply correct formatting', async () => {
|
||||
@@ -63,27 +55,9 @@ describe('BreakdownTable', () => {
|
||||
'1,256.00',
|
||||
'1,256.00',
|
||||
];
|
||||
cells.slice(0, -1).forEach((cell, i) => {
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
expect(screen.getByTestId(marginHealthChartTestId)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays margin health chart only for margin account', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<BreakdownTable
|
||||
data={[
|
||||
{
|
||||
...singleRow,
|
||||
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
market: null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(screen.queryByTestId(marginHealthChartTestId)).toBeNull();
|
||||
});
|
||||
|
||||
it('should get correct account data', () => {
|
||||
|
||||
@@ -1,38 +1,67 @@
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { forwardRef } from 'react';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Intent, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import type {
|
||||
ValueProps,
|
||||
VegaValueFormatterParams,
|
||||
VegaICellRendererParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { ProgressBarCell } from '@vegaprotocol/datagrid';
|
||||
import { progressBarCellRendererSelector } from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid, PriceCell } from '@vegaprotocol/datagrid';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||
import { accountValuesComparator } from './accounts-table';
|
||||
import { MarginHealthChart } from './margin-health-chart';
|
||||
import { MarketNameCell } from '@vegaprotocol/datagrid';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
|
||||
export const progressBarValueFormatter = ({
|
||||
data,
|
||||
node,
|
||||
}: ValueFormatterParams): ValueProps['valueFormatted'] | undefined => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.used);
|
||||
const mid = BigInt(data.available);
|
||||
const max = BigInt(data.total);
|
||||
const range = max > min ? max : min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(min.toString(), data.asset.decimals),
|
||||
high: addDecimalsFormatNumber(mid.toString(), data.asset.decimals),
|
||||
value: range ? Number((min * BigInt(100)) / range) : 0,
|
||||
intent: Intent.Warning,
|
||||
};
|
||||
};
|
||||
|
||||
interface BreakdownTableProps extends AgGridReactProps {
|
||||
data: AccountFields[] | null;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
({ data }, ref) => {
|
||||
const coldefs = useMemo(() => {
|
||||
const defs: ColDef[] = [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.name',
|
||||
valueFormatter: ({
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('Collateral not used')}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: AccountFields }) =>
|
||||
`${data.asset.id}-${data.type}-${data.market?.id}`
|
||||
}
|
||||
ref={ref}
|
||||
rowHeight={34}
|
||||
components={{ PriceCell }}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
}}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="market.tradableInstrument.instrument.name"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
AccountFields,
|
||||
@@ -40,101 +69,32 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
>) => {
|
||||
if (!value) return 'None';
|
||||
return value;
|
||||
},
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
headerName: t('Account type'),
|
||||
field: 'type',
|
||||
maxWidth: 300,
|
||||
valueFormatter: ({
|
||||
}}
|
||||
minWidth={200}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Account type')}
|
||||
field="type"
|
||||
maxWidth={300}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<AccountFields, 'type'>) => {
|
||||
return value
|
||||
}: VegaValueFormatterParams<AccountFields, 'type'>) =>
|
||||
value
|
||||
? AccountTypeMapping[value as keyof typeof AccountTypeMapping]
|
||||
: '';
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Balance'),
|
||||
field: 'used',
|
||||
flex: 2,
|
||||
maxWidth: 500,
|
||||
type: 'rightAligned',
|
||||
tooltipComponent: TooltipCellComponent,
|
||||
tooltipValueGetter: ({ value, data }) => {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
},
|
||||
cellRenderer: ({
|
||||
data,
|
||||
node,
|
||||
}: VegaICellRendererParams<AccountFields, 'used'>) => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.used);
|
||||
const mid = BigInt(data.available);
|
||||
const max = BigInt(data.total);
|
||||
const range = max > min ? max : min;
|
||||
const formattedData = {
|
||||
low: addDecimalsFormatNumberQuantum(
|
||||
min.toString(),
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
),
|
||||
high: addDecimalsFormatNumberQuantum(
|
||||
mid.toString(),
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
),
|
||||
value: range ? Number((min * BigInt(100)) / range) : 0,
|
||||
intent: Intent.Warning,
|
||||
};
|
||||
return <ProgressBarCell valueFormatted={formattedData} />;
|
||||
},
|
||||
comparator: accountValuesComparator,
|
||||
},
|
||||
{
|
||||
headerName: t('Margin health'),
|
||||
field: 'market.id',
|
||||
flex: 2,
|
||||
maxWidth: 500,
|
||||
sortable: false,
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields, 'market.id'>) =>
|
||||
data?.market?.id &&
|
||||
data.type === AccountType['ACCOUNT_TYPE_MARGIN'] &&
|
||||
data?.asset.id ? (
|
||||
<MarginHealthChart
|
||||
marketId={data.market.id}
|
||||
assetId={data.asset.id}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
return defs;
|
||||
}, []);
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('Collateral not used')}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: AccountFields }) =>
|
||||
`${data.asset.id}:${data.type}:${data.market?.id}`
|
||||
}
|
||||
ref={ref}
|
||||
rowHeight={34}
|
||||
components={{ PriceCell, MarketNameCell, ProgressBarCell }}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
}}
|
||||
columnDefs={coldefs}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Balance')}
|
||||
field="used"
|
||||
flex={2}
|
||||
maxWidth={500}
|
||||
cellRendererSelector={progressBarCellRendererSelector}
|
||||
valueFormatter={progressBarValueFormatter}
|
||||
comparator={accountValuesComparator}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,6 +8,3 @@ export * from './use-account-balance';
|
||||
export * from './get-settlement-account';
|
||||
export * from './use-market-account-balance';
|
||||
export * from './transfer-dialog';
|
||||
export * from './__generated__/Margins';
|
||||
export { MarginHealthChart } from './margin-health-chart';
|
||||
export * from './margin-data-provider';
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketMarginDataProvider } from './margin-data-provider';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useAccountBalance } from './use-account-balance';
|
||||
import { useMarketAccountBalance } from './use-market-account-balance';
|
||||
|
||||
const MarginHealthChartTooltipRow = ({
|
||||
label,
|
||||
value,
|
||||
decimals,
|
||||
href,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
decimals: number;
|
||||
href?: string;
|
||||
}) => (
|
||||
<>
|
||||
<div
|
||||
className="float-left clear-left"
|
||||
key="label"
|
||||
data-testid="margin-health-tooltip-label"
|
||||
>
|
||||
{href ? (
|
||||
<ExternalLink href={href} target="_blank">
|
||||
{label}
|
||||
</ExternalLink>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="float-right"
|
||||
key="value"
|
||||
data-testid="margin-health-tooltip-value"
|
||||
>
|
||||
{addDecimalsFormatNumber(value, decimals)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const MarginHealthChartTooltip = ({
|
||||
maintenanceLevel,
|
||||
searchLevel,
|
||||
initialLevel,
|
||||
collateralReleaseLevel,
|
||||
decimals,
|
||||
marginAccountBalance,
|
||||
}: {
|
||||
maintenanceLevel: string;
|
||||
searchLevel: string;
|
||||
initialLevel: string;
|
||||
collateralReleaseLevel: string;
|
||||
decimals: number;
|
||||
marginAccountBalance?: string;
|
||||
}) => {
|
||||
const tooltipContent = [
|
||||
<MarginHealthChartTooltipRow
|
||||
key={'maintenance'}
|
||||
label={t('maintenance level')}
|
||||
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance"
|
||||
value={maintenanceLevel}
|
||||
decimals={decimals}
|
||||
/>,
|
||||
<MarginHealthChartTooltipRow
|
||||
key={'search'}
|
||||
label={t('search level')}
|
||||
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-searching-for-collateral"
|
||||
value={searchLevel}
|
||||
decimals={decimals}
|
||||
/>,
|
||||
<MarginHealthChartTooltipRow
|
||||
key={'initial'}
|
||||
label={t('initial level')}
|
||||
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-initial"
|
||||
value={initialLevel}
|
||||
decimals={decimals}
|
||||
/>,
|
||||
<MarginHealthChartTooltipRow
|
||||
key={'release'}
|
||||
label={t('release level')}
|
||||
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-releasing-collateral"
|
||||
value={collateralReleaseLevel}
|
||||
decimals={decimals}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (marginAccountBalance) {
|
||||
const balance = (
|
||||
<MarginHealthChartTooltipRow
|
||||
key={'balance'}
|
||||
label={t('balance')}
|
||||
value={marginAccountBalance}
|
||||
decimals={decimals}
|
||||
/>
|
||||
);
|
||||
if (BigInt(marginAccountBalance) < BigInt(searchLevel)) {
|
||||
tooltipContent.splice(1, 0, balance);
|
||||
} else if (BigInt(marginAccountBalance) < BigInt(initialLevel)) {
|
||||
tooltipContent.splice(2, 0, balance);
|
||||
} else if (BigInt(marginAccountBalance) < BigInt(collateralReleaseLevel)) {
|
||||
tooltipContent.splice(3, 0, balance);
|
||||
} else {
|
||||
tooltipContent.push(balance);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="overflow-hidden" data-testid="margin-health-tooltip">
|
||||
{tooltipContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarginHealthChart = ({
|
||||
marketId,
|
||||
assetId,
|
||||
}: {
|
||||
marketId: string;
|
||||
assetId: string;
|
||||
}) => {
|
||||
const { data: assetsMap } = useAssetsMapProvider();
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId, partyId: partyId ?? '' },
|
||||
skip: !partyId,
|
||||
});
|
||||
const { accountBalance: rawGeneralAccountBalance } =
|
||||
useAccountBalance(assetId);
|
||||
const { accountBalance: rawMarginAccountBalance } =
|
||||
useMarketAccountBalance(marketId);
|
||||
const asset = assetsMap && assetsMap[assetId];
|
||||
if (!data || !asset) {
|
||||
return null;
|
||||
}
|
||||
const { decimals } = asset;
|
||||
|
||||
const collateralReleaseLevel = Number(data.collateralReleaseLevel);
|
||||
const initialLevel = Number(data.initialLevel);
|
||||
const maintenanceLevel = Number(data.maintenanceLevel);
|
||||
const searchLevel = Number(data.searchLevel);
|
||||
const marginAccountBalance = Number(rawMarginAccountBalance);
|
||||
const generalAccountBalance = Number(rawGeneralAccountBalance);
|
||||
const max = Math.max(
|
||||
marginAccountBalance + generalAccountBalance,
|
||||
collateralReleaseLevel
|
||||
);
|
||||
|
||||
const red = maintenanceLevel / max;
|
||||
const orange = (searchLevel - maintenanceLevel) / max;
|
||||
const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max;
|
||||
const green = (collateralReleaseLevel - initialLevel) / max + yellow;
|
||||
const balanceMarker = marginAccountBalance / max;
|
||||
|
||||
const tooltip = (
|
||||
<MarginHealthChartTooltip
|
||||
maintenanceLevel={data.maintenanceLevel}
|
||||
searchLevel={data.searchLevel}
|
||||
initialLevel={data.initialLevel}
|
||||
collateralReleaseLevel={data.collateralReleaseLevel}
|
||||
marginAccountBalance={rawMarginAccountBalance}
|
||||
decimals={decimals}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testid="margin-health-chart">
|
||||
{addDecimalsFormatNumber(
|
||||
(BigInt(marginAccountBalance) - BigInt(maintenanceLevel)).toString(),
|
||||
decimals
|
||||
)}{' '}
|
||||
{t('above')}{' '}
|
||||
<ExternalLink href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance">
|
||||
{t('maintenance level')}
|
||||
</ExternalLink>
|
||||
<Tooltip description={tooltip}>
|
||||
<div
|
||||
data-testid="margin-health-chart-track"
|
||||
className="relative bg-vega-green-650"
|
||||
style={{
|
||||
height: '6px',
|
||||
marginBottom: '1px',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-testid="margin-health-chart-red"
|
||||
className="bg-vega-pink-550"
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${red * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
data-testid="margin-health-chart-orange"
|
||||
className="bg-vega-orange"
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${orange * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
data-testid="margin-health-chart-yellow"
|
||||
className="bg-vega-yellow"
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${yellow * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
data-testid="margin-health-chart-green"
|
||||
className="bg-vega-green-600"
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${green * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
{balanceMarker > 0 && balanceMarker < 100 && (
|
||||
<div
|
||||
data-testid="margin-health-chart-balance"
|
||||
className="absolute bg-vega-blue"
|
||||
style={{
|
||||
height: '8px',
|
||||
width: '8px',
|
||||
top: '-1px',
|
||||
transform: 'translate(-4px, 0px)',
|
||||
borderRadius: '50%',
|
||||
border: '1px solid white',
|
||||
backgroundColor: 'blue',
|
||||
left: `${balanceMarker * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,154 +0,0 @@
|
||||
import {
|
||||
MarginHealthChart,
|
||||
MarginHealthChartTooltip,
|
||||
} from './margin-health-chart';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import type { MarginFieldsFragment } from './__generated__/Margins';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
const asset: AssetFieldsFragment = {
|
||||
id: 'assetId',
|
||||
decimals: 2,
|
||||
} as AssetFieldsFragment;
|
||||
const margins: MarginFieldsFragment = {
|
||||
asset: {
|
||||
id: 'assetId',
|
||||
},
|
||||
collateralReleaseLevel: '1000',
|
||||
initialLevel: '800',
|
||||
searchLevel: '600',
|
||||
maintenanceLevel: '400',
|
||||
market: {
|
||||
id: 'marketId',
|
||||
},
|
||||
};
|
||||
|
||||
const getMargins = jest.fn(() => margins);
|
||||
const getBalance = jest.fn(() => '0');
|
||||
|
||||
jest.mock('./margin-data-provider', () => ({}));
|
||||
|
||||
jest.mock('@vegaprotocol/assets', () => ({
|
||||
useAssetsMapProvider: () => {
|
||||
return {
|
||||
data: {
|
||||
assetId: asset,
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
useVegaWallet: () => {
|
||||
return {
|
||||
pubKey: 'partyId',
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
useDataProvider: () => {
|
||||
return {
|
||||
data: getMargins(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./use-account-balance', () => ({
|
||||
useAccountBalance: () => {
|
||||
return {
|
||||
accountBalance: getBalance(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./use-market-account-balance', () => ({
|
||||
useMarketAccountBalance: () => {
|
||||
return {
|
||||
accountBalance: '700',
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('MarginHealthChart', () => {
|
||||
it('should render correct values', async () => {
|
||||
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
|
||||
const chart = screen.getByTestId('margin-health-chart');
|
||||
expect(chart).toHaveTextContent('3.00 above maintenance level');
|
||||
const red = screen.getByTestId('margin-health-chart-red');
|
||||
const orange = screen.getByTestId('margin-health-chart-orange');
|
||||
const yellow = screen.getByTestId('margin-health-chart-yellow');
|
||||
const green = screen.getByTestId('margin-health-chart-green');
|
||||
const balance = screen.getByTestId('margin-health-chart-balance');
|
||||
expect(parseInt(red.style.width)).toBe(40);
|
||||
expect(parseInt(orange.style.width)).toBe(20);
|
||||
expect(parseInt(yellow.style.width)).toBe(10);
|
||||
expect(parseInt(green.style.width)).toBe(30);
|
||||
expect(parseInt(balance.style.left)).toBe(70);
|
||||
});
|
||||
|
||||
it('should use correct scale', async () => {
|
||||
getBalance.mockReturnValueOnce('1300');
|
||||
await act(async () => {
|
||||
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
|
||||
});
|
||||
await screen.findByTestId('margin-health-chart');
|
||||
const red = screen.getByTestId('margin-health-chart-red');
|
||||
expect(parseInt(red.style.width)).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarginHealthChartTooltip', () => {
|
||||
it('renders correct values and labels', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MarginHealthChartTooltip
|
||||
{...margins}
|
||||
decimals={asset.decimals}
|
||||
marginAccountBalance="500"
|
||||
/>
|
||||
);
|
||||
});
|
||||
const labels = await screen.findAllByTestId('margin-health-tooltip-label');
|
||||
const expectedLabels = [
|
||||
'maintenance level',
|
||||
'balance',
|
||||
'search level',
|
||||
'initial level',
|
||||
'release level',
|
||||
];
|
||||
labels.forEach((value, i) => {
|
||||
expect(value).toHaveTextContent(expectedLabels[i]);
|
||||
});
|
||||
const values = await screen.findAllByTestId('margin-health-tooltip-value');
|
||||
const expectedValues = ['4.00', '5.00', '6.00', '8.00', '10.00'];
|
||||
values.forEach((value, i) => {
|
||||
expect(value).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders balance in correct place', async () => {
|
||||
const { rerender } = render(
|
||||
<MarginHealthChartTooltip
|
||||
{...margins}
|
||||
decimals={asset.decimals}
|
||||
marginAccountBalance="700"
|
||||
/>
|
||||
);
|
||||
|
||||
let values = await screen.findAllByTestId('margin-health-tooltip-value');
|
||||
expect(values[2]).toHaveTextContent('7.00');
|
||||
|
||||
rerender(
|
||||
<MarginHealthChartTooltip
|
||||
{...margins}
|
||||
decimals={asset.decimals}
|
||||
marginAccountBalance="900"
|
||||
/>
|
||||
);
|
||||
|
||||
values = await screen.findAllByTestId('margin-health-tooltip-value');
|
||||
expect(values.length).toBe(5);
|
||||
expect(values[3]).toHaveTextContent('9.00');
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ export const useAccountBalance = (assetId?: string) => {
|
||||
},
|
||||
[assetId]
|
||||
);
|
||||
|
||||
useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables,
|
||||
|
||||
@@ -22,6 +22,7 @@ export const useMarketAccountBalance = (marketId: string) => {
|
||||
},
|
||||
[marketId]
|
||||
);
|
||||
|
||||
useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
|
||||
@@ -28,4 +28,3 @@ export * from '../trades/src/lib/trades.mock';
|
||||
export * from '../withdraws/src/lib/withdrawal.mock';
|
||||
export * from '../proposals/src/lib/protocol-upgrade-proposals/protocol-statistics-proposals.mock';
|
||||
export * from '../proposals/src/lib/protocol-upgrade-proposals/block-statistics.mock';
|
||||
export * from '../liquidity/src/lib/liquidity.mock';
|
||||
|
||||
@@ -38,6 +38,7 @@ type Data = Item[];
|
||||
type QueryData = {
|
||||
data: Data;
|
||||
pageInfo?: PageInfo;
|
||||
totalCount?: number;
|
||||
};
|
||||
|
||||
type CombinedData = {
|
||||
@@ -114,6 +115,7 @@ const paginatedSubscribe = makeDataProvider<
|
||||
first,
|
||||
append: defaultAppend,
|
||||
getPageInfo: (r) => r?.pageInfo ?? null,
|
||||
getTotalCount: (r) => r?.totalCount,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -371,10 +373,32 @@ describe('data provider', () => {
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('loads requested data blocks', async () => {
|
||||
it('fills data with nulls if pagination is enabled', async () => {
|
||||
const totalCount = 1000;
|
||||
const data: Item[] = new Array(first).fill(null).map((v, i) => ({
|
||||
cursor: i.toString(),
|
||||
node: {
|
||||
id: i.toString(),
|
||||
},
|
||||
}));
|
||||
const subscription = paginatedSubscribe(callback, client, variables);
|
||||
await resolveQuery({
|
||||
data,
|
||||
totalCount,
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
},
|
||||
});
|
||||
expect(callback.mock.calls[1][0].data?.length).toBe(totalCount);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('loads requested data blocks and inserts data with total count', async () => {
|
||||
const totalCount = 1000;
|
||||
const subscription = paginatedSubscribe(callback, client, variables);
|
||||
await resolveQuery({
|
||||
data: generateData(),
|
||||
totalCount,
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '100',
|
||||
@@ -383,25 +407,168 @@ describe('data provider', () => {
|
||||
|
||||
// load next page
|
||||
subscription.load && subscription.load();
|
||||
const lastQueryArgs =
|
||||
let lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.['pagination']).toEqual({
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '100',
|
||||
first,
|
||||
});
|
||||
await resolveQuery({
|
||||
data: generateData(100),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasNextPage: true,
|
||||
endCursor: '200',
|
||||
},
|
||||
});
|
||||
|
||||
// load page with skip
|
||||
subscription.load && subscription.load(500, 600);
|
||||
lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '200',
|
||||
first,
|
||||
skip: 300,
|
||||
});
|
||||
await resolveQuery({
|
||||
data: generateData(500),
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '600',
|
||||
},
|
||||
});
|
||||
|
||||
// load in the gap
|
||||
subscription.load && subscription.load(400, 500);
|
||||
lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '200',
|
||||
first,
|
||||
skip: 200,
|
||||
});
|
||||
await resolveQuery({
|
||||
data: generateData(400),
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '500',
|
||||
},
|
||||
});
|
||||
|
||||
// load page after last block
|
||||
subscription.load && subscription.load(700, 800);
|
||||
lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '600',
|
||||
first,
|
||||
skip: 100,
|
||||
});
|
||||
await resolveQuery({
|
||||
data: generateData(700),
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '800',
|
||||
},
|
||||
});
|
||||
|
||||
// load last page shorter than expected
|
||||
subscription.load && subscription.load(950, 1050);
|
||||
lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '800',
|
||||
first,
|
||||
skip: 150,
|
||||
});
|
||||
await resolveQuery({
|
||||
data: generateData(950, 20),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
endCursor: '970',
|
||||
},
|
||||
});
|
||||
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||
expect(lastCallbackArgs[0].totalCount).toBe(970);
|
||||
|
||||
// load next page when pageInfo.hasNextPage === false
|
||||
const clientQueryCallsLength = clientQuery.mock.calls.length;
|
||||
subscription.load && subscription.load();
|
||||
expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength);
|
||||
|
||||
// load last page longer than expected
|
||||
subscription.load && subscription.load(960, 1000);
|
||||
lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '960',
|
||||
first,
|
||||
});
|
||||
await resolveQuery({
|
||||
data: generateData(960, 40),
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '1000',
|
||||
},
|
||||
});
|
||||
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||
expect(lastCallbackArgs[0].totalCount).toBe(1000);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('loads requested data blocks and inserts data without totalCount', async () => {
|
||||
const totalCount = undefined;
|
||||
const subscription = paginatedSubscribe(callback, client, variables);
|
||||
await resolveQuery({
|
||||
data: generateData(),
|
||||
totalCount,
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '100',
|
||||
},
|
||||
});
|
||||
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
|
||||
|
||||
// load next page
|
||||
subscription.load && subscription.load();
|
||||
await resolveQuery({
|
||||
data: generateData(100),
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '200',
|
||||
},
|
||||
});
|
||||
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
|
||||
|
||||
// load last page
|
||||
subscription.load && subscription.load();
|
||||
await resolveQuery({
|
||||
data: generateData(200, 50),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
endCursor: '250',
|
||||
},
|
||||
});
|
||||
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||
expect(lastCallbackArgs[0].totalCount).toBe(250);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('sets total count when first page has no next page', async () => {
|
||||
const subscription = paginatedSubscribe(callback, client, variables);
|
||||
await resolveQuery({
|
||||
data: generateData(),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
endCursor: '100',
|
||||
},
|
||||
});
|
||||
const lastCallbackArgs =
|
||||
callback.mock.calls[callback.mock.calls.length - 1];
|
||||
expect(lastCallbackArgs[0].totalCount).toBe(100);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
@@ -585,7 +752,7 @@ describe('derived data provider', () => {
|
||||
subscription.load && subscription.load();
|
||||
const lastQueryArgs =
|
||||
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||
expect(lastQueryArgs?.variables?.['pagination']).toEqual({
|
||||
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||
after: '100',
|
||||
first,
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface UpdateCallback<Data, Delta> {
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
pageInfo: PageInfo | null;
|
||||
totalCount?: number;
|
||||
}
|
||||
): void;
|
||||
}
|
||||
@@ -39,7 +40,9 @@ export interface Reload {
|
||||
(forceReset?: boolean): void;
|
||||
}
|
||||
|
||||
type Pagination = Schema.Pagination;
|
||||
type Pagination = Schema.Pagination & {
|
||||
skip?: number;
|
||||
};
|
||||
|
||||
export interface PageInfo {
|
||||
startCursor?: string;
|
||||
@@ -80,8 +83,12 @@ export interface Append<Data> {
|
||||
data: Data | null,
|
||||
insertionData: Data | null,
|
||||
insertionPageInfo: PageInfo | null,
|
||||
pagination?: Pagination
|
||||
): Data | null;
|
||||
pagination?: Pagination,
|
||||
totalCount?: number
|
||||
): {
|
||||
data: Data | null;
|
||||
totalCount?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetData<QueryData, Data, Variables> {
|
||||
@@ -92,6 +99,10 @@ interface GetPageInfo<QueryData> {
|
||||
(queryData: QueryData): PageInfo | null;
|
||||
}
|
||||
|
||||
interface GetTotalCount<QueryData> {
|
||||
(queryData: QueryData): number | undefined;
|
||||
}
|
||||
|
||||
interface GetDelta<SubscriptionData, Delta, Variables> {
|
||||
(
|
||||
subscriptionData: SubscriptionData,
|
||||
@@ -108,32 +119,44 @@ export interface Edge<T extends Node> extends Cursor {
|
||||
node: T;
|
||||
}
|
||||
|
||||
export function defaultAppend<T extends Cursor>(
|
||||
data: T[] | null,
|
||||
insertionData: T[] | null,
|
||||
export function defaultAppend<Data>(
|
||||
data: Data | null,
|
||||
insertionData: Data | null,
|
||||
insertionPageInfo: PageInfo | null,
|
||||
pagination?: Pagination
|
||||
pagination?: Pagination,
|
||||
totalCount?: number
|
||||
) {
|
||||
if (data && insertionData && insertionPageInfo) {
|
||||
if (!(data instanceof Array) || !(insertionData instanceof Array)) {
|
||||
throw new Error(
|
||||
'data needs to be instance of Array[] when using pagination'
|
||||
'data needs to be instance of Edge[] when using pagination'
|
||||
);
|
||||
}
|
||||
if (pagination?.after) {
|
||||
if (data[data.length - 1].cursor === pagination?.after) {
|
||||
return [...data, ...insertionData];
|
||||
}
|
||||
const cursors = data.map((item) => item && item.cursor);
|
||||
const startIndex = cursors.lastIndexOf(pagination.after);
|
||||
if (startIndex !== -1) {
|
||||
const start = startIndex + 1;
|
||||
const updatedData = [...data.slice(0, start), ...insertionData];
|
||||
return updatedData;
|
||||
const start = startIndex + 1 + (pagination.skip ?? 0);
|
||||
const end = start + insertionData.length;
|
||||
let updatedData = [
|
||||
...data.slice(0, start),
|
||||
...insertionData,
|
||||
...data.slice(end),
|
||||
];
|
||||
if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) {
|
||||
// adjust totalCount if last page is shorter or longer than expected
|
||||
totalCount = end;
|
||||
updatedData = updatedData.slice(0, end);
|
||||
}
|
||||
return {
|
||||
data: updatedData,
|
||||
// increase totalCount if last page is longer than expected
|
||||
totalCount: totalCount && Math.max(updatedData.length, totalCount),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
return { data, totalCount };
|
||||
}
|
||||
|
||||
interface DataProviderParams<
|
||||
@@ -152,6 +175,7 @@ interface DataProviderParams<
|
||||
getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
|
||||
pagination?: {
|
||||
getPageInfo: GetPageInfo<QueryData>;
|
||||
getTotalCount?: GetTotalCount<QueryData>;
|
||||
append: Append<Data>;
|
||||
first: number;
|
||||
};
|
||||
@@ -221,6 +245,7 @@ function makeDataProviderInternal<
|
||||
let client: ApolloClient<object>;
|
||||
let subscription: Subscription[] | undefined;
|
||||
let pageInfo: PageInfo | null = null;
|
||||
let totalCount: number | undefined;
|
||||
|
||||
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext
|
||||
const notify = (
|
||||
@@ -233,6 +258,7 @@ function makeDataProviderInternal<
|
||||
loading,
|
||||
loaded,
|
||||
pageInfo,
|
||||
totalCount,
|
||||
...updateData,
|
||||
});
|
||||
};
|
||||
@@ -275,41 +301,59 @@ function makeDataProviderInternal<
|
||||
}
|
||||
});
|
||||
|
||||
const load = async () => {
|
||||
const load = async (start?: number) => {
|
||||
if (!pagination) {
|
||||
return Promise.reject();
|
||||
}
|
||||
if (!pageInfo?.hasNextPage) {
|
||||
return null;
|
||||
}
|
||||
const paginationVariables: Pagination = {
|
||||
first: pagination.first,
|
||||
after: pageInfo?.endCursor,
|
||||
};
|
||||
if (data) {
|
||||
const endCursor = (data as Cursor[])[(data as Cursor[]).length - 1]
|
||||
.cursor;
|
||||
if (endCursor) {
|
||||
paginationVariables.after = endCursor;
|
||||
if (start !== undefined && data instanceof Array) {
|
||||
if (!start) {
|
||||
paginationVariables.after = undefined;
|
||||
} else if (data && data[start - 1]) {
|
||||
paginationVariables.after = (data[start - 1] as Cursor).cursor;
|
||||
} else {
|
||||
let skip = 1;
|
||||
while (!data[start - 1 - skip] && skip <= start) {
|
||||
skip += 1;
|
||||
}
|
||||
paginationVariables.skip = skip;
|
||||
if (skip === start) {
|
||||
paginationVariables.after = undefined;
|
||||
} else {
|
||||
paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor;
|
||||
}
|
||||
}
|
||||
} else if (!pageInfo?.hasNextPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await call(paginationVariables);
|
||||
|
||||
const insertionData = getData(res.data, variables);
|
||||
const insertionPageInfo = pagination.getPageInfo(res.data);
|
||||
data = pagination.append(
|
||||
({ data, totalCount } = pagination.append(
|
||||
data,
|
||||
insertionData,
|
||||
insertionPageInfo,
|
||||
paginationVariables
|
||||
);
|
||||
paginationVariables,
|
||||
totalCount
|
||||
));
|
||||
pageInfo = insertionPageInfo;
|
||||
totalCount =
|
||||
(pagination.getTotalCount && pagination.getTotalCount(res.data)) ??
|
||||
totalCount;
|
||||
notifyAll({ insertionData, isInsert: true });
|
||||
return insertionData;
|
||||
};
|
||||
|
||||
const setData = (updatedData: Data | null) => {
|
||||
data = updatedData;
|
||||
if (totalCount !== undefined && data instanceof Array) {
|
||||
totalCount = data.length;
|
||||
}
|
||||
};
|
||||
|
||||
const subscriptionSubscribe = () => {
|
||||
@@ -356,6 +400,16 @@ function makeDataProviderInternal<
|
||||
);
|
||||
}
|
||||
pageInfo = pagination.getPageInfo(res.data);
|
||||
if (pageInfo && !pageInfo.hasNextPage) {
|
||||
totalCount = data.length;
|
||||
} else {
|
||||
totalCount =
|
||||
pagination.getTotalCount && pagination.getTotalCount(res.data);
|
||||
}
|
||||
|
||||
if (data && totalCount && data.length < totalCount) {
|
||||
data.push(...new Array(totalCount - data.length).fill(null));
|
||||
}
|
||||
}
|
||||
// if there was some updates received from subscription during initial query loading apply them on just received data
|
||||
if (update && data && updateQueue && updateQueue.length > 0) {
|
||||
@@ -363,6 +417,9 @@ function makeDataProviderInternal<
|
||||
const delta = updateQueue.shift();
|
||||
if (delta) {
|
||||
setData(update(data, delta, reload, variables));
|
||||
if (totalCount !== undefined && data instanceof Array) {
|
||||
totalCount = data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,7 +590,7 @@ const memoize = <
|
||||
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider
|
||||
* @param getData transforms received query data to format that will be stored in data provider
|
||||
* @param getDelta transforms delta data to format that will be stored in data provider
|
||||
* @param pagination pagination related functions { getPageInfo, append, first }
|
||||
* @param pagination pagination related functions { getPageInfo, getTotalCount, append, first }
|
||||
* @returns Subscribe<Data, Delta> subscribe function
|
||||
* @example
|
||||
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({
|
||||
|
||||
@@ -12,13 +12,23 @@ export interface useDataProviderParams<
|
||||
Variables extends OperationVariables | undefined = undefined
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
|
||||
update?: ({
|
||||
delta,
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
delta?: Delta;
|
||||
data: Data | null;
|
||||
totalCount?: number;
|
||||
}) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
insertionData?: Data | null;
|
||||
data: Data | null;
|
||||
totalCount?: number;
|
||||
}) => boolean;
|
||||
variables: Variables;
|
||||
skipUpdates?: boolean;
|
||||
@@ -46,6 +56,7 @@ export const useDataProvider = <
|
||||
}: useDataProviderParams<Data, Delta, Variables>) => {
|
||||
const client = useApolloClient();
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [totalCount, setTotalCount] = useState<number>();
|
||||
const [loading, setLoading] = useState<boolean>(!skip);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
@@ -90,6 +101,7 @@ export const useDataProvider = <
|
||||
error,
|
||||
loading,
|
||||
insertionData,
|
||||
totalCount,
|
||||
isInsert,
|
||||
isUpdate,
|
||||
loaded,
|
||||
@@ -104,18 +116,19 @@ export const useDataProvider = <
|
||||
(skipUpdatesRef.current ||
|
||||
(!skipUpdatesRef.current &&
|
||||
updateRef.current &&
|
||||
updateRef.current({ delta, data })))
|
||||
updateRef.current({ delta, data, totalCount })))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isInsert &&
|
||||
insertRef.current &&
|
||||
insertRef.current({ insertionData, data })
|
||||
insertRef.current({ insertionData, data, totalCount })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTotalCount(totalCount);
|
||||
setData(data);
|
||||
if (!loading && !isUpdate && updateRef.current) {
|
||||
updateRef.current({ data });
|
||||
@@ -137,6 +150,7 @@ export const useDataProvider = <
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setError(undefined);
|
||||
setTotalCount(undefined);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
@@ -170,6 +184,7 @@ export const useDataProvider = <
|
||||
flush,
|
||||
reload,
|
||||
load,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export const AgGridThemed = ({
|
||||
enableCellTextSelection: true,
|
||||
overlayLoadingTemplate: t('Loading...'),
|
||||
overlayNoRowsTemplate: t('No data'),
|
||||
suppressCellFocus: true,
|
||||
};
|
||||
|
||||
const wrapperClasses = classNames('vega-ag-grid', {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
CellRendererSelectorResult,
|
||||
ICellRendererParams,
|
||||
} from 'ag-grid-community';
|
||||
|
||||
export interface ValueProps {
|
||||
valueFormatted?: {
|
||||
@@ -15,7 +19,7 @@ export const EmptyCell = () => '';
|
||||
export const ProgressBarCell = ({ valueFormatted }: ValueProps) => {
|
||||
return valueFormatted ? (
|
||||
<>
|
||||
<div className="text-right leading-tight font-mono">
|
||||
<div className="flex justify-between leading-tight font-mono">
|
||||
<div>
|
||||
{valueFormatted.low} ({valueFormatted.value}%)
|
||||
</div>
|
||||
@@ -28,3 +32,11 @@ export const ProgressBarCell = ({ valueFormatted }: ValueProps) => {
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const progressBarCellRendererSelector = (
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: ProgressBarCell,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
|
||||
@@ -7,7 +7,6 @@ interface MarketNameCellProps {
|
||||
data?: { id?: string; marketId?: string; market?: { id: string } };
|
||||
idPath?: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
defaultValue?: ReactNode;
|
||||
}
|
||||
|
||||
export const MarketNameCell = ({
|
||||
@@ -27,13 +26,10 @@ export const MarketNameCell = ({
|
||||
},
|
||||
[id, onMarketClick]
|
||||
);
|
||||
if (!value || !data) return null;
|
||||
return onMarketClick ? (
|
||||
if (!data) return null;
|
||||
return (
|
||||
<button onClick={handleOnClick} tabIndex={0}>
|
||||
{value}
|
||||
</button>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>{value}</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => {
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="deal-ticket-warning-margin"
|
||||
testId="dealticket-warning-margin"
|
||||
message={`You may not have enough margin available to open this position. ${addDecimalsFormatNumber(
|
||||
margin,
|
||||
asset.decimals
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ZeroBalanceError = ({
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="deal-ticket-error-message-zero-balance"
|
||||
testId="dealticket-error-message-zero-balance"
|
||||
message={
|
||||
<>
|
||||
{t(
|
||||
|
||||
@@ -7,13 +7,11 @@ import { DealTicket } from './deal-ticket';
|
||||
|
||||
export interface DealTicketContainerProps {
|
||||
marketId: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
onClickCollateral?: () => void;
|
||||
}
|
||||
|
||||
export const DealTicketContainer = ({
|
||||
marketId,
|
||||
onMarketClick,
|
||||
onClickCollateral,
|
||||
}: DealTicketContainerProps) => {
|
||||
const {
|
||||
@@ -49,7 +47,6 @@ export const DealTicketContainer = ({
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
onClickCollateral={onClickCollateral}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
|
||||
@@ -1,70 +1,14 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FeesBreakdown } from '@vegaprotocol/markets';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { getFeeDetailsValues } from '../../hooks/use-fee-deal-ticket-details';
|
||||
import type { FeeDetails } from '../../hooks/use-fee-deal-ticket-details';
|
||||
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
isNumeric,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
import {
|
||||
NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
|
||||
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 {
|
||||
export interface DealTicketFeeDetailProps {
|
||||
label: string;
|
||||
value?: string | null | undefined;
|
||||
symbol: string;
|
||||
indent?: boolean | undefined;
|
||||
labelDescription?: ReactNode;
|
||||
formattedValue?: string;
|
||||
onClick?: () => void;
|
||||
value?: string | number | null;
|
||||
labelDescription?: string | ReactNode;
|
||||
symbol?: string;
|
||||
}
|
||||
|
||||
export const DealTicketFeeDetail = ({
|
||||
@@ -72,322 +16,51 @@ export const DealTicketFeeDetail = ({
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
indent,
|
||||
onClick,
|
||||
formattedValue,
|
||||
}: DealTicketFeeDetailPros) => {
|
||||
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
|
||||
const valueElement = onClick ? (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="text-neutral-500 dark:text-neutral-300"
|
||||
>
|
||||
{displayValue}
|
||||
</button>
|
||||
) : (
|
||||
<div className="text-neutral-500 dark:text-neutral-300">{displayValue}</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
data-testid={
|
||||
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
|
||||
}
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className={classnames(
|
||||
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
>
|
||||
}: DealTicketFeeDetailProps) => (
|
||||
<div className="text-xs mt-2 flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div>{label}</div>
|
||||
</Tooltip>
|
||||
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
|
||||
{valueElement}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface DealTicketFeeDetailsProps {
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
market: Market;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
|
||||
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
||||
}
|
||||
|
||||
export const DealTicketFeeDetails = ({
|
||||
marginAccountBalance,
|
||||
generalAccountBalance,
|
||||
assetSymbol,
|
||||
feeEstimate,
|
||||
market,
|
||||
onMarketClick,
|
||||
notionalSize,
|
||||
positionEstimate,
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId: market.id, partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
});
|
||||
const liquidationEstimate = positionEstimate?.liquidation;
|
||||
const marginEstimate = positionEstimate?.margin;
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
|
||||
const { settlementAsset: asset } =
|
||||
market.tradableInstrument.instrument.product;
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
let marginRequiredBestCase: string | undefined = undefined;
|
||||
let marginRequiredWorstCase: string | undefined = undefined;
|
||||
if (marginEstimate) {
|
||||
if (currentMargins) {
|
||||
marginRequiredBestCase = (
|
||||
BigInt(marginEstimate.bestCase.initialLevel) -
|
||||
BigInt(currentMargins.initialLevel)
|
||||
).toString();
|
||||
if (marginRequiredBestCase.startsWith('-')) {
|
||||
marginRequiredBestCase = '0';
|
||||
}
|
||||
marginRequiredWorstCase = (
|
||||
BigInt(marginEstimate.worstCase.initialLevel) -
|
||||
BigInt(currentMargins.initialLevel)
|
||||
).toString();
|
||||
if (marginRequiredWorstCase.startsWith('-')) {
|
||||
marginRequiredWorstCase = '0';
|
||||
}
|
||||
} else {
|
||||
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
|
||||
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
|
||||
}
|
||||
}
|
||||
|
||||
const totalMarginAvailable = (
|
||||
currentMargins
|
||||
? totalBalance - BigInt(currentMargins.maintenanceLevel)
|
||||
: totalBalance
|
||||
).toString();
|
||||
|
||||
let deductionFromCollateral = null;
|
||||
let projectedMargin = null;
|
||||
if (marginAccountBalance) {
|
||||
const deductionFromCollateralBestCase =
|
||||
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
|
||||
BigInt(marginAccountBalance);
|
||||
|
||||
const deductionFromCollateralWorstCase =
|
||||
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
|
||||
BigInt(marginAccountBalance);
|
||||
|
||||
deductionFromCollateral = (
|
||||
<DealTicketFeeDetail
|
||||
indent
|
||||
label={t('Deduction from collateral')}
|
||||
value={formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol)}
|
||||
/>
|
||||
);
|
||||
projectedMargin = (
|
||||
<DealTicketFeeDetail
|
||||
label={t('Projected margin')}
|
||||
value={formatRange(
|
||||
marginEstimate?.bestCase.initialLevel,
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatRange(
|
||||
marginEstimate?.bestCase.initialLevel,
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={EST_TOTAL_MARGIN_TOOLTIP_TEXT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateFormatted;
|
||||
|
||||
if (liquidationEstimate) {
|
||||
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
|
||||
liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateBestCaseIncludingSellOrders = BigInt(
|
||||
liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateBestCase =
|
||||
liquidationEstimateBestCaseIncludingBuyOrders >
|
||||
liquidationEstimateBestCaseIncludingSellOrders
|
||||
? liquidationEstimateBestCaseIncludingBuyOrders
|
||||
: liquidationEstimateBestCaseIncludingSellOrders;
|
||||
|
||||
const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt(
|
||||
liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateWorstCaseIncludingSellOrders = BigInt(
|
||||
liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateWorstCase =
|
||||
liquidationEstimateWorstCaseIncludingBuyOrders >
|
||||
liquidationEstimateWorstCaseIncludingSellOrders
|
||||
? liquidationEstimateWorstCaseIncludingBuyOrders
|
||||
: liquidationEstimateWorstCaseIncludingSellOrders;
|
||||
liquidationPriceEstimate = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
(liquidationEstimateBestCase > liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals
|
||||
);
|
||||
liquidationPriceEstimateFormatted = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
(liquidationEstimateBestCase > liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
);
|
||||
}
|
||||
|
||||
const onAccountBreakdownDialogClose = useCallback(
|
||||
() => setBreakdownDialog(false),
|
||||
[]
|
||||
);
|
||||
<div className="text-neutral-500 dark:text-neutral-300">{`${value ?? '-'} ${
|
||||
symbol || ''
|
||||
}`}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DealTicketFeeDetails = (props: FeeDetails) => {
|
||||
const details = getFeeDetailsValues(props);
|
||||
return (
|
||||
<div>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Notional')}
|
||||
value={formatValue(notionalSize, assetDecimals)}
|
||||
formattedValue={formatValue(notionalSize, assetDecimals, quantum)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol)}
|
||||
/>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Fees')}
|
||||
value={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||
)}
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Margin required')}
|
||||
value={formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
|
||||
symbol={assetSymbol}
|
||||
/>
|
||||
<DealTicketFeeDetail
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={TOTAL_MARGIN_AVAILABLE(
|
||||
formatValue(generalAccountBalance, assetDecimals, quantum),
|
||||
formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
formatValue(currentMargins?.maintenanceLevel, assetDecimals, quantum),
|
||||
assetSymbol
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<DealTicketFeeDetail
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
|
||||
formattedValue={formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
{projectedMargin}
|
||||
<DealTicketFeeDetail
|
||||
label={t('Liquidation price estimate')}
|
||||
value={liquidationPriceEstimate}
|
||||
formattedValue={liquidationPriceEstimateFormatted}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
|
||||
/>
|
||||
{partyId && (
|
||||
<AccountBreakdownDialog
|
||||
assetId={breakdownDialog ? asset.id : undefined}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
onClose={onAccountBreakdownDialogClose}
|
||||
/>
|
||||
{details.map(
|
||||
({
|
||||
label,
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
indent,
|
||||
formattedValue,
|
||||
}) => (
|
||||
<div
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className={classnames(
|
||||
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div>{label}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
|
||||
<div className="text-neutral-500 dark:text-neutral-300">{`${
|
||||
formattedValue ?? '-'
|
||||
} ${symbol || ''}`}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DealTicketLimitAmount = ({
|
||||
const renderError = () => {
|
||||
if (sizeError) {
|
||||
return (
|
||||
<InputError testId="deal-ticket-error-message-size-limit">
|
||||
<InputError testId="dealticket-error-message-size-limit">
|
||||
{sizeError}
|
||||
</InputError>
|
||||
);
|
||||
@@ -33,7 +33,7 @@ export const DealTicketLimitAmount = ({
|
||||
|
||||
if (priceError) {
|
||||
return (
|
||||
<InputError testId="deal-ticket-error-message-price-limit">
|
||||
<InputError testId="dealticket-error-message-price-limit">
|
||||
{priceError}
|
||||
</InputError>
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ export const DealTicketMarketAmount = ({
|
||||
{sizeError && (
|
||||
<InputError
|
||||
intent="danger"
|
||||
testId="deal-ticket-error-message-size-market"
|
||||
testId="dealticket-error-message-size-market"
|
||||
>
|
||||
{sizeError}
|
||||
</InputError>
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from '@vegaprotocol/positions';
|
||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
import { useEstimateFees } from '../../hooks/use-fee-deal-ticket-details';
|
||||
import { getDerivedPrice } from '../../utils/get-price';
|
||||
import type { OrderInfo } from '@vegaprotocol/types';
|
||||
|
||||
@@ -55,17 +55,17 @@ import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/positions';
|
||||
|
||||
export interface DealTicketProps {
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
submit: (order: OrderSubmission) => void;
|
||||
onClickCollateral?: () => void;
|
||||
}
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
onMarketClick,
|
||||
marketData,
|
||||
submit,
|
||||
onClickCollateral,
|
||||
@@ -146,7 +146,7 @@ export const DealTicket = ({
|
||||
});
|
||||
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
|
||||
const orders = activeOrders
|
||||
? activeOrders.map<OrderInfo>((order) => ({
|
||||
? activeOrders.map<OrderInfo>(({ node: order }) => ({
|
||||
isMarketOrder: order.type === OrderType.TYPE_MARKET,
|
||||
price: order.price,
|
||||
remaining: order.remaining,
|
||||
@@ -176,6 +176,12 @@ export const DealTicket = ({
|
||||
const assetSymbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId: market.id, partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!pubKey) {
|
||||
setError('summary', {
|
||||
@@ -194,8 +200,7 @@ export const DealTicket = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNoBalance =
|
||||
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
||||
const hasNoBalance = !BigInt(generalAccountBalance);
|
||||
if (hasNoBalance) {
|
||||
setError('summary', {
|
||||
message: SummaryValidationType.NoCollateral,
|
||||
@@ -219,7 +224,6 @@ export const DealTicket = ({
|
||||
marketState,
|
||||
marketTradingMode,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
pubKey,
|
||||
setError,
|
||||
clearErrors,
|
||||
@@ -483,7 +487,6 @@ export const DealTicket = ({
|
||||
}
|
||||
/>
|
||||
<DealTicketFeeDetails
|
||||
onMarketClick={onMarketClick}
|
||||
feeEstimate={feeEstimate}
|
||||
notionalSize={notionalSize}
|
||||
assetSymbol={assetSymbol}
|
||||
@@ -491,6 +494,8 @@ export const DealTicket = ({
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
positionEstimate={positionEstimate?.estimatePosition}
|
||||
market={market}
|
||||
currentInitialMargin={currentMargins?.initialLevel}
|
||||
currentMaintenanceMargin={currentMargins?.maintenanceLevel}
|
||||
/>
|
||||
</form>
|
||||
</TinyScroll>
|
||||
@@ -531,7 +536,7 @@ const SummaryMessage = memo(
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
<InputError testId="dealticket-error-message-summary">
|
||||
{
|
||||
'You need to connect your own wallet to start trading on this market'
|
||||
}
|
||||
@@ -580,7 +585,7 @@ const SummaryMessage = memo(
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
<InputError testId="dealticket-error-message-summary">
|
||||
{errorMessage}
|
||||
</InputError>
|
||||
</div>
|
||||
@@ -608,7 +613,7 @@ const SummaryMessage = memo(
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'deal-ticket-warning-auction'}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ExpirySelector = ({
|
||||
min={minDate}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<InputError testId="deal-ticket-error-message-expiry">
|
||||
<InputError testId="dealticket-error-message-expiry">
|
||||
{errorMessage}
|
||||
</InputError>
|
||||
)}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const TimeInForceSelector = ({
|
||||
))}
|
||||
</Select>
|
||||
{errorMessage && (
|
||||
<InputError testId="deal-ticket-error-message-tif">
|
||||
<InputError testId="dealticket-error-message-tif">
|
||||
{renderError(errorMessage)}
|
||||
</InputError>
|
||||
)}
|
||||
|
||||
@@ -83,7 +83,7 @@ export const TypeSelector = ({
|
||||
onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<InputError testId="deal-ticket-error-message-type">
|
||||
<InputError testId="dealticket-error-message-type">
|
||||
{renderError(errorMessage as MarketModeValidationType)}
|
||||
</InputError>
|
||||
)}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './__generated__/EstimateOrder';
|
||||
export * from './use-estimate-fees';
|
||||
export * from './use-fee-deal-ticket-details';
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
|
||||
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
export const useEstimateFees = (
|
||||
order?: OrderSubmissionBody['orderSubmission']
|
||||
) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data } = useEstimateFeesQuery({
|
||||
variables: order && {
|
||||
marketId: order.marketId,
|
||||
partyId: pubKey || '',
|
||||
price: order.price,
|
||||
size: order.size,
|
||||
side: order.side,
|
||||
timeInForce: order.timeInForce,
|
||||
type: order.type,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
skip: !pubKey || !order?.size || !order?.price,
|
||||
});
|
||||
return data?.estimateFees;
|
||||
};
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { formatRange, formatValue } from './deal-ticket-fee-details';
|
||||
import { formatRange, formatValue } from './use-fee-deal-ticket-details';
|
||||
|
||||
describe('formatRange, formatValue', () => {
|
||||
describe('useFeeDealTicketDetails', () => {
|
||||
it.each([
|
||||
{ v: 123000, d: 5, o: '1.23' },
|
||||
{ v: 123000, d: 3, o: '123.00' },
|
||||
@@ -0,0 +1,327 @@
|
||||
import { FeesBreakdown } from '@vegaprotocol/markets';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
} from '../constants';
|
||||
|
||||
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
export const useEstimateFees = (
|
||||
order?: OrderSubmissionBody['orderSubmission']
|
||||
) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data } = useEstimateFeesQuery({
|
||||
variables: order && {
|
||||
marketId: order.marketId,
|
||||
partyId: pubKey || '',
|
||||
price: order.price,
|
||||
size: order.size,
|
||||
side: order.side,
|
||||
timeInForce: order.timeInForce,
|
||||
type: order.type,
|
||||
},
|
||||
skip: !pubKey || !order?.size || !order?.price,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
return data?.estimateFees;
|
||||
};
|
||||
|
||||
export interface FeeDetails {
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
market: Market;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
|
||||
currentInitialMargin?: string;
|
||||
currentMaintenanceMargin?: string;
|
||||
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
||||
}
|
||||
|
||||
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 const getFeeDetailsValues = ({
|
||||
marginAccountBalance,
|
||||
generalAccountBalance,
|
||||
assetSymbol,
|
||||
feeEstimate,
|
||||
market,
|
||||
notionalSize,
|
||||
currentInitialMargin,
|
||||
currentMaintenanceMargin,
|
||||
positionEstimate,
|
||||
}: FeeDetails) => {
|
||||
const liquidationEstimate = positionEstimate?.liquidation;
|
||||
const marginEstimate = positionEstimate?.margin;
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
const quantum =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.quantum;
|
||||
const details: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
formattedValue?: string | null;
|
||||
symbol: string;
|
||||
indent?: boolean;
|
||||
labelDescription?: React.ReactNode;
|
||||
}[] = [
|
||||
{
|
||||
label: t('Notional'),
|
||||
value: formatValue(notionalSize, assetDecimals),
|
||||
formattedValue: formatValue(notionalSize, assetDecimals, quantum),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
{
|
||||
label: t('Fees'),
|
||||
value:
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`,
|
||||
formattedValue:
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`,
|
||||
labelDescription: (
|
||||
<>
|
||||
<span>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||
)}
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
},
|
||||
];
|
||||
let marginRequiredBestCase: string | undefined = undefined;
|
||||
let marginRequiredWorstCase: string | undefined = undefined;
|
||||
if (marginEstimate) {
|
||||
if (currentInitialMargin) {
|
||||
marginRequiredBestCase = (
|
||||
BigInt(marginEstimate.bestCase.initialLevel) -
|
||||
BigInt(currentInitialMargin)
|
||||
).toString();
|
||||
if (marginRequiredBestCase.startsWith('-')) {
|
||||
marginRequiredBestCase = '0';
|
||||
}
|
||||
marginRequiredWorstCase = (
|
||||
BigInt(marginEstimate.worstCase.initialLevel) -
|
||||
BigInt(currentInitialMargin)
|
||||
).toString();
|
||||
if (marginRequiredWorstCase.startsWith('-')) {
|
||||
marginRequiredWorstCase = '0';
|
||||
}
|
||||
} else {
|
||||
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
|
||||
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
|
||||
}
|
||||
}
|
||||
details.push({
|
||||
label: t('Margin required'),
|
||||
formattedValue: formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
value: formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol),
|
||||
});
|
||||
|
||||
const totalMarginAvailable = (
|
||||
currentMaintenanceMargin
|
||||
? totalBalance - BigInt(currentMaintenanceMargin)
|
||||
: totalBalance
|
||||
).toString();
|
||||
|
||||
details.push({
|
||||
indent: true,
|
||||
label: t('Total margin available'),
|
||||
formattedValue: formatValue(totalMarginAvailable, assetDecimals, quantum),
|
||||
value: formatValue(totalMarginAvailable, assetDecimals),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: TOTAL_MARGIN_AVAILABLE(
|
||||
formatValue(generalAccountBalance, assetDecimals, quantum),
|
||||
formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
formatValue(currentMaintenanceMargin, assetDecimals, quantum),
|
||||
assetSymbol
|
||||
),
|
||||
});
|
||||
|
||||
if (marginAccountBalance) {
|
||||
const deductionFromCollateralBestCase =
|
||||
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
|
||||
BigInt(marginAccountBalance);
|
||||
|
||||
const deductionFromCollateralWorstCase =
|
||||
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
|
||||
BigInt(marginAccountBalance);
|
||||
|
||||
details.push({
|
||||
indent: true,
|
||||
label: t('Deduction from collateral'),
|
||||
value: formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals
|
||||
),
|
||||
formattedValue: formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol),
|
||||
});
|
||||
|
||||
details.push({
|
||||
label: t('Projected margin'),
|
||||
value: formatRange(
|
||||
marginEstimate?.bestCase.initialLevel,
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
assetDecimals
|
||||
),
|
||||
formattedValue: formatRange(
|
||||
marginEstimate?.bestCase.initialLevel,
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
});
|
||||
}
|
||||
details.push({
|
||||
label: t('Current margin allocation'),
|
||||
value: formatValue(marginAccountBalance, assetDecimals),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
formattedValue: formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
});
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateFormatted;
|
||||
|
||||
if (liquidationEstimate) {
|
||||
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
|
||||
liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateBestCaseIncludingSellOrders = BigInt(
|
||||
liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateBestCase =
|
||||
liquidationEstimateBestCaseIncludingBuyOrders >
|
||||
liquidationEstimateBestCaseIncludingSellOrders
|
||||
? liquidationEstimateBestCaseIncludingBuyOrders
|
||||
: liquidationEstimateBestCaseIncludingSellOrders;
|
||||
|
||||
const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt(
|
||||
liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateWorstCaseIncludingSellOrders = BigInt(
|
||||
liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '')
|
||||
);
|
||||
const liquidationEstimateWorstCase =
|
||||
liquidationEstimateWorstCaseIncludingBuyOrders >
|
||||
liquidationEstimateWorstCaseIncludingSellOrders
|
||||
? liquidationEstimateWorstCaseIncludingBuyOrders
|
||||
: liquidationEstimateWorstCaseIncludingSellOrders;
|
||||
liquidationPriceEstimate = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
(liquidationEstimateBestCase > liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals
|
||||
);
|
||||
liquidationPriceEstimateFormatted = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
(liquidationEstimateBestCase > liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
);
|
||||
}
|
||||
|
||||
details.push({
|
||||
label: t('Liquidation price estimate'),
|
||||
value: liquidationPriceEstimate,
|
||||
formattedValue: liquidationPriceEstimateFormatted,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
});
|
||||
return details;
|
||||
};
|
||||
@@ -13,24 +13,6 @@ import { Networks } from '../../';
|
||||
jest.mock('../../hooks/use-environment');
|
||||
|
||||
describe('Network switcher', () => {
|
||||
const { location } = window;
|
||||
let hrefSetSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
hrefSetSpy = jest.fn();
|
||||
// @ts-ignore can't set location as optional
|
||||
delete window.location;
|
||||
window.location = {} as Location;
|
||||
Object.defineProperty(window.location, 'href', {
|
||||
// @ts-ignore set cannot take SpyInstance
|
||||
set: hrefSetSpy,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.location = location;
|
||||
});
|
||||
|
||||
it.each`
|
||||
network | label
|
||||
${Networks.CUSTOM} | ${envTriggerMapping[Networks.CUSTOM]}
|
||||
@@ -67,29 +49,21 @@ describe('Network switcher', () => {
|
||||
render(<NetworkSwitcher />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
let links = screen.getAllByRole('link');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]);
|
||||
expect(links[0]).not.toHaveTextContent(t('current'));
|
||||
expect(links[1]).not.toHaveTextContent(t('current'));
|
||||
expect(links[0]).not.toHaveTextContent(t('not available'));
|
||||
expect(links[1]).not.toHaveTextContent(t('not available'));
|
||||
expect(links[2]).toHaveTextContent(t('Propose a network parameter change'));
|
||||
|
||||
const menuitems = screen.getAllByRole('menuitem');
|
||||
|
||||
expect(menuitems[0]).toHaveTextContent('Advanced');
|
||||
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(menuitems[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]);
|
||||
expect(menuitems[0]).not.toHaveTextContent(t('current'));
|
||||
expect(menuitems[1]).not.toHaveTextContent(t('current'));
|
||||
expect(menuitems[0]).not.toHaveTextContent(t('not available'));
|
||||
expect(menuitems[1]).not.toHaveTextContent(t('not available'));
|
||||
expect(menuitems[2]).toHaveTextContent(t('Advanced'));
|
||||
|
||||
await userEvent.click(links[0]);
|
||||
expect(hrefSetSpy).toHaveBeenCalledWith(mainnetUrl);
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
// re open dropdown as clicking an item will close it
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
links = screen.getAllByRole('link');
|
||||
|
||||
await userEvent.click(links[1]);
|
||||
expect(hrefSetSpy).toHaveBeenCalledWith(testnetUrl);
|
||||
expect(links[0]).toHaveAttribute('href', mainnetUrl);
|
||||
expect(links[1]).toHaveAttribute('href', testnetUrl);
|
||||
});
|
||||
|
||||
it('displays the correct selected network on the default dropdown view', async () => {
|
||||
@@ -108,10 +82,10 @@ describe('Network switcher', () => {
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
const menuitems = screen.getAllByRole('menuitem');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[0]).toHaveTextContent(t('current'));
|
||||
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(menuitems[0]).toHaveTextContent(t('current'));
|
||||
});
|
||||
|
||||
it('displays the correct selected network on the default dropdown view when it does not have an associated url', async () => {
|
||||
@@ -129,10 +103,10 @@ describe('Network switcher', () => {
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
const menuitems = screen.getAllByRole('menuitem');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[0]).toHaveTextContent(t('current'));
|
||||
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(menuitems[0]).toHaveTextContent(t('current'));
|
||||
});
|
||||
|
||||
it('displays the correct state for a network without url on the default dropdown view', async () => {
|
||||
@@ -149,59 +123,44 @@ describe('Network switcher', () => {
|
||||
render(<NetworkSwitcher />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[0]).toHaveTextContent(t('not available'));
|
||||
const menuitems = screen.getAllByRole('menuitem');
|
||||
|
||||
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(menuitems[0]).toHaveTextContent(t('not available'));
|
||||
});
|
||||
|
||||
it.each([Networks.MAINNET, Networks.TESTNET, Networks.DEVNET])(
|
||||
'displays the advanced view in the correct state',
|
||||
async (network) => {
|
||||
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
|
||||
[Networks.CUSTOM]: undefined,
|
||||
[Networks.MAINNET]: 'https://main.net',
|
||||
[Networks.TESTNET]: 'https://test.net',
|
||||
[Networks.VALIDATOR_TESTNET]: 'https://validator-test.net',
|
||||
[Networks.DEVNET]: 'https://dev.net',
|
||||
[Networks.STAGNET1]: 'https://stag1.net',
|
||||
};
|
||||
// @ts-ignore Typescript doesn't know about this module being mocked
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_ENV: Networks.DEVNET,
|
||||
VEGA_NETWORKS,
|
||||
}));
|
||||
it('displays the advanced view in the correct state', async () => {
|
||||
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
|
||||
[Networks.CUSTOM]: undefined,
|
||||
[Networks.MAINNET]: 'https://main.net',
|
||||
[Networks.TESTNET]: 'https://test.net',
|
||||
[Networks.VALIDATOR_TESTNET]: 'https://validator-test.net',
|
||||
[Networks.DEVNET]: 'https://dev.net',
|
||||
[Networks.STAGNET1]: 'https://stag1.net',
|
||||
};
|
||||
// @ts-ignore Typescript doesn't know about this module being mocked
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_ENV: Networks.DEVNET,
|
||||
VEGA_NETWORKS,
|
||||
}));
|
||||
|
||||
render(<NetworkSwitcher />);
|
||||
render(<NetworkSwitcher />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('network-switcher'));
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
await userEvent.click(
|
||||
screen.getByRole('menuitem', { name: t('Advanced') })
|
||||
);
|
||||
|
||||
[Networks.MAINNET, Networks.TESTNET, Networks.DEVNET].forEach((network) => {
|
||||
expect(
|
||||
await screen.findByRole('menuitem', { name: t('Advanced') })
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('menuitem', { name: t('Advanced') })
|
||||
);
|
||||
|
||||
screen.getByRole('link', { name: envNameMapping[network] })
|
||||
).toHaveAttribute('href', VEGA_NETWORKS[network]);
|
||||
expect(
|
||||
await screen.findByText(envDescriptionMapping[network])
|
||||
screen.getByText(envDescriptionMapping[network])
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', {
|
||||
name: new RegExp(`^${envNameMapping[network]}`),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('link', {
|
||||
name: new RegExp(`^${envNameMapping[network]}`),
|
||||
})
|
||||
);
|
||||
|
||||
expect(hrefSetSpy).toHaveBeenCalledWith(VEGA_NETWORKS[network]);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('labels the selected network in the advanced view', async () => {
|
||||
const selectedNetwork = Networks.DEVNET;
|
||||
@@ -229,7 +188,7 @@ describe('Network switcher', () => {
|
||||
const label = screen.getByText(`(${t('current')})`);
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
|
||||
expect(label.parentNode?.firstElementChild).toHaveTextContent(
|
||||
envNameMapping[selectedNetwork]
|
||||
);
|
||||
});
|
||||
@@ -258,7 +217,7 @@ describe('Network switcher', () => {
|
||||
const label = screen.getByText('(not available)');
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
|
||||
expect(label.parentNode?.firstElementChild).toHaveTextContent(
|
||||
envNameMapping[Networks.MAINNET]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
Link,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -98,6 +99,7 @@ export const NetworkSwitcher = ({
|
||||
},
|
||||
[setOpen, setAdvancedView]
|
||||
);
|
||||
const menuRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const current = currentNetwork || VEGA_ENV;
|
||||
|
||||
@@ -108,6 +110,7 @@ export const NetworkSwitcher = ({
|
||||
trigger={
|
||||
<DropdownMenuTrigger
|
||||
data-testid="network-switcher"
|
||||
ref={menuRef}
|
||||
className={classNames(
|
||||
'flex justify-between items-center text-sm text-vega-dark-600 dark:text-vega-light-600 py-1 px-2 rounded border border-vega-dark-200 whitespace-nowrap dark:hover:bg-vega-dark-500 hover:bg-vega-light-500',
|
||||
className
|
||||
@@ -120,7 +123,10 @@ export const NetworkSwitcher = ({
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
style={{ minWidth: `${menuRef.current?.offsetWidth || 290}px` }}
|
||||
>
|
||||
{!isAdvancedView && (
|
||||
<>
|
||||
{standardNetworkKeys.map((key) => (
|
||||
@@ -128,16 +134,14 @@ export const NetworkSwitcher = ({
|
||||
key={key}
|
||||
data-testid="network-item"
|
||||
disabled={!VEGA_NETWORKS[key]}
|
||||
role="link"
|
||||
onClick={() =>
|
||||
(window.location.href = VEGA_NETWORKS[key] || '')
|
||||
}
|
||||
>
|
||||
{envNameMapping[key]}
|
||||
<NetworkLabel
|
||||
isCurrent={current === key}
|
||||
isAvailable={!!VEGA_NETWORKS[key]}
|
||||
/>
|
||||
<a href={VEGA_NETWORKS[key]}>
|
||||
{envNameMapping[key]}
|
||||
<NetworkLabel
|
||||
isCurrent={current === key}
|
||||
isAvailable={!!VEGA_NETWORKS[key]}
|
||||
/>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItem
|
||||
@@ -155,17 +159,10 @@ export const NetworkSwitcher = ({
|
||||
{isAdvancedView && (
|
||||
<>
|
||||
{advancedNetworkKeys.map((key) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
data-testid="network-item-advanced"
|
||||
role="link"
|
||||
onClick={() =>
|
||||
(window.location.href = VEGA_NETWORKS[key] || '')
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem key={key} data-testid="network-item-advanced">
|
||||
<div className="w-full flex justify-between gap-2">
|
||||
<div>
|
||||
{envNameMapping[key]}
|
||||
<Link href={VEGA_NETWORKS[key]}>{envNameMapping[key]}</Link>
|
||||
<NetworkLabel
|
||||
isCurrent={current === key}
|
||||
isAvailable={!!VEGA_NETWORKS[key]}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './lib/fills-container';
|
||||
export * from './lib/use-fills-list';
|
||||
export * from './lib/fills-data-provider';
|
||||
export * from './lib/__generated__/Fills';
|
||||
|
||||
@@ -48,32 +48,28 @@ query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
}
|
||||
}
|
||||
|
||||
fragment FillUpdateFields on TradeUpdate {
|
||||
id
|
||||
marketId
|
||||
buyOrder
|
||||
sellOrder
|
||||
buyerId
|
||||
sellerId
|
||||
aggressor
|
||||
price
|
||||
size
|
||||
createdAt
|
||||
type
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
}
|
||||
|
||||
subscription FillsEvent($filter: TradesSubscriptionFilter!) {
|
||||
tradesStream(filter: $filter) {
|
||||
...FillUpdateFields
|
||||
id
|
||||
marketId
|
||||
buyOrder
|
||||
sellOrder
|
||||
buyerId
|
||||
sellerId
|
||||
aggressor
|
||||
price
|
||||
size
|
||||
createdAt
|
||||
type
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-29
@@ -15,8 +15,6 @@ export type FillsQueryVariables = Types.Exact<{
|
||||
|
||||
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
|
||||
|
||||
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
|
||||
|
||||
export type FillsEventSubscriptionVariables = Types.Exact<{
|
||||
filter: Types.TradesSubscriptionFilter;
|
||||
}>;
|
||||
@@ -62,31 +60,6 @@ export const FillEdgeFragmentDoc = gql`
|
||||
cursor
|
||||
}
|
||||
${FillFieldsFragmentDoc}`;
|
||||
export const FillUpdateFieldsFragmentDoc = gql`
|
||||
fragment FillUpdateFields on TradeUpdate {
|
||||
id
|
||||
marketId
|
||||
buyOrder
|
||||
sellOrder
|
||||
buyerId
|
||||
sellerId
|
||||
aggressor
|
||||
price
|
||||
size
|
||||
createdAt
|
||||
type
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const FillsDocument = gql`
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
@@ -134,10 +107,30 @@ export type FillsQueryResult = Apollo.QueryResult<FillsQuery, FillsQueryVariable
|
||||
export const FillsEventDocument = gql`
|
||||
subscription FillsEvent($filter: TradesSubscriptionFilter!) {
|
||||
tradesStream(filter: $filter) {
|
||||
...FillUpdateFields
|
||||
id
|
||||
marketId
|
||||
buyOrder
|
||||
sellOrder
|
||||
buyerId
|
||||
sellerId
|
||||
aggressor
|
||||
price
|
||||
size
|
||||
createdAt
|
||||
type
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
}
|
||||
}
|
||||
${FillUpdateFieldsFragmentDoc}`;
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useFillsEventSubscription__
|
||||
|
||||
@@ -1,34 +1,74 @@
|
||||
import produce from 'immer';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import type { PageInfo, Cursor } from '@vegaprotocol/data-provider';
|
||||
import {} from '@vegaprotocol/utils';
|
||||
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
defaultAppend as append,
|
||||
paginatedCombineDelta as combineDelta,
|
||||
paginatedCombineInsertionData as combineInsertionData,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||
import { marketsProvider } from '@vegaprotocol/markets';
|
||||
import { FillsDocument, FillsEventDocument } from './__generated__/Fills';
|
||||
import type {
|
||||
FillsQuery,
|
||||
FillsQueryVariables,
|
||||
FillFieldsFragment,
|
||||
FillEdgeFragment,
|
||||
FillsEventSubscription,
|
||||
FillUpdateFieldsFragment,
|
||||
FillsEventSubscriptionVariables,
|
||||
} from './__generated__/Fills';
|
||||
|
||||
const update = (
|
||||
data: FillEdgeFragment[] | null,
|
||||
delta: FillsEventSubscription['tradesStream']
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
orderBy(delta, 'createdAt').forEach((node) => {
|
||||
if (draft === null) {
|
||||
return;
|
||||
}
|
||||
const index = draft.findIndex((edge) => edge?.node.id === node.id);
|
||||
if (index !== -1) {
|
||||
if (draft[index]?.node) {
|
||||
Object.assign(draft[index]?.node as FillFieldsFragment, node);
|
||||
}
|
||||
} else {
|
||||
const firstNode = draft[0]?.node;
|
||||
if (
|
||||
(firstNode && node.createdAt >= firstNode.createdAt) ||
|
||||
!firstNode
|
||||
) {
|
||||
const { buyerId, sellerId, marketId, ...trade } = node;
|
||||
draft.unshift({
|
||||
node: {
|
||||
...trade,
|
||||
__typename: 'Trade',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: marketId,
|
||||
},
|
||||
buyer: { id: buyerId, __typename: 'Party' },
|
||||
seller: { id: buyerId, __typename: 'Party' },
|
||||
},
|
||||
cursor: '',
|
||||
__typename: 'TradeEdge',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export type Trade = Omit<FillFieldsFragment, 'market'> & {
|
||||
market?: Market;
|
||||
isLastPlaceholder?: boolean;
|
||||
};
|
||||
export type TradeEdge = Edge<Trade>;
|
||||
|
||||
const getData = (
|
||||
responseData: FillsQuery | null
|
||||
): (FillFieldsFragment & Cursor)[] =>
|
||||
responseData?.trades?.edges.map<FillFieldsFragment & Cursor>((edge) => ({
|
||||
...edge.node,
|
||||
cursor: edge.cursor,
|
||||
})) || [];
|
||||
const getData = (responseData: FillsQuery | null): FillEdgeFragment[] =>
|
||||
responseData?.trades?.edges || [];
|
||||
|
||||
const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
|
||||
responseData?.trades?.pageInfo || null;
|
||||
@@ -36,65 +76,16 @@ const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
|
||||
const getDelta = (subscriptionData: FillsEventSubscription) =>
|
||||
subscriptionData.tradesStream || [];
|
||||
|
||||
const mapFillUpdateToFill = (
|
||||
fillUpdate: FillUpdateFieldsFragment
|
||||
): FillFieldsFragment => {
|
||||
const { buyerId, sellerId, marketId, ...fill } = fillUpdate;
|
||||
return {
|
||||
...fill,
|
||||
__typename: 'Trade',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: marketId,
|
||||
},
|
||||
buyer: { id: buyerId, __typename: 'Party' },
|
||||
seller: { id: buyerId, __typename: 'Party' },
|
||||
};
|
||||
};
|
||||
|
||||
const mapFillUpdateToFillWithMarket =
|
||||
(markets: Record<string, Market>) =>
|
||||
(fillUpdate: FillUpdateFieldsFragment): Trade => {
|
||||
const { market, ...fill } = mapFillUpdateToFill(fillUpdate);
|
||||
return {
|
||||
...fill,
|
||||
market: markets[market.id],
|
||||
};
|
||||
};
|
||||
|
||||
const update = <T extends Omit<FillFieldsFragment, 'market'> & Cursor>(
|
||||
data: T[] | null,
|
||||
delta: ReturnType<typeof getDelta>,
|
||||
variables: FillsQueryVariables,
|
||||
mapDeltaToData: (delta: FillUpdateFieldsFragment) => T
|
||||
): T[] => {
|
||||
const updatedData = data ? [...data] : ([] as T[]);
|
||||
orderBy(delta, 'createdAt', 'desc').forEach((fillUpdate) => {
|
||||
const index = data?.findIndex((fill) => fill.id === fillUpdate.id) ?? -1;
|
||||
if (index !== -1) {
|
||||
updatedData[index] = {
|
||||
...updatedData[index],
|
||||
...mapDeltaToData(fillUpdate),
|
||||
};
|
||||
} else if (!data?.length || fillUpdate.createdAt >= data[0].createdAt) {
|
||||
updatedData.unshift(mapDeltaToData(fillUpdate));
|
||||
}
|
||||
});
|
||||
return updatedData;
|
||||
};
|
||||
|
||||
export const fillsProvider = makeDataProvider<
|
||||
Parameters<typeof getData>['0'],
|
||||
ReturnType<typeof getData>,
|
||||
Parameters<typeof getDelta>['0'],
|
||||
ReturnType<typeof getDelta>,
|
||||
FillsQueryVariables,
|
||||
FillsEventSubscriptionVariables
|
||||
FillsQueryVariables
|
||||
>({
|
||||
query: FillsDocument,
|
||||
subscriptionQuery: FillsEventDocument,
|
||||
update: (data, delta, reload, variables) =>
|
||||
update(data, delta, variables, mapFillUpdateToFill),
|
||||
update,
|
||||
getData,
|
||||
getDelta,
|
||||
pagination: {
|
||||
@@ -102,41 +93,30 @@ export const fillsProvider = makeDataProvider<
|
||||
append,
|
||||
first: 100,
|
||||
},
|
||||
getSubscriptionVariables: ({ filter }) => {
|
||||
const variables: FillsEventSubscriptionVariables = { filter: {} };
|
||||
if (filter) {
|
||||
variables.filter = {
|
||||
partyIds: filter.partyIds,
|
||||
marketIds: filter.marketIds,
|
||||
};
|
||||
}
|
||||
return variables;
|
||||
},
|
||||
});
|
||||
|
||||
export const fillsWithMarketProvider = makeDerivedDataProvider<
|
||||
(TradeEdge | null)[],
|
||||
Trade[],
|
||||
never,
|
||||
FillsQueryVariables
|
||||
>(
|
||||
[
|
||||
fillsProvider,
|
||||
(callback, client) => marketsMapProvider(callback, client, undefined),
|
||||
(callback, client) => marketsProvider(callback, client, undefined),
|
||||
],
|
||||
(partsData, variables, prevData, parts): Trade[] | null => {
|
||||
if (prevData && parts[0].isUpdate) {
|
||||
return update(
|
||||
prevData,
|
||||
parts[0].delta as ReturnType<typeof getDelta>,
|
||||
variables,
|
||||
mapFillUpdateToFillWithMarket(partsData[1] as Record<string, Market>)
|
||||
);
|
||||
}
|
||||
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
|
||||
(trade) => ({
|
||||
...trade,
|
||||
market: (partsData[1] as Record<string, Market>)[trade.market.id],
|
||||
})
|
||||
);
|
||||
}
|
||||
(partsData): (TradeEdge | null)[] =>
|
||||
(partsData[0] as ReturnType<typeof getData>)?.map(
|
||||
(edge) =>
|
||||
edge && {
|
||||
cursor: edge.cursor,
|
||||
node: {
|
||||
...edge.node,
|
||||
market: (partsData[1] as Market[]).find(
|
||||
(market) => market.id === edge.node.market.id
|
||||
),
|
||||
},
|
||||
}
|
||||
) || null,
|
||||
combineDelta<Trade, ReturnType<typeof getDelta>['0']>,
|
||||
combineInsertionData<Trade>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import compact from 'lodash/compact';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FillsTable } from './fills-table';
|
||||
import { useFillsList } from './use-fills-list';
|
||||
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@@ -21,36 +20,31 @@ export const FillsManager = ({
|
||||
storeKey,
|
||||
}: FillsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
|
||||
partyIds: [partyId],
|
||||
};
|
||||
if (marketId) {
|
||||
filter.marketIds = [marketId];
|
||||
}
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
update: ({ data }) => {
|
||||
if (data?.length && gridRef.current?.api) {
|
||||
gridRef.current?.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
variables: { filter },
|
||||
const scrolledToTop = useRef(true);
|
||||
const { data, error } = useFillsList({
|
||||
partyId,
|
||||
marketId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
});
|
||||
|
||||
const bottomPlaceholderProps = useBottomPlaceholder({
|
||||
gridRef,
|
||||
});
|
||||
|
||||
const fills = compact(data).map((e) => e.node);
|
||||
|
||||
return (
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
storeKey={storeKey}
|
||||
{...bottomPlaceholderProps}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
/>
|
||||
<div className="h-full relative">
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={fills}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
storeKey={storeKey}
|
||||
{...bottomPlaceholderProps}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useFillsList } from './use-fills-list';
|
||||
import type { TradeEdge } from './fills-data-provider';
|
||||
|
||||
let mockData = null;
|
||||
let mockDataProviderData = {
|
||||
data: mockData as (TradeEdge | null)[] | null,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
let updateMock: jest.Mock;
|
||||
const mockDataProvider = jest.fn((args) => {
|
||||
updateMock = args.update;
|
||||
return mockDataProviderData;
|
||||
});
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn((args) => mockDataProvider(args)),
|
||||
}));
|
||||
|
||||
describe('useFillsList Hook', () => {
|
||||
const mockRefreshAgGridApi = jest.fn();
|
||||
const partyId = 'partyId';
|
||||
const gridRef = {
|
||||
current: {
|
||||
api: {
|
||||
refreshInfiniteCache: mockRefreshAgGridApi,
|
||||
getModel: () => ({ getType: () => 'infinite' }),
|
||||
},
|
||||
} as unknown as AgGridReact,
|
||||
};
|
||||
const scrolledToTop = {
|
||||
current: false,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return proper dataProvider results', () => {
|
||||
const { result } = renderHook(
|
||||
() => useFillsList({ partyId, gridRef, scrolledToTop }),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toMatchObject({
|
||||
data: null,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
addNewRows: expect.any(Function),
|
||||
getRows: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('return proper mocked results', () => {
|
||||
mockData = [
|
||||
{
|
||||
node: {
|
||||
id: 'data_id_1',
|
||||
},
|
||||
} as unknown as TradeEdge,
|
||||
{
|
||||
node: {
|
||||
id: 'data_id_2',
|
||||
},
|
||||
} as unknown as TradeEdge,
|
||||
];
|
||||
mockDataProviderData = {
|
||||
...mockDataProviderData,
|
||||
data: mockData,
|
||||
loading: false,
|
||||
};
|
||||
const { result } = renderHook(
|
||||
() => useFillsList({ partyId, gridRef, scrolledToTop }),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toMatchObject({
|
||||
data: mockData,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
addNewRows: expect.any(Function),
|
||||
getRows: expect.any(Function),
|
||||
});
|
||||
updateMock({ data: mockData });
|
||||
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
|
||||
updateMock({ data: mockData });
|
||||
expect(mockRefreshAgGridApi).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
import { updateGridData } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { Trade, TradeEdge } from './fills-data-provider';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
|
||||
interface Props {
|
||||
partyId: string;
|
||||
marketId?: string;
|
||||
gridRef: RefObject<AgGridReact>;
|
||||
scrolledToTop: RefObject<boolean>;
|
||||
}
|
||||
|
||||
export const useFillsList = ({
|
||||
partyId,
|
||||
marketId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
}: Props) => {
|
||||
const dataRef = useRef<(TradeEdge | null)[] | null>(null);
|
||||
const totalCountRef = useRef<number | undefined>(undefined);
|
||||
const newRows = useRef(0);
|
||||
const placeholderAdded = useRef(-1);
|
||||
|
||||
const makeBottomPlaceholders = useCallback((trade?: Trade) => {
|
||||
if (!trade) {
|
||||
if (placeholderAdded.current >= 0) {
|
||||
dataRef.current?.splice(placeholderAdded.current, 1);
|
||||
}
|
||||
placeholderAdded.current = -1;
|
||||
} else if (placeholderAdded.current === -1) {
|
||||
dataRef.current?.push({
|
||||
node: { ...trade, id: `${trade?.id}-1`, isLastPlaceholder: true },
|
||||
});
|
||||
placeholderAdded.current = (dataRef.current?.length || 0) - 1;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addNewRows = useCallback(() => {
|
||||
if (newRows.current === 0) {
|
||||
return;
|
||||
}
|
||||
if (totalCountRef.current !== undefined) {
|
||||
totalCountRef.current += newRows.current;
|
||||
}
|
||||
newRows.current = 0;
|
||||
gridRef.current?.api?.refreshInfiniteCache();
|
||||
}, [gridRef]);
|
||||
|
||||
const update = useCallback(
|
||||
({
|
||||
data,
|
||||
delta,
|
||||
}: {
|
||||
data: (TradeEdge | null)[] | null;
|
||||
delta?: Trade[];
|
||||
}) => {
|
||||
if (dataRef.current?.length) {
|
||||
if (!scrolledToTop.current) {
|
||||
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
||||
if (createdAt) {
|
||||
newRows.current += (delta || []).filter(
|
||||
(trade) => trade.createdAt > createdAt
|
||||
).length;
|
||||
}
|
||||
}
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
}
|
||||
dataRef.current = data;
|
||||
return false;
|
||||
},
|
||||
[gridRef, scrolledToTop]
|
||||
);
|
||||
|
||||
const insert = useCallback(
|
||||
({
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
data: (TradeEdge | null)[] | null;
|
||||
totalCount?: number;
|
||||
}) => {
|
||||
totalCountRef.current = totalCount;
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
|
||||
const filter: Types.TradesFilter & Types.TradesSubscriptionFilter = {
|
||||
partyIds: [partyId],
|
||||
};
|
||||
if (marketId) {
|
||||
filter.marketIds = [marketId];
|
||||
}
|
||||
|
||||
const { data, error, loading, load, totalCount, reload } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
update,
|
||||
insert,
|
||||
variables: { filter },
|
||||
});
|
||||
totalCountRef.current = totalCount;
|
||||
|
||||
const getRows = makeInfiniteScrollGetRows<TradeEdge>(
|
||||
dataRef,
|
||||
totalCountRef,
|
||||
load,
|
||||
newRows
|
||||
);
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
addNewRows,
|
||||
getRows,
|
||||
reload,
|
||||
makeBottomPlaceholders,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,22 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { assetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { assetsProvider } from '@vegaprotocol/assets';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||
import { marketsProvider } from '@vegaprotocol/markets';
|
||||
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
|
||||
import { updateGridData } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
useDataProvider,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import produce from 'immer';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { Filter } from './ledger-manager';
|
||||
import type {
|
||||
LedgerEntriesQuery,
|
||||
LedgerEntriesQueryVariables,
|
||||
@@ -20,58 +30,171 @@ export type LedgerEntry = LedgerEntryFragment & {
|
||||
marketReceiver: Market | null | undefined;
|
||||
};
|
||||
|
||||
type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number];
|
||||
|
||||
const isLedgerEntryEdge = (entry: Edge): entry is NonNullable<Edge> =>
|
||||
entry !== null;
|
||||
|
||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||
return (
|
||||
responseData?.ledgerEntries?.edges
|
||||
.filter(isLedgerEntryEdge)
|
||||
.map((edge) => edge.node) || []
|
||||
);
|
||||
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
||||
export type AggregatedLedgerEntriesNode = Omit<
|
||||
AggregatedLedgerEntriesEdge,
|
||||
'node'
|
||||
> & {
|
||||
node: LedgerEntry;
|
||||
};
|
||||
|
||||
const ledgerEntriesOnlyProvider = makeDataProvider<
|
||||
LedgerEntriesQuery,
|
||||
ReturnType<typeof getData>,
|
||||
never,
|
||||
never,
|
||||
LedgerEntriesQueryVariables
|
||||
>({
|
||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||
return responseData?.ledgerEntries?.edges || [];
|
||||
};
|
||||
|
||||
export const update = (
|
||||
data: ReturnType<typeof getData> | null,
|
||||
delta: ReturnType<typeof getData>,
|
||||
reload: () => void,
|
||||
variables: LedgerEntriesQueryVariables
|
||||
) => {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
return produce(data, (draft) => {
|
||||
// A single update can contain the same order with multiple updates, so we need to find
|
||||
// the latest version of the order and only update using that
|
||||
const incoming = uniqBy(
|
||||
orderBy(delta, (entry) => entry?.node.vegaTime, 'desc'),
|
||||
'id'
|
||||
);
|
||||
|
||||
// Add or update incoming orders
|
||||
incoming.reverse().forEach((node) => {
|
||||
const index = draft.findIndex(
|
||||
(edge) => edge?.node.vegaTime === node?.node.vegaTime
|
||||
);
|
||||
const newer =
|
||||
draft.length === 0 || node?.node.vegaTime >= draft[0]?.node.vegaTime;
|
||||
let doesFilterPass = true;
|
||||
if (
|
||||
doesFilterPass &&
|
||||
variables?.dateRange?.start &&
|
||||
new Date(node?.node.vegaTime) <= new Date(variables?.dateRange?.start)
|
||||
) {
|
||||
doesFilterPass = false;
|
||||
}
|
||||
if (
|
||||
doesFilterPass &&
|
||||
variables?.dateRange?.end &&
|
||||
new Date(node?.node.vegaTime) >= new Date(variables?.dateRange?.end)
|
||||
) {
|
||||
doesFilterPass = false;
|
||||
}
|
||||
if (index !== -1) {
|
||||
if (doesFilterPass) {
|
||||
// Object.assign(draft[index]?.node, node?.node);
|
||||
if (newer) {
|
||||
draft.unshift(...draft.splice(index, 1));
|
||||
}
|
||||
} else {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
} else if (newer && doesFilterPass) {
|
||||
draft.unshift(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const ledgerEntriesOnlyProvider = makeDataProvider({
|
||||
query: LedgerEntriesDocument,
|
||||
getData,
|
||||
getDelta: getData,
|
||||
update,
|
||||
additionalContext: {
|
||||
isEnlargedTimeout: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const ledgerEntriesProvider = makeDerivedDataProvider<
|
||||
LedgerEntry[],
|
||||
never,
|
||||
AggregatedLedgerEntriesNode[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
LedgerEntriesQueryVariables
|
||||
>(
|
||||
[
|
||||
ledgerEntriesOnlyProvider,
|
||||
(callback, client) => assetsMapProvider(callback, client, undefined),
|
||||
(callback, client) => marketsMapProvider(callback, client, undefined),
|
||||
(callback, client) => assetsProvider(callback, client, undefined),
|
||||
(callback, client) => marketsProvider(callback, client, undefined),
|
||||
],
|
||||
(partsData) => {
|
||||
const entries = partsData[0] as ReturnType<typeof getData>;
|
||||
const assets = partsData[1] as Record<string, Asset>;
|
||||
const markets = partsData[1] as Record<string, Market>;
|
||||
return entries.map((entry) => {
|
||||
const asset = entry.assetId
|
||||
? (assets as Record<string, Asset>)[entry.assetId]
|
||||
: null;
|
||||
const marketSender = entry.fromAccountMarketId
|
||||
? markets[entry.fromAccountMarketId]
|
||||
: null;
|
||||
const marketReceiver = entry.toAccountMarketId
|
||||
? markets[entry.toAccountMarketId]
|
||||
: null;
|
||||
return { ...entry, asset, marketSender, marketReceiver };
|
||||
([entries, assets, markets]) => {
|
||||
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
|
||||
const entry = edge.node;
|
||||
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
|
||||
const marketSender = markets.find(
|
||||
(market: Market) => market.id === entry.fromAccountMarketId
|
||||
);
|
||||
const marketReceiver = markets.find(
|
||||
(market: Market) => market.id === entry.toAccountMarketId
|
||||
);
|
||||
const cursor = edge?.cursor;
|
||||
return {
|
||||
node: { ...entry, asset, marketSender, marketReceiver },
|
||||
cursor,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
partyId: string;
|
||||
filter?: Filter;
|
||||
gridRef: RefObject<AgGridReact>;
|
||||
}
|
||||
|
||||
export const useLedgerEntriesDataProvider = ({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
}: Props) => {
|
||||
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
|
||||
const totalCountRef = useRef<number>();
|
||||
|
||||
const variables = useMemo<LedgerEntriesQueryVariables>(
|
||||
() => ({
|
||||
partyId,
|
||||
dateRange: filter?.vegaTime?.value,
|
||||
pagination: {
|
||||
first: 5000,
|
||||
},
|
||||
}),
|
||||
[partyId, filter?.vegaTime?.value]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
|
||||
const insert = useCallback(
|
||||
({
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
data: AggregatedLedgerEntriesEdge[] | null;
|
||||
totalCount?: number;
|
||||
}) => {
|
||||
totalCountRef.current = totalCount;
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
|
||||
const { data, error, loading, load, totalCount, reload } = useDataProvider({
|
||||
dataProvider: ledgerEntriesProvider,
|
||||
update,
|
||||
insert,
|
||||
variables,
|
||||
skip: !variables.partyId,
|
||||
});
|
||||
totalCountRef.current = totalCount;
|
||||
|
||||
const getRows = makeInfiniteScrollGetRows<AggregatedLedgerEntriesEdge>(
|
||||
dataRef,
|
||||
totalCountRef,
|
||||
load
|
||||
);
|
||||
return { loading, error, data, getRows, reload };
|
||||
};
|
||||
|
||||
@@ -2,12 +2,10 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef, useState, useMemo } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { subDays, formatRFC3339 } from 'date-fns';
|
||||
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
|
||||
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
|
||||
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
|
||||
import { LedgerTable } from './ledger-table';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
import { LedgerExportLink } from './ledger-export-link';
|
||||
|
||||
@@ -28,21 +26,10 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [filter, setFilter] = useState<Filter>(defaultFilter);
|
||||
|
||||
const variables = useMemo<LedgerEntriesQueryVariables>(
|
||||
() => ({
|
||||
partyId,
|
||||
dateRange: filter?.vegaTime?.value,
|
||||
pagination: {
|
||||
first: 5000,
|
||||
},
|
||||
}),
|
||||
[partyId, filter?.vegaTime?.value]
|
||||
);
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: ledgerEntriesProvider,
|
||||
variables,
|
||||
skip: !variables.partyId,
|
||||
const { data, error } = useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
|
||||
@@ -50,15 +37,20 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
|
||||
setFilter(updatedFilter);
|
||||
}, []);
|
||||
|
||||
// allow passing undefined to grid so that loading state is shown
|
||||
const extractedData = data?.map((item) => item.node);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<LedgerTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
rowData={extractedData}
|
||||
onFilterChanged={onFilterChanged}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No entries')}
|
||||
/>
|
||||
{data && <LedgerExportLink entries={data} partyId={partyId} />}
|
||||
{extractedData && (
|
||||
<LedgerExportLink entries={extractedData} partyId={partyId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
# MarketLp
|
||||
|
||||
query MarketLp($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
code
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data {
|
||||
market {
|
||||
id
|
||||
}
|
||||
marketTradingMode
|
||||
suppliedStake
|
||||
openInterest
|
||||
targetStake
|
||||
trigger
|
||||
marketValueProxy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Liquidity Provisions
|
||||
|
||||
fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
|
||||
@@ -3,6 +3,13 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketLpQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarketLpQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', code: string, name: string, product: { __typename?: 'Future', settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } } } }, data?: { __typename?: 'MarketData', marketTradingMode: Types.MarketTradingMode, suppliedStake?: string | null, openInterest: string, targetStake?: string | null, trigger: Types.AuctionTrigger, marketValueProxy: string, market: { __typename?: 'Market', id: string } } | null } | null };
|
||||
|
||||
export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } };
|
||||
|
||||
export type LiquidityProvisionsQueryVariables = Types.Exact<{
|
||||
@@ -58,6 +65,69 @@ export const LiquidityProviderFeeShareFieldsFragmentDoc = gql`
|
||||
averageEntryValuation
|
||||
}
|
||||
`;
|
||||
export const MarketLpDocument = gql`
|
||||
query MarketLp($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
code
|
||||
name
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data {
|
||||
market {
|
||||
id
|
||||
}
|
||||
marketTradingMode
|
||||
suppliedStake
|
||||
openInterest
|
||||
targetStake
|
||||
trigger
|
||||
marketValueProxy
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMarketLpQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMarketLpQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMarketLpQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMarketLpQuery({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMarketLpQuery(baseOptions: Apollo.QueryHookOptions<MarketLpQuery, MarketLpQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MarketLpQuery, MarketLpQueryVariables>(MarketLpDocument, options);
|
||||
}
|
||||
export function useMarketLpLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarketLpQuery, MarketLpQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MarketLpQuery, MarketLpQueryVariables>(MarketLpDocument, options);
|
||||
}
|
||||
export type MarketLpQueryHookResult = ReturnType<typeof useMarketLpQuery>;
|
||||
export type MarketLpLazyQueryHookResult = ReturnType<typeof useMarketLpLazyQuery>;
|
||||
export type MarketLpQueryResult = Apollo.QueryResult<MarketLpQuery, MarketLpQueryVariables>;
|
||||
export const LiquidityProvisionsDocument = gql`
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user