Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ef3c25c7c | ||
|
|
7dd41fc59e | ||
|
|
67a1f10b1c | ||
|
|
2eee4c071f | ||
|
|
5a90ebe12e | ||
|
|
6be117086a | ||
|
|
9c6dd9f254 | ||
|
|
a675576a52 | ||
|
|
98d248e02e | ||
|
|
c8d55c6293 | ||
|
|
3467feb3e5 | ||
|
|
bdf1663709 | ||
|
|
d775573543 | ||
|
|
c0fdc8d570 | ||
|
|
a9ebb36b9b | ||
|
|
f6c0082f2d | ||
|
|
931ad75cc9 | ||
|
|
f25a58a3c3 | ||
|
|
37340b4dc3 | ||
|
|
17ba3eb351 | ||
|
|
8f966a4ba6 | ||
|
|
cc7f403718 | ||
|
|
9184c2374c | ||
|
|
37247a504a | ||
|
|
2e7a6a6458 | ||
|
|
bf6c13f523 | ||
|
|
a6f3a08d2f | ||
|
|
f13e456a29 | ||
|
|
5cab040189 |
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,7 +5,7 @@ NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
||||
NX_VEGA_URL=https://api.vega.community/graphql
|
||||
NX_VEGA_ENV=MAINNET
|
||||
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
|
||||
NX_BLOCK_EXPLORER=https://be.vega.community/rest
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"executor": "@nrwl/workspace:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.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>
|
||||
<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
|
||||
|
||||
@@ -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
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 || [],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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
@@ -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__
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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({
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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
@@ -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,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 });
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user