Compare commits

...
Author SHA1 Message Date
Matthew Russellandmaciek 5f7e60d22f chore(orders): update orders filters (#3277)
Co-authored-by: maciek <maciek@vegaprotocol.io>
2023-03-29 13:12:29 +02:00
Joe Tsang 351583ce8f chore(cypress): fix error seen in CI for staking test (#3304) 2023-03-29 11:52:46 +01:00
Maciek 88bf58b974 chore(trading): remove files of envs to kill (#3303) 2023-03-29 10:57:33 +01:00
Maciek fcda4d7fcd chore(wallet,positions): use reduceOnly and IOC order for close position tx (#3263) 2023-03-28 23:50:11 -07:00
dexturr b1b0039b33 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-29 06:08:40 +00:00
Art 6854980b32 fix(trading): volumes formatting (#3293) 2023-03-28 22:12:52 -07:00
dexturr d8aac83f2d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-29 00:18:30 +00:00
Sam Keen 5c17045752 fix(governance): paginate rewards as well as delegations data (#3288) 2023-03-28 20:35:29 +01:00
Bartłomiej Głownia 8863209342 feat(trading): persist state of chart intervals studies and overlays (#3261) 2023-03-28 21:03:53 +02:00
Joe Tsang 462bf6b8b5 test(governance): my stake e2e tests (#3295) 2023-03-28 19:03:58 +01:00
Ciaran McGhie e0b701e05a chore(react-helpers): react-helpers v0.1.0 (#3289) 2023-03-28 16:13:34 +01:00
Sam KeenandJoe eca212eee0 feat(governance): see user stake on validators table (#3250)
Co-authored-by: Joe <joe@vega.xyz>
2023-03-28 15:28:15 +01:00
Sam Keen c25858037a fix(governance): order proposals list by closing date (#3276) 2023-03-28 14:04:24 +01:00
Ciaran McGhie 081f5b48e2 chore(ui-toolkit): version 0.9.0 - includes healthbar in ui-toolkit (#3287) 2023-03-28 13:36:29 +01:00
dexturr e306d1c9fa chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-28 12:08:16 +00:00
dexturr 737dec4f5f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-28 06:08:37 +00:00
dexturr 6780b2f618 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-28 00:15:54 +00:00
Penny Andrews 08dc60086d chore(ci): update name of release issue template (#3279) 2023-03-27 11:47:35 -07:00
Bartłomiej Głownia 4595db07b0 fix(trading): throttle deal ticket submit (#3282) 2023-03-27 11:46:40 -07:00
dexturr c3da31146f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 18:08:26 +00:00
dexturr b6a9faaa7c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 12:10:42 +00:00
daro-maj 477d523582 test(trading): fixed capsule tests (#3281) 2023-03-27 12:50:22 +02:00
Maciek a2b112d898 fix(positions): make positions table smoothly updated (again) (#3266) 2023-03-27 12:30:55 +02:00
Mikołaj Młodzikowski 621ba497e8 feat: remove env vars file after setting up js file 2023-03-27 11:03:06 +02:00
dexturr a6964bc3cf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 06:06:57 +00:00
dexturr 48a9998441 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 00:16:27 +00:00
dexturr bd1bd94ee7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 18:07:12 +00:00
dexturr 2fdebbd694 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 12:10:22 +00:00
dexturr 96af8035d0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 06:07:03 +00:00
dexturr 051e3da366 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 00:17:44 +00:00
dexturr af5e666d5e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 18:09:35 +00:00
dexturr 333a7d6206 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 12:10:21 +00:00
dexturr cb6b988466 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 06:09:33 +00:00
dexturr 806ea74f7e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 00:14:18 +00:00
Matthew Russell 8693f3bfd8 chore(trading): dont show faucet button until connected (#3251) 2023-03-24 17:06:22 -07:00
Bartłomiej Głownia 58c6652e29 feat(trading): use mini scroll in deal ticket, market info for and landing dialog (#3269) 2023-03-24 17:01:12 -07:00
Bartłomiej Głownia e8adff25c4 fix(trading): handle lack of approval in trading deposit flow (#3278) 2023-03-24 16:57:00 -07:00
Penny Andrews 3e594a783a chore(ci): add release issue template (#3272) 2023-03-24 16:55:22 -07:00
dexturr 1f7117075b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 18:09:07 +00:00
Bartłomiej Głownia 4c850ecead fix(trading): change etherscan link label on toast (#3262) 2023-03-24 17:57:59 +01:00
Mikołaj Młodzikowski 19f8fa909e feat(explorer,trading,governance): use nx to know which apps needs building (#3267) 2023-03-24 16:42:33 +01:00
Sam Keen 8214858685 feat(governance): lp vote now reflected on proposal cards (#3256) 2023-03-24 14:56:56 +00:00
dexturr 868f8e21dc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 12:08:16 +00:00
dexturr 6ead84b57c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 06:09:30 +00:00
dexturr 3975477985 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 00:14:29 +00:00
86 changed files with 1888 additions and 1192 deletions
+4
View File
@@ -3,3 +3,7 @@ apps/**/node_modules/*
tmp/*
.dockerignore
dockerfiles
node_modules
.git
.github
.vscode
+22
View File
@@ -0,0 +1,22 @@
---
name: Release
about:
A template to outline the steps needed to for a successful release of our frontend apps
title: 'Release [add dapp version]-core-[add core version]'
labels:
assignees: ''
---
### Tasks
- [ ] Review [link to core release](xxx)
- [ ] Tag frontend-monorepo
- [ ] Create release and generate release notes
- [ ] Run `@smoke` tests
- [ ] Run `@regression` tests
- [ ] Run `@slow` tests
- [ ] Explorative testing of key flows
- [ ] Set `release/[network]` to tagged commit
- [ ] Verify builds (on Netlify and Fleek) are successful
- [ ] Verify build has been deployed
- [ ] Smoke testing on deployed app
+2
View File
@@ -1,3 +1,4 @@
name: Cypress Run
on:
workflow_call:
inputs:
@@ -17,6 +18,7 @@ jobs:
fail-fast: false
matrix:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 30
steps:
@@ -1,4 +1,4 @@
name: Cypress tests - PR
name: PR Validations
on:
push:
@@ -20,14 +20,16 @@ jobs:
with:
fetch-depth: 0
- name: Check node version
id: node-version
run: |
npmVersion=$(cat .nvmrc | head -n 1)
echo ::set-output name=npmVersion::${npmVersion}
- name: Setup node
uses: actions/setup-node@v3
- name: Remove package.json & yarn.lock to avoid installing everything
run: rm package.json yarn.lock
- name: Install nx
run: yarn add nx
with:
node-version: ${{ steps.node-version.outputs.npmVersion }}
# Check SHAs
- name: Derive appropriate SHAs for base and head for `nx affected` commands
@@ -39,38 +41,51 @@ jobs:
# See affected apps
- name: See affected apps
run: |
nx_version=$(cat package.json | grep '"nx"' | cut -d ':' -f 2 | tr -d '",[:space:]')
rm package.json yarn.lock
yarn add nx@$nx_version
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected"
projects=""
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV
projects_e2e=""
if [[ $affected == *"governance"* ]]; then projects_e2e+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects_e2e+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects_e2e+='"explorer-e2e" '; fi
if [[ -z "$projects_e2e" ]]; then projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects_e2e=${projects_e2e%?}
projects_e2e=[${projects_e2e// /,}]
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }}
run:
run-cypress:
needs: pr
if: ${{ needs.pr.outputs.projects != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.pr.outputs.projects }}
projects: ${{ needs.pr.outputs.projects-e2e }}
tags: '@smoke @regression'
run-docker-build:
needs: pr
if: ${{ needs.pr.outputs.projects != '[]' }}
uses: ./.github/workflows/publish-docker-containers.yml
secrets: inherit
with:
projects: ${{ needs.pr.outputs.projects }}
# Report single result at the end, to avoid mess with required checks in PR
result:
if: ${{ always() }}
needs: run
needs: run-cypress
runs-on: ubuntu-latest
name: Cypress result
steps:
- run: |
result="${{ needs.run.result }}"
result="${{ needs.run-cypress.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
+17 -12
View File
@@ -1,16 +1,19 @@
name: Publish docker containers
name: Docker build
'on':
pull_request:
on:
workflow_call:
inputs:
projects:
required: true
type: string
jobs:
master:
strategy:
fail-fast: false
# to be replaced by nix: https://github.com/vegaprotocol/frontend-monorepo/blob/develop/tools/ipfs-deploy.js#L106-L108
matrix:
app: ${{ fromJson('["explorer"]') }}
name: Build the ${{ matrix.app }} image
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-latest
steps:
- name: Check out code
@@ -45,13 +48,10 @@ jobs:
- name: Build and export to local Docker
uses: docker/build-push-action@v3
with:
context: .
push: false
file: dockerfiles/Dockerfile
load: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
load: true
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
@@ -75,9 +75,7 @@ jobs:
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
push: true
file: dockerfiles/Dockerfile
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
@@ -85,5 +83,12 @@ jobs:
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:latest
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ steps.tags.outputs.version }}
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' }}
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+8 -6
View File
@@ -4,13 +4,15 @@ FROM --platform=amd64 node:${NODE_VERSION}-alpine3.16 as build
WORKDIR /app
# Argument to allow building of different apps
ARG APP
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY yarn.lock ./
RUN apk add --update --no-cache \
python3 \
make \
gcc \
g++
COPY . ./
RUN apk add python3 make gcc g++
RUN yarn --network-timeout 100000 --pure-lockfile
RUN yarn nx build ${APP} --network-timeout 100000 --pure-lockfile
# work around for different build process in trading
RUN sh ./docker-build.sh
# Server environment
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
@@ -25,7 +27,7 @@ CMD ["/entrypoint.sh"]
# Copy dist
WORKDIR /usr/share/nginx/html
COPY --from=build /app/dist/apps/${APP} /usr/share/nginx/html
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/apps/${APP} /usr/share/nginx/html
COPY ./apps/${APP}/.env .env
RUN ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash
@@ -341,7 +341,7 @@ context(
'Currently expected to pass'
);
getProposalInformationFromTable('Expected to pass')
.contains('👍 by Token vote')
.contains('👍 by token vote')
.should('be.visible');
});
@@ -25,22 +25,24 @@ import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
const stakeValidatorListValidatorStake = '[col-id="stake"] > div > span';
const stakeRemoveStakeRadioButton = '[data-testid="remove-stake-radio"]';
const stakeTokenAmountInputBox = '[data-testid="token-amount-input"]';
const stakeTokenSubmitButton = '[data-testid="token-input-submit-button"]';
const stakeAddStakeRadioButton = '[data-testid="add-stake-radio"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const totalStake = '[data-testid="total-stake"]';
const stakeShare = '[data-testid="stake-percentage"]';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
const userStakeBtn = 'my-stake-btn';
const userStake = 'user-stake';
const userStakeShare = 'user-stake-share';
const viewAllValidatorsToggle = 'validators-view-toggle-all';
const viewStakedByMeToggle = 'validators-view-toggle-myStake';
const stakeRemoveStakeRadioButton = 'remove-stake-radio';
const stakeTokenAmountInputBox = 'token-amount-input';
const stakeTokenSubmitButton = 'token-input-submit-button';
const stakeAddStakeRadioButton = 'add-stake-radio';
const stakeMaximumTokens = 'token-amount-use-maximum';
const vegaWalletAssociatedBalance = 'currency-value';
const vegaWalletStakedBalances = 'vega-wallet-balance-staked-validators';
const ethWalletContainer = 'ethereum-wallet';
const vegaWallet = 'vega-wallet';
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
@@ -91,6 +93,39 @@ context(
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
});
it('Able to view validators staked by me', function () {
ensureSpecifiedUnstakedTokensAreAssociated('4');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
closeStakingDialog();
navigateTo(navigation.validators);
cy.getByTestId(userStake, epochTimeout)
.first()
.should('have.text', '2.00');
cy.getByTestId('total-stake').first().realHover();
cy.getByTestId('staked-by-user-tooltip')
.first()
.should('have.text', 'Staked by me: 2.00');
cy.getByTestId('total-pending-stake').first().realHover();
cy.getByTestId('pending-user-stake-tooltip')
.first()
.should('have.text', 'My pending stake: 0.00');
cy.getByTestId(userStakeShare).invoke('text').should('not.be.empty'); // Adjust when #3286 is resolved
cy.getByTestId(userStakeBtn).should('exist').click();
verifyThisEpochValue(2.0);
navigateTo(navigation.validators);
cy.getByTestId(viewStakedByMeToggle).click();
cy.getByTestId(userStakeBtn).should('have.length', 1);
cy.getByTestId(viewAllValidatorsToggle).click();
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('2');
closeStakingDialog();
navigateTo(navigation.validators);
cy.getByTestId(viewStakedByMeToggle).click();
cy.getByTestId(userStakeBtn).should('have.length', 2);
});
it('Able to stake against a validator - using vega from vesting contract', function () {
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
@@ -109,14 +144,13 @@ context(
});
it('Able to stake against a validator - using vega from both wallet and vesting contract', function () {
vegaWalletTeardown();
stakingPageAssociateTokens('3', { type: 'contract' });
navigateTo(navigation.validators);
stakingPageAssociateTokens('4', { type: 'wallet' });
verifyUnstakedBalance(7.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletTotalAssociatedBalance('4.0');
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletTotalAssociatedBalance('4.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('6');
@@ -136,7 +170,7 @@ context(
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(3.0);
cy.get(vegaWalletStakedBalances, txTimeout)
cy.getByTestId(vegaWalletStakedBalances, txTimeout)
.parent()
.should('contain', 2.0, txTimeout);
closeStakingDialog();
@@ -144,36 +178,36 @@ context(
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('1');
verifyUnstakedBalance(2.0);
cy.get(vegaWalletStakedBalances, txTimeout)
cy.getByTestId(vegaWalletStakedBalances, txTimeout)
.should('have.length', 4, txTimeout)
.eq(0)
.should('contain', 2.0, txTimeout);
cy.get(vegaWalletStakedBalances, txTimeout)
cy.getByTestId(vegaWalletStakedBalances, txTimeout)
.eq(1)
.should('contain', 1.0, txTimeout);
closeStakingDialog();
navigateTo(navigation.validators);
cy.get(`[row-id="${0}"]`).within(() => {
cy.get(stakeValidatorListTotalStake)
cy.getByTestId(stakeValidatorListTotalStake)
.should('have.text', '2.00')
.and('be.visible');
cy.get(stakeValidatorListTotalShare)
cy.getByTestId(stakeValidatorListTotalShare)
.should('have.text', '66.67%')
.and('be.visible');
cy.get(stakeValidatorListValidatorStake)
cy.getByTestId(stakeValidatorListTotalStake)
.scrollIntoView()
.should('have.text', '2.00')
.and('be.visible');
});
cy.get(`[row-id="${1}"]`).within(() => {
cy.get(stakeValidatorListTotalStake)
cy.getByTestId(stakeValidatorListTotalStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
cy.get(stakeValidatorListTotalShare)
cy.getByTestId(stakeValidatorListTotalShare)
.should('have.text', '33.33%')
.and('be.visible');
cy.get(stakeValidatorListValidatorStake)
cy.getByTestId(stakeValidatorListTotalStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
@@ -203,9 +237,15 @@ context(
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0);
verifyThisEpochValue(2.0);
cy.get(totalStake, epochTimeout).should('contain.text', '2');
cy.getByTestId(stakeValidatorListTotalStake, epochTimeout).should(
'contain.text',
'2'
);
waitForBeginningOfEpoch();
cy.get(stakeShare).should('have.text', '100%');
cy.getByTestId(stakeValidatorListStakePercentage).should(
'have.text',
'100%'
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
}
@@ -227,12 +267,16 @@ context(
verifyUnstakedBalance(3.0);
verifyNextEpochValue(0.0);
verifyThisEpochValue(0.0);
cy.get(vegaWalletStakedBalances, txTimeout).should(
cy.getByTestId(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '0.00', '0.00%');
cy.getByTestId(userStakeBtn).should('not.exist');
cy.getByTestId(userStake).should('not.exist');
cy.getByTestId(userStakeShare).should('not.exist');
});
it('Unable to remove a stake with a negative value for a validator', function () {
@@ -246,10 +290,10 @@ context(
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
cy.get(stakeRemoveStakeRadioButton, txTimeout).click();
cy.get(stakeTokenAmountInputBox).type('-0.1');
cy.getByTestId(stakeRemoveStakeRadioButton, txTimeout).click();
cy.getByTestId(stakeTokenAmountInputBox).type('-0.1');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.get(stakeTokenSubmitButton)
cy.getByTestId(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove -0.1 $VEGA tokens at the end of epoch`)
.and('be.visible');
@@ -266,10 +310,10 @@ context(
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
cy.get(stakeRemoveStakeRadioButton).click();
cy.get(stakeTokenAmountInputBox).type('4');
cy.getByTestId(stakeRemoveStakeRadioButton).click();
cy.getByTestId(stakeTokenAmountInputBox).type('4');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.get(stakeTokenSubmitButton)
cy.getByTestId(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove 4 $VEGA tokens at the end of epoch`)
.and('be.visible');
@@ -285,17 +329,17 @@ context(
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateAllTokens();
cy.get(ethWalletContainer).within(() => {
cy.getByTestId(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
verifyEthWalletTotalAssociatedBalance('0.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout).should(
cy.getByTestId(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
@@ -313,17 +357,17 @@ context(
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateAllTokens('contract');
cy.get(ethWalletContainer).within(() => {
cy.getByTestId(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
verifyEthWalletTotalAssociatedBalance('0.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout).should(
cy.getByTestId(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
@@ -342,8 +386,8 @@ context(
closeStakingDialog();
stakingPageDisassociateTokens('1');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'2.00'
);
@@ -409,8 +453,8 @@ context(
verifyUnstakedBalance(0.0);
closeStakingDialog();
stakingPageAssociateTokens('6');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'12.00'
);
@@ -429,9 +473,12 @@ context(
verifyUnstakedBalance(1.0);
closeStakingDialog();
clickOnValidatorFromList(0);
cy.get(stakeAddStakeRadioButton).click();
cy.get(stakeMaximumTokens, { timeout: 60000 }).click();
cy.get(stakeTokenSubmitButton).should('contain', 'Add 1 $VEGA tokens');
cy.getByTestId(stakeAddStakeRadioButton).click();
cy.getByTestId(stakeMaximumTokens, { timeout: 60000 }).click();
cy.getByTestId(stakeTokenSubmitButton).should(
'contain',
'Add 1 $VEGA tokens'
);
});
afterEach('Teardown Wallet', function () {
@@ -26,19 +26,19 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.connectPublicKey(vegaWalletPubKey);
});
it('Able to connect public key via wallet', function () {
verifyConnectedToPubKey();
cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)');
});
it('Able to connect public key using url', function () {
cy.getByTestId('exit-view').click();
cy.visit(`/?address=${vegaWalletPubKey}`);
verifyConnectedToPubKey();
});
it('Able to connect public key via wallet and view assets in wallet', function () {
verifyConnectedToPubKey();
cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)');
});
it('Unable to submit proposal with public key', function () {
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
@@ -74,7 +74,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator stake', function () {
cy.get('[col-id="stake"] > div > span > span')
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
@@ -82,7 +82,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator stake tooltip', function () {
cy.get('[col-id="stake"] > div > span > span').first().realHover();
cy.getByTestId('total-stake').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
@@ -96,17 +96,15 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator normalised voting power', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator voting power tooltip', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.first()
.realHover();
it('Should be able to see validator normalised voting power tooltip', function () {
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
@@ -118,7 +116,7 @@ context('Validators Page - verify elements on page', function () {
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.get('[col-id="totalPenalties"] > div > span > span')
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
@@ -126,7 +124,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator penalties tooltip', function () {
cy.get('[col-id="totalPenalties"] > div > span > span').realHover();
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
@@ -140,7 +138,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator pending stake', function () {
cy.get('[col-id="pendingStake"] > div > span')
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
@@ -14,8 +14,8 @@ const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const associateContractRadioButton = '[data-testid="associate-radio-contract"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const stakeValidatorListPendingStake = '[col-id="pendingStake"]';
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListName = '[col-id="validator"]';
const vegaKeySelector = '#vega-key-selector';
const dialogCloseButton = '[data-testid="dialog-close"]';
@@ -185,11 +185,11 @@ export function validateValidatorListTotalStakeAndShare(
cy.contains('Loading...', epochTimeout).should('not.exist');
waitForBeginningOfEpoch();
cy.get(`[row-id="${positionOnList}"]`).within(() => {
cy.get(stakeValidatorListTotalStake, epochTimeout).should(
cy.getByTestId(stakeValidatorListTotalStake, epochTimeout).should(
'have.text',
expectedTotalStake
);
cy.get(stakeValidatorListTotalShare, epochTimeout).should(
cy.getByTestId(stakeValidatorListTotalShare, epochTimeout).should(
'have.text',
expectedTotalShare
);
@@ -73,9 +73,6 @@ export async function vegaWalletTeardown() {
}
});
cy.get(vegaWalletContainer).within(() => {
cy.getByTestId('vega-wallet-balance-staked-validators', {
timeout: transactionTimeout,
}).should('not.exist');
cy.getByTestId('associated-amount', {
timeout: transactionTimeout,
}).contains('0.00', {
@@ -125,6 +122,9 @@ async function vegaWalletTeardownVesting(vestingContract: TokenVesting) {
log: false,
}).then((vestingAmount) => {
if (Number(vestingAmount) != 0) {
// Wait needed to allow time for ganache to process tx for stakingBridgeContract.remove_stake
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.wrap(
vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey),
{ timeout: transactionTimeout, log: false }
@@ -221,8 +221,9 @@
"votePending": "Casting vote",
"voteError": "Something went wrong, and your vote was not seen by the network",
"back": "back",
"byTokenVote": "by Token vote",
"byLiquidityVote": "by Liquidity vote",
"byTokenVote": "by token vote",
"byLiquidityVote": "by liquidity vote",
"byLPVote": "by LP vote",
"youDidNotVote": "Voting has ended. You did not vote",
"voteState_Yes": "For",
"voteState_No": "Against",
@@ -601,11 +602,14 @@
"noValidators": "No validators",
"validator": "Validator",
"stake": "Stake",
"myStake": "My stake",
"stakeShare": "Stake share",
"stakedByOperator": "Staked by operator",
"stakedByDelegates": "Staked by delegates",
"stakedByMe": "Staked by me",
"totalStake": "Total stake",
"pendingStake": "Pending stake",
"myPendingStake": "My pending stake",
"totalPenalties": "Total penalties",
"noPenaltyDataFromLastEpoch": "No penalty data from last epoch",
"stakeNeededForPromotion": "Stake needed for promotion",
+2 -2
View File
@@ -12,14 +12,14 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated___/Nodes';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated___/Nodes';
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
const nodesToShow = 6;
@@ -86,7 +86,7 @@ describe('Proposal Votes Table', () => {
it('displays if an update market proposal will pass by token vote', () => {
renderComponent(updateMarketProposal, updateMarketProposalType);
expect(screen.getByText('👍 by Token vote')).toBeInTheDocument();
expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
});
it('displays if an update market proposal will pass by LP vote', () => {
@@ -110,6 +110,6 @@ describe('Proposal Votes Table', () => {
}),
updateMarketProposalType
);
expect(screen.getByText('👍 by Liquidity vote')).toBeInTheDocument();
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
});
});
@@ -136,6 +136,82 @@ describe('Proposals list item details', () => {
);
});
it('Renders proposal state: Update market proposal - set to pass by LP vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '1000',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Set to pass by LP vote'
);
});
it('Renders proposal state: Update market proposal - set to pass by token vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(1000, 1000),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Set to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - set to fail', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail');
});
it('Renders proposal state: Open - 5 minutes left to vote', () => {
renderComponent(
generateProposal({
@@ -41,12 +41,22 @@ export const ProposalsListItemDetails = ({
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const state = proposal?.state;
const { willPassByTokenVote, majorityMet, participationMet } =
useVoteInformation({
proposal,
});
const {
willPassByTokenVote,
willPassByLPVote,
majorityMet,
participationMet,
} = useVoteInformation({
proposal,
});
const { t } = useTranslation();
const { voteState } = useUserVote(proposal?.id);
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLPVote');
let proposalStatus: ReactNode;
let voteDetails: ReactNode;
let voteStatus: ReactNode;
@@ -128,6 +138,19 @@ export const ProposalsListItemDetails = ({
</>
);
voteStatus =
(isUpdateMarket &&
(updateMarketWillPass ? (
<>
{t('Set to')}{' '}
<StatusPass>
{t('pass')} {updateMarketVotePassMethod}
</StatusPass>
</>
) : (
<>
{t('Set to')} <StatusFail>{t('fail')}</StatusFail>
</>
))) ||
(!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />) ||
(willPassByTokenVote ? (
@@ -32,6 +32,7 @@ export const VoteDetails = ({
totalTokensPercentage,
participationMet,
totalTokensVoted,
totalLPTokensPercentage,
noPercentage,
noLPPercentage,
yesPercentage,
@@ -41,6 +42,8 @@ export const VoteDetails = ({
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation,
requiredParticipationLP,
participationLPMet,
} = useVoteInformation({ proposal });
const { t } = useTranslation();
@@ -101,6 +104,25 @@ export const VoteDetails = ({
</tr>
</tbody>
</table>
<p className="mb-6">
{t('participation')}
{': '}
{participationLPMet ? (
<span className="text-vega-green mx-4">{t('met')}</span>
) : (
<span className="text-danger mx-4">{t('notMet')}</span>
)}{' '}
{formatNumber(totalLPTokensPercentage, defaultDecimals)}%
<span className="ml-4">
{requiredParticipationLP && (
<>
({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '}
{t('governanceRequired')})
</>
)}
</span>
</p>
</section>
)}
<section data-testid="votes-table">
@@ -16,12 +16,8 @@ import orderBy from 'lodash/orderBy';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
arr,
[
(p) => new Date(p?.terms?.enactmentDatetime || 0).getTime(), // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered.
(p) => new Date(p?.terms?.closingDatetime).getTime(),
(p) => p.id,
],
['desc', 'desc', 'desc']
[(p) => new Date(p?.terms?.closingDatetime).getTime(), (p) => p.id],
['desc', 'desc']
);
export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
@@ -21,6 +21,12 @@ export const EpochIndividualRewards = () => {
first: Number(delegationsPagination),
}
: undefined,
// we can use the same value for rewardsPagination as delegationsPagination
rewardsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
},
skip: !pubKey,
});
@@ -21,10 +21,14 @@ fragment DelegationFields on Delegation {
epoch
}
query Rewards($partyId: ID!, $delegationsPagination: Pagination) {
query Rewards(
$partyId: ID!
$delegationsPagination: Pagination
$rewardsPagination: Pagination
) {
party(id: $partyId) {
id
rewardsConnection {
rewardsConnection(pagination: $rewardsPagination) {
edges {
node {
...RewardFields
@@ -10,6 +10,7 @@ export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: stri
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
rewardsPagination?: Types.InputMaybe<Types.Pagination>;
}>;
@@ -75,10 +76,10 @@ export const EpochFieldsFragmentDoc = gql`
}
`;
export const RewardsDocument = gql`
query Rewards($partyId: ID!, $delegationsPagination: Pagination) {
query Rewards($partyId: ID!, $delegationsPagination: Pagination, $rewardsPagination: Pagination) {
party(id: $partyId) {
id
rewardsConnection {
rewardsConnection(pagination: $rewardsPagination) {
edges {
node {
...RewardFields
@@ -119,6 +120,7 @@ ${DelegationFieldsFragmentDoc}`;
* variables: {
* partyId: // value for 'partyId'
* delegationsPagination: // value for 'delegationsPagination'
* rewardsPagination: // value for 'rewardsPagination'
* },
* });
*/
@@ -104,7 +104,7 @@ export const RewardsPage = () => {
</section>
)}
<section className="grid xl:grid-cols-2 gap-12 items-center mb-8">
<section className="grid xl:grid-cols-[1fr_auto] gap-12 items-center mb-8">
<div>
<SubHeading title={t('rewardsAndFeesReceived')} />
<p>
@@ -114,7 +114,7 @@ export const RewardsPage = () => {
</p>
</div>
<div className="max-w-[600px]">
<div className="w-[440px]">
<Toggle
name="epoch-reward-view-toggle"
toggles={[
@@ -23,6 +23,17 @@ fragment StakingNodeFields on Node {
}
}
fragment StakingDelegationFields on Delegation {
amount
epoch
node {
id
}
party {
id
}
}
query Staking($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) {
id
@@ -32,11 +43,7 @@ query Staking($partyId: ID!, $delegationsPagination: Pagination) {
delegationsConnection(pagination: $delegationsPagination) {
edges {
node {
amount
epoch
node {
id
}
...StakingDelegationFields
}
}
}
@@ -5,13 +5,15 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type StakingNodeFieldsFragment = { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } };
export type StakingDelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string }, party: { __typename?: 'Party', id: string } };
export type StakingQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type StakingQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number, uptime: number } | null };
export type StakingQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string }, party: { __typename?: 'Party', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number, uptime: number } | null };
export const StakingNodeFieldsFragmentDoc = gql`
fragment StakingNodeFields on Node {
@@ -39,6 +41,18 @@ export const StakingNodeFieldsFragmentDoc = gql`
}
}
`;
export const StakingDelegationFieldsFragmentDoc = gql`
fragment StakingDelegationFields on Delegation {
amount
epoch
node {
id
}
party {
id
}
}
`;
export const StakingDocument = gql`
query Staking($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) {
@@ -49,11 +63,7 @@ export const StakingDocument = gql`
delegationsConnection(pagination: $delegationsPagination) {
edges {
node {
amount
epoch
node {
id
}
...StakingDelegationFields
}
}
}
@@ -80,7 +90,8 @@ export const StakingDocument = gql`
uptime
}
}
${StakingNodeFieldsFragmentDoc}`;
${StakingDelegationFieldsFragmentDoc}
${StakingNodeFieldsFragmentDoc}`;
/**
* __useStakingQuery__
@@ -1,106 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PreviousEpochQueryVariables = Types.Exact<{
epochId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type PreviousEpochQuery = {
__typename?: 'Query';
epoch: {
__typename?: 'Epoch';
id: string;
validatorsConnection?: {
__typename?: 'NodesConnection';
edges?: Array<{
__typename?: 'NodeEdge';
node: {
__typename?: 'Node';
id: string;
rewardScore?: {
__typename?: 'RewardScore';
rawValidatorScore: string;
} | null;
rankingScore: {
__typename?: 'RankingScore';
performanceScore: string;
};
};
} | null> | null;
} | null;
};
};
export const PreviousEpochDocument = gql`
query PreviousEpoch($epochId: ID) {
epoch(id: $epochId) {
id
validatorsConnection {
edges {
node {
id
rewardScore {
rawValidatorScore
}
rankingScore {
performanceScore
}
}
}
}
}
}
`;
/**
* __usePreviousEpochQuery__
*
* To run a query within a React component, call `usePreviousEpochQuery` and pass it any options that fit your needs.
* When your component renders, `usePreviousEpochQuery` 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 } = usePreviousEpochQuery({
* variables: {
* epochId: // value for 'epochId'
* },
* });
*/
export function usePreviousEpochQuery(
baseOptions?: Apollo.QueryHookOptions<
PreviousEpochQuery,
PreviousEpochQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<PreviousEpochQuery, PreviousEpochQueryVariables>(
PreviousEpochDocument,
options
);
}
export function usePreviousEpochLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
PreviousEpochQuery,
PreviousEpochQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<PreviousEpochQuery, PreviousEpochQueryVariables>(
PreviousEpochDocument,
options
);
}
export type PreviousEpochQueryHookResult = ReturnType<
typeof usePreviousEpochQuery
>;
export type PreviousEpochLazyQueryHookResult = ReturnType<
typeof usePreviousEpochLazyQuery
>;
export type PreviousEpochQueryResult = Apollo.QueryResult<
PreviousEpochQuery,
PreviousEpochQueryVariables
>;
@@ -1,114 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LinkingsFieldsFragment = {
__typename?: 'StakeLinking';
id: string;
txHash: string;
status: Types.StakeLinkingStatus;
};
export type PartyStakeLinkingsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyStakeLinkingsQuery = {
__typename?: 'Query';
party?: {
__typename?: 'Party';
id: string;
stakingSummary: {
__typename?: 'StakingSummary';
linkings: {
__typename?: 'StakesConnection';
edges?: Array<{
__typename?: 'StakeLinkingEdge';
node: {
__typename?: 'StakeLinking';
id: string;
txHash: string;
status: Types.StakeLinkingStatus;
};
} | null> | null;
};
};
} | null;
};
export const LinkingsFieldsFragmentDoc = gql`
fragment LinkingsFields on StakeLinking {
id
txHash
status
}
`;
export const PartyStakeLinkingsDocument = gql`
query PartyStakeLinkings($partyId: ID!) {
party(id: $partyId) {
id
stakingSummary {
linkings {
edges {
node {
...LinkingsFields
}
}
}
}
}
}
${LinkingsFieldsFragmentDoc}
`;
/**
* __usePartyStakeLinkingsQuery__
*
* To run a query within a React component, call `usePartyStakeLinkingsQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyStakeLinkingsQuery` 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 } = usePartyStakeLinkingsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyStakeLinkingsQuery(
baseOptions: Apollo.QueryHookOptions<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>(PartyStakeLinkingsDocument, options);
}
export function usePartyStakeLinkingsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>(PartyStakeLinkingsDocument, options);
}
export type PartyStakeLinkingsQueryHookResult = ReturnType<
typeof usePartyStakeLinkingsQuery
>;
export type PartyStakeLinkingsLazyQueryHookResult = ReturnType<
typeof usePartyStakeLinkingsLazyQuery
>;
export type PartyStakeLinkingsQueryResult = Apollo.QueryResult<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>;
@@ -15,7 +15,7 @@ import {
} from '../../../hooks/transaction-reducer';
import Routes from '../../routes';
import { truncateMiddle } from '../../../lib/truncate-middle';
import type { LinkingsFieldsFragment } from './__generated___/PartyStakeLinkings';
import type { LinkingsFieldsFragment } from './__generated__/PartyStakeLinkings';
export const AssociateTransaction = ({
amount,
@@ -15,8 +15,8 @@ import type {
LinkingsFieldsFragment,
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables,
} from './__generated___/PartyStakeLinkings';
import { PartyStakeLinkingsDocument } from './__generated___/PartyStakeLinkings';
} from './__generated__/PartyStakeLinkings';
import { PartyStakeLinkingsDocument } from './__generated__/PartyStakeLinkings';
export const useAddStake = (
address: string,
@@ -1,149 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NodesFragmentFragment = {
__typename?: 'Node';
avatarUrl?: string | null;
id: string;
name: string;
pubkey: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
pendingStake: string;
rankingScore: {
__typename?: 'RankingScore';
rankingScore: string;
stakeScore: string;
performanceScore: string;
votingPower: string;
status: Types.ValidatorStatus;
};
};
export type NodesQueryVariables = Types.Exact<{ [key: string]: never }>;
export type NodesQuery = {
__typename?: 'Query';
epoch: {
__typename?: 'Epoch';
id: string;
timestamps: {
__typename?: 'EpochTimestamps';
start?: any | null;
end?: any | null;
expiry?: any | null;
};
};
nodesConnection: {
__typename?: 'NodesConnection';
edges?: Array<{
__typename?: 'NodeEdge';
node: {
__typename?: 'Node';
avatarUrl?: string | null;
id: string;
name: string;
pubkey: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
pendingStake: string;
rankingScore: {
__typename?: 'RankingScore';
rankingScore: string;
stakeScore: string;
performanceScore: string;
votingPower: string;
status: Types.ValidatorStatus;
};
};
} | null> | null;
};
nodeData?: { __typename?: 'NodeData'; stakedTotal: string } | null;
};
export const NodesFragmentFragmentDoc = gql`
fragment NodesFragment on Node {
avatarUrl
id
name
pubkey
stakedByOperator
stakedByDelegates
stakedTotal
pendingStake
rankingScore {
rankingScore
stakeScore
performanceScore
votingPower
status
}
}
`;
export const NodesDocument = gql`
query Nodes {
epoch {
id
timestamps {
start
end
expiry
}
}
nodesConnection {
edges {
node {
...NodesFragment
}
}
}
nodeData {
stakedTotal
}
}
${NodesFragmentFragmentDoc}
`;
/**
* __useNodesQuery__
*
* To run a query within a React component, call `useNodesQuery` and pass it any options that fit your needs.
* When your component renders, `useNodesQuery` 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 } = useNodesQuery({
* variables: {
* },
* });
*/
export function useNodesQuery(
baseOptions?: Apollo.QueryHookOptions<NodesQuery, NodesQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<NodesQuery, NodesQueryVariables>(
NodesDocument,
options
);
}
export function useNodesLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<NodesQuery, NodesQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<NodesQuery, NodesQueryVariables>(
NodesDocument,
options
);
}
export type NodesQueryHookResult = ReturnType<typeof useNodesQuery>;
export type NodesLazyQueryHookResult = ReturnType<typeof useNodesLazyQuery>;
export type NodesQueryResult = Apollo.QueryResult<
NodesQuery,
NodesQueryVariables
>;
@@ -1,36 +1,53 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { useNodesQuery } from './__generated___/Nodes';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
import { useNodesQuery } from './__generated__/Nodes';
import { useStakingQuery } from '../__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { ValidatorTables } from './validator-tables';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const EpochData = () => {
// errorPolicy due to vegaprotocol/vega issue 5898
const { data, error, loading, refetch } = useNodesQuery();
const { pubKey } = useVegaWallet();
const {
data: nodesData,
error: nodesError,
loading: nodesLoading,
refetch,
} = useNodesQuery();
const { data: userStakingData } = useStakingQuery({
variables: {
partyId: pubKey || '',
},
});
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(data?.epoch.id) - 1).toString(),
epochId: (Number(nodesData?.epoch.id) - 1).toString(),
},
skip: !data?.epoch.id,
skip: !nodesData?.epoch.id,
});
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
useRefreshAfterEpoch(nodesData?.epoch.timestamps.expiry, refetch);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
{data?.epoch &&
data.epoch.timestamps.start &&
data?.epoch.timestamps.expiry && (
<AsyncRenderer loading={nodesLoading} error={nodesError} data={nodesData}>
{nodesData?.epoch &&
nodesData.epoch.timestamps.start &&
nodesData?.epoch.timestamps.expiry && (
<div className="mb-10">
<EpochCountdown
id={data.epoch.id}
startDate={new Date(data.epoch.timestamps.start)}
endDate={new Date(data.epoch.timestamps.expiry)}
id={nodesData.epoch.id}
startDate={new Date(nodesData.epoch.timestamps.start)}
endDate={new Date(nodesData.epoch.timestamps.expiry)}
/>
</div>
)}
<ValidatorTables data={data} previousEpochData={previousEpochData} />
<ValidatorTables
nodesData={nodesData}
userStakingData={userStakingData}
previousEpochData={previousEpochData}
/>
</AsyncRenderer>
);
};
@@ -3,14 +3,15 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import { ConsensusValidatorsTable } from './consensus-validators-table';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { NodesDocument } from '../__generated___/Nodes';
import { PreviousEpochDocument } from '../../__generated___/PreviousEpoch';
import { NodesDocument } from '../__generated__/Nodes';
import { PreviousEpochDocument } from '../../__generated__/PreviousEpoch';
import * as Schema from '@vegaprotocol/types';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import type { MockedResponse } from '@apollo/client/testing';
import type { PartialDeep } from 'type-fest';
import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import type { NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { ValidatorsView } from './validator-tables';
const nodeFactory = (
overrides?: PartialDeep<NodesFragmentFragment>
@@ -142,7 +143,8 @@ const MOCK_TOTAL_STAKE = '28832590188747439203824';
const renderValidatorsTable = (
data = MOCK_NODES,
previousEpochData = MOCK_PREVIOUS_EPOCH
previousEpochData = MOCK_PREVIOUS_EPOCH,
validatorsView: ValidatorsView = 'all'
) => {
return render(
<AppStateProvider initialState={{ decimals: 18 }}>
@@ -152,6 +154,7 @@ const renderValidatorsTable = (
data={data}
previousEpochData={previousEpochData}
totalStake={MOCK_TOTAL_STAKE}
validatorsView={validatorsView}
/>
</MockedProvider>
</MemoryRouter>
@@ -19,7 +19,9 @@ import {
import {
defaultColDef,
NODE_LIST_GRID_STYLES,
PendingStakeRenderer,
stakedTotalPercentage,
StakeShareRenderer,
TotalPenaltiesRenderer,
TotalStakeRenderer,
ValidatorFields,
@@ -54,6 +56,9 @@ interface CanonisedConsensusNodeProps {
[ValidatorFields.OVERSTAKING_PENALTY]: string;
[ValidatorFields.TOTAL_PENALTIES]: string;
[ValidatorFields.PENDING_STAKE]: string;
[ValidatorFields.STAKED_BY_USER]: string | undefined;
[ValidatorFields.PENDING_USER_STAKE]: string | undefined;
[ValidatorFields.USER_STAKE_SHARE]: string | undefined;
}
const getRowHeight = (params: RowHeightParams) => {
@@ -61,7 +66,7 @@ const getRowHeight = (params: RowHeightParams) => {
// Note: this value will change if the height of the top third cell renderer changes
return 138;
}
return 52;
return 68;
};
const TopThirdCellRenderer = (
@@ -117,6 +122,7 @@ export const ConsensusValidatorsTable = ({
data,
previousEpochData,
totalStake,
validatorsView,
}: ValidatorsTableProps) => {
const { t } = useTranslation();
const {
@@ -129,7 +135,7 @@ export const ConsensusValidatorsTable = ({
const nodes = useMemo(() => {
if (!data || !previousEpochData) return [];
const canonisedNodes = data
let canonisedNodes = data
.sort((a, b) => {
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
@@ -154,6 +160,9 @@ export const ConsensusValidatorsTable = ({
rankingScore: { stakeScore, votingPower },
pendingStake,
votingPowerRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
}) => {
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
@@ -201,12 +210,28 @@ export const ConsensusValidatorsTable = ({
totalStake
),
[ValidatorFields.PENDING_STAKE]: pendingStake,
decimals,
[ValidatorFields.STAKED_BY_USER]: stakedByUser
? formatNumber(toBigNum(stakedByUser, decimals), 2)
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? stakedTotalPercentage(userStakeShare)
: undefined,
};
}
);
if (canonisedNodes.length < 3 || !hideTopThird) {
if (validatorsView === 'myStake') {
canonisedNodes = canonisedNodes.filter(
(node) => node[ValidatorFields.STAKED_BY_USER] !== undefined
);
}
if (
canonisedNodes.length < 3 ||
!hideTopThird ||
validatorsView === 'myStake'
) {
return canonisedNodes;
}
@@ -291,7 +316,14 @@ export const ConsensusValidatorsTable = ({
},
...remaining,
];
}, [data, decimals, hideTopThird, previousEpochData, totalStake]);
}, [
data,
decimals,
hideTopThird,
previousEpochData,
totalStake,
validatorsView,
]);
const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>(
@@ -311,7 +343,7 @@ export const ConsensusValidatorsTable = ({
return a > b ? 1 : -1;
},
pinned: 'left',
width: 240,
width: 260,
},
{
field: ValidatorFields.STAKE,
@@ -320,6 +352,20 @@ export const ConsensusValidatorsTable = ({
cellRenderer: TotalStakeRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
cellRenderer: PendingStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 120,
},
{
field: ValidatorFields.NORMALISED_VOTING_POWER,
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
@@ -328,12 +374,6 @@ export const ConsensusValidatorsTable = ({
width: 200,
sort: 'desc',
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
width: 100,
},
{
field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
@@ -341,14 +381,6 @@ export const ConsensusValidatorsTable = ({
cellRenderer: TotalPenaltiesRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
valueFormatter: ({ value }) =>
formatNumber(toBigNum(value, decimals), 2),
width: 110,
},
],
[]
);
@@ -1,70 +0,0 @@
import {
getLastEpochScoreAndPerformance,
getTotalPenalties,
} from '../../shared';
import { stakedTotalPercentage } from './shared';
const MOCK_PREVIOUS_EPOCH = {
epoch: {
id: '1',
validatorsConnection: {
edges: [
{
node: {
id: '0x123',
rewardScore: {
rawValidatorScore: '0.25',
},
rankingScore: {
performanceScore: '0.75',
},
},
},
],
},
},
};
describe('stakedTotalPercentage', () => {
it('should return the correct percentage as a string, 2dp', () => {
expect(stakedTotalPercentage('1.2345')).toBe('123.45%');
});
});
describe('totalPenalties', () => {
it('should return the correct penalty based on arbitrary values, test 1', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.1',
'5000',
'100000'
)
).toBe('50.00%');
});
it('should return the correct penalty based on lower performance score than first test', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.05',
'5000',
'100000'
)
).toBe('75.00%');
});
it('should return the correct penalty based on higher amount of stake than other tests (great penalty due to anti-whaling)', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.1',
'5000',
'5500'
)
).toBe('97.25%');
});
});
@@ -10,9 +10,12 @@ import {
Tooltip,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import { BigNumber } from '../../../../lib/bignumber';
import type { NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorsView } from './validator-tables';
export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex',
@@ -31,12 +34,49 @@ export enum ValidatorFields {
PERFORMANCE_PENALTY = 'performancePenalty',
OVERSTAKED_AMOUNT = 'overstakedAmount',
OVERSTAKING_PENALTY = 'overstakingPenalty',
// the following are additional fields added to the validator object displaying user data
STAKED_BY_USER = 'stakedByUser',
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
}
export const addUserDataToValidator = (
validator: NodesFragmentFragment,
currentEpochUserStaking: StakingDelegationFieldsFragment | undefined,
nextEpochUserStaking: StakingDelegationFieldsFragment | undefined,
currentUserStakeAvailable: string
) => {
return {
...validator,
[ValidatorFields.STAKED_BY_USER]:
currentEpochUserStaking && Number(currentEpochUserStaking?.amount) > 0
? currentEpochUserStaking.amount
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: nextEpochUserStaking
? new BigNumber(nextEpochUserStaking?.amount)
.minus(new BigNumber(currentEpochUserStaking?.amount || 0))
.toString()
: undefined,
[ValidatorFields.USER_STAKE_SHARE]:
currentEpochUserStaking && Number(currentEpochUserStaking.amount) > 0
? new BigNumber(currentEpochUserStaking.amount).dividedBy(
new BigNumber(currentUserStakeAvailable)
)
: undefined,
};
};
export type ValidatorWithUserData = NodesFragmentFragment & {
stakedByUser?: string;
pendingUserStake?: string;
userStakeShare?: string;
};
export interface ValidatorsTableProps {
data: NodesFragmentFragment[] | undefined;
data: ValidatorWithUserData[] | undefined;
previousEpochData: PreviousEpochQuery | undefined;
totalStake: string;
validatorsView: ValidatorsView;
}
// Custom styling to account for the scrollbar. This is needed because the
@@ -58,19 +98,23 @@ export const defaultColDef = {
resizable: true,
autoHeight: true,
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
cellStyle: { margin: '10px 0', padding: '0 12px' },
tooltipComponent: TooltipCellComponent,
cellStyle: { display: 'flex', alignItems: 'center', padding: '0 10px' },
};
interface ValidatorRendererProps {
data: { id: string; validator: { avatarUrl: string; name: string } };
data: {
id: string;
validator: { avatarUrl: string; name: string };
stakedByUser: string | undefined;
};
}
export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
const { t } = useTranslation();
const { avatarUrl, name } = data.validator;
return (
<div className="grid grid-cols-[1fr_auto] gap-2 items-center">
<div className="w-[238px] grid grid-cols-[1fr_auto] gap-2 items-center">
<span className="flex overflow-hidden">
{avatarUrl && (
<img
@@ -83,9 +127,19 @@ export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
<span>{name}</span>
</span>
<Link to={data.id}>
<Button size="sm" fill={true}>
{t('Stake')}
</Button>
{data.stakedByUser ? (
<Button
data-testid="my-stake-btn"
size="sm"
className="text-vega-green border-vega-green"
>
{t('myStake')}
</Button>
) : (
<Button data-testid="stake-btn" size="sm" fill={true}>
{t('Stake')}
</Button>
)}
</Link>
</div>
);
@@ -102,8 +156,14 @@ export const StakeNeededForPromotionRenderer = ({
data,
}: StakeNeededForPromotionRendererProps) => {
return (
<Tooltip description={data.stakeNeededForPromotionDescription}>
<span>
<Tooltip
description={
<span data-testid="stake-needed-for-promotion-tooltip">
{data.stakeNeededForPromotionDescription}
</span>
}
>
<span data-testid="stake-needed-for-promotion">
{data.stakeNeededForPromotion &&
formatNumber(data.stakeNeededForPromotion, 2)}
</span>
@@ -134,7 +194,59 @@ export const VotingPowerRenderer = ({ data }: VotingPowerRendererProps) => {
</>
}
>
<span>{data.normalisedVotingPower}</span>
<span data-testid="normalised-voting-power">
{data.normalisedVotingPower}
</span>
</Tooltip>
);
};
interface PendingStakeRendererProps {
data: {
pendingStake: string;
pendingUserStake: string | undefined;
};
}
export const PendingStakeRenderer = ({ data }: PendingStakeRendererProps) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
return (
<Tooltip
description={
<>
<div data-testid="pending-stake-tooltip">
{t('pendingStake')}:{' '}
{formatNumber(toBigNum(data.pendingStake, decimals), decimals)}
</div>
{data.pendingUserStake && (
<div
className="text-vega-green border-t border-t-vega-dark-200 mt-1.5 pt-1"
data-testid="pending-user-stake-tooltip"
>
{t('myPendingStake')}:{' '}
{formatNumber(
toBigNum(data.pendingUserStake, decimals),
decimals
)}
</div>
)}
</>
}
>
<div className="flex flex-col">
{data.pendingUserStake && data.pendingStake !== '0' && (
<span data-testid="pending-user-stake" className="text-vega-green">
{formatNumber(toBigNum(data.pendingUserStake, decimals), 2)}
</span>
)}
<span data-testid="total-pending-stake">
{formatNumber(toBigNum(data.pendingStake, decimals), 2)}
</span>
</div>
</Tooltip>
);
};
@@ -144,6 +256,7 @@ interface TotalStakeRendererProps {
stake: string;
stakedByDelegates: string;
stakedByOperator: string;
stakedByUser: string | undefined;
};
}
@@ -165,18 +278,52 @@ export const TotalStakeRenderer = ({ data }: TotalStakeRendererProps) => {
<div data-testid="staked-delegates-tooltip">
{t('stakedByDelegates')}: {data.stakedByDelegates.toString()}
</div>
<div data-testid="total-staked-tooltip">
{t('totalStake')}:{' '}
<span className="font-bold">{formattedStake}</span>
<div className="font-bold" data-testid="total-staked-tooltip">
{t('totalStake')}: {formattedStake}
</div>
{data.stakedByUser && (
<div
className="text-vega-green border-t border-t-vega-dark-200 mt-1.5 pt-1"
data-testid="staked-by-user-tooltip"
>
{t('stakedByMe')}: {data.stakedByUser}
</div>
)}
</>
}
>
<span>{formattedStake}</span>
<div className="flex flex-col">
{data.stakedByUser && (
<span data-testid="user-stake" className="text-vega-green">
{data.stakedByUser}
</span>
)}
<span data-testid="total-stake">{formattedStake}</span>
</div>
</Tooltip>
);
};
interface StakeShareRendererProps {
data: {
stakeShare: string;
userStakeShare: string | undefined;
};
}
export const StakeShareRenderer = ({ data }: StakeShareRendererProps) => {
return (
<div className="flex flex-col">
{data.userStakeShare && (
<span data-testid="user-stake-share" className="text-vega-green">
{data.userStakeShare}
</span>
)}
<span data-testid="total-stake-share">{data.stakeShare}</span>
</div>
);
};
interface TotalPenaltiesRendererProps {
data: {
performanceScore: string;
@@ -209,7 +356,7 @@ export const TotalPenaltiesRenderer = ({
</>
}
>
<span>{data.totalPenalties}</span>
<span data-testid="total-penalty">{data.totalPenalties}</span>
</Tooltip>
);
};
@@ -21,6 +21,8 @@ import {
ValidatorRenderer,
TotalPenaltiesRenderer,
TotalStakeRenderer,
StakeShareRenderer,
PendingStakeRenderer,
} from './shared';
import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
@@ -38,6 +40,7 @@ export const StandbyPendingValidatorsTable = ({
totalStake,
stakeNeededForPromotion,
stakeNeededForPromotionDescription,
validatorsView,
}: StandbyPendingValidatorsTableProps) => {
const { t } = useTranslation();
const {
@@ -47,7 +50,7 @@ export const StandbyPendingValidatorsTable = ({
const gridRef = useRef<AgGridReact | null>(null);
const nodes = useMemo(() => {
let nodes = useMemo(() => {
if (!data) return [];
return data
@@ -75,6 +78,9 @@ export const StandbyPendingValidatorsTable = ({
rankingScore: { stakeScore },
pendingStake,
votingPowerRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
}) => {
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
@@ -152,6 +158,13 @@ export const StandbyPendingValidatorsTable = ({
totalStake
),
[ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.STAKED_BY_USER]: stakedByUser
? formatNumber(toBigNum(stakedByUser, decimals), 2)
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? stakedTotalPercentage(userStakeShare)
: undefined,
};
}
);
@@ -165,6 +178,12 @@ export const StandbyPendingValidatorsTable = ({
totalStake,
]);
if (validatorsView === 'myStake') {
nodes = nodes.filter(
(node) => node[ValidatorFields.STAKED_BY_USER] !== undefined
);
}
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>(
() => [
@@ -180,7 +199,7 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: ValidatorRenderer,
comparator: ({ name: a }, { name: b }) => Math.sign(a - b),
pinned: 'left',
width: 240,
width: 260,
},
{
field: ValidatorFields.STAKE,
@@ -189,6 +208,20 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: TotalStakeRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
cellRenderer: PendingStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 100,
},
{
field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(),
@@ -199,12 +232,6 @@ export const StandbyPendingValidatorsTable = ({
width: 210,
sort: 'asc',
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
width: 100,
},
{
field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
@@ -212,14 +239,6 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: TotalPenaltiesRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
valueFormatter: ({ value }) =>
formatNumber(toBigNum(value, decimals), 2),
width: 110,
},
],
[]
);
@@ -230,7 +249,7 @@ export const StandbyPendingValidatorsTable = ({
domLayout="autoHeight"
style={{ width: '100%' }}
customThemeParams={NODE_LIST_GRID_STYLES}
rowHeight={52}
rowHeight={68}
defaultColDef={defaultColDef}
tooltipShowDelay={0}
animateRows={true}
@@ -1,27 +1,30 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { ConsensusValidatorsTable } from './consensus-validators-table';
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
import BigNumber from 'bignumber.js';
import * as Schema from '@vegaprotocol/types';
import { formatNumber } from '../../../../lib/format-number';
import {
createDocsLinks,
removePaginationWrapper,
toBigNum,
} from '@vegaprotocol/utils';
import { Link as UTLink } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { useEnvironment } from '@vegaprotocol/environment';
import { Link as UTLink, Toggle } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../../lib/format-number';
import { SubHeading } from '../../../../components/heading';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type {
NodesQuery,
NodesFragmentFragment,
} from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import BigNumber from 'bignumber.js';
import { addUserDataToValidator } from './shared';
import { ConsensusValidatorsTable } from './consensus-validators-table';
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
import type { NodesQuery, NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { StakingQuery } from '../../__generated__/Staking';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorWithUserData } from './shared';
export interface ValidatorsTableProps {
data: NodesQuery | undefined;
nodesData: NodesQuery | undefined;
userStakingData: StakingQuery | undefined;
previousEpochData: PreviousEpochQuery | undefined;
}
@@ -31,8 +34,11 @@ interface SortedValidatorsProps {
pendingValidators: NodesFragmentFragment[];
}
export type ValidatorsView = 'all' | 'myStake';
export const ValidatorTables = ({
data,
nodesData,
userStakingData,
previousEpochData,
}: ValidatorsTableProps) => {
const { t } = useTranslation();
@@ -40,25 +46,69 @@ export const ValidatorTables = ({
const {
appState: { decimals },
} = useAppState();
const [validatorsView, setValidatorsView] = useState<ValidatorsView>('all');
const totalStake = useMemo(
() => data?.nodeData?.stakedTotal || '0',
[data?.nodeData?.stakedTotal]
() => nodesData?.nodeData?.stakedTotal || '0',
[nodesData?.nodeData?.stakedTotal]
);
const epochId = useMemo(() => nodesData?.epoch.id, [nodesData?.epoch.id]);
const currentUserStakeAvailable = useMemo(
() => userStakingData?.party?.stakingSummary.currentStakeAvailable || '0',
[userStakingData?.party?.stakingSummary.currentStakeAvailable]
);
let stakeNeededForPromotion = undefined;
let delegations: StakingDelegationFieldsFragment[] | undefined = undefined;
if (userStakingData) {
delegations = removePaginationWrapper(
userStakingData?.party?.delegationsConnection?.edges
);
}
const { consensusValidators, standbyValidators, pendingValidators } = useMemo(
() =>
removePaginationWrapper(data?.nodesConnection.edges).reduce(
removePaginationWrapper(nodesData?.nodesConnection.edges).reduce(
(acc: SortedValidatorsProps, validator) => {
const validatorId = validator.id;
const currentDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId)
);
const nextDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId) + 1
);
switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(validator);
acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(validator);
acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(validator);
acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
}
return acc;
},
@@ -68,7 +118,12 @@ export const ValidatorTables = ({
pendingValidators: [],
}
),
[data?.nodesConnection.edges]
[
currentUserStakeAvailable,
delegations,
epochId,
nodesData?.nodesConnection.edges,
]
);
if (
@@ -76,7 +131,7 @@ export const ValidatorTables = ({
(standbyValidators.length || pendingValidators.length)
) {
const lowestRankingConsensusScore = consensusValidators.reduce(
(lowest: NodesFragmentFragment, validator: NodesFragmentFragment) => {
(lowest: ValidatorWithUserData, validator: ValidatorWithUserData) => {
if (
Number(validator.rankingScore.rankingScore) <
Number(lowest.rankingScore.rankingScore)
@@ -98,19 +153,42 @@ export const ValidatorTables = ({
).toString();
}
return (
<div data-testid="validator-tables">
<section data-testid="validator-tables">
<div className="grid w-full justify-end">
<div className="w-[400px]">
<Toggle
name="validators-view-toggle"
toggles={[
{
label: t('ALL VALIDATORS'),
value: 'all',
},
{
label: t('STAKED BY ME'),
value: 'myStake',
},
]}
checkedValue={validatorsView}
onChange={(e) =>
setValidatorsView(e.target.value as ValidatorsView)
}
/>
</div>
</div>
{consensusValidators.length > 0 && (
<>
<div className="mb-10">
<SubHeading title={t('status-tendermint')} />
<ConsensusValidatorsTable
data={consensusValidators}
previousEpochData={previousEpochData}
totalStake={totalStake}
validatorsView={validatorsView}
/>
</>
</div>
)}
{standbyValidators.length > 0 && (
<>
<div className="mb-10">
<SubHeading title={t('status-ersatz')} />
<p>
<Trans
@@ -126,8 +204,9 @@ export const ValidatorTables = ({
totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription="StakeNeededForPromotionStandbyDescription"
validatorsView={validatorsView}
/>
</>
</div>
)}
{pendingValidators.length > 0 && (
<>
@@ -155,9 +234,10 @@ export const ValidatorTables = ({
totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription="StakeNeededForPromotionCandidateDescription"
validatorsView={validatorsView}
/>
</>
)}
</div>
</section>
);
};
@@ -20,8 +20,8 @@ import NodeContainer from './nodes-container';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Heading, SubHeading } from '../../../components/heading';
import Routes from '../../routes';
import type { StakingQuery } from './__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
interface StakingNodeProps {
data?: StakingQuery;
@@ -116,13 +116,6 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
t('validatorTitle', { nodeName: t('validatorTitleFallback') })
}
/>
<section className="mb-4">
<ValidatorTable
node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
previousEpochData={previousEpochData}
/>
</section>
{data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && (
<section className="mb-10">
<EpochCountdown
@@ -157,6 +150,13 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
<ConnectToVega />
</>
)}
<section className="mb-4">
<ValidatorTable
node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
previousEpochData={previousEpochData}
/>
</section>
</div>
);
};
@@ -4,11 +4,11 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useTranslation } from 'react-i18next';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { SplashLoader } from '../../../components/splash-loader';
import { useStakingQuery } from './__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
import { useStakingQuery } from '../__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import type { ReactElement } from 'react';
import type { StakingQuery } from './__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
// TODO should only request a single node. When migrating from deprecated APIs we should address this.
@@ -28,8 +28,8 @@ import {
getStakePercentage,
} from '../shared';
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from './__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
import type { StakingNodeFieldsFragment } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
const statuses = {
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
@@ -111,7 +111,7 @@ export const ValidatorTable = ({
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper>
<RoundedWrapper paddingBottom={true}>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
@@ -156,7 +156,7 @@ export const ValidatorTable = ({
</div>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true}>
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
@@ -187,7 +187,7 @@ export const ValidatorTable = ({
</RoundedWrapper>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true}>
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
@@ -238,7 +238,7 @@ export const ValidatorTable = ({
</RoundedWrapper>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true}>
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
@@ -270,7 +270,7 @@ export const ValidatorTable = ({
</RoundedWrapper>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true}>
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
@@ -23,7 +23,7 @@ export const YourStake = ({
return (
<div data-testid="your-stake">
<SubHeading title={t('Your stake')} />
<RoundedWrapper>
<RoundedWrapper paddingBottom={true}>
<KeyValueTable>
<KeyValueTableRow>
{t('Your Stake On Node (This Epoch)')}
+1 -1
View File
@@ -2,7 +2,7 @@ import {
formatNumberPercentage,
removePaginationWrapper,
} from '@vegaprotocol/utils';
import type { PreviousEpochQuery } from './__generated___/PreviousEpoch';
import type { PreviousEpochQuery } from './__generated__/PreviousEpoch';
import { BigNumber } from '../../lib/bignumber';
export const getLastEpochScoreAndPerformance = (
File diff suppressed because it is too large Load Diff
+13 -7
View File
@@ -135,7 +135,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(toastContent).should(
'contain.text',
'Transfer completeYour transaction has been confirmed TransferTo 7f9cf0…c255351.00 tBTC'
'Transfer completeYour transaction has been confirmed View in block explorerTransferTo 7f9cf0…c255351.00 tBTC'
);
cy.getByTestId(toastCloseBtn).click();
});
@@ -283,7 +283,7 @@ describe('capsule', { tags: '@slow' }, () => {
});
});
// comment because of bug #2695
it.skip('can edit order', function () {
it('can edit order', function () {
cy.getByTestId(ordersTab).click();
cy.getByTestId('edit').first().should('be.visible').click();
cy.getByTestId('dialog-title').should('contain.text', 'Edit order');
@@ -422,7 +422,11 @@ describe('capsule', { tags: '@slow' }, () => {
cy.getByTestId(depositSubmit).click();
cy.getByTestId('input-error-text').should(
'contain.text',
'Amount is above approved amount.Update approve amount'
'Amount is above approved amount'
);
cy.getByTestId('reapprove-default').should(
'contain.text',
'Approve again to deposit more than'
);
});
@@ -437,9 +441,11 @@ describe('capsule', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
cy.getByTestId('deposit-submit').click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click();
cy.getByTestId('approve-submit').click();
cy.getByTestId('approve-confirmed').should(
'contain.text',
'You can now make deposits in VEGA, up to a maximum of'
);
cy.get(amountField).clear().type('10000');
cy.getByTestId('deposit-submit').click();
cy.getByTestId(toastContent, txTimeout).should(
@@ -447,7 +453,7 @@ describe('capsule', { tags: '@slow' }, () => {
`Your transaction has been confirmed.`,
{ matchCase: false }
);
cy.getByTestId(toastCloseBtn).click();
cy.getByTestId(toastCloseBtn).click({ multiple: true });
cy.getByTestId('Collateral').click();
cy.highlight('deposit verification');
@@ -39,7 +39,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('market volume displayed', () => {
cy.getByTestId(marketTitle).contains('Market volume').click();
validateMarketDataRow(0, '24 Hour Volume', '1');
validateMarketDataRow(1, 'Open Interest', '0');
validateMarketDataRow(1, 'Open Interest', '-');
validateMarketDataRow(2, 'Best Bid Volume', '1');
validateMarketDataRow(3, 'Best Offer Volume', '3');
validateMarketDataRow(4, 'Best Static Bid Volume', '2');
@@ -0,0 +1,28 @@
describe('chart', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('config should persist', () => {
cy.getByTestId('Chart').click();
cy.get('[data-testid="tab-chart"] button').as('control-buttons');
cy.get('@control-buttons').each(($button) => {
cy.wrap($button).click();
cy.get(
'[role="menuitemradio"]:first, [role="menuitemcheckbox"]:first'
).click();
});
cy.getByTestId('Depth').click();
cy.getByTestId('Chart').click();
cy.get('@control-buttons').each(($button) => {
cy.wrap($button).click();
cy.get('[role="menuitemradio"]:first, [role="menuitemcheckbox"]:first')
.within(($lastMenuItem) => {
expect($lastMenuItem.data('state')).to.equal('checked');
})
.click();
});
});
});
-13
View File
@@ -1,13 +0,0 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_ENV=MIRROR
NX_VEGA_EXPLORER_URL=https://mainnet-mirror.explorer.vega.xyz
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"STAGNET3\":\"https://stagnet3.console.vega.xyz\"}
NX_VEGA_TOKEN_URL=https://mainnet-mirror.token.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
-11
View File
@@ -1,11 +0,0 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.sandbox.vega.xyz
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_ENV=SANDBOX
NX_VEGA_NETWORKS={\"DEVNET\":\"https://dev.token.vega.xyz\",\"STAGNET3\":\"https://stagnet3.token.vega.xyz\",\"STAGNET1\":\"https://stagnet1.token.vega.xyz\",\"TESTNET\":\"https://token.fairground.wtf\",\"MAINNET\":\"https://token.vega.xyz\"}
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { t } from '@vegaprotocol/i18n';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Link as UILink } from '@vegaprotocol/ui-toolkit';
import { Link as UILink, TinyScroll } from '@vegaprotocol/ui-toolkit';
import type { OnCellClickHandler } from '../select-market';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import {
@@ -55,8 +55,8 @@ export const SelectMarketLandingTable = ({
const showProposed = (markets?.length || 0) <= 5;
return (
<>
<div
className="max-h-[60vh] overflow-x-auto"
<TinyScroll
className="max-h-[60vh] overflow-x-auto -mr-4 pr-4"
data-testid="select-market-list"
>
<p className="text-neutral-500 dark:text-neutral-400 mb-4">
@@ -78,7 +78,7 @@ export const SelectMarketLandingTable = ({
))}
</tbody>
</table>
</div>
</TinyScroll>
<div className="mt-4 text-md">
<Link
to={Links[Routes.MARKETS]()}
@@ -103,7 +103,9 @@ const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
<>
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
<p>{t('Please wait for your transaction to be confirmed.')}</p>
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
{tx.txHash && (
<EtherscanLink tx={tx.txHash}>{t('View on Etherscan')}</EtherscanLink>
)}
<EthTransactionDetails tx={tx} />
</>
);
@@ -131,7 +133,9 @@ const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
<>
<ToastHeading>{t('Transaction confirmed')}</ToastHeading>
<p>{t('Your transaction has been confirmed.')}</p>
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
{tx.txHash && (
<EtherscanLink tx={tx.txHash}>{t('View on Etherscan')}</EtherscanLink>
)}
<EthTransactionDetails tx={tx} />
</>
);
@@ -148,7 +152,9 @@ const EthTxCompletedToastContent = ({ tx }: EthTxToastContentProps) => {
{t('Your transaction has been completed.')}{' '}
{isDeposit && t('Waiting for deposit confirmation.')}
</p>
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
{tx.txHash && (
<EtherscanLink tx={tx.txHash}>{t('View on Etherscan')}</EtherscanLink>
)}
<EthTransactionDetails tx={tx} />
</>
);
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh -eux
export PATH="/app/node_modules/.bin:$PATH"
if [ "${APP}" = "trading" ]; then
yarn nx export ${APP} --network-timeout 100000 --pure-lockfile
mv /app/dist/apps/trading/exported/ /app/tmp
rm -rf /app/dist/apps/trading
mv /app/tmp /app/dist/apps/trading
else
yarn nx build ${APP} --network-timeout 100000 --pure-lockfile
fi
+7 -1
View File
@@ -7,6 +7,10 @@ mkdir -p $(dirname $env_file)
rm -rf $env_file || echo "no file to delete"
touch $env_file
env_vars_file=/usr/share/nginx/html/.env
sed -i '/^#/d' $env_vars_file # remove comment lines
sed -i '/^$/d' $env_vars_file # remove empty lines
# Add assignment
echo "window._env_ = {" >> $env_file
@@ -29,7 +33,9 @@ do
if [ ! -z "$varname" ]; then
echo " $varname: \"$value\"," >> $env_file
fi
done < /usr/share/nginx/html/.env
done < $env_vars_file
rm $env_vars_file
echo "}" >> $env_file
+87 -12
View File
@@ -12,9 +12,13 @@ import {
} from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import {
useThemeSwitcher,
getValidItem,
getValidSubset,
} from '@vegaprotocol/react-helpers';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -28,6 +32,54 @@ import {
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { t } from '@vegaprotocol/i18n';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface StoredSettings {
interval?: Interval;
type?: ChartType;
overlays?: Overlay[];
studies?: Study[];
}
export const useCandlesChartSettings = create<
StoredSettings & {
merge: (settings: StoredSettings) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
}
>()(
persist(
immer((set) => ({
merge: (settings: StoredSettings) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type: ChartType) =>
set((state) => {
state.type = type;
}),
setInterval: (interval: Interval) =>
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays: Overlay[]) =>
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies: Study[]) =>
set((state) => {
state.studies = studies;
}),
})),
{
name: 'console-candles',
}
)
);
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
@@ -47,10 +99,31 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const [interval, setInterval] = useState<Interval>(Interval.I15M);
const [chartType, setChartType] = useState<ChartType>(ChartType.CANDLE);
const [overlays, setOverlays] = useState<Overlay[]>([]);
const [studies, setStudies] = useState<Study[]>([Study.VOLUME]);
const settings = useCandlesChartSettings();
const interval: Interval = getValidItem(
settings.interval,
Object.values(Interval),
Interval.I15M
);
const chartType: ChartType = getValidItem(
settings.type,
Object.values(ChartType),
ChartType.CANDLE
);
const overlays: Overlay[] = getValidSubset(
settings.overlays,
Object.values(Overlay),
[]
);
const studies: Study[] = getValidSubset(
settings.studies,
Object.values(Study),
[Study.VOLUME]
);
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey);
@@ -70,7 +143,7 @@ export const CandlesChartContainer = ({
<DropdownMenuRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
settings.setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
@@ -97,7 +170,7 @@ export const CandlesChartContainer = ({
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
setChartType(value as ChartType);
settings.setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
@@ -125,7 +198,7 @@ export const CandlesChartContainer = ({
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
setOverlays(newOverlays);
settings.setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
@@ -150,7 +223,7 @@ export const CandlesChartContainer = ({
? newStudies.splice(index, 1)
: newStudies.push(study);
setStudies(newStudies);
settings.setStudies(newStudies);
}}
>
{studyLabels[study]}
@@ -176,8 +249,10 @@ export const CandlesChartContainer = ({
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
setOverlays(options.overlays ?? []);
setStudies(options.studies ?? []);
settings.merge({
overlays: options.overlays,
studies: options.studies,
});
}}
/>
</div>
@@ -1,6 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { memo, useCallback, useEffect, useState } from 'react';
import { memo, useCallback, useEffect, useState, useRef } from 'react';
import { Controller } from 'react-hook-form';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
@@ -20,6 +20,7 @@ import {
InputError,
Intent,
Notification,
TinyScroll,
} from '@vegaprotocol/ui-toolkit';
import {
@@ -74,6 +75,8 @@ export const DealTicket = ({
handleSubmit,
} = useOrderForm(market.id);
const lastSubmitTime = useRef(0);
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const { accountBalance: marginAccountBalance } = useMarketAccountBalance(
@@ -145,6 +148,10 @@ export const DealTicket = ({
const onSubmit = useCallback(
(order: OrderSubmission) => {
const now = new Date().getTime();
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
return;
}
submit(
normalizeOrderSubmission(
order,
@@ -152,6 +159,7 @@ export const DealTicket = ({
market.positionDecimalPlaces
)
);
lastSubmitTime.current = now;
},
[submit, market.decimalPlaces, market.positionDecimalPlaces]
);
@@ -160,136 +168,140 @@ export const DealTicket = ({
if (!order || !normalizedOrder) return null;
return (
<form
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
className="p-4"
noValidate
>
<Controller
name="type"
control={control}
rules={{
validate: validateType(
marketData.marketTradingMode,
marketData.trigger
),
}}
render={() => (
<TypeSelector
value={order.type}
onSelect={(type) => {
if (type === OrderType.TYPE_NETWORK) return;
update({
type,
// when changing type also update the tif to what was last used of new type
timeInForce: lastTIF[type] || order.timeInForce,
expiresAt: undefined,
});
clearErrors('expiresAt');
}}
market={market}
marketData={marketData}
errorMessage={errors.type?.message}
/>
)}
/>
<Controller
name="side"
control={control}
render={() => (
<SideSelector
value={order.side}
onSelect={(side) => {
update({ side });
}}
/>
)}
/>
<DealTicketAmount
control={control}
orderType={order.type}
market={market}
marketData={marketData}
sizeError={errors.size?.message}
priceError={errors.price?.message}
update={update}
size={order.size}
price={order.price}
/>
<Controller
name="timeInForce"
control={control}
rules={{
validate: validateTimeInForce(
marketData.marketTradingMode,
marketData.trigger
),
}}
render={() => (
<TimeInForceSelector
value={order.timeInForce}
orderType={order.type}
onSelect={(timeInForce) => {
update({ timeInForce });
// Set tif value for the given order type, so that when switching
// types we know the last used TIF for the given order type
setLastTIF((curr) => ({
...curr,
[order.type]: timeInForce,
expiresAt: undefined,
}));
clearErrors('expiresAt');
}}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
/>
)}
/>
{order.type === Schema.OrderType.TYPE_LIMIT &&
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
control={control}
rules={{
validate: validateExpiration,
}}
render={() => (
<ExpirySelector
value={order.expiresAt}
onSelect={(expiresAt) =>
update({
expiresAt: expiresAt || undefined,
})
}
errorMessage={errors.expiresAt?.message}
/>
)}
/>
)}
<SummaryMessage
errorMessage={errors.summary?.message}
asset={asset}
marketTradingMode={marketData.marketTradingMode}
balance={balance}
margin={totalMargin}
isReadOnly={isReadOnly}
pubKey={pubKey}
onClickCollateral={onClickCollateral}
/>
<DealTicketButton
disabled={Object.keys(errors).length >= 1 || isReadOnly}
variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'}
/>
<DealTicketFeeDetails
order={normalizedOrder}
market={market}
marketData={marketData}
margin={margin}
totalMargin={totalMargin}
balance={marginAccountBalance}
/>
</form>
<TinyScroll className="h-full overflow-auto">
<form
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
className="p-4"
noValidate
>
<Controller
name="type"
control={control}
rules={{
validate: validateType(
marketData.marketTradingMode,
marketData.trigger
),
}}
render={() => (
<TypeSelector
value={order.type}
onSelect={(type) => {
if (type === OrderType.TYPE_NETWORK) return;
update({
type,
// when changing type also update the tif to what was last used of new type
timeInForce: lastTIF[type] || order.timeInForce,
expiresAt: undefined,
});
clearErrors('expiresAt');
}}
market={market}
marketData={marketData}
errorMessage={errors.type?.message}
/>
)}
/>
<Controller
name="side"
control={control}
render={() => (
<SideSelector
value={order.side}
onSelect={(side) => {
update({ side });
}}
/>
)}
/>
<DealTicketAmount
control={control}
orderType={order.type}
market={market}
marketData={marketData}
sizeError={errors.size?.message}
priceError={errors.price?.message}
update={update}
size={order.size}
price={order.price}
/>
<Controller
name="timeInForce"
control={control}
rules={{
validate: validateTimeInForce(
marketData.marketTradingMode,
marketData.trigger
),
}}
render={() => (
<TimeInForceSelector
value={order.timeInForce}
orderType={order.type}
onSelect={(timeInForce) => {
update({ timeInForce });
// Set tif value for the given order type, so that when switching
// types we know the last used TIF for the given order type
setLastTIF((curr) => ({
...curr,
[order.type]: timeInForce,
expiresAt: undefined,
}));
clearErrors('expiresAt');
}}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
/>
)}
/>
{order.type === Schema.OrderType.TYPE_LIMIT &&
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
control={control}
rules={{
validate: validateExpiration,
}}
render={() => (
<ExpirySelector
value={order.expiresAt}
onSelect={(expiresAt) =>
update({
expiresAt: expiresAt || undefined,
})
}
errorMessage={errors.expiresAt?.message}
/>
)}
/>
)}
<SummaryMessage
errorMessage={errors.summary?.message}
asset={asset}
marketTradingMode={marketData.marketTradingMode}
balance={balance}
margin={totalMargin}
isReadOnly={isReadOnly}
pubKey={pubKey}
onClickCollateral={onClickCollateral}
/>
<DealTicketButton
disabled={Object.keys(errors).length >= 1 || isReadOnly}
variant={
order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'
}
/>
<DealTicketFeeDetails
order={normalizedOrder}
market={market}
marketData={marketData}
margin={margin}
totalMargin={totalMargin}
balance={marginAccountBalance}
/>
</form>
</TinyScroll>
);
};
@@ -16,6 +16,7 @@ interface ApproveNotificationProps {
balances: DepositBalances | null;
amount: string;
approveTxId: number | null;
intent?: Intent;
}
export const ApproveNotification = ({
@@ -26,6 +27,7 @@ export const ApproveNotification = ({
balances,
approved,
approveTxId,
intent = Intent.Warning,
}: ApproveNotificationProps) => {
const tx = useEthTransactionStore((state) => {
return state.transactions.find((t) => t?.id === approveTxId);
@@ -46,7 +48,7 @@ export const ApproveNotification = ({
const approvePrompt = (
<div className="mb-4">
<Notification
intent={Intent.Warning}
intent={intent}
testId="approve-default"
message={t(
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
@@ -63,7 +65,7 @@ export const ApproveNotification = ({
const reApprovePrompt = (
<div className="mb-4">
<Notification
intent={Intent.Warning}
intent={intent}
testId="reapprove-default"
message={t(
`Approve again to deposit more than ${formatNumber(
+12 -4
View File
@@ -83,6 +83,8 @@ export const DepositForm = ({
const openDialog = useWeb3ConnectStore((store) => store.open);
const { isActive, account } = useWeb3React();
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
const [approveNotificationIntent, setApproveNotificationIntent] =
useState<Intent>(Intent.Warning);
const {
register,
handleSubmit,
@@ -103,8 +105,10 @@ export const DepositForm = ({
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
throw new Error('Invalid asset');
}
if (!approved) throw new Error('Deposits not approved');
if (!approved) {
setApproveNotificationIntent(Intent.Danger);
return;
}
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: fields.amount,
@@ -219,7 +223,7 @@ export const DepositForm = ({
{errors.asset.message}
</InputError>
)}
{isFaucetable && selectedAsset && (
{isActive && isFaucetable && selectedAsset && (
<UseButton onClick={submitFaucet}>
{t(`Get ${selectedAsset.symbol}`)}
</UseButton>
@@ -355,9 +359,13 @@ export const DepositForm = ({
isActive={isActive}
approveTxId={approveTxId}
selectedAsset={selectedAsset}
onApprove={submitApprove}
onApprove={() => {
submitApprove();
setApproveNotificationIntent(Intent.Warning);
}}
balances={balances}
approved={approved}
intent={approveNotificationIntent}
amount={amount}
/>
<FormButton approved={approved} selectedAsset={selectedAsset} />
+4 -5
View File
@@ -1,4 +1,3 @@
import styles from './orderbook.module.scss';
import colors from 'tailwindcss/colors';
import { useEffect, useRef, useState, useCallback, Fragment } from 'react';
import classNames from 'classnames';
@@ -14,7 +13,7 @@ import {
import * as Schema from '@vegaprotocol/types';
import { OrderbookRow } from './orderbook-row';
import { createRow } from './orderbook-data';
import { Checkbox, Icon, Splash } from '@vegaprotocol/ui-toolkit';
import { Checkbox, Icon, Splash, TinyScroll } from '@vegaprotocol/ui-toolkit';
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
interface OrderbookProps extends OrderbookData {
@@ -547,8 +546,8 @@ export const Orderbook = ({
{t('Cumulative vol')}
</div>
</div>
<div
className={`h-full overflow-auto relative ${styles['scroll']}`}
<TinyScroll
className="h-full overflow-auto relative"
onScroll={onScroll}
ref={scrollElement}
data-testid="scroll"
@@ -580,7 +579,7 @@ export const Orderbook = ({
testId={'best-static-offer-price'}
/>
)}
</div>
</TinyScroll>
<div
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t border-default mt-2 z-10 bg-white dark:bg-black w-full"
ref={footerElement}
@@ -106,7 +106,7 @@ export const MarketInfoTable = ({
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted || key.toLowerCase().includes('volume')}
unformatted={unformatted}
noBorder={noBorder}
/>
))}
@@ -9,6 +9,7 @@ import {
ExternalLink,
Link as UILink,
Splash,
TinyScroll,
} from '@vegaprotocol/ui-toolkit';
import { useMemo } from 'react';
import { generatePath, Link } from 'react-router-dom';
@@ -71,7 +72,9 @@ export const MarketInfoContainer = ({
return (
<AsyncRenderer data={data} loading={loading} error={error} reload={reload}>
{data ? (
<Info market={data} onSelect={(id) => onSelect?.(id)} />
<TinyScroll className="h-full overflow-auto">
<Info market={data} onSelect={(id) => onSelect?.(id)} />
</TinyScroll>
) : (
<Splash>
<p>{t('Could not load market')}</p>
@@ -96,21 +96,19 @@ export const MarketVolumeInfoPanel = ({
...props
}: MarketInfoWithDataAndCandlesProps & PanelProps) => {
const last24hourVolume = market.candles && calcCandleVolume(market.candles);
const dash = (value: string | undefined) =>
value && value !== '0' ? value : '-';
return (
<MarketInfoTable
data={{
'24hourVolume':
last24hourVolume && last24hourVolume !== '0'
? addDecimalsFormatNumber(
last24hourVolume,
market.positionDecimalPlaces
)
: '-',
openInterest: market.data?.openInterest,
bestBidVolume: market.data?.bestBidVolume,
bestOfferVolume: market.data?.bestOfferVolume,
bestStaticBidVolume: market.data?.bestStaticBidVolume,
bestStaticOfferVolume: market.data?.bestStaticOfferVolume,
'24hourVolume': dash(last24hourVolume),
openInterest: dash(market.data?.openInterest),
bestBidVolume: dash(market.data?.bestBidVolume),
bestOfferVolume: dash(market.data?.bestOfferVolume),
bestStaticBidVolume: dash(market.data?.bestStaticBidVolume),
bestStaticOfferVolume: dash(market.data?.bestStaticOfferVolume),
}}
decimalPlaces={market.positionDecimalPlaces}
{...props}
@@ -31,18 +31,11 @@ query OrderById($orderId: ID!) {
query Orders(
$partyId: ID!
$pagination: Pagination
$dateRange: DateRange
$filter: OrderFilter
$marketId: ID
$filter: OrderByMarketIdsFilter
) {
party(id: $partyId) {
id
ordersConnection(
pagination: $pagination
dateRange: $dateRange
filter: $filter
marketId: $marketId
) {
ordersConnection(pagination: $pagination, filter: $filter) {
edges {
node {
...OrderFields
@@ -79,8 +72,8 @@ fragment OrderUpdateFields on OrderUpdate {
}
}
subscription OrdersUpdate($partyId: ID!, $marketId: ID) {
orders(partyId: $partyId, marketId: $marketId) {
subscription OrdersUpdate($partyId: ID!) {
orders(filter: { partyIds: [$partyId] }) {
...OrderUpdateFields
}
}
@@ -15,9 +15,7 @@ export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: '
export type OrdersQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
pagination?: Types.InputMaybe<Types.Pagination>;
dateRange?: Types.InputMaybe<Types.DateRange>;
filter?: Types.InputMaybe<Types.OrderFilter>;
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
filter?: Types.InputMaybe<Types.OrderByMarketIdsFilter>;
}>;
@@ -27,7 +25,6 @@ export type OrderUpdateFieldsFragment = { __typename?: 'OrderUpdate', id: string
export type OrdersUpdateSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
@@ -115,15 +112,10 @@ export type OrderByIdQueryHookResult = ReturnType<typeof useOrderByIdQuery>;
export type OrderByIdLazyQueryHookResult = ReturnType<typeof useOrderByIdLazyQuery>;
export type OrderByIdQueryResult = Apollo.QueryResult<OrderByIdQuery, OrderByIdQueryVariables>;
export const OrdersDocument = gql`
query Orders($partyId: ID!, $pagination: Pagination, $dateRange: DateRange, $filter: OrderFilter, $marketId: ID) {
query Orders($partyId: ID!, $pagination: Pagination, $filter: OrderByMarketIdsFilter) {
party(id: $partyId) {
id
ordersConnection(
pagination: $pagination
dateRange: $dateRange
filter: $filter
marketId: $marketId
) {
ordersConnection(pagination: $pagination, filter: $filter) {
edges {
node {
...OrderFields
@@ -155,9 +147,7 @@ export const OrdersDocument = gql`
* variables: {
* partyId: // value for 'partyId'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* filter: // value for 'filter'
* marketId: // value for 'marketId'
* },
* });
*/
@@ -173,8 +163,8 @@ export type OrdersQueryHookResult = ReturnType<typeof useOrdersQuery>;
export type OrdersLazyQueryHookResult = ReturnType<typeof useOrdersLazyQuery>;
export type OrdersQueryResult = Apollo.QueryResult<OrdersQuery, OrdersQueryVariables>;
export const OrdersUpdateDocument = gql`
subscription OrdersUpdate($partyId: ID!, $marketId: ID) {
orders(partyId: $partyId, marketId: $marketId) {
subscription OrdersUpdate($partyId: ID!) {
orders(filter: {partyIds: [$partyId]}) {
...OrderUpdateFields
}
}
@@ -193,7 +183,6 @@ export const OrdersUpdateDocument = gql`
* const { data, loading, error } = useOrdersUpdateSubscription({
* variables: {
* partyId: // value for 'partyId'
* marketId: // value for 'marketId'
* },
* });
*/
@@ -1,6 +1,7 @@
import { update } from './order-data-provider';
import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../';
import type { Edge } from '@vegaprotocol/utils';
describe('order data provider', () => {
it('puts incoming data in proper place', () => {
const data = [
@@ -110,7 +111,11 @@ describe('order data provider', () => {
const updatedData = update(data, delta, () => null, {
partyId: '0x123',
dateRange: { end: new Date('2022-02-01').toISOString() },
filter: {
order: {
dateRange: { end: new Date('2022-02-01').toISOString() },
},
},
});
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
@@ -35,40 +35,45 @@ const orderMatchFilters = (
return true;
}
if (
variables?.filter?.status &&
!(order.status && variables.filter.status.includes(order.status))
variables?.filter?.order?.status &&
!(order.status && variables.filter.order.status.includes(order.status))
) {
return false;
}
if (
variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type))
variables?.filter?.order?.types &&
!(order.type && variables.filter.order.types.includes(order.type))
) {
return false;
}
if (
variables?.filter?.timeInForce &&
!variables.filter.timeInForce.includes(order.timeInForce)
variables?.filter?.order?.timeInForce &&
!variables.filter.order.timeInForce.includes(order.timeInForce)
) {
return false;
}
if (variables?.filter?.excludeLiquidity && order.liquidityProvisionId) {
if (
variables?.filter?.order?.excludeLiquidity &&
order.liquidityProvisionId
) {
return false;
}
if (
variables?.dateRange?.start &&
variables?.filter?.order?.dateRange?.start &&
!(
(order.updatedAt || order.createdAt) &&
variables.dateRange.start < (order.updatedAt || order.createdAt)
variables.filter.order.dateRange.start <
(order.updatedAt || order.createdAt)
)
) {
return false;
}
if (
variables?.dateRange?.end &&
variables?.filter?.order?.dateRange?.end &&
!(
(order.updatedAt || order.createdAt) &&
variables.dateRange.end > (order.updatedAt || order.createdAt)
variables.filter.order.dateRange.end >
(order.updatedAt || order.createdAt)
)
) {
return false;
@@ -241,8 +246,10 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
(callback, client, variables) =>
hasActiveOrderProviderInternal(callback, client, {
filter: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
order: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
},
},
pagination: {
first: 1,
@@ -67,24 +67,25 @@ export const useOrderListData = ({
}
}, []);
const variables = useMemo<
OrdersQueryVariables & OrdersUpdateSubscriptionVariables
>(
() => ({
const variables = useMemo(() => {
// define variable as const to get type safety, using generic with useMemo resulted in lost type safety
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
partyId,
dateRange: filter?.updatedAt?.value,
marketId,
filter: {
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
order: {
dateRange: filter?.updatedAt?.value,
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
},
},
pagination: {
first: 1000,
},
}),
[partyId, marketId, filter]
);
};
return allVars;
}, [partyId, filter]);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
@@ -13,7 +13,7 @@ fragment OrderSubFields on OrderUpdate {
}
subscription OrderSub($partyId: ID!) {
orders(partyId: $partyId) {
orders(filter: { partyIds: [$partyId] }) {
...OrderSubFields
}
}
@@ -29,7 +29,7 @@ export const OrderSubFieldsFragmentDoc = gql`
`;
export const OrderSubDocument = gql`
subscription OrderSub($partyId: ID!) {
orders(partyId: $partyId) {
orders(filter: {partyIds: [$partyId]}) {
...OrderSubFields
}
}
@@ -53,12 +53,14 @@ export const useActiveOrdersVolumeAndMargin = (
update,
variables: {
partyId: partyId || '',
marketId,
filter: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
marketIds: [marketId],
order: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
},
},
},
skip: !partyId,
@@ -353,10 +353,12 @@ export const volumeAndMarginProvider = makeDerivedDataProvider<
ordersProvider(callback, client, {
...variables,
filter: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
order: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
},
},
}),
(callback, client, variables) =>
+4 -7
View File
@@ -24,11 +24,7 @@ export const PositionsManager = ({
}: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const { data, error, loading, reload } = usePositionsData(
partyId,
gridRef,
true
);
const { data, error, loading, reload } = usePositionsData(partyId, gridRef);
const create = useVegaTransactionStore((store) => store.create);
const onClose = ({
marketId,
@@ -49,11 +45,12 @@ export const PositionsManager = ({
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
reduceOnly: true,
},
],
},
@@ -72,7 +69,7 @@ export const PositionsManager = ({
});
useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data?.length]);
}, [data]);
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
+25 -4
View File
@@ -6,13 +6,13 @@ import { positionsMetricsProvider } from './positions-data-providers';
import type { PositionsQueryVariables } from './__generated__/Positions';
import { useDataProvider, updateGridData } from '@vegaprotocol/react-helpers';
import type { GetRowsParams } from '@vegaprotocol/datagrid';
import isEqual from 'lodash/isEqual';
export const getRowId = ({ data }: { data: Position }) => data.marketId;
export const usePositionsData = (
partyId: string,
gridRef: RefObject<AgGridReact>,
clientSideModel?: boolean
gridRef: RefObject<AgGridReact>
) => {
const variables = useMemo<PositionsQueryVariables>(
() => ({ partyId }),
@@ -21,9 +21,30 @@ export const usePositionsData = (
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(
({ data }: { data: Position[] | null }) => {
return clientSideModel ? false : updateGridData(dataRef, data, gridRef);
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
return updateGridData(dataRef, data, gridRef);
}
const update: Position[] = [];
const add: Position[] = [];
data?.forEach((d) => {
const rowNode = gridRef.current?.api?.getRowNode(d.marketId);
if (rowNode) {
if (!isEqual(rowNode.data, d)) {
update.push(d);
}
} else {
add.push(d);
}
});
gridRef.current?.api?.applyTransaction({
update,
add,
addIndex: 0,
});
return true;
},
[gridRef, clientSideModel]
[gridRef]
);
const { data, error, loading, reload } = useDataProvider({
dataProvider: positionsMetricsProvider,
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/react-helpers",
"version": "0.0.1"
"version": "0.1.0"
}
+1
View File
@@ -2,3 +2,4 @@ export * from './__generated__/ChainId';
export * from './ag-grid-update';
export * from './format';
export * from './grid';
export * from './validation';
+17
View File
@@ -0,0 +1,17 @@
export const getValidItem = <T>(
value: T | null | undefined,
set: T[],
defaultValue: T
) =>
value !== null && value !== undefined && set.includes(value)
? value
: defaultValue;
export const getValidSubset = <T>(
value: T[] | null | undefined,
set: T[],
defaultValue: T[]
) =>
value !== null && value !== undefined
? value.filter((item) => set.includes(item))
: defaultValue;
+1 -1
View File
@@ -1 +1 @@
GRAPHQL_SCHEMA_PATH=https://api.n06.testnet.vega.xyz/graphql
GRAPHQL_SCHEMA_PATH=https://api.n07.testnet.vega.xyz/graphql
+27 -7
View File
@@ -1513,6 +1513,7 @@ export type MarketdepthArgs = {
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
export type MarketliquidityProvisionsConnectionArgs = {
live?: InputMaybe<Scalars['Boolean']>;
pagination?: InputMaybe<Pagination>;
partyId?: InputMaybe<Scalars['ID']>;
};
@@ -1520,8 +1521,7 @@ export type MarketliquidityProvisionsConnectionArgs = {
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
export type MarketordersConnectionArgs = {
dateRange?: InputMaybe<DateRange>;
filter?: InputMaybe<OrderFilter>;
filter?: InputMaybe<OrderByPartyIdsFilter>;
pagination?: InputMaybe<Pagination>;
};
@@ -2193,8 +2193,12 @@ export type Order = {
party: Party;
/** PeggedOrder contains the details about a pegged order */
peggedOrder?: Maybe<PeggedOrder>;
/** Is this a post only order */
postOnly?: Maybe<Scalars['Boolean']>;
/** The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64) */
price: Scalars['String'];
/** Is this a reduce only order */
reduceOnly?: Maybe<Scalars['Boolean']>;
/** The external reference (if available) for the order */
reference: Scalars['String'];
/** Why the order was rejected */
@@ -2226,6 +2230,22 @@ export type OrdertradesConnectionArgs = {
pagination?: InputMaybe<Pagination>;
};
export type OrderByMarketAndPartyIdsFilter = {
marketIds?: InputMaybe<Array<Scalars['ID']>>;
order?: InputMaybe<OrderFilter>;
partyIds?: InputMaybe<Array<Scalars['ID']>>;
};
export type OrderByMarketIdsFilter = {
marketIds?: InputMaybe<Array<Scalars['ID']>>;
order?: InputMaybe<OrderFilter>;
};
export type OrderByPartyIdsFilter = {
order?: InputMaybe<OrderFilter>;
partyIds?: InputMaybe<Array<Scalars['ID']>>;
};
/** Connection type for retrieving cursor-based paginated order information */
export type OrderConnection = {
__typename?: 'OrderConnection';
@@ -2256,6 +2276,8 @@ export type OrderEstimate = {
};
export type OrderFilter = {
/** Date range to retrieve orders from/to. Start and end time should be expressed as an integer value of nano-seconds past the Unix epoch */
dateRange?: InputMaybe<DateRange>;
excludeLiquidity?: InputMaybe<Scalars['Boolean']>;
status?: InputMaybe<Array<OrderStatus>>;
timeInForce?: InputMaybe<Array<OrderTimeInForce>>;
@@ -2539,6 +2561,7 @@ export type PartydepositsConnectionArgs = {
/** Represents a party on Vega, could be an ethereum wallet address in the future */
export type PartyliquidityProvisionsConnectionArgs = {
live?: InputMaybe<Scalars['Boolean']>;
marketId?: InputMaybe<Scalars['ID']>;
pagination?: InputMaybe<Pagination>;
reference?: InputMaybe<Scalars['String']>;
@@ -2554,9 +2577,7 @@ export type PartymarginsConnectionArgs = {
/** Represents a party on Vega, could be an ethereum wallet address in the future */
export type PartyordersConnectionArgs = {
dateRange?: InputMaybe<DateRange>;
filter?: InputMaybe<OrderFilter>;
marketId?: InputMaybe<Scalars['ID']>;
filter?: InputMaybe<OrderByMarketIdsFilter>;
pagination?: InputMaybe<Pagination>;
};
@@ -4050,8 +4071,7 @@ export type SubscriptionmarketsDepthUpdateArgs = {
/** Subscriptions allow a caller to receive new information as it is available from the Vega network. */
export type SubscriptionordersArgs = {
marketId?: InputMaybe<Scalars['ID']>;
partyId?: InputMaybe<Scalars['ID']>;
filter?: InputMaybe<OrderByMarketAndPartyIdsFilter>;
};
+1
View File
@@ -88,6 +88,7 @@ export const DepositStatusMapping: {
export const IntervalMapping: {
[T in Interval]: string;
} = {
// @ts-ignore - temporarily suppressing this as it's a valid value
INTERVAL_BLOCK: '1 block',
INTERVAL_I15M: 'I15M',
INTERVAL_I1D: 'I1D',
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/ui-toolkit",
"version": "0.8.0"
"version": "0.9.0"
}
+1
View File
@@ -40,6 +40,7 @@ export * from './tabs';
export * from './text-area';
export * from './theme-switcher';
export * from './thumbs';
export * from './tiny-scroll';
export * from './toast';
export * from './toggle';
export * from './tooltip';
+15 -2
View File
@@ -1,5 +1,8 @@
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
import {
useLocalStorageSnapshot,
getValidItem,
} from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import type { ReactElement, ReactNode } from 'react';
import { Children, isValidElement, useState } from 'react';
@@ -93,12 +96,22 @@ export const Tab = ({ children, ...props }: TabProps) => {
export const LocalStoragePersistTabs = ({
storageKey,
children,
...props
}: Omit<TabsProps, 'value' | 'onValueChange'> & { storageKey: string }) => {
const [value, onValueChange] = useLocalStorageSnapshot(
`active-tab-${storageKey}`
);
return (
<Tabs {...props} value={value || undefined} onValueChange={onValueChange} />
<Tabs
{...props}
children={children}
value={getValidItem(
value,
Children.map(children, (child) => child.props.id),
undefined
)}
onValueChange={onValueChange}
/>
);
};
@@ -0,0 +1 @@
export * from './tiny-scroll';
@@ -0,0 +1,20 @@
import styles from './tiny-scroll.module.scss';
import { forwardRef } from 'react';
import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
export interface Props extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
export const TinyScroll = forwardRef<HTMLDivElement, Props>(
({ children, className, ...props }, ref) => (
<div
ref={ref}
className={classNames(className, styles['scroll'])}
{...props}
>
{children}
</div>
)
);
+1 -1
View File
@@ -68,7 +68,7 @@ fragment OrderTxUpdateFields on OrderUpdate {
}
subscription OrderTxUpdate($partyId: ID!) {
orders(partyId: $partyId) {
orders(filter: { partyIds: [$partyId] }) {
...OrderTxUpdateFields
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ export type WithdrawalBusEventSubscriptionHookResult = ReturnType<typeof useWith
export type WithdrawalBusEventSubscriptionResult = Apollo.SubscriptionResult<WithdrawalBusEventSubscription>;
export const OrderTxUpdateDocument = gql`
subscription OrderTxUpdate($partyId: ID!) {
orders(partyId: $partyId) {
orders(filter: {partyIds: [$partyId]}) {
...OrderTxUpdateFields
}
}
@@ -45,6 +45,7 @@ export interface OrderSubmission {
size: string;
price?: string;
expiresAt?: string;
reduceOnly?: boolean;
}
export interface OrderCancellation {