Compare commits

...
Author SHA1 Message Date
Matthew Russell 8ef3c25c7c chore: use shorthand version for merging object 2023-06-15 11:46:47 -07:00
Matthew Russell 7dd41fc59e chore: add no cache for positions and accoutns queries 2023-06-15 11:44:00 -07:00
Matthew Russell 67a1f10b1c chore: add no cache for trades 2023-06-15 10:55:24 -07:00
Matthew Russell 2eee4c071f chore: add merge function for stats, organised ignored update types 2023-06-15 10:45:20 -07:00
Edd 5a90ebe12e feat(explorer): transaction list filtering 2023-06-15 17:10:17 +01:00
Mikołaj Młodzikowski 6be117086a feat(ci): align releases for all apps (#4111) 2023-06-15 09:03:59 -07:00
daro-maj 9c6dd9f254 test(trading): markets selector ACs update (#4107) 2023-06-15 12:52:18 +02:00
Mikołaj Młodzikowski a675576a52 feat(ci): turn off nx cloud (#4078) 2023-06-15 11:32:04 +01:00
Matthew RussellandBartłomiej Głownia 98d248e02e feat(accounts): accounts table quantum formatting (#4076)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-06-15 11:31:43 +01:00
Joe Tsang c8d55c6293 chore(governance): fix for withdrawal tests (#4047) 2023-06-15 09:15:52 +01:00
Sam Keen 3467feb3e5 feat(governance): add 'all proposals' link on proposal details page (#4074) 2023-06-14 15:35:12 +01:00
daro-maj bdf1663709 test(trading): update e2e tests for acs - closed all and proposed markets (#4094) 2023-06-14 16:00:04 +02:00
daro-majandBartłomiej Głownia d775573543 test(trading): add e2e tests for liquidity 5002-LIQP (#4082)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-06-14 14:55:32 +02:00
Mikołaj Młodzikowski c0fdc8d570 feat(ci): add deploy & build info to ui-toolkit README 2023-06-14 13:35:50 +02:00
Mikołaj Młodzikowski a9ebb36b9b feat(ci): deploy ui-toolkit to s3 (#4089) 2023-06-14 10:37:41 +00:00
Radosław Szpiech f6c0082f2d test(trading): #3506 orderbook AC coverage (#4088) 2023-06-14 12:32:33 +02:00
m.ray 931ad75cc9 fix(trading): oracle markets matching (#4080) 2023-06-14 11:06:35 +02:00
m.ray f25a58a3c3 fix(liquidity): update liquidity table row id (#4081) 2023-06-13 14:24:29 -07:00
Mikołaj Młodzikowski 37340b4dc3 feat(ci): modify static content to trigger push 2023-06-13 13:43:54 +02:00
Mikołaj Młodzikowski 17ba3eb351 feat(ci): setup static on s3 (#4075) 2023-06-13 11:29:05 +00:00
Matthew Russell 8f966a4ba6 chore: remove copied dir 2023-06-12 12:38:55 -07:00
Sam Keen cc7f403718 feat(governance): hide proposal details for freeform proposals (#4073) 2023-06-12 12:31:03 +01:00
MaciekandMatthew Russell 9184c2374c fix(markets): back with updating by grid api (#4059)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-10 07:49:10 -07:00
MaciekandMatthew Russell 37247a504a fix(trading): update click area in network switcher (#4057)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-10 07:48:48 -07:00
Bartłomiej Głownia 2e7a6a6458 chore(trading): cleanup paginated data solution, improve performance (#4036) 2023-06-09 19:02:23 -07:00
Ben bf6c13f523 test(trading): 3502-POSI-e2e-tests (#4064) 2023-06-09 17:37:50 -07:00
Matthew Russell a6f3a08d2f chore(trading,governance,explorer): release v0.20.18-core-0.71.8 2023-06-09 15:50:54 -07:00
Matthew Russell f13e456a29 ci(trading,governance,explorer): remove affected project text manipulation 2023-06-09 10:56:42 -07:00
Sam Keen 5cab040189 fix(governance): temporarily skipped flaky staking tests (#4068) 2023-06-09 16:13:44 +01:00
93 changed files with 3877 additions and 2726 deletions
@@ -9,6 +9,11 @@ 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
@@ -27,14 +32,16 @@ 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 }}"
until docker pull vegaprotocol/trading:${{ github.event.release.tag_name }}; do
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
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:${{ github.event.release.tag_name }} cat /ipfs-hash > ipfs-hash
docker run --rm vegaprotocol/trading:$commit 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"
+19 -8
View File
@@ -6,8 +6,6 @@ on:
- release/*
- develop
- main
tags:
- v*
pull_request:
types:
- opened
@@ -136,22 +134,35 @@ jobs:
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
projects="$(echo $projects_e2e | sed 's|-e2e||g')"
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
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
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects+=' "multisig-signer" '
fi
if [[ "${{ github.ref }}" =~ .*develop$ ]]; then
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
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
+54 -37
View File
@@ -31,6 +31,7 @@ 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
@@ -39,7 +40,7 @@ jobs:
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -74,10 +75,16 @@ 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
@@ -106,14 +113,15 @@ jobs:
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
flags="--env=${{ env.ENV_NAME }}"
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 }}
@@ -148,7 +156,9 @@ jobs:
- name: Publish dist as docker image (ghcr)
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }}
continue-on-error: true
id: ghcr-push
if: ${{ github.event_name == 'pull_request' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -161,7 +171,9 @@ jobs:
- name: Publish dist as docker image (docker hub)
uses: docker/build-push-action@v3
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -170,13 +182,41 @@ jobs:
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.ref_name }}
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 }}:mainnet
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') && ( matrix.app != 'trading' || (matrix.app == 'trading' && !endsWith(github.ref, 'main') ) ) }}
# 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') ) ) }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -194,7 +234,8 @@ jobs:
number: ${{ github.event.number }}
- name: Trigger fleek deployment
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
# 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') }}
run: |
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
@@ -209,7 +250,7 @@ jobs:
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
@@ -218,7 +259,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' && startsWith(github.ref, 'refs/tags/v') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
@@ -254,27 +295,3 @@ 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}
+1 -1
View File
@@ -5,7 +5,7 @@ NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
NX_BLOCK_EXPLORER=https://be.vega.community/rest
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
+1 -1
View File
@@ -70,7 +70,7 @@
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/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.71.4/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
@@ -1,29 +1,13 @@
import WS from 'jest-websocket-mock';
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 { render, screen } from '@testing-library/react';
import { BlocksRefetch } from './blocks-refetch';
const BlocksRefetchInWebsocketProvider = ({
callback,
mocketLocation,
}: {
callback: () => null;
mocketLocation: string;
}) => {
const contextShape = useWebSocket(mocketLocation);
return (
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
<BlocksRefetch refetch={callback} />
</TendermintWebsocketContext.Provider>
);
return <BlocksRefetch refetch={callback} />;
};
describe('Blocks refetch', () => {
@@ -32,111 +16,8 @@ describe('Blocks refetch', () => {
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
render(<BlocksRefetchInWebsocketProvider callback={() => null} />);
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,36 +1,19 @@
import { useState, useEffect } from 'react';
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { Button, Icon } 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 (
<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>
<Button onClick={refresh} data-testid="refresh" size="xs">
<Icon name="refresh" className="!align-baseline mr-2" size={3} />
{t('Load new')}
</Button>
);
};
@@ -0,0 +1,24 @@
import { t } from '@vegaprotocol/i18n';
export interface FilterLabelProps {
filters: Set<string>;
}
/**
* Renders the list (currently limited to 1) of filters set by the
* Transaction Filter
*/
export function FilterLabel({ filters }: FilterLabelProps) {
if (!filters || filters.size !== 1) {
return <span className="uppercase">{t('Filter')}</span>;
}
return (
<div>
<span className="uppercase">{t('Filters')}:</span>&nbsp;
<code className="bg-vega-light-150 px-2 rounded-md capitalize">
{Array.from(filters)[0]}
</code>
</div>
);
}
@@ -0,0 +1,164 @@
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 from 'react';
import React, { useEffect, useRef } from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/i18n';
@@ -69,6 +69,17 @@ 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) {
@@ -110,6 +121,7 @@ export const TxsInfiniteList = ({
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
ref={infiniteLoaderRef}
>
{({ onItemsRendered, ref }) => (
<List
+9 -1
View File
@@ -33,7 +33,7 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
// Hacky fix for param as array
let urlAsString = url.toString();
if (filters) {
urlAsString += '&' + filters;
urlAsString += '&' + filters.replace(' ', '%20');
}
return urlAsString;
@@ -65,6 +65,14 @@ export const useTxsData = ({ limit, filters }: IUseTxsData) => {
}
}, [setTxsState, data]);
useEffect(() => {
setTxsState((prev) => ({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
}));
}, [filters]);
const loadTxs = useCallback(() => {
return refetch({
limit: limit,
@@ -5,17 +5,43 @@ import { TxsInfiniteList } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const BE_TXS_PER_REQUEST = 20;
import { useState } from 'react';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
const BE_TXS_PER_REQUEST = 15;
export const TxsList = () => {
useDocumentTitle(['Transactions']);
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({ limit: BE_TXS_PER_REQUEST });
return (
<section className="md:p-2 lg:p-4 xl:p-6">
<section className="md:p-2 lg:p-4 xl:p-6 relative">
<RouteTitle>{t('Transactions')}</RouteTitle>
<BlocksRefetch refetch={refreshTxs} />
<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,
});
return (
<>
<menu className="mb-2">
<BlocksRefetch refetch={refreshTxs} />
<TxsFilter filters={filters} setFilters={setFilters} />
</menu>
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
@@ -24,6 +50,6 @@ export const TxsList = () => {
error={error}
className="mb-28"
/>
</section>
</>
);
};
+428 -421
View File
File diff suppressed because it is too large Load Diff
@@ -63,7 +63,7 @@ context(
vegaWalletSetSpecifiedApprovalAmount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
describe.skip('Eth wallet - contains VEGA tokens', function () {
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
@@ -381,6 +381,8 @@ 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,7 +69,10 @@ context(
it('Unable to submit withdrawal with invalid fields', function () {
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should('have.length', 1);
@@ -93,7 +96,10 @@ context(
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
@@ -159,7 +165,10 @@ context(
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(ethAddressInput).should('be.empty');
cy.getByTestId(amountInput).click().type('110');
cy.getByTestId(submitWithdrawalButton).click();
@@ -219,7 +228,10 @@ context(
it('Should be able to see withdrawal details from toast', function () {
cy.getByTestId(withdraw).click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
@@ -274,7 +286,10 @@ context(
cy.connectPublicKey(vegaWalletPubKey);
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(amountInput).click().type('100');
cy.pause();
@@ -824,5 +824,7 @@
"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"
"learnMore": "Learn more",
"AllValidators": "All validators",
"AllProposals": "All proposals"
}
@@ -1,3 +1,4 @@
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
@@ -41,19 +42,35 @@ 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();
render(
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
);
renderComponent(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();
render(
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
);
renderComponent(proposal);
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
@@ -80,8 +97,7 @@ it('renders whitelist section if proposal is new asset and source is erc20', asy
},
},
});
render(
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
);
renderComponent(proposal);
expect(screen.getByTestId('proposal-list-asset')).toBeInTheDocument();
});
@@ -2,7 +2,7 @@ import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -13,6 +13,10 @@ 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',
@@ -29,6 +33,7 @@ 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,
@@ -81,6 +86,15 @@ 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">
@@ -102,7 +116,8 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div>
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' && (
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
<div className="mb-4">
<ProposalTerms data={proposal.terms} />
</div>
@@ -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('All validators')}
{t('AllValidators')}
</Link>
</div>
<Heading
@@ -7,31 +7,30 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import {
getFeeLevels,
sumLiquidityCommitted,
marketLiquidityDataProvider,
lpAggregatedDataProvider,
} from '@vegaprotocol/liquidity';
import type { MarketLpQuery } from '@vegaprotocol/liquidity';
import { marketWithDataProvider } from '@vegaprotocol/markets';
import type { MarketWithData } from '@vegaprotocol/markets';
import { Market } from './market';
import { Header } from './header';
import { LPProvidersGrid } from './providers';
const formatMarket = (data: MarketLpQuery) => {
const formatMarket = (market: MarketWithData) => {
return {
name: data?.market?.tradableInstrument.instrument.name,
name: market?.tradableInstrument.instrument.name,
symbol:
data?.market?.tradableInstrument.instrument.product.settlementAsset
.symbol,
market?.tradableInstrument.instrument.product.settlementAsset.symbol,
settlementAsset:
data?.market?.tradableInstrument.instrument.product.settlementAsset,
targetStake: data?.market?.data?.targetStake,
tradingMode: data?.market?.data?.marketTradingMode,
trigger: data?.market?.data?.trigger,
market?.tradableInstrument.instrument.product.settlementAsset,
targetStake: market?.data?.targetStake,
tradingMode: market?.data?.marketTradingMode,
trigger: market?.data?.trigger,
};
};
export const lpDataProvider = makeDerivedDataProvider(
[marketLiquidityDataProvider, lpAggregatedDataProvider],
[marketWithDataProvider, lpAggregatedDataProvider],
([market, lpAggregatedData]) => ({
market: { ...formatMarket(market) },
liquidityProviders: lpAggregatedData || [],
+3 -1
View File
@@ -1,3 +1,5 @@
# Static
A static CDN for Vega assets
A static CDN for Vega assets: `static.vega.xyz`
prepare assets by running: `yarn nx build static`
@@ -1,5 +1,5 @@
// #region consts
const asset = 'asset';
const assetColId = '[col-id="asset.symbol"]';
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.getByTestId(asset).contains(assetName).click();
cy.get(assetColId).contains(assetName).click();
};
const testTooltip = (index: number, testId: string, tooltip: string) => {
@@ -450,3 +450,17 @@ 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,7 +1,10 @@
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(() => {
@@ -60,7 +63,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
// 6001-MARK-035
cy.get(rowSelector)
.first()
.find('[col-id="tradableInstrument.instrument.code"]')
.find(colInstrumentCode)
.should('have.text', 'SOLUSD');
// 6001-MARK-036
@@ -155,6 +158,7 @@ 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',
@@ -167,8 +171,38 @@ 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('[col-id="tradableInstrument.instrument.code"]')
.find(colInstrumentCode)
.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');
});
});
@@ -0,0 +1,326 @@
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,6 +25,7 @@ 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();
@@ -67,6 +68,7 @@ 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()
@@ -84,8 +86,19 @@ describe('markets selector', { tags: '@smoke' }, () => {
});
});
// 6001-MARK-27
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');
});
});
it('can use the filter options', () => {
// 6001-MARK-027
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
@@ -94,7 +107,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-29
// 6001-MARK-029
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');
@@ -103,4 +116,29 @@ 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,16 +1,16 @@
import { checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see table headers', () => {
@@ -35,10 +35,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
it('renders markets correctly', () => {
// 6001-MARK-049
cy.get(rowSelector)
.first()
.find('[col-id="market"]')
.should('have.text', 'ETHUSD');
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
// 6001-MARK-050
cy.get(rowSelector)
@@ -119,6 +116,7 @@ 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',
@@ -196,4 +194,31 @@ 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');
});
});
@@ -0,0 +1,102 @@
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,6 +20,7 @@ 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();
@@ -34,28 +35,23 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="used"]')
.should('have.text', '1.010.00%');
.should('have.text', '1.01' + '1.00%');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="available"]')
.should('have.text', '100,000.00');
.should('have.text', '100.00');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="total"]')
.should('have.text', '100,001.01');
.should('have.text', '101.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"]')
@@ -101,7 +97,7 @@ describe('accounts', { tags: '@smoke' }, () => {
'Liquidity provision fee reward account balance',
'Market proposer reward account balance',
];
cy.getByTestId('asset').contains('tEURO').click();
cy.get('[col-id="asset.symbol"]').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]);
@@ -112,7 +108,7 @@ describe('accounts', { tags: '@smoke' }, () => {
it('should open usage breakdown dialog when clicked on used', () => {
// 7001-COLL-009
cy.getByTestId('breakdown').contains('1.01').click();
cy.get('[col-id="used"]').contains('1.01').click();
const headers = ['Market', 'Account type', 'Balance'];
cy.getByTestId('usage-breakdown').within(($headers) => {
cy.wrap($headers)
@@ -133,11 +129,12 @@ 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 = ['tEURO', 'tDAI', 'tBTC', 'tBTC'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'asset.symbol',
marketsSortedDefault,
@@ -149,23 +146,14 @@ describe('accounts', { tags: '@smoke' }, () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.00',
'1,000.01',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00001',
'1,000.00002',
'1,000.01',
];
const 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,
@@ -176,24 +164,22 @@ 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.000.00%',
'0.010.00%',
'0.000.00%',
'0.000.00%',
'0.00' + '0.00%',
'0.01' + '0.00%',
'0.00' + '0.00%',
'0.00' + '0.00%',
];
const marketsSortedAsc = [
'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%',
'0.00' + '0.00%',
'0.00' + '0.00%',
'0.00' + '0.00%',
'0.01' + '0.00%',
];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'used',
marketsSortedDefault,
@@ -205,23 +191,13 @@ describe('accounts', { tags: '@smoke' }, () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.00',
'1,000.01',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00001',
'1,000.00002',
'1,000.01',
];
const 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',
@@ -102,7 +102,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
.within(() => {
cy.get('[data-state="closed"]').should(
'have.text',
'Total margin available100,000.01 tDAI'
'Total margin available' + '100.01 tDAI'
);
});
});
@@ -3,6 +3,18 @@ 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();
@@ -11,8 +23,9 @@ beforeEach(() => {
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
it('renders positions on trading page', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId('Positions').click();
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed();
});
@@ -35,174 +48,306 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}
aliasGQLQuery(req, 'Positions', positions);
});
cy.visit('/#/portfolio');
cy.getByTestId('Positions').click();
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed(true);
});
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',
},
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',
},
];
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', '-');
});
},
];
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', '-');
});
});
});
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.get('[data-testid="tab-positions"]').contains('no market data');
});
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);
});
});
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');
});
});
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
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'
);
});
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="unrealisedPNL"]',
'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
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="openVolume"]',
'text-vega-green',
'text-vega-pink'
);
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
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(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
.invoke('text')
.should('not.be.empty');
cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
($openVolume) => {
cy.wrap($openVolume).invoke('text').should('not.be.empty');
}
);
cy.get('[col-id="openVolume"]').should('not.be.empty');
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
cy.getByTestId('flash-cell').should('not.be.empty');
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="currentLeverage"]').should('contain.text', '2,767.3');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
});
cy.get('[col-id="unrealisedPNL"]').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.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
});
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();
}
+8
View File
@@ -31,6 +31,8 @@ import {
protocolUpgradeProposalsQuery,
blockStatisticsQuery,
networkParamQuery,
liquidityProvisionsQuery,
liquidityProviderFeeShareQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
@@ -158,6 +160,12 @@ 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());
+1 -1
View File
@@ -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.16-core-0.71.5
NX_APP_VERSION=v0.20.18-core-0.71.8
@@ -150,6 +150,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat
heading={t('Target stake')}
description={tooltipMapping['targetStake']}
testId="target-stake"
>
<div>
{targetStake
@@ -163,6 +164,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat
heading={t('Supplied stake')}
description={tooltipMapping['suppliedStake']}
testId="supplied-stake"
>
<div>
{suppliedStake
@@ -178,10 +180,10 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
{formatNumberPercentage(percentage, 2)}
</HeaderStat>
<HeaderStat heading={t('Market ID')}>
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
<HeaderStat heading={t('Learn more')} testId="liquidity-learn-more">
{DocsLinks ? (
<ExternalLink href={DocsLinks.LIQUIDITY}>
{t('Providing liquidity')}
@@ -151,7 +151,11 @@ 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'} className="flex items-center gap-x-2">
<Link
to={'/markets/all'}
data-testid="all-markets-link"
className="flex items-center gap-x-2"
>
{t('All markets')}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
@@ -143,7 +143,6 @@ const MarketBottomPanel = memo(
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
noBottomPlaceholder
hideButtons
storeKey="marketCollateral"
/>
@@ -12,12 +12,10 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
export const AccountsContainer = ({
pinnedAsset,
hideButtons,
noBottomPlaceholder,
storeKey,
}: {
pinnedAsset?: PinnedAsset;
hideButtons?: boolean;
noBottomPlaceholder?: boolean;
storeKey?: string;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
@@ -50,7 +48,6 @@ export const AccountsContainer = ({
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
noBottomPlaceholder={noBottomPlaceholder}
storeKey={storeKey}
/>
{!isReadOnly && !hideButtons && (
@@ -49,6 +49,9 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
Statistics: {
merge: true,
},
Account: {
keyFields: false,
fields: {
@@ -80,12 +83,6 @@ const cacheConfig: InMemoryCacheConfig = {
ERC20: {
keyFields: ['contractAddress'],
},
PositionUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
Party: {
keyFields: false,
},
@@ -95,8 +92,16 @@ const cacheConfig: InMemoryCacheConfig = {
Fees: {
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
// The folling types are cached by the data provider and not by apollo
PositionUpdate: {
keyFields: false,
},
TradeUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
OrderUpdate: {
keyFields: false,
},
@@ -94,6 +94,7 @@ export const accountsOnlyDataProvider = makeDataProvider<
update,
getData,
getDelta,
fetchPolicy: 'no-cache',
});
export interface AccountFields extends Account {
+1 -40
View File
@@ -1,17 +1,14 @@
import { useRef, memo, useCallback, useState } from 'react';
import { useRef, memo, 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';
@@ -54,7 +51,6 @@ interface AccountManagerProps {
onClickDeposit?: (assetId?: string) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
noBottomPlaceholder?: boolean;
storeKey?: string;
}
@@ -65,48 +61,14 @@ export const AccountManager = ({
partyId,
isReadOnly,
pinnedAsset,
noBottomPlaceholder,
storeKey,
}: 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,
});
return (
@@ -121,7 +83,6 @@ export const AccountManager = ({
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
storeKey={storeKey}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No accounts')}
/>
<Dialog
+138 -138
View File
@@ -1,6 +1,7 @@
import { forwardRef, useMemo, useCallback } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
isNumeric,
toBigNum,
} from '@vegaprotocol/utils';
@@ -10,21 +11,15 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import {
ButtonLink,
Button,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { 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';
@@ -35,29 +30,16 @@ import classNames from 'classnames';
import { AccountsActionsDropdown } from './accounts-actions-dropdown';
const colorClass = (percentageUsed: number, neutral = false) => {
return classNames({
return classNames('text-right', {
'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) =>
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 percentageValue = (part: string, total: string) => {
total = !total || total === '0' ? '1' : total;
return new BigNumber(part).dividedBy(total).multipliedBy(100).toNumber();
};
export const accountValuesComparator = (
@@ -81,15 +63,10 @@ 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;
@@ -148,83 +125,55 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
const showDepositButton = pinnedAsset?.balance === '0';
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(
const colDefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Asset'),
field: 'asset.symbol',
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
)}
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(
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
},
{
headerName: t('Used'),
type: 'rightAligned',
field: 'used',
headerTooltip: t(
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
)}
cellRenderer={({
),
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: ({
data,
value,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!data) return null;
if (!value || !data) return '-';
const percentageUsed = percentageValue(value, data.total);
const valueFormatted = formatWithAssetDecimals(data, value);
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
return data.breakdown ? (
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
onClickBreakdown && onClickBreakdown(data.asset.id);
}}
>
<span>{valueFormatted}</span>
</ButtonLink>
<span className="underline">{valueFormatted}</span>
<span
className={classNames(
colorClass(percentageUsed),
'ml-2 inline-block w-14'
'ml-1 inline-block w-14'
)}
>
{percentageUsed.toFixed(2)}%
@@ -232,59 +181,77 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
</>
) : (
<>
<span>{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-neutral-500 dark:text-neutral-400">
0.00%
<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>
</>
);
}}
/>
<AgGridColumn
headerName={t('Available')}
field="available"
type="rightAligned"
headerTooltip={t(
},
},
{
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.'
)}
cellRenderer={({
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaICellRendererParams<AccountFields, 'available'>) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return (
<span className={colorClass(percentageUsed, true)}>
{formatWithAssetDecimals(data, value)}
</span>
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
}}
/>
<AgGridColumn
headerName={t('Total')}
type="rightAligned"
field="total"
headerTooltip={t(
},
},
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
)}
valueFormatter={({
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
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,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
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?.balance === '0') {
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
@@ -319,9 +286,42 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
}}
/>
);
}}
/>
</AgGrid>
},
},
];
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}
/>
);
}
);
+9 -1
View File
@@ -35,6 +35,7 @@ export const accountFields: AccountFieldsFragment[] = [
balance: '100000000',
market: null,
asset: {
// tEURO
__typename: 'Asset',
id: 'asset-id',
},
@@ -44,6 +45,7 @@ export const accountFields: AccountFieldsFragment[] = [
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
balance: '100000000',
asset: {
// tDAI
__typename: 'Asset',
id: 'asset-id-2',
},
@@ -57,6 +59,7 @@ export const accountFields: AccountFieldsFragment[] = [
id: 'market-2',
},
asset: {
// tEURO
__typename: 'Asset',
id: 'asset-id',
},
@@ -70,6 +73,7 @@ export const accountFields: AccountFieldsFragment[] = [
id: 'market-0',
},
asset: {
// AST0
__typename: 'Asset',
id: 'asset-0',
},
@@ -83,6 +87,7 @@ export const accountFields: AccountFieldsFragment[] = [
id: 'market-3',
},
asset: {
// AST0
__typename: 'Asset',
id: 'asset-0',
},
@@ -90,9 +95,10 @@ export const accountFields: AccountFieldsFragment[] = [
{
__typename: 'AccountBalance',
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
balance: '10000000000',
balance: '10000000',
market: null,
asset: {
// AST0
__typename: 'Asset',
id: 'asset-0',
},
@@ -104,6 +110,7 @@ export const accountFields: AccountFieldsFragment[] = [
balance: '100000001',
market: null,
asset: {
// tBTC (sepolia)
__typename: 'Asset',
id: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
},
@@ -114,6 +121,7 @@ export const accountFields: AccountFieldsFragment[] = [
balance: '100000002',
market: null,
asset: {
// tBTC (test)
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
},
+83 -65
View File
@@ -1,52 +1,106 @@
import { forwardRef } from 'react';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { forwardRef, useMemo } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Intent } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import { Intent, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import { AccountTypeMapping } from '@vegaprotocol/types';
import type {
ValueProps,
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { progressBarCellRendererSelector } from '@vegaprotocol/datagrid';
import { ProgressBarCell } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, PriceCell } from '@vegaprotocol/datagrid';
import type { ValueFormatterParams } from 'ag-grid-community';
import type { ColDef } from 'ag-grid-community';
import { accountValuesComparator } from './accounts-table';
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;
}
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
({ data }, ref) => {
const coldefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.name',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
AccountFields,
'market.tradableInstrument.instrument.name'
>) => {
if (!value) return 'None';
return value;
},
minWidth: 200,
},
{
headerName: t('Account type'),
field: 'type',
maxWidth: 300,
valueFormatter: ({
value,
}: VegaValueFormatterParams<AccountFields, 'type'>) => {
return 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,
},
];
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}`
`${data.asset.id}:${data.type}:${data.market?.id}`
}
ref={ref}
rowHeight={34}
@@ -57,44 +111,8 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
resizable: true,
sortable: true,
}}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.name"
valueFormatter={({
value,
}: VegaValueFormatterParams<
AccountFields,
'market.tradableInstrument.instrument.name'
>) => {
if (!value) return 'None';
return value;
}}
minWidth={200}
/>
<AgGridColumn
headerName={t('Account type')}
field="type"
maxWidth={300}
valueFormatter={({
value,
}: VegaValueFormatterParams<AccountFields, 'type'>) =>
value
? AccountTypeMapping[value as keyof typeof AccountTypeMapping]
: ''
}
/>
<AgGridColumn
headerName={t('Balance')}
field="used"
flex={2}
maxWidth={500}
cellRendererSelector={progressBarCellRendererSelector}
valueFormatter={progressBarValueFormatter}
comparator={accountValuesComparator}
/>
</AgGrid>
columnDefs={coldefs}
/>
);
}
);
+1
View File
@@ -28,3 +28,4 @@ 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,7 +38,6 @@ type Data = Item[];
type QueryData = {
data: Data;
pageInfo?: PageInfo;
totalCount?: number;
};
type CombinedData = {
@@ -115,7 +114,6 @@ const paginatedSubscribe = makeDataProvider<
first,
append: defaultAppend,
getPageInfo: (r) => r?.pageInfo ?? null,
getTotalCount: (r) => r?.totalCount,
},
});
@@ -373,32 +371,10 @@ describe('data provider', () => {
subscription.unsubscribe();
});
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;
it('loads requested data blocks', async () => {
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
totalCount,
pageInfo: {
hasNextPage: true,
endCursor: '100',
@@ -407,168 +383,25 @@ describe('data provider', () => {
// load next page
subscription.load && subscription.load();
let lastQueryArgs =
const 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: true,
hasNextPage: false,
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();
});
@@ -752,7 +585,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,
});
+28 -85
View File
@@ -27,7 +27,6 @@ export interface UpdateCallback<Data, Delta> {
loading: boolean;
loaded: boolean;
pageInfo: PageInfo | null;
totalCount?: number;
}
): void;
}
@@ -40,9 +39,7 @@ export interface Reload {
(forceReset?: boolean): void;
}
type Pagination = Schema.Pagination & {
skip?: number;
};
type Pagination = Schema.Pagination;
export interface PageInfo {
startCursor?: string;
@@ -83,12 +80,8 @@ export interface Append<Data> {
data: Data | null,
insertionData: Data | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination,
totalCount?: number
): {
data: Data | null;
totalCount?: number;
};
pagination?: Pagination
): Data | null;
}
interface GetData<QueryData, Data, Variables> {
@@ -99,10 +92,6 @@ interface GetPageInfo<QueryData> {
(queryData: QueryData): PageInfo | null;
}
interface GetTotalCount<QueryData> {
(queryData: QueryData): number | undefined;
}
interface GetDelta<SubscriptionData, Delta, Variables> {
(
subscriptionData: SubscriptionData,
@@ -119,44 +108,32 @@ export interface Edge<T extends Node> extends Cursor {
node: T;
}
export function defaultAppend<Data>(
data: Data | null,
insertionData: Data | null,
export function defaultAppend<T extends Cursor>(
data: T[] | null,
insertionData: T[] | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination,
totalCount?: number
pagination?: Pagination
) {
if (data && insertionData && insertionPageInfo) {
if (!(data instanceof Array) || !(insertionData instanceof Array)) {
throw new Error(
'data needs to be instance of Edge[] when using pagination'
'data needs to be instance of Array[] 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 + (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),
};
const start = startIndex + 1;
const updatedData = [...data.slice(0, start), ...insertionData];
return updatedData;
}
}
}
return { data, totalCount };
return data;
}
interface DataProviderParams<
@@ -175,7 +152,6 @@ interface DataProviderParams<
getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
};
@@ -245,7 +221,6 @@ 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 = (
@@ -258,7 +233,6 @@ function makeDataProviderInternal<
loading,
loaded,
pageInfo,
totalCount,
...updateData,
});
};
@@ -301,59 +275,41 @@ function makeDataProviderInternal<
}
});
const load = async (start?: number) => {
const load = async () => {
if (!pagination) {
return Promise.reject();
}
if (!pageInfo?.hasNextPage) {
return null;
}
const paginationVariables: Pagination = {
first: pagination.first,
after: pageInfo?.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;
}
if (data) {
const endCursor = (data as Cursor[])[(data as Cursor[]).length - 1]
.cursor;
if (endCursor) {
paginationVariables.after = endCursor;
}
} else if (!pageInfo?.hasNextPage) {
return null;
}
const res = await call(paginationVariables);
const insertionData = getData(res.data, variables);
const insertionPageInfo = pagination.getPageInfo(res.data);
({ data, totalCount } = pagination.append(
data = pagination.append(
data,
insertionData,
insertionPageInfo,
paginationVariables,
totalCount
));
paginationVariables
);
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 = () => {
@@ -400,16 +356,6 @@ 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) {
@@ -417,9 +363,6 @@ function makeDataProviderInternal<
const delta = updateQueue.shift();
if (delta) {
setData(update(data, delta, reload, variables));
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
}
}
}
@@ -590,7 +533,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, getTotalCount, append, first }
* @param pagination pagination related functions { getPageInfo, append, first }
* @returns Subscribe<Data, Delta> subscribe function
* @example
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({
+3 -18
View File
@@ -12,23 +12,13 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({
delta,
data,
totalCount,
}: {
delta?: Delta;
data: Data | null;
totalCount?: number;
}) => boolean;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
insert?: ({
insertionData,
data,
totalCount,
}: {
insertionData?: Data | null;
data: Data | null;
totalCount?: number;
}) => boolean;
variables: Variables;
skipUpdates?: boolean;
@@ -56,7 +46,6 @@ 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);
@@ -101,7 +90,6 @@ export const useDataProvider = <
error,
loading,
insertionData,
totalCount,
isInsert,
isUpdate,
loaded,
@@ -116,19 +104,18 @@ export const useDataProvider = <
(skipUpdatesRef.current ||
(!skipUpdatesRef.current &&
updateRef.current &&
updateRef.current({ delta, data, totalCount })))
updateRef.current({ delta, data })))
) {
return;
}
if (
isInsert &&
insertRef.current &&
insertRef.current({ insertionData, data, totalCount })
insertRef.current({ insertionData, data })
) {
return;
}
}
setTotalCount(totalCount);
setData(data);
if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data });
@@ -150,7 +137,6 @@ export const useDataProvider = <
useEffect(() => {
setData(null);
setError(undefined);
setTotalCount(undefined);
if (updateRef.current) {
updateRef.current({ data: null });
}
@@ -184,7 +170,6 @@ export const useDataProvider = <
flush,
reload,
load,
totalCount,
};
};
@@ -26,6 +26,7 @@ export const AgGridThemed = ({
enableCellTextSelection: true,
overlayLoadingTemplate: t('Loading...'),
overlayNoRowsTemplate: t('No data'),
suppressCellFocus: true,
};
const wrapperClasses = classNames('vega-ag-grid', {
@@ -1,9 +1,5 @@
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?: {
@@ -19,7 +15,7 @@ export const EmptyCell = () => '';
export const ProgressBarCell = ({ valueFormatted }: ValueProps) => {
return valueFormatted ? (
<>
<div className="flex justify-between leading-tight font-mono">
<div className="text-right leading-tight font-mono">
<div>
{valueFormatted.low} ({valueFormatted.value}%)
</div>
@@ -32,11 +28,3 @@ export const ProgressBarCell = ({ valueFormatted }: ValueProps) => {
</>
) : null;
};
export const progressBarCellRendererSelector = (
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: ProgressBarCell,
};
};
@@ -146,7 +146,7 @@ export const DealTicket = ({
});
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders
? activeOrders.map<OrderInfo>(({ node: order }) => ({
? activeOrders.map<OrderInfo>((order) => ({
isMarketOrder: order.type === OrderType.TYPE_MARKET,
price: order.price,
remaining: order.remaining,
@@ -13,6 +13,24 @@ 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]}
@@ -49,21 +67,29 @@ 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(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'));
expect(menuitems[0]).toHaveTextContent('Advanced');
const links = screen.getAllByRole('link');
await userEvent.click(links[0]);
expect(hrefSetSpy).toHaveBeenCalledWith(mainnetUrl);
expect(links[0]).toHaveAttribute('href', mainnetUrl);
expect(links[1]).toHaveAttribute('href', testnetUrl);
// 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);
});
it('displays the correct selected network on the default dropdown view', async () => {
@@ -82,10 +108,10 @@ describe('Network switcher', () => {
await userEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
const links = screen.getAllByRole('link');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('current'));
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[0]).toHaveTextContent(t('current'));
});
it('displays the correct selected network on the default dropdown view when it does not have an associated url', async () => {
@@ -103,10 +129,10 @@ describe('Network switcher', () => {
await userEvent.click(screen.getByRole('button'));
const menuitems = screen.getAllByRole('menuitem');
const links = screen.getAllByRole('link');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('current'));
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[0]).toHaveTextContent(t('current'));
});
it('displays the correct state for a network without url on the default dropdown view', async () => {
@@ -123,44 +149,59 @@ describe('Network switcher', () => {
render(<NetworkSwitcher />);
await userEvent.click(screen.getByRole('button'));
const links = screen.getAllByRole('link');
const menuitems = screen.getAllByRole('menuitem');
expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(menuitems[0]).toHaveTextContent(t('not available'));
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
expect(links[0]).toHaveTextContent(t('not available'));
});
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,
}));
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,
}));
render(<NetworkSwitcher />);
render(<NetworkSwitcher />);
await userEvent.click(screen.getByRole('button'));
await userEvent.click(
screen.getByRole('menuitem', { name: t('Advanced') })
);
await userEvent.click(screen.getByTestId('network-switcher'));
[Networks.MAINNET, Networks.TESTNET, Networks.DEVNET].forEach((network) => {
expect(
screen.getByRole('link', { name: envNameMapping[network] })
).toHaveAttribute('href', VEGA_NETWORKS[network]);
expect(
screen.getByText(envDescriptionMapping[network])
await screen.findByRole('menuitem', { name: t('Advanced') })
).toBeInTheDocument();
});
});
await userEvent.click(
screen.getByRole('menuitem', { name: t('Advanced') })
);
expect(
await screen.findByText(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;
@@ -188,7 +229,7 @@ describe('Network switcher', () => {
const label = screen.getByText(`(${t('current')})`);
expect(label).toBeInTheDocument();
expect(label.parentNode?.firstElementChild).toHaveTextContent(
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
envNameMapping[selectedNetwork]
);
});
@@ -217,7 +258,7 @@ describe('Network switcher', () => {
const label = screen.getByText('(not available)');
expect(label).toBeInTheDocument();
expect(label.parentNode?.firstElementChild).toHaveTextContent(
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
envNameMapping[Networks.MAINNET]
);
});
@@ -1,7 +1,6 @@
import { useState, useCallback, useRef } from 'react';
import { useState, useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import {
Link,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -99,7 +98,6 @@ export const NetworkSwitcher = ({
},
[setOpen, setAdvancedView]
);
const menuRef = useRef<HTMLButtonElement | null>(null);
const current = currentNetwork || VEGA_ENV;
@@ -110,7 +108,6 @@ 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
@@ -123,10 +120,7 @@ export const NetworkSwitcher = ({
</DropdownMenuTrigger>
}
>
<DropdownMenuContent
align="start"
style={{ minWidth: `${menuRef.current?.offsetWidth || 290}px` }}
>
<DropdownMenuContent align="start">
{!isAdvancedView && (
<>
{standardNetworkKeys.map((key) => (
@@ -134,14 +128,16 @@ export const NetworkSwitcher = ({
key={key}
data-testid="network-item"
disabled={!VEGA_NETWORKS[key]}
role="link"
onClick={() =>
(window.location.href = VEGA_NETWORKS[key] || '')
}
>
<a href={VEGA_NETWORKS[key]}>
{envNameMapping[key]}
<NetworkLabel
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</a>
{envNameMapping[key]}
<NetworkLabel
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
/>
</DropdownMenuItem>
))}
<DropdownMenuItem
@@ -159,10 +155,17 @@ export const NetworkSwitcher = ({
{isAdvancedView && (
<>
{advancedNetworkKeys.map((key) => (
<DropdownMenuItem key={key} data-testid="network-item-advanced">
<DropdownMenuItem
key={key}
data-testid="network-item-advanced"
role="link"
onClick={() =>
(window.location.href = VEGA_NETWORKS[key] || '')
}
>
<div className="w-full flex justify-between gap-2">
<div>
<Link href={VEGA_NETWORKS[key]}>{envNameMapping[key]}</Link>
{envNameMapping[key]}
<NetworkLabel
isCurrent={current === key}
isAvailable={!!VEGA_NETWORKS[key]}
-1
View File
@@ -1,4 +1,3 @@
export * from './lib/fills-container';
export * from './lib/use-fills-list';
export * from './lib/fills-data-provider';
export * from './lib/__generated__/Fills';
+25 -21
View File
@@ -48,28 +48,32 @@ 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) {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
...FillUpdateFields
}
}
+29 -22
View File
@@ -15,6 +15,8 @@ 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;
}>;
@@ -60,6 +62,31 @@ 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) {
@@ -107,30 +134,10 @@ export type FillsQueryResult = Apollo.QueryResult<FillsQuery, FillsQueryVariable
export const FillsEventDocument = gql`
subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
...FillUpdateFields
}
}
`;
${FillUpdateFieldsFragmentDoc}`;
/**
* __useFillsEventSubscription__
+90 -70
View File
@@ -1,74 +1,34 @@
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import {} from '@vegaprotocol/utils';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import type { PageInfo, Cursor } 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 { marketsProvider } from '@vegaprotocol/markets';
import { marketsMapProvider } 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): FillEdgeFragment[] =>
responseData?.trades?.edges || [];
const getData = (
responseData: FillsQuery | null
): (FillFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<FillFieldsFragment & Cursor>((edge) => ({
...edge.node,
cursor: edge.cursor,
})) || [];
const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
responseData?.trades?.pageInfo || null;
@@ -76,16 +36,65 @@ 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
FillsQueryVariables,
FillsEventSubscriptionVariables
>({
query: FillsDocument,
subscriptionQuery: FillsEventDocument,
update,
update: (data, delta, reload, variables) =>
update(data, delta, variables, mapFillUpdateToFill),
getData,
getDelta,
pagination: {
@@ -93,30 +102,41 @@ 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) => marketsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(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>
(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],
})
);
}
);
+28 -22
View File
@@ -1,10 +1,11 @@
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;
@@ -20,31 +21,36 @@ export const FillsManager = ({
storeKey,
}: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const scrolledToTop = useRef(true);
const { data, error } = useFillsList({
partyId,
marketId,
gridRef,
scrolledToTop,
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 bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
});
const fills = compact(data).map((e) => e.node);
return (
<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>
<FillsTable
ref={gridRef}
rowData={data}
partyId={partyId}
onMarketClick={onMarketClick}
storeKey={storeKey}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
/>
);
};
-95
View File
@@ -1,95 +0,0 @@
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();
});
});
-123
View File
@@ -1,123 +0,0 @@
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,22 +1,12 @@
import type { Asset } from '@vegaprotocol/assets';
import { assetsProvider } from '@vegaprotocol/assets';
import { assetsMapProvider } from '@vegaprotocol/assets';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import { updateGridData } from '@vegaprotocol/datagrid';
import { marketsMapProvider } from '@vegaprotocol/markets';
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,
@@ -30,171 +20,58 @@ export type LedgerEntry = LedgerEntryFragment & {
marketReceiver: Market | null | undefined;
};
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
export type AggregatedLedgerEntriesNode = Omit<
AggregatedLedgerEntriesEdge,
'node'
> & {
node: LedgerEntry;
};
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 || [];
return (
responseData?.ledgerEntries?.edges
.filter(isLedgerEntryEdge)
.map((edge) => edge.node) || []
);
};
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({
const ledgerEntriesOnlyProvider = makeDataProvider<
LedgerEntriesQuery,
ReturnType<typeof getData>,
never,
never,
LedgerEntriesQueryVariables
>({
query: LedgerEntriesDocument,
getData,
getDelta: getData,
update,
additionalContext: {
isEnlargedTimeout: true,
},
});
export const ledgerEntriesProvider = makeDerivedDataProvider<
AggregatedLedgerEntriesNode[],
AggregatedLedgerEntriesNode[],
LedgerEntry[],
never,
LedgerEntriesQueryVariables
>(
[
ledgerEntriesOnlyProvider,
(callback, client) => assetsProvider(callback, client, undefined),
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => assetsMapProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
([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,
};
(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 };
});
}
);
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 };
};
+21 -13
View File
@@ -2,10 +2,12 @@ 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 } from 'react';
import { useCallback, useRef, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns';
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
import { LedgerTable } from './ledger-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { LedgerExportLink } from './ledger-export-link';
@@ -26,10 +28,21 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter);
const { data, error } = useLedgerEntriesDataProvider({
partyId,
filter,
gridRef,
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 onFilterChanged = useCallback((event: FilterChangedEvent) => {
@@ -37,20 +50,15 @@ 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={extractedData}
rowData={data}
onFilterChanged={onFilterChanged}
overlayNoRowsTemplate={error ? error.message : t('No entries')}
/>
{extractedData && (
<LedgerExportLink entries={extractedData} partyId={partyId} />
)}
{data && <LedgerExportLink entries={data} partyId={partyId} />}
</div>
);
};
@@ -1,39 +1,3 @@
# 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 {
-70
View File
@@ -3,13 +3,6 @@ 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<{
@@ -65,69 +58,6 @@ 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) {
@@ -1,10 +1,7 @@
import type { LiquidityProviderFeeShare } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { getLiquidityProvision } from './liquidity-data-provider';
import type {
LiquidityProvisionFieldsFragment,
MarketLpQuery,
} from './__generated__/MarketLiquidity';
import type { LiquidityProvisionFieldsFragment } from './__generated__/MarketLiquidity';
const input = {
liquidityProvisions: [
@@ -34,44 +31,6 @@ const input = {
__typename: 'LiquidityProvision',
} as LiquidityProvisionFieldsFragment,
],
marketLiquidity: {
market: {
id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed',
decimalPlaces: 5,
positionDecimalPlaces: 3,
tradableInstrument: {
instrument: {
code: 'UNIDAI.MF21',
name: 'UNIDAI Monthly (Dec 2022)',
product: {
settlementAsset: {
id: '16ae5dbb1fd7aa2ddef725703bfe66b3647a4da7b844bfdd04e985756f53d9d6',
symbol: 'tDAI',
decimals: 18,
__typename: 'Asset',
},
__typename: 'Future',
},
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
data: {
market: {
id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed',
__typename: 'Market',
},
marketTradingMode: 'TRADING_MODE_CONTINUOUS',
suppliedStake: '18003328918633596575000',
openInterest: '89660',
targetStake: '70159269843504000000',
trigger: 'AUCTION_TRIGGER_UNSPECIFIED',
marketValueProxy: '18003328918633596575000',
__typename: 'MarketData',
},
__typename: 'Market',
},
} as MarketLpQuery,
liquidityFeeShare: [
{
party: {
@@ -88,7 +47,6 @@ const input = {
const result = [
{
__typename: 'LiquidityProvision',
assetDecimalPlaces: 18,
averageEntryValuation: '12064118310408958216220.7224556301338111',
balance: '1.8003328918633596575e+22',
commitmentAmount: '18003328918633596575000',
@@ -119,74 +77,25 @@ const result = [
describe('getLiquidityProvision', () => {
it('should return an empty array when no data is provided', () => {
const data = getLiquidityProvision([], {}, []);
const data = getLiquidityProvision([], []);
expect(data).toEqual([]);
});
it('should return correct array when correct liquidity provision parameters are provided', () => {
const data = getLiquidityProvision(
input.liquidityProvisions,
input.marketLiquidity,
input.liquidityFeeShare
);
expect(data).toStrictEqual(result);
});
it('should return empty array when no liquidity provision parameters are provided', () => {
const data = getLiquidityProvision(
[],
input.marketLiquidity,
input.liquidityFeeShare
);
const data = getLiquidityProvision([], input.liquidityFeeShare);
expect(data).toStrictEqual([]);
});
it('should return empty array when no market lp query parameter is provided', () => {
const data = getLiquidityProvision(
input.liquidityProvisions,
{},
input.liquidityFeeShare
);
const result = [
{
__typename: 'LiquidityProvision',
assetDecimalPlaces: undefined,
averageEntryValuation: '12064118310408958216220.7224556301338111',
balance: '1.8003328918633596575e+22',
commitmentAmount: '18003328918633596575000',
createdAt: '2022-12-16T09:28:29.071781Z',
equityLikeShare: '1',
fee: '0.001',
party: {
__typename: 'Party',
accountsConnection: {
__typename: 'AccountsConnection',
edges: [
{
__typename: 'AccountEdge',
node: {
__typename: 'AccountBalance',
balance: '18003328918633596575000',
type: 'ACCOUNT_TYPE_BOND',
},
},
],
},
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
},
status: 'STATUS_ACTIVE',
updatedAt: '2023-01-04T22:13:27.761985Z',
},
];
expect(data).toStrictEqual(result);
});
it('should return empty array when no liquidity fee share param is provided', () => {
const data = getLiquidityProvision(
input.liquidityProvisions,
input.marketLiquidity,
[]
);
const data = getLiquidityProvision(input.liquidityProvisions, []);
const result = [
{
__typename: 'LiquidityProvision',
@@ -11,12 +11,9 @@ import {
LiquidityProviderFeeShareDocument,
LiquidityProvisionsDocument,
LiquidityProvisionsUpdateDocument,
MarketLpDocument,
} from './__generated__/MarketLiquidity';
import type {
MarketLpQuery,
MarketLpQueryVariables,
LiquidityProviderFeeShareFieldsFragment,
LiquidityProviderFeeShareQuery,
LiquidityProviderFeeShareQueryVariables,
@@ -78,19 +75,6 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
},
});
export const marketLiquidityDataProvider = makeDataProvider<
MarketLpQuery,
MarketLpQuery,
never,
never,
MarketLpQueryVariables
>({
query: MarketLpDocument,
getData: (responseData: MarketLpQuery | null) => {
return responseData;
},
});
export const liquidityFeeShareDataProvider = makeDataProvider<
LiquidityProviderFeeShareQuery,
LiquidityProviderFeeShareFieldsFragment[],
@@ -109,29 +93,24 @@ export type Filter = { partyId?: string; active?: boolean };
export const lpAggregatedDataProvider = makeDerivedDataProvider<
LiquidityProvisionData[],
never,
MarketLpQueryVariables & { filter?: Filter }
LiquidityProvisionsQueryVariables & { filter?: Filter }
>(
[
(callback, client, variables) =>
liquidityProvisionsDataProvider(callback, client, {
marketId: variables.marketId,
}),
(callback, client, variables) =>
marketLiquidityDataProvider(callback, client, {
marketId: variables.marketId,
}),
(callback, client, variables) =>
liquidityFeeShareDataProvider(callback, client, {
marketId: variables.marketId,
}),
],
(
[liquidityProvisions, marketLiquidity, liquidityFeeShare],
[liquidityProvisions, liquidityFeeShare],
{ filter }
): LiquidityProvisionData[] => {
return getLiquidityProvision(
liquidityProvisions,
marketLiquidity,
liquidityFeeShare,
filter
);
@@ -162,7 +141,6 @@ export const matchFilter = (
export const getLiquidityProvision = (
liquidityProvisions: LiquidityProvisionFieldsFragment[],
marketLiquidity: MarketLpQuery,
liquidityFeeShare: LiquidityProviderFeeShareFieldsFragment[],
filter?: Filter
): LiquidityProvisionData[] => {
@@ -183,7 +161,6 @@ export const getLiquidityProvision = (
return true;
})
.map((lp) => {
const market = marketLiquidity?.market;
const feeShare = liquidityFeeShare.find(
(f) => f.party.id === lp.party.id
);
@@ -205,9 +182,6 @@ export const getLiquidityProvision = (
...lp,
averageEntryValuation: feeShare?.averageEntryValuation,
equityLikeShare: feeShare?.equityLikeShare,
assetDecimalPlaces:
market?.tradableInstrument.instrument.product.settlementAsset
.decimals,
balance,
};
});
+1 -1
View File
@@ -55,7 +55,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }) => data.id}
getRowId={({ data }) => `${data.party.id}-${data.status}`}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
+119
View File
@@ -0,0 +1,119 @@
import merge from 'lodash/merge';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type {
LiquidityProviderFeeShareQuery,
LiquidityProvisionsQuery,
} from './__generated__/MarketLiquidity';
import type { LiquidityProvisionFieldsFragment } from './__generated__/MarketLiquidity';
export const liquidityProvisionsQuery = (
override?: PartialDeep<LiquidityProvisionsQuery>
): LiquidityProvisionsQuery => {
const defaultResult: LiquidityProvisionsQuery = {
market: {
liquidityProvisionsConnection: {
__typename: 'LiquidityProvisionsConnection',
edges: liquidityFields.map((node) => {
return {
__typename: 'LiquidityProvisionsEdge',
node,
};
}),
},
},
};
return merge(defaultResult, override);
};
export const liquidityProviderFeeShareQuery = (
override?: PartialDeep<LiquidityProviderFeeShareQuery>
): LiquidityProviderFeeShareQuery => {
const defaultResult: LiquidityProviderFeeShareQuery = {
market: {
id: 'market-0',
data: {
market: {
id: 'market-0',
__typename: 'Market',
},
liquidityProviderFeeShare: [
{
party: {
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
__typename: 'Party',
},
equityLikeShare: '1',
averageEntryValuation: '68585293691.5598054356207737',
__typename: 'LiquidityProviderFeeShare',
},
{
party: {
id: 'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
__typename: 'Party',
},
equityLikeShare: '1',
averageEntryValuation: '68585293691.5598054356207737',
__typename: 'LiquidityProviderFeeShare',
},
],
__typename: 'MarketData',
},
__typename: 'Market',
},
};
return merge(defaultResult, override);
};
export const liquidityFields: LiquidityProvisionFieldsFragment[] = [
{
party: {
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
accountsConnection: {
edges: [
{
node: {
type: Schema.AccountType.ACCOUNT_TYPE_BOND,
balance: '400000000',
__typename: 'AccountBalance',
},
__typename: 'AccountEdge',
},
],
__typename: 'AccountsConnection',
},
__typename: 'Party',
},
createdAt: '2023-05-15T11:47:15.132571Z',
updatedAt: '2023-05-15T11:47:15.132571Z',
commitmentAmount: '400000000',
fee: '0.0009',
status: Schema.LiquidityProvisionStatus.STATUS_ACTIVE,
__typename: 'LiquidityProvision',
},
{
party: {
id: 'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
accountsConnection: {
edges: [
{
node: {
type: Schema.AccountType.ACCOUNT_TYPE_BOND,
balance: '200000000',
__typename: 'AccountBalance',
},
__typename: 'AccountEdge',
},
],
__typename: 'AccountsConnection',
},
__typename: 'Party',
},
createdAt: '2023-05-15T11:47:15.132571Z',
updatedAt: '2023-05-15T11:47:15.132571Z',
commitmentAmount: '400000000',
fee: '0.004',
status: Schema.LiquidityProvisionStatus.STATUS_PENDING,
__typename: 'LiquidityProvision',
},
];
-6
View File
@@ -1,6 +0,0 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -1,29 +1,14 @@
import { forwardRef } from 'react';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type {
VegaValueGetterParams,
VegaValueFormatterParams,
VegaICellRendererParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
PriceFlashCell,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketTableActions } from './market-table-actions';
import { OracleStatus } from './oracle-status';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
const { MarketTradingMode, AuctionTrigger } = Schema;
import { useColumnDefs } from './use-column-defs';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
@@ -51,199 +36,34 @@ const MarketName = (props: MarketNameCellProps) => (
</>
);
const defaultColDef = {
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
};
export const MarketListTable = forwardRef<
AgGridReact,
TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
}
>(({ onMarketClick, ...props }, ref) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const columnDefs = useColumnDefs({ onMarketClick });
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
getRowId={getRowId}
ref={ref}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
}}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
suppressCellFocus
components={{ PriceFlashCell, MarketName }}
storeKey="allMarkets"
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="tradableInstrument.instrument.code"
cellRenderer="MarketName"
cellRendererParams={{ onMarketClick }}
/>
<AgGridColumn
headerName={t('Description')}
field="tradableInstrument.instrument.name"
/>
<AgGridColumn
headerName={t('Trading mode')}
field="tradingMode"
minWidth={170}
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return undefined;
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[marketTradingMode];
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketTradingModeMapping,
}}
/>
<AgGridColumn
headerName={t('Status')}
field="state"
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketStateMapping,
}}
/>
<AgGridColumn
headerName={t('Best bid')}
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? undefined
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
)
}
/>
<AgGridColumn
headerName={t('Best offer')}
field="data.bestOfferPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? undefined
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
)
}
/>
<AgGridColumn
headerName={t('Mark price')}
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? undefined
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces)
}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol"
cellRenderer={({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
}}
/>
<AgGridColumn
colId="market-actions"
field="id"
{...COL_DEFS.actions}
cellRenderer={({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketTableActions
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
}}
/>
</AgGrid>
/>
);
});
@@ -0,0 +1,107 @@
import { render, screen, act, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as DataProviders from '@vegaprotocol/data-provider';
import { MockedProvider } from '@apollo/react-testing';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketsContainer } from './markets-container';
const market = {
id: 'id-1',
tradableInstrument: {
instrument: {
product: { settlementAsset: { id: 'assetId-1' } },
},
},
decimalPlaces: 1,
positionDecimalPlaces: 1,
state: 'STATE_ACTIVE',
tradingMode: 'TRADING_MODE_OPENING_AUCTION',
data: {
bestBidPrice: 100,
},
} as unknown as MarketMaybeWithData;
describe('MarketsContainer', () => {
it('context menu should stay open', async () => {
const spyOnSelect = jest.fn();
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
.mockImplementation(() => {
return {
error: null,
reload: jest.fn(),
data: [market],
};
});
let rerenderRef: (ui: React.ReactElement) => void;
await act(async () => {
const { rerender } = render(
<MockedProvider>
<MarketsContainer onSelect={spyOnSelect} />
</MockedProvider>
);
rerenderRef = rerender;
});
// make sure ag grid is finished initializaing
const rowContainer = await screen.findByRole('rowgroup', {
name: (_name, element) =>
element.classList.contains('ag-center-cols-container'),
});
expect(within(rowContainer).getAllByRole('row')).toHaveLength(1);
expect(
screen.getByRole('rowgroup', {
name: (_name, element) =>
element.classList.contains('ag-pinned-right-cols-container'),
})
).toBeInTheDocument();
// open the dropdown
await userEvent.click(
screen.getByRole('button', {
name: (_name, element) =>
(element.parentNode as Element)?.getAttribute('id') ===
'cell-market-actions-8',
})
);
await checkDropdown();
// reset the mock and rerender so the component
// updates with new data
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
.mockImplementation(() => {
return {
error: null,
reload: jest.fn(),
data: [{ ...market, state: 'STATE_PENDING' }],
};
});
// @ts-ignore we await the act above so rerenderRef is definitely defined
rerenderRef(
<MockedProvider mocks={[]}>
<MarketsContainer onSelect={spyOnSelect} />
</MockedProvider>
);
// make sure dropdown is still open
await checkDropdown();
async function checkDropdown() {
const dropdownContent = await screen.findByTestId(
'market-actions-content'
);
expect(dropdownContent).toBeInTheDocument();
expect(
within(dropdownContent).getByRole('menuitem', {
name: 'Copy Market ID',
})
).toBeInTheDocument();
}
});
});
@@ -0,0 +1,196 @@
import { useMemo } from 'react';
import type { ColDef } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketTableActions } from './market-table-actions';
interface Props {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
}
const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = ({ onMarketClick }: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>(
() => [
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
cellRenderer: 'MarketName',
cellRendererParams: { onMarketClick },
},
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
minWidth: 170,
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return '-';
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[marketTradingMode];
},
filter: SetFilter,
filterParams: {
set: Schema.MarketTradingModeMapping,
},
},
{
headerName: t('Status'),
field: 'state',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
},
filter: SetFilter,
filterParams: {
set: Schema.MarketStateMapping,
},
},
{
headerName: t('Best bid'),
field: 'data.bestBidPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
),
},
{
headerName: t('Best offer'),
field: 'data.bestOfferPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
),
},
{
headerName: t('Mark price'),
field: 'data.markPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces),
},
{
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
},
{
colId: 'market-actions',
field: 'id',
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketTableActions
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
},
},
],
[onMarketClick, openAssetDetailsDialog]
);
};
@@ -104,9 +104,7 @@ export const OracleFullProfile = ({
<div className="mb-2">{message}</div>
<div className="mb-2">
<ReactMarkdown
className="react-markdown-container [word-break:break-word]"
skipHtml={true}
disallowedElements={['img']}
className="[word-break:break-word]"
linkTarget="_blank"
>
{showMore
@@ -84,7 +84,7 @@ describe('useMarketOracle', () => {
type: 'eth_address',
},
],
oracle: {},
oracle: { eth_address: 'eth_address' },
} as Provider,
{
proofs: [
@@ -93,7 +93,7 @@ describe('useMarketOracle', () => {
type: 'eth_address',
},
],
oracle: {},
oracle: { eth_address: address },
} as Provider,
];
mockOracleProofs.mockReturnValueOnce({
@@ -113,7 +113,9 @@ describe('useMarketOracle', () => {
type: 'public_key',
},
],
oracle: {},
oracle: {
public_key: 'public_key',
},
} as Provider,
{
proofs: [
@@ -122,7 +124,9 @@ describe('useMarketOracle', () => {
type: 'public_key',
},
],
oracle: {},
oracle: {
public_key: key,
},
} as Provider,
];
mockOracleProofs.mockReturnValueOnce({
+24 -26
View File
@@ -9,32 +9,30 @@ import type { DataSourceSpecFragment } from '../__generated__';
export const getMatchingOracleProvider = (
dataSourceSpec: DataSourceSpecFragment,
providers: Provider[]
) =>
providers.find((provider) =>
provider.proofs.some((proof) => {
if (
proof.type === 'eth_address' &&
dataSourceSpec.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
return dataSourceSpec.sourceType.sourceType.signers?.some(
(signer) =>
signer.signer.__typename === 'ETHAddress' &&
signer.signer.address === proof.eth_address
);
}
if (
proof.type === 'public_key' &&
dataSourceSpec.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
return dataSourceSpec.sourceType.sourceType.signers?.some(
(signer) =>
signer.signer.__typename === 'PubKey' &&
signer.signer.key === proof.public_key
);
}
return false;
})
);
) => {
return providers.find((provider) => {
let oracleSignature: string;
const oracle = provider.oracle;
if ('public_key' in oracle && oracle.public_key) {
oracleSignature = oracle.public_key;
} else if ('eth_address' in oracle && oracle.eth_address) {
oracleSignature = oracle.eth_address;
}
if (
dataSourceSpec.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
return dataSourceSpec.sourceType.sourceType.signers?.some(
(signer) =>
(signer.signer.__typename === 'ETHAddress' &&
signer.signer.address === oracleSignature) ||
(signer.signer.__typename === 'PubKey' &&
signer.signer.key === oracleSignature)
);
}
return false;
});
};
export const useMarketOracle = (
marketId: string,
@@ -0,0 +1,676 @@
import { renderHook } from '@testing-library/react';
import type { Provider } from '../oracle-schema';
import { useOracleMarkets } from './use-oracle-markets';
const mockMarkets = jest.fn<{ data: unknown | null }, unknown[]>(() => ({
data: marketsData,
}));
jest.mock('../__generated__/OracleMarketsSpec', () => ({
useOracleMarketsSpecQuery: jest.fn((args) => mockMarkets()),
}));
describe('useOracleMarkets', () => {
it('returns undefined if no market data present', () => {
mockMarkets.mockReturnValueOnce({ data: null });
const { result } = renderHook(() => useOracleMarkets(mockProvider));
expect(result.current).toBeUndefined();
});
it('returns correct market list for the given provider', () => {
mockMarkets.mockReturnValueOnce({ data: marketsData });
const { result } = renderHook(() => useOracleMarkets(mockProvider));
console.log(JSON.stringify(result.current));
expect(result.current).toStrictEqual(oracleMarkets);
});
});
const mockProvider: Provider = {
name: 'Mock Oracle',
url: 'https://Mock.com',
description_markdown: 'mock oracle description',
oracle: {
status: 'GOOD',
status_reason: '',
first_verified: '2023-05-22T00:00:00.000Z',
last_verified: '2023-05-22T00:00:00.000Z',
type: 'eth_address',
eth_address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
proofs: [
{
format: 'url',
available: true,
type: 'web',
url: 'https://web.archive.org/web/20200923175817/https://docs.pro.Mock.com/#oracle',
},
],
github_link:
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xaddress.toml',
};
const marketsData = {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '2dca7baa5f7269b08d053668bca03f97f72e9a162327eebd941c54f1f9fb8f80',
state: 'STATE_ACTIVE',
tradingMode: 'TRADING_MODE_CONTINUOUS',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'BTC/USDT expiry 2023 June 30th',
code: 'BTC/USDT-230630',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '6eb55cdb9e3d1697d9df2eb2d97c4560da3519652fdf7e542f5801fc0919c32d',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address: '0xaddress',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.BTC.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.BTC.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: '01d86d6182ee2e03cf02f9091734494932d39ab5ae6f23f7f5fd1fbe6668d422',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.BTC.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '84025e68387cf61c2b91228d768dcdd4f10a9ee5cd2824fdea35b259976f59c1',
state: 'STATE_ACTIVE',
tradingMode: 'TRADING_MODE_CONTINUOUS',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'LINK/USDT expiry 2023 June 30th',
code: 'LINK/USDT-230630',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'bd709a4e6820d8714241f4c9576dffa71108179663c1f8442c991121fa1b0251',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address: '0xaddress',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.LINK.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.LINK.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: '01d86d6182ee2e03cf02f9091734494932d39ab5ae6f23f7f5fd1fbe6668d422',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.LINK.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '4507930a8c508eef6731f1342720adfa5f46096a8ef7a5848450740132ab78ab',
state: 'STATE_ACTIVE',
tradingMode: 'TRADING_MODE_CONTINUOUS',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'ETH/USDT expiry 2023 June 30th',
code: 'ETH/USDT-230630',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '2687518113f63219a0b7594688dc78be62c86936a8ce306f50032ec70bdce493',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address: '0xaddress',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.ETH.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.ETH.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: '01d86d6182ee2e03cf02f9091734494932d39ab5ae6f23f7f5fd1fbe6668d422',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.ETH.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '074c929bba8faeeeba352b2569fc5360a59e12cdcbf60f915b492c4ac228b566',
state: 'STATE_PROPOSED',
tradingMode: 'TRADING_MODE_NO_TRADING',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'LINK/USDT expiry 2023 Sept 30th',
code: 'LINK/USDT-230930',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'cda7643a04cb45f62fdb06851a6fea2dc18d94931f2eab58f6918c6c13352fb6',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address: '0xaddress',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.LINK.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.LINK.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.LINK.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '2c2ea995d7366e423be7604f63ce047aa7186eb030ecc7b77395eae2fcbffcc5',
state: 'STATE_PROPOSED',
tradingMode: 'TRADING_MODE_NO_TRADING',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'ETH/USDT expiry 2023 Sept 30th',
code: 'ETH/USDT-230930',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'bb59cbdfbe167abc714954bf474354ac80b2feb798b907d6d86554fdd551f804',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address:
'0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.ETH.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.ETH.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.ETH.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: '5b05109662e7434fea498c4a1c91d3179b80e9b8950d6106cec60e1f342fc604',
state: 'STATE_PROPOSED',
tradingMode: 'TRADING_MODE_NO_TRADING',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'BTC/USDT expiry 2023 Sept 30th',
code: 'BTC/USDT-230930',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '99a1551b8cc7b75a3628a768e0772dde4c5a1ddf6c647507079c2e111d614a28',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address:
'0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.BTC.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.BTC.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.BTC.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
},
],
},
};
const oracleMarkets = [
{
__typename: 'Market',
id: '2c2ea995d7366e423be7604f63ce047aa7186eb030ecc7b77395eae2fcbffcc5',
state: 'STATE_PROPOSED',
tradingMode: 'TRADING_MODE_NO_TRADING',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'ETH/USDT expiry 2023 Sept 30th',
code: 'ETH/USDT-230930',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'bb59cbdfbe167abc714954bf474354ac80b2feb798b907d6d86554fdd551f804',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.ETH.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.ETH.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115',
data: {
__typename: 'DataSourceDefinition',
sourceType: { __typename: 'DataSourceDefinitionInternal' },
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.ETH.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
{
__typename: 'Market',
id: '5b05109662e7434fea498c4a1c91d3179b80e9b8950d6106cec60e1f342fc604',
state: 'STATE_PROPOSED',
tradingMode: 'TRADING_MODE_NO_TRADING',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'BTC/USDT expiry 2023 Sept 30th',
code: 'BTC/USDT-230930',
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '99a1551b8cc7b75a3628a768e0772dde4c5a1ddf6c647507079c2e111d614a28',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'ETHAddress',
address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
},
},
],
filters: [
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.BTC.value',
type: 'TYPE_INTEGER',
numberDecimalPlaces: 6,
},
},
{
__typename: 'Filter',
key: {
__typename: 'PropertyKey',
name: 'prices.BTC.timestamp',
type: 'TYPE_TIMESTAMP',
numberDecimalPlaces: null,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115',
data: {
__typename: 'DataSourceDefinition',
sourceType: { __typename: 'DataSourceDefinitionInternal' },
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.BTC.value',
tradingTerminationProperty: 'vegaprotocol.builtin.timestamp',
},
},
},
},
},
];
@@ -5,9 +5,14 @@ import { useOracleMarketsSpecQuery } from '../__generated__/OracleMarketsSpec';
export const useOracleMarkets = (
provider: Provider
): OracleMarketSpecFieldsFragment[] | undefined => {
const signedProofs = provider.proofs.filter(
(proof) => proof.format === 'signed_message' && proof.available === true
);
let oracleSignature: string;
const oracle = provider.oracle;
if ('public_key' in oracle && oracle.public_key) {
oracleSignature = oracle.public_key;
}
if ('eth_address' in oracle && oracle.eth_address) {
oracleSignature = oracle.eth_address;
}
const { data: markets } = useOracleMarketsSpecQuery();
@@ -20,30 +25,16 @@ export const useOracleMarkets = (
return false;
}
const signers = sourceType?.sourceType.signers;
const signerKeys = signers?.filter(Boolean).map((signer) => {
if (signer.signer.__typename === 'ETHAddress') {
return signer.signer.address;
}
if (signer.signer.__typename === 'PubKey') {
return signer.signer.key;
}
return undefined;
});
const signedProofsKeys = signedProofs.map((proof) => {
if ('public_key' in proof && proof.public_key) {
return proof.public_key;
}
if ('eth_address' in proof && proof.eth_address) {
return proof.eth_address;
}
return undefined;
});
const key = signedProofsKeys.find((key) => signerKeys?.includes(key));
const key = signerKeys?.find((key) => key === oracleSignature);
return !!key;
});
return oracleMarkets;
@@ -1,23 +1,23 @@
import { update } from './order-data-provider';
import {
update,
mapOrderUpdateToOrder,
filterOrderUpdates,
} from './order-data-provider';
import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../';
import type { Edge } from '@vegaprotocol/data-provider';
describe('order data provider', () => {
it('puts incoming data in proper place', () => {
const data = [
{
node: {
id: '2',
createdAt: new Date('2022-01-29').toISOString(),
},
id: '2',
createdAt: new Date('2022-01-29').toISOString(),
},
{
node: {
id: '1',
createdAt: new Date('2022-01-28').toISOString(),
},
id: '1',
createdAt: new Date('2022-01-28').toISOString(),
},
] as Edge<OrderFieldsFragment>[];
] as OrderFieldsFragment[];
const delta = [
// this one should be dropped because id don't exits and it's older than newest
@@ -52,39 +52,41 @@ describe('order data provider', () => {
createdAt: new Date('2022-02-05').toISOString(),
},
] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, { partyId: '0x123' });
const updatedData = update(
data,
filterOrderUpdates(delta),
{ partyId: '0x123' },
mapOrderUpdateToOrder
);
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
-1
);
expect(updatedData && updatedData[3].id).toEqual(delta[2].id);
expect(updatedData && updatedData[3].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[0].id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[4].updatedAt);
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(updatedData && updatedData[3].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[3].node.updatedAt).toEqual(
delta[2].updatedAt
);
expect(updatedData && updatedData[0].node.id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].node.id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[4].updatedAt
);
expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual(
5
);
update(
[],
filterOrderUpdates(delta),
{ partyId: '0x123' },
mapOrderUpdateToOrder
)?.length
).toEqual(5);
});
it('add only data matching date range filter', () => {
const data = [
{
node: {
id: '1',
createdAt: new Date('2022-01-29').toISOString(),
},
id: '1',
createdAt: new Date('2022-01-29').toISOString(),
},
{
node: {
id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
] as Edge<OrderFieldsFragment>[];
] as OrderFieldsFragment[];
const delta = [
// this one should be ignored because it does not match date range
@@ -105,22 +107,23 @@ describe('order data provider', () => {
},
] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, {
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
const updatedData = update(
data,
filterOrderUpdates(delta),
{
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
},
},
});
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].node.updatedAt).toEqual(
delta[2].updatedAt
mapOrderUpdateToOrder
);
expect(updatedData && updatedData[2].node.id).toEqual(delta[1].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[1].updatedAt
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
-1
);
expect(updatedData && updatedData[0].id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[2].id).toEqual(delta[1].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[1].updatedAt);
});
});
@@ -6,8 +6,8 @@ import {
defaultAppend as append,
} from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import { marketsMapProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge, Cursor } from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types';
import type {
OrderFieldsFragment,
@@ -15,6 +15,7 @@ import type {
OrdersQuery,
OrdersUpdateSubscription,
OrdersQueryVariables,
OrdersUpdateSubscriptionVariables,
} from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
import type { ApolloClient } from '@apollo/client';
@@ -37,18 +38,21 @@ const orderMatchFilters = (
if (!order) {
return true;
}
if (
variables?.filter?.status &&
!(order.status && variables.filter.status.includes(order.status))
) {
return false;
}
if (
variables?.filter?.liveOnly &&
!(order.status && liveOnlyOrderStatuses.includes(order.status))
) {
return false;
}
if (
variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type))
@@ -76,10 +80,11 @@ const orderMatchFilters = (
) {
return false;
}
return true;
};
const mapOrderUpdateToOrder = (
export const mapOrderUpdateToOrder = (
orderUpdate: OrderUpdateFieldsFragment
): OrderFieldsFragment => {
const { marketId, liquidityProvisionId, ...order } = orderUpdate;
@@ -101,10 +106,36 @@ const mapOrderUpdateToOrder = (
};
};
const mapOrderUpdateToOrderWithMarket =
(markets: Record<string, Market>) =>
(orderUpdate: OrderUpdateFieldsFragment): Order => {
const { market, ...order } = mapOrderUpdateToOrder(orderUpdate);
return {
...order,
market: markets[market.id],
};
};
const getData = (
responseData: OrdersQuery | null
): Edge<OrderFieldsFragment>[] =>
responseData?.party?.ordersConnection?.edges || [];
): (OrderFieldsFragment & Cursor)[] =>
responseData?.party?.ordersConnection?.edges?.map<
OrderFieldsFragment & Cursor
>((edge) => ({ ...edge.node, cursor: edge.cursor })) || [];
export const filterOrderUpdates = (
orders: OrdersUpdateSubscription['orders']
) => {
// 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
return orderBy(
uniqBy(
orderBy(orders, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
};
const getDelta = (
subscriptionData: OrdersUpdateSubscription,
@@ -114,49 +145,32 @@ const getDelta = (
if (!subscriptionData.orders) {
return [];
}
return subscriptionData.orders;
return filterOrderUpdates(subscriptionData.orders);
};
export const update = (
data: ReturnType<typeof getData> | null,
export const update = <T extends Omit<OrderFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
reload: () => void,
variables?: OrdersQueryVariables
) => {
if (!data) {
return data;
}
// 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 = orderBy(
uniqBy(
orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
const updatedData = [...data];
incoming.forEach((orderUpdate) => {
const index = data.findIndex((edge) => edge.node.id === orderUpdate.id);
const newer =
data.length === 0 || orderUpdate.createdAt >= data[0].node.createdAt;
variables: OrdersQueryVariables,
mapDeltaToData: (delta: OrderUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
delta.forEach((orderUpdate) => {
const index = data?.findIndex((order) => order.id === orderUpdate.id) ?? -1;
const newer = !data?.length || orderUpdate.createdAt >= data[0].createdAt;
const doesFilterPass =
!variables || orderMatchFilters(orderUpdate, variables);
if (index !== -1) {
if (doesFilterPass) {
updatedData[index] = {
...updatedData[index],
node: mapOrderUpdateToOrder(orderUpdate),
...mapDeltaToData(orderUpdate),
};
} else {
updatedData.splice(index, 1);
}
} else if (newer && doesFilterPass) {
updatedData.unshift({
node: mapOrderUpdateToOrder(orderUpdate),
cursor: '',
});
updatedData.unshift(mapDeltaToData(orderUpdate));
}
});
return updatedData;
@@ -170,11 +184,13 @@ export const ordersProvider = makeDataProvider<
ReturnType<typeof getData>,
OrdersUpdateSubscription,
ReturnType<typeof getDelta>,
OrdersQueryVariables
OrdersQueryVariables,
OrdersUpdateSubscriptionVariables
>({
query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument,
update,
update: (data, delta, reload, variables) =>
update(data, delta, variables, mapOrderUpdateToOrder),
getData,
getDelta,
pagination: {
@@ -185,6 +201,10 @@ export const ordersProvider = makeDataProvider<
resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true },
fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ partyId, marketIds }) => ({
partyId,
marketIds,
}),
});
export const activeOrdersProvider = makeDerivedDataProvider<
@@ -208,27 +228,36 @@ export const activeOrdersProvider = makeDerivedDataProvider<
}
const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
? orders.filter((order) => variables.marketId === order.market.id)
: orders;
}
);
export const ordersWithMarketProvider = makeDerivedDataProvider<
(Order | null)[],
Order[],
(Order & Cursor)[],
never,
OrdersQueryVariables
>(
[
ordersProvider,
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData): Order[] =>
((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({
...edge.node,
market: (partsData[1] as Market[]).find(
(market) => market.id === edge.node.market.id
),
}))
(partsData, variables, prevData, parts): Order[] => {
if (prevData && parts[0].isUpdate) {
return update(
prevData,
parts[0].delta,
variables,
mapOrderUpdateToOrderWithMarket(partsData[1] as Record<string, Market>)
);
}
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
(order) => ({
...order,
market: (partsData[1] as Record<string, Market>)[order.market.id],
})
);
}
);
export const hasActiveOrderProvider = makeDerivedDataProvider<
@@ -244,7 +273,7 @@ export const hasAmendableOrderProvider = makeDerivedDataProvider<
>([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some(
(edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder)
(order) => !(order.liquidityProvision || order.peggedOrder)
);
return hasAmendableOrder;
});
@@ -32,7 +32,6 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
totalCount: undefined,
});
await act(async () => {
render(generateJsx());
@@ -87,7 +87,7 @@ export const OrderListManager = ({
gridRef.current.api.setRowData(data);
return true;
}
return true;
return false;
},
});
@@ -197,6 +197,7 @@ const positionsDataProvider = makeDataProvider<
getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
subscriptionData.positions,
getSubscriptionVariables,
fetchPolicy: 'no-cache',
});
const positionDataProvider = makeDerivedDataProvider<
+20 -2
View File
@@ -47,7 +47,7 @@ const positionFields: PositionFieldsFragment[] = [
{
__typename: 'Position',
realisedPNL: '230000',
openVolume: '6',
openVolume: '-6',
unrealisedPNL: '895000',
averageEntryPrice: '1129935',
updatedAt: '2022-07-28T15:09:34.441143Z',
@@ -82,7 +82,7 @@ const positionFields: PositionFieldsFragment[] = [
},
{
__typename: 'Position',
realisedPNL: '230000',
realisedPNL: '-230000',
openVolume: '1',
unrealisedPNL: '-22519',
averageEntryPrice: '84400088',
@@ -98,6 +98,24 @@ const positionFields: PositionFieldsFragment[] = [
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
{
__typename: 'Position',
realisedPNL: '-303295252',
openVolume: '0',
unrealisedPNL: '0',
averageEntryPrice: '6126312',
updatedAt: '2022-07-28T14:53:54.725477Z',
positionStatus: PositionStatus.POSITION_STATUS_CLOSED_OUT,
lossSocializationAmount: '261',
market: {
id: 'market-3',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
},
];
const marginsFields: MarginFieldsFragment[] = [
+22 -21
View File
@@ -10,32 +10,33 @@ fragment TradeFields on Trade {
}
query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) {
id
tradesConnection(pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
trades(filter: { marketIds: [$marketId] }, pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
subscription TradesUpdate($marketId: ID!) {
trades(marketId: $marketId) {
id
price
size
createdAt
marketId
aggressor
tradesStream(filter: { marketIds: [$marketId] }) {
...TradeUpdateFields
}
}
+28 -24
View File
@@ -11,14 +11,16 @@ export type TradesQueryVariables = Types.Exact<{
}>;
export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null };
export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side };
export type TradesUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
export const TradeFieldsFragmentDoc = gql`
fragment TradeFields on Trade {
@@ -32,23 +34,30 @@ export const TradeFieldsFragmentDoc = gql`
}
}
`;
export const TradeUpdateFieldsFragmentDoc = gql`
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
`;
export const TradesDocument = gql`
query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) {
id
tradesConnection(pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
trades(filter: {marketIds: [$marketId]}, pagination: $pagination) {
edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
@@ -84,16 +93,11 @@ export type TradesLazyQueryHookResult = ReturnType<typeof useTradesLazyQuery>;
export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>;
export const TradesUpdateDocument = gql`
subscription TradesUpdate($marketId: ID!) {
trades(marketId: $marketId) {
id
price
size
createdAt
marketId
aggressor
tradesStream(filter: {marketIds: [$marketId]}) {
...TradeUpdateFields
}
}
`;
${TradeUpdateFieldsFragmentDoc}`;
/**
* __useTradesUpdateSubscription__
+1 -3
View File
@@ -1,4 +1,3 @@
import compact from 'lodash/compact';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react';
@@ -19,12 +18,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
dataProvider: tradesWithMarketProvider,
variables: { marketId },
});
const trades = compact(data).map((d) => d.node);
return (
<TradesTable
ref={gridRef}
rowData={trades}
rowData={data}
onClick={(price?: string) => {
if (price) {
updateOrder(marketId, { price });
+76 -57
View File
@@ -3,75 +3,95 @@ import {
makeDerivedDataProvider,
defaultAppend as append,
} from '@vegaprotocol/data-provider';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import type { PageInfo, Cursor } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets';
import type {
TradesQuery,
TradesQueryVariables,
TradeFieldsFragment,
TradesUpdateSubscription,
TradeUpdateFieldsFragment,
TradesUpdateSubscriptionVariables,
} from './__generated__/Trades';
import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades';
import orderBy from 'lodash/orderBy';
import produce from 'immer';
export const MAX_TRADES = 500;
const getData = (
responseData: TradesQuery | null
): ({
cursor: string;
node: TradeFieldsFragment;
} | null)[] => responseData?.market?.tradesConnection?.edges || [];
): (TradeFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<TradeFieldsFragment & Cursor>((edge) => ({
...edge.node,
cursor: edge.cursor,
})) || [];
const getDelta = (subscriptionData: TradesUpdateSubscription) =>
subscriptionData?.trades || [];
subscriptionData?.tradesStream || [];
const update = (
data: ReturnType<typeof getData> | null,
delta: ReturnType<typeof getDelta>
) => {
if (!data) return data;
return produce(data, (draft) => {
// for each incoming trade add it to the beginning and remove oldest trade
orderBy(delta, 'createdAt', 'desc').forEach((node) => {
const { marketId, ...nodeData } = node;
draft.unshift({
node: {
...nodeData,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
},
cursor: '',
});
const mapTradeUpdateToTrade = (
tradeUpdate: TradeUpdateFieldsFragment
): TradeFieldsFragment => {
const { marketId, ...trade } = tradeUpdate;
return {
...trade,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
};
};
if (draft.length > MAX_TRADES) {
draft.pop();
}
});
const mapTradeUpdateToTradeWithMarket =
(markets: Record<string, Market>) =>
(tradeUpdate: TradeUpdateFieldsFragment): Trade => {
const { market, ...trade } = mapTradeUpdateToTrade(tradeUpdate);
return {
...trade,
market: markets[market.id],
};
};
const update = <T extends Omit<TradeFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: TradesQueryVariables,
mapDeltaToData: (delta: TradeUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((tradeUpdate) => {
const index = data?.findIndex((trade) => trade.id === tradeUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(tradeUpdate),
};
} else if (!data?.length || tradeUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(tradeUpdate));
}
});
return updatedData.slice(0, MAX_TRADES);
};
export type Trade = Omit<TradeFieldsFragment, 'market'> & { market?: Market };
export type TradeEdge = Edge<Trade>;
const getPageInfo = (responseData: TradesQuery | null): PageInfo | null =>
responseData?.market?.tradesConnection?.pageInfo || null;
responseData?.trades?.pageInfo || null;
export const tradesProvider = makeDataProvider<
Parameters<typeof getData>['0'],
ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>,
TradesQueryVariables
TradesQueryVariables,
TradesUpdateSubscriptionVariables
>({
query: TradesDocument,
subscriptionQuery: TradesUpdateDocument,
update,
update: (data, delta, reload, variables) =>
update(data, delta, variables, mapTradeUpdateToTrade),
getData,
getDelta,
pagination: {
@@ -79,34 +99,33 @@ export const tradesProvider = makeDataProvider<
append,
first: MAX_TRADES,
},
fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ marketId }) => ({ marketId }),
});
export const tradesWithMarketProvider = makeDerivedDataProvider<
(TradeEdge | null)[],
Trade[],
(Trade & Cursor)[],
never,
TradesQueryVariables
>(
[
tradesProvider,
(callback, client) => marketsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData): (TradeEdge | null)[] | null => {
const edges = partsData[0] as ReturnType<typeof getData>;
return edges.map((edge) => {
if (edge === null) {
return null;
}
const node = {
...edge.node,
market: (partsData[1] as Market[]).find(
(market) => market.id === edge.node.market.id
),
};
const cursor = edge?.cursor || '';
return {
cursor,
node,
};
});
(partsData, variables, prevData, parts): Trade[] | null => {
if (prevData && parts[0].isUpdate) {
return update(
prevData,
parts[0].delta as ReturnType<typeof getDelta>,
variables,
mapTradeUpdateToTradeWithMarket(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],
})
);
}
);
+14 -18
View File
@@ -11,24 +11,20 @@ export const tradesQuery = (
override?: PartialDeep<TradesQuery>
): TradesQuery => {
const defaultResult: TradesQuery = {
market: {
id: 'market-0',
tradesConnection: {
__typename: 'TradeConnection',
edges: trades.map((node, i) => ({
__typename: 'TradeEdge',
node,
cursor: (i + 1).toString(),
})),
pageInfo: {
__typename: 'PageInfo',
startCursor: '0',
endCursor: trades.length.toString(),
hasNextPage: false,
hasPreviousPage: false,
},
trades: {
__typename: 'TradeConnection',
edges: trades.map((node, i) => ({
__typename: 'TradeEdge',
node,
cursor: (i + 1).toString(),
})),
pageInfo: {
__typename: 'PageInfo',
startCursor: '0',
endCursor: trades.length.toString(),
hasNextPage: false,
hasPreviousPage: false,
},
__typename: 'Market',
},
};
@@ -40,7 +36,7 @@ export const tradesUpdateSubscription = (
): TradesUpdateSubscription => {
const defaultResult: TradesUpdateSubscription = {
__typename: 'Subscription',
trades: [
tradesStream: [
{
__typename: 'TradeUpdate',
id: '1234567890',
+8
View File
@@ -5,3 +5,11 @@ This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test ui-toolkit` to execute the unit tests via [Jest](https://jestjs.io).
## Build
Run `yarn nx run ui-toolkit:build-storybook`
## Deployment
deployed at: `ui.vega.rocks`
@@ -161,6 +161,51 @@ export const DropdownMenuSeparator = forwardRef<
/>
));
/**
* Container element for submenus
*/
export const DropdownMenuSub = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Sub>,
React.ComponentProps<typeof DropdownMenuPrimitive.Sub>
>(({ ...subProps }) => <DropdownMenuPrimitive.Sub {...subProps} />);
/**
* Container within a DropdownMenuSub specifically for the content
*/
export const DropdownMenuSubContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...subContentProps }, forwardedRef) => (
<DropdownMenuPrimitive.SubContent
ref={forwardedRef}
className={classNames('bg-vega-light-150 dark:bg-vega-dark-150', className)}
{...subContentProps}
/>
));
/**
* Equivalent to trigger, but for triggering sub menus
*/
export const DropdownMenuSubTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger>
>(({ className, ...subTriggerProps }, forwardedRef) => (
<DropdownMenuPrimitive.SubTrigger
className={classNames(className, itemClass)}
ref={forwardedRef}
{...subTriggerProps}
/>
));
/**
* Portal to ensure menu portions are rendered outwith where they appear in the
* DOM.
*/
export const DropdownMenuPortal = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Portal>,
React.ComponentProps<typeof DropdownMenuPrimitive.Portal>
>(({ ...portalProps }) => <DropdownMenuPrimitive.Portal {...portalProps} />);
/**
* Wraps a regular DropdownMenuItem with copy to clip board functionality
*/
@@ -9,6 +9,8 @@ import {
} from '@radix-ui/react-tooltip';
import type { ITooltipParams } from 'ag-grid-community';
const tooltipContentClasses =
'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs break-word';
export interface TooltipProps {
children: React.ReactElement;
description?: string | ReactNode;
@@ -40,7 +42,7 @@ export const Tooltip = ({
align={align}
side={side}
alignOffset={8}
className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm text-black dark:text-white break-word"
className={tooltipContentClasses}
>
<div className="relative z-0" data-testid="tooltip-content">
{description}
@@ -55,9 +57,5 @@ export const Tooltip = ({
);
export const TooltipCellComponent = (props: ITooltipParams) => {
return (
<p className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white">
{props.value}
</p>
);
return <p className={tooltipContentClasses}>{props.value}</p>;
};
+1 -1
View File
@@ -12,7 +12,7 @@
},
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"],
"accessToken": "OTY4ZjdlZTItNGIwNy00NDcyLTllZjctOWIzYTg1NWE0Yzg1fHJlYWQtd3JpdGU=",
+1 -1
View File
@@ -161,7 +161,7 @@
"babel-loader": "8.1.0",
"cypress": "^12.9.0",
"cypress-mochawesome-reporter": "^3.3.0",
"cypress-real-events": "^1.7.6",
"cypress-real-events": "^1.8.1",
"dotenv": "^16.0.1",
"eslint": "8.15.0",
"eslint-config-next": "12.2.3",
+4 -4
View File
@@ -11882,10 +11882,10 @@ cypress-mochawesome-reporter@^3.3.0:
mochawesome-merge "^4.2.1"
mochawesome-report-generator "^6.2.0"
cypress-real-events@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.6.tgz#6f17e0b2ceea1d6dc60f6737d8f84cc517bbbb4c"
integrity sha512-yP6GnRrbm6HK5q4DH6Nnupz37nOfZu/xn1xFYqsE2o4G73giPWQOdu6375QYpwfU1cvHNCgyD2bQ2hPH9D7NMw==
cypress-real-events@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.8.1.tgz#d00c7fe93124bbe7c0f27296684838614d24a840"
integrity sha512-8fFnA8EzS3EVbAmpSEUf3A8yZCmfU3IPOSGUDVFCdE1ke1gYL1A+gvXXV6HKUbTPRuvKKt2vpaMbUwYLpDRswQ==
cypress@^12.9.0:
version "12.9.0"