Compare commits

..

1 Commits

Author SHA1 Message Date
Edd
dac1ed19a4
fix(explorer): fix display of amends with sizedelta 2024-02-19 11:53:45 +00:00
648 changed files with 14008 additions and 13194 deletions

View File

@ -4,5 +4,6 @@ tmp/*
.dockerignore .dockerignore
dockerfiles dockerfiles
node_modules node_modules
.git
.github .github
.vscode .vscode

View File

@ -196,9 +196,9 @@ jobs:
cypress: cypress:
needs: [build-sources, check-e2e-needed] needs: [build-sources, check-e2e-needed]
name: '(CI) cypress' name: '(CI) cypress'
if: ${{ needs.check-e2e-needed.outputs.run-tests == 'true' }}
uses: ./.github/workflows/cypress-run.yml uses: ./.github/workflows/cypress-run.yml
secrets: inherit secrets: inherit
if: needs.check-e2e-needed.outputs.run-tests == 'true' && (contains(needs.build-sources.outputs.projects, 'governance') || contains(needs.build-sources.outputs.projects, 'explorer'))
with: with:
projects: ${{ needs.build-sources.outputs.projects-e2e }} projects: ${{ needs.build-sources.outputs.projects-e2e }}
tags: '@smoke' tags: '@smoke'
@ -287,7 +287,6 @@ jobs:
steps: steps:
- run: | - run: |
result="${{ needs.cypress.result }}" result="${{ needs.cypress.result }}"
echo "Result: $result"
if [[ $result == "success" || $result == "skipped" ]]; then if [[ $result == "success" || $result == "skipped" ]]; then
exit 0 exit 0
else else

36
.github/workflows/cypress-live-test.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Cypress Console tests -- live environment
# This workflow runs using provided url
on:
workflow_dispatch:
inputs:
url:
description: 'Url'
required: true
type: string
jobs:
cypress-run:
name: Run Cypress Trading tests -- live environment
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 20
id: Node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Run Cypress tests
uses: cypress-io/github-action@v4
with:
browser: chrome
record: true
project: ./apps/trading-e2e
config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -12,6 +12,7 @@ on:
options: options:
- explorer-e2e - explorer-e2e
- governance-e2e - governance-e2e
- trading-e2e
tags: tags:
description: 'Test tags to run' description: 'Test tags to run'
required: true required: true

View File

@ -10,5 +10,5 @@ jobs:
uses: ./.github/workflows/cypress-run.yml uses: ./.github/workflows/cypress-run.yml
secrets: inherit secrets: inherit
with: with:
projects: '["explorer-e2e","governance-e2e"]' projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
tags: '@smoke @regression @slow' tags: '@smoke @regression @slow'

View File

@ -25,7 +25,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
rm package.json rm package.json
npm install --no-save @commitlint/cli@16.3.0 @commitlint/config-conventional@18.6.1 @commitlint/config-nx-scopes@18.6.1 nx@17.1.2 npm install --no-save @commitlint/cli @commitlint/config-conventional @commitlint/config-nx-scopes nx
- name: Check PR title - name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx @commitlint/cli@16.3.0 --config ./commitlint.config-ci.js run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js

View File

@ -1,28 +0,0 @@
# path to a directory with all packages
storage: ../tmp/local-registry/storage
# a list of other known repositories we can talk to
uplinks:
npmjs:
url: https://registry.yarnpkg.com
maxage: 60m
packages:
'**':
# give all users (including non-authenticated users) full access
# because it is a local registry
access: $all
publish: $all
unpublish: $all
# if package is not available locally, proxy requests to npm registry
proxy: npmjs
# log settings
logs:
type: stdout
format: pretty
level: warn
publish:
allow_offline: true # set offline to true to allow publish offline

View File

@ -1,7 +1,7 @@
import { getNewAssetTxBody } from '../support/governance.functions'; import { getNewAssetTxBody } from '../support/governance.functions';
context('Proposal page', { tags: '@smoke' }, function () { context('Proposal page', { tags: '@smoke' }, function () {
describe.skip('Verify elements on page', function () { describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading'; const proposalHeading = 'proposals-heading';
const dateTimeRegex = const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm; /(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;

View File

@ -1,9 +1,7 @@
const { join } = require('path'); const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind'); const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const { theme } = require('../../libs/tailwindcss-config/src/theme'); const theme = require('../../libs/tailwindcss-config/src/theme');
const { const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
vegaCustomClasses,
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
module.exports = { module.exports = {
content: [ content: [

View File

@ -7,7 +7,6 @@ import {
navigateTo, navigateTo,
navigation, navigation,
turnTelemetryOff, turnTelemetryOff,
setRiskAccepted,
} from '../../support/common.functions'; } from '../../support/common.functions';
import { import {
clickOnValidatorFromList, clickOnValidatorFromList,
@ -58,7 +57,6 @@ context(
// 1002-STKE-002, 1002-STKE-032 // 1002-STKE-002, 1002-STKE-032
before('visit staking tab and connect vega wallet', function () { before('visit staking tab and connect vega wallet', function () {
cy.visit('/'); cy.visit('/');
setRiskAccepted();
ethereumWalletConnect(); ethereumWalletConnect();
cy.connectVegaWallet(); cy.connectVegaWallet();
vegaWalletSetSpecifiedApprovalAmount('1000'); vegaWalletSetSpecifiedApprovalAmount('1000');

View File

@ -5,7 +5,6 @@ import {
navigateTo, navigateTo,
navigation, navigation,
turnTelemetryOff, turnTelemetryOff,
setRiskAccepted,
} from '../../support/common.functions'; } from '../../support/common.functions';
import { import {
stakingPageAssociateTokens, stakingPageAssociateTokens,
@ -58,7 +57,6 @@ context(
function () { function () {
cy.clearLocalStorage(); cy.clearLocalStorage();
turnTelemetryOff(); turnTelemetryOff();
setRiskAccepted();
cy.mockChainId(); cy.mockChainId();
cy.reload(); cy.reload();
waitForSpinner(); waitForSpinner();

View File

@ -7,7 +7,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
describe('Links and buttons', function () { describe('Links and buttons', function () {
it('should have link for proposal page', function () { it.skip('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => { cy.getByTestId('home-proposals').within(() => {
cy.get('[href="/proposals"]') cy.get('[href="/proposals"]')
.should('exist') .should('exist')
@ -51,7 +51,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
}); });
it('should have external link for governance', function () { it.skip('should have external link for governance', function () {
cy.getByTestId('home-proposals').within(() => { cy.getByTestId('home-proposals').within(() => {
cy.getByTestId('external-link') cy.getByTestId('external-link')
.should('have.attr', 'href') .should('have.attr', 'href')
@ -59,7 +59,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
}); });
it('should have link for validator page', function () { it.skip('should have link for validator page', function () {
cy.getByTestId('home-validators').within(() => { cy.getByTestId('home-validators').within(() => {
cy.get('[href="/validators"]') cy.get('[href="/validators"]')
.first() .first()
@ -68,7 +68,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
}); });
it('should have external link for validators', function () { it.skip('should have external link for validators', function () {
cy.getByTestId('home-validators').within(() => { cy.getByTestId('home-validators').within(() => {
cy.getByTestId('external-link') cy.getByTestId('external-link')
.should('have.attr', 'href') .should('have.attr', 'href')
@ -101,7 +101,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
}); });
it('should have link for rewards page', function () { it.skip('should have link for rewards page', function () {
cy.getByTestId('home-rewards').within(() => { cy.getByTestId('home-rewards').within(() => {
cy.get('[href="/rewards"]') cy.get('[href="/rewards"]')
.first() .first()
@ -110,7 +110,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
}); });
it('should have link for withdrawal page', function () { it.skip('should have link for withdrawal page', function () {
cy.getByTestId('home-vega-token').within(() => { cy.getByTestId('home-vega-token').within(() => {
cy.get('[href="/token/withdraw"]') cy.get('[href="/token/withdraw"]')
.first() .first()
@ -132,7 +132,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
}); });
// 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020 // 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020
it('should have option to switch to different network node', function () { it.skip('should have option to switch to different network node', function () {
cy.getByTestId('git-network-data').within(() => { cy.getByTestId('git-network-data').within(() => {
cy.getByTestId('link').click(); cy.getByTestId('link').click();
}); });
@ -153,13 +153,13 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
.invoke('text') .invoke('text')
.should('not.eq', currentBlockHeight); .should('not.eq', currentBlockHeight);
}); });
cy.getByTestId('subscription-cell').should('be.be.visible'); cy.getByTestId('subscription-cell').should('have.text', 'Yes');
}); });
cy.getByTestId('connect').should('be.disabled'); cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('node-url-custom').click({ force: true }); cy.getByTestId('node-url-custom').click({ force: true });
cy.get('input').should('exist'); cy.get('input').should('exist');
cy.getByTestId('connect').should('be.disabled'); cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('dialog-close').click(); cy.getByTestId('icon-cross').click();
}); });
it('should display eth data', function () { it('should display eth data', function () {

View File

@ -33,12 +33,12 @@ context(
verifyTabHighlighted(navigation.proposals); verifyTabHighlighted(navigation.proposals);
}); });
it('should have GOVERNANCE header visible', function () { it.skip('should have GOVERNANCE header visible', function () {
verifyPageHeader('Proposals'); verifyPageHeader('Proposals');
}); });
// 3002-PROP-023 3004-PMAC-002 3005-PASN-002 3006-PASC-002 3007-PNEC-002 3008-PFRO-003 // 3002-PROP-023 3004-PMAC-002 3005-PASN-002 3006-PASC-002 3007-PNEC-002 3008-PFRO-003
it('new proposal page should have button for link to more information on proposals', function () { it.skip('new proposal page should have button for link to more information on proposals', function () {
cy.getByTestId('new-proposal-link').click(); cy.getByTestId('new-proposal-link').click();
cy.url().should('include', '/proposals/propose/raw'); cy.url().should('include', '/proposals/propose/raw');
cy.contains('To see Explorer data on proposals visit').within(() => { cy.contains('To see Explorer data on proposals visit').within(() => {
@ -73,7 +73,7 @@ context(
navigateTo(navigation.proposals); navigateTo(navigation.proposals);
}); });
it('should be able to see a working link for - find out more about Vega governance', function () { it.skip('should be able to see a working link for - find out more about Vega governance', function () {
// 3001-VOTE-001 // 3002-PROP-001 // 3001-VOTE-001 // 3002-PROP-001
cy.getByTestId(proposalDocumentationLink) cy.getByTestId(proposalDocumentationLink)
.should('be.visible') .should('be.visible')

View File

@ -34,10 +34,16 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.connectPublicKey(vegaWalletPubKey); cy.connectPublicKey(vegaWalletPubKey);
}); });
it.skip('Able to connect public key via wallet and view assets in wallet', function () { 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(); verifyConnectedToPubKey();
cy.getByTestId('currency-title', { timeout: 10000 }) cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 2) .should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)'); .and('contain.text', 'USDC (fake)');
}); });

View File

@ -3,7 +3,6 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress'; import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { import {
navigation, navigation,
setRiskAccepted,
verifyPageHeader, verifyPageHeader,
verifyTabHighlighted, verifyTabHighlighted,
} from '../../support/common.functions'; } from '../../support/common.functions';
@ -47,11 +46,11 @@ context('Validators Page - verify elements on page', function () {
// @ts-ignore clash between jest and cypress // @ts-ignore clash between jest and cypress
describe('with wallets disconnected', { tags: '@smoke' }, function () { describe('with wallets disconnected', { tags: '@smoke' }, function () {
it('Should have validators tab highlighted', function () { it.skip('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators); verifyTabHighlighted(navigation.validators);
}); });
it('Should have validators ON VEGA header visible', function () { it.skip('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators'); verifyPageHeader('Validators');
}); });
@ -188,13 +187,12 @@ context('Validators Page - verify elements on page', function () {
before('connect wallets and click on validator', function () { before('connect wallets and click on validator', function () {
cy.mockChainId(); cy.mockChainId();
cy.visit('/validators'); cy.visit('/validators');
setRiskAccepted();
cy.connectVegaWallet(); cy.connectVegaWallet();
clickOnValidatorFromList(0); clickOnValidatorFromList(0);
}); });
// 1002-STKE-006 // 1002-STKE-006
it('Should be able to see validator name', function () { it.skip('Should be able to see validator name', function () {
cy.getByTestId(validatorTitle).should('not.be.empty'); cy.getByTestId(validatorTitle).should('not.be.empty');
}); });

View File

@ -1,8 +1,5 @@
import { truncateByChars } from '@vegaprotocol/utils'; import { truncateByChars } from '@vegaprotocol/utils';
import { import { waitForSpinner } from '../../support/common.functions';
setRiskAccepted,
waitForSpinner,
} from '../../support/common.functions';
import { import {
vegaWalletFaucetAssetsWithoutCheck, vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown, vegaWalletTeardown,
@ -14,6 +11,7 @@ const connectButton = 'connect-vega-wallet';
const getVegaLink = 'link'; const getVegaLink = 'link';
const dialog = '[role="dialog"]:visible'; const dialog = '[role="dialog"]:visible';
const dialogHeader = 'dialog-title'; const dialogHeader = 'dialog-title';
const walletDialogHeader = 'wallet-dialog-title';
const connectorsList = 'connectors-list'; const connectorsList = 'connectors-list';
const dialogCloseBtn = 'dialog-close'; const dialogCloseBtn = 'dialog-close';
const accountNo = 'vega-account-truncated'; const accountNo = 'vega-account-truncated';
@ -36,7 +34,6 @@ context(
() => { () => {
before('visit token home page', () => { before('visit token home page', () => {
cy.visit('/'); cy.visit('/');
setRiskAccepted();
cy.get(walletContainer, { timeout: 60000 }).should('be.visible'); cy.get(walletContainer, { timeout: 60000 }).should('be.visible');
}); });
@ -66,12 +63,17 @@ context(
it('should have Connect Vega header visible', () => { it('should have Connect Vega header visible', () => {
cy.get(dialog).within(() => { cy.get(dialog).within(() => {
cy.getByTestId(connectorsList) cy.getByTestId(walletDialogHeader)
.should('be.visible') .should('be.visible')
.and( .and('have.text', 'Get a Vega wallet');
'have.text', });
'Get the Vega WalletGet MetaMask>_Command Line WalletView as public key' });
);
it('should have jsonRpc and hosted connection options visible on list', function () {
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-jsonRpc')
.should('be.visible')
.and('have.text', 'Use the Desktop App/CLI');
}); });
}); });
@ -86,6 +88,7 @@ context(
before('connect vega wallet', function () { before('connect vega wallet', function () {
cy.mockChainId(); cy.mockChainId();
cy.visit('/'); cy.visit('/');
cy.wait('@ChainId');
cy.connectVegaWallet(); cy.connectVegaWallet();
vegaWalletTeardown(); vegaWalletTeardown();
}); });

View File

@ -102,12 +102,6 @@ export function turnTelemetryOff() {
); );
} }
export function setRiskAccepted() {
cy.window().then((win) =>
win.localStorage.setItem('vega_wallet_risk_accepted', 'true')
);
}
export function dissociateFromSecondWalletKey() { export function dissociateFromSecondWalletKey() {
const secondWalletKey = Cypress.env('vegaWalletPublicKey2Short'); const secondWalletKey = Cypress.env('vegaWalletPublicKey2Short');
cy.getByTestId('vega-in-wallet') cy.getByTestId('vega-in-wallet')

View File

@ -14,6 +14,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
@ -23,7 +24,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
#Test configuration variables #Test configuration variables
CYPRESS_FAIRGROUND=false CYPRESS_FAIRGROUND=false
@ -32,7 +33,7 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=true NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=true NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=true NX_GOVERNANCE_TRANSFERS=false

View File

@ -15,11 +15,12 @@ NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit suppl
NX_LOCAL_PROVIDER_URL=http://localhost:8545/ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789 NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/ NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json

View File

@ -8,13 +8,14 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=# NX_VEGA_EXPLORER_URL=#
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/ NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket

View File

@ -10,6 +10,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/ NX_VEGA_REST_URL=https://api.vega.community/api/v2/

View File

@ -9,6 +9,7 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/ NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/

View File

@ -5,6 +5,7 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","STAGNET1":"https:
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
@ -12,7 +13,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket

View File

@ -9,6 +9,7 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
@ -17,7 +18,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket

View File

@ -14,7 +14,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega. NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.

View File

@ -1,7 +1,7 @@
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { toBigNum } from '@vegaprotocol/utils'; import { toBigNum } from '@vegaprotocol/utils';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet-react'; import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
import { useFeatureFlags, useEnvironment } from '@vegaprotocol/environment'; import { useFeatureFlags, useEnvironment } from '@vegaprotocol/environment';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
@ -15,6 +15,20 @@ import {
} from './contexts/app-state/app-state-context'; } from './contexts/app-state/app-state-context';
import { useContracts } from './contexts/contracts/contracts-context'; import { useContracts } from './contexts/contracts/contracts-context';
import { useRefreshAssociatedBalances } from './hooks/use-refresh-associated-balances'; import { useRefreshAssociatedBalances } from './hooks/use-refresh-associated-balances';
import { useConnectors } from './lib/vega-connectors';
import { useSearchParams } from 'react-router-dom';
const useVegaWalletEagerConnect = () => {
const connectors = useConnectors();
const vegaConnecting = useEagerConnect(connectors);
const { pubKey, connect } = useVegaWallet();
const [searchParams] = useSearchParams();
const [query] = React.useState(searchParams.get('address'));
if (query && !pubKey) {
connect(connectors.view);
}
return vegaConnecting;
};
export const AppLoader = ({ children }: { children: React.ReactElement }) => { export const AppLoader = ({ children }: { children: React.ReactElement }) => {
const featureFlags = useFeatureFlags((state) => state.flags); const featureFlags = useFeatureFlags((state) => state.flags);
@ -26,9 +40,9 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
const { token, staking, vesting } = useContracts(); const { token, staking, vesting } = useContracts();
const setAssociatedBalances = useRefreshAssociatedBalances(); const setAssociatedBalances = useRefreshAssociatedBalances();
const [balancesLoaded, setBalancesLoaded] = React.useState(false); const [balancesLoaded, setBalancesLoaded] = React.useState(false);
const vegaWalletStatus = useEagerConnect(); const vegaConnecting = useVegaWalletEagerConnect();
const loaded = balancesLoaded && vegaWalletStatus !== 'connecting'; const loaded = balancesLoaded && !vegaConnecting;
React.useEffect(() => { React.useEffect(() => {
const run = async () => { const run = async () => {
@ -169,5 +183,3 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
} }
return <Suspense fallback={loading}>{children}</Suspense>; return <Suspense fallback={loading}>{children}</Suspense>;
}; };
AppLoader.displayName = 'AppLoader';

View File

@ -1,6 +1,7 @@
import './i18n'; import './i18n';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { BrowserRouter as Router, useLocation } from 'react-router-dom'; import { BrowserRouter as Router, useLocation } from 'react-router-dom';
import { AppLoader } from './app-loader'; import { AppLoader } from './app-loader';
import { NetworkInfo } from '@vegaprotocol/network-info'; import { NetworkInfo } from '@vegaprotocol/network-info';
@ -25,7 +26,7 @@ import {
} from '@vegaprotocol/web3'; } from '@vegaprotocol/web3';
import { Web3Provider } from '@vegaprotocol/web3'; import { Web3Provider } from '@vegaprotocol/web3';
import { VegaWalletDialogs } from './components/vega-wallet-dialogs'; import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
import { WalletProvider } from '@vegaprotocol/wallet-react'; import { VegaWalletProvider, useChainId } from '@vegaprotocol/wallet';
import { import {
useVegaTransactionManager, useVegaTransactionManager,
useVegaTransactionUpdater, useVegaTransactionUpdater,
@ -35,30 +36,32 @@ import { useEthereumConfig } from '@vegaprotocol/web3';
import { import {
useEnvironment, useEnvironment,
NetworkLoader, NetworkLoader,
useInitializeEnv,
NodeGuard, NodeGuard,
NodeSwitcherDialog, NodeSwitcherDialog,
useNodeSwitcherStore, useNodeSwitcherStore,
DocsLinks,
NodeFailure, NodeFailure,
AppLoader as Loader, AppLoader as Loader,
useInitializeEnv,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client'; import type { InMemoryCacheConfig } from '@apollo/client';
import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws'; import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws';
import { SplashLoader } from './components/splash-loader'; import { SplashLoader } from './components/splash-loader';
import { ToastsManager } from './toasts-manager'; import { ToastsManager } from './toasts-manager';
import { TelemetryDialog } from './components/telemetry-dialog/telemetry-dialog'; import {
TelemetryDialog,
TELEMETRY_ON,
} from './components/telemetry-dialog/telemetry-dialog';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSentryInit } from './hooks/use-sentry-init'; import { isPartyNotFoundError } from './lib/party';
import { useVegaWalletConfig } from './hooks/use-vega-wallet-config';
const cache: InMemoryCacheConfig = { const cache: InMemoryCacheConfig = {
typePolicies: { typePolicies: {
Account: { Account: {
keyFields: false, keyFields: false,
}, },
Instrument: {
keyFields: ['code'],
},
Delegation: { Delegation: {
keyFields: false, keyFields: false,
// Only get full updates // Only get full updates
@ -98,12 +101,32 @@ const Web3Container = ({
/** Ethereum provider url */ /** Ethereum provider url */
providerUrl: string; providerUrl: string;
}) => { }) => {
const InitializeHandlers = () => {
useVegaTransactionManager();
useVegaTransactionUpdater();
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
return null;
};
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [ const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
store.connectors, store.connectors,
store.initialize, store.initialize,
]); ]);
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } = const {
useEnvironment(); ETHEREUM_PROVIDER_URL,
ETH_LOCAL_PROVIDER_URL,
ETH_WALLET_MNEMONIC,
VEGA_ENV,
VEGA_URL,
VEGA_EXPLORER_URL,
CHROME_EXTENSION_URL,
MOZILLA_EXTENSION_URL,
VEGA_WALLET_URL,
} = useEnvironment();
const vegaChainId = useChainId(VEGA_URL);
useEffect(() => { useEffect(() => {
if (chainId) { if (chainId) {
@ -124,31 +147,50 @@ const Web3Container = ({
ETH_LOCAL_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL,
ETH_WALLET_MNEMONIC, ETH_WALLET_MNEMONIC,
]); ]);
const sideBar = React.useMemo(() => {
return [<EthWallet />, <VegaWallet />];
}, []);
const vegaWalletConfig = useVegaWalletConfig(); if (connectors.length === 0) {
if (!vegaWalletConfig || connectors.length === 0) {
// Prevent loading when the connectors are not initialized // Prevent loading when the connectors are not initialized
return <SplashLoader />; return <SplashLoader />;
} }
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
!VEGA_EXPLORER_URL ||
!DocsLinks ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL ||
!vegaChainId
) {
return null;
}
return ( return (
<Web3Provider connectors={connectors}> <Web3Provider connectors={connectors}>
<Web3Connector connectors={connectors} chainId={Number(chainId)}> <Web3Connector connectors={connectors} chainId={Number(chainId)}>
<WalletProvider config={vegaWalletConfig}> <VegaWalletProvider
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
chainId: vegaChainId,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
concepts: DocsLinks?.VEGA_WALLET_CONCEPTS_URL,
chromeExtensionUrl: CHROME_EXTENSION_URL,
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
},
}}
>
<ContractsProvider> <ContractsProvider>
<AppLoader> <AppLoader>
<BalanceManager> <BalanceManager>
<> <>
<AppLayout> <AppLayout>
<TemplateSidebar <TemplateSidebar sidebar={sideBar}>
sidebar={
<>
<EthWallet />
<VegaWallet />
</>
}
>
<AppRouter /> <AppRouter />
</TemplateSidebar> </TemplateSidebar>
<footer className="p-4 break-all border-t border-neutral-700"> <footer className="p-4 break-all border-t border-neutral-700">
@ -166,7 +208,7 @@ const Web3Container = ({
</BalanceManager> </BalanceManager>
</AppLoader> </AppLoader>
</ContractsProvider> </ContractsProvider>
</WalletProvider> </VegaWalletProvider>
</Web3Connector> </Web3Connector>
</Web3Provider> </Web3Provider>
); );
@ -186,9 +228,20 @@ const ScrollToTop = () => {
return null; return null;
}; };
const removeQueryParams = (url: string) => {
return url.split('?')[0];
};
const AppContainer = () => { const AppContainer = () => {
const { config, loading, error } = useEthereumConfig(); const { config, loading, error } = useEthereumConfig();
const { VEGA_URL, ETHEREUM_PROVIDER_URL } = useEnvironment(); const {
VEGA_ENV,
VEGA_URL,
GIT_COMMIT_HASH,
GIT_BRANCH,
ETHEREUM_PROVIDER_URL,
} = useEnvironment();
const [telemetryOn] = useLocalStorage(TELEMETRY_ON);
const { t } = useTranslation(); const { t } = useTranslation();
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [ const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
store.dialogOpen, store.dialogOpen,
@ -198,7 +251,70 @@ const AppContainer = () => {
// Hacky skip all the loading & web3 init for geo restricted users // Hacky skip all the loading & web3 init for geo restricted users
const isRestricted = document?.location?.pathname?.includes('/restricted'); const isRestricted = document?.location?.pathname?.includes('/restricted');
useSentryInit(); useEffect(() => {
if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({
dsn: ENV.dsn,
tracesSampleRate: 0.1,
enabled: true,
environment: VEGA_ENV,
release: GIT_COMMIT_HASH,
beforeSend(event, hint) {
const error = hint?.originalException;
const errorIsString = typeof error === 'string';
const errorIsObject = error instanceof Error;
const requestUrl = event.request?.url;
const transaction = event.transaction;
if (
(errorIsString && isPartyNotFoundError({ message: error })) ||
(errorIsObject && isPartyNotFoundError(error))
) {
// This error is caused by a pubkey making an API request before
// it has interacted with the chain. This isn't needed in Sentry.
return null;
}
const updatedRequest =
requestUrl && requestUrl.includes('/claim?')
? { ...event.request, url: removeQueryParams(requestUrl) }
: event.request;
const updatedTransaction =
transaction && transaction.includes('/claim?')
? removeQueryParams(transaction)
: transaction;
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
if (
breadcrumb.type === 'navigation' &&
breadcrumb.data?.to?.includes('/claim?')
) {
return {
...breadcrumb,
data: {
...breadcrumb.data,
to: removeQueryParams(breadcrumb.data.to),
},
};
}
return breadcrumb;
});
return {
...event,
request: updatedRequest,
transaction: updatedTransaction,
breadcrumbs: updatedBreadcrumbs ?? event.breadcrumbs,
};
},
});
Sentry.setTag('branch', GIT_BRANCH);
Sentry.setTag('commit', GIT_COMMIT_HASH);
} else {
Sentry.close();
}
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
if (isRestricted) { if (isRestricted) {
return ( return (
@ -240,15 +356,6 @@ const AppContainer = () => {
); );
}; };
const InitializeHandlers = () => {
useVegaTransactionManager();
useVegaTransactionUpdater();
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
return null;
};
function App() { function App() {
useInitializeEnv(); useInitializeEnv();

View File

@ -7,7 +7,7 @@ import { useGetAssociationBreakdown } from '../../hooks/use-get-association-brea
import { useGetUserBalances } from '../../hooks/use-get-user-balances'; import { useGetUserBalances } from '../../hooks/use-get-user-balances';
import { useBalances } from '../../lib/balances/balances-store'; import { useBalances } from '../../lib/balances/balances-store';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useListenForStakingEvents as useListenForAssociationEvents } from '../../hooks/use-listen-for-staking-events'; import { useListenForStakingEvents as useListenForAssociationEvents } from '../../hooks/use-listen-for-staking-events';
import { useTranches } from '../../lib/tranches/tranches-store'; import { useTranches } from '../../lib/tranches/tranches-store';
import { useUserTrancheBalances } from '../../routes/redemption/hooks'; import { useUserTrancheBalances } from '../../routes/redemption/hooks';

View File

@ -15,16 +15,19 @@ export const CollapsibleToggle = ({
dataTestId, dataTestId,
children, children,
}: CollapsibleToggleProps) => { }: CollapsibleToggleProps) => {
const classes = classnames('transition-transform ease-in-out duration-300', { const classes = classnames(
'rotate-180': toggleState, 'mb-4 transition-transform ease-in-out duration-300',
}); {
'rotate-180': toggleState,
}
);
return ( return (
<button <button
onClick={() => setToggleState(!toggleState)} onClick={() => setToggleState(!toggleState)}
data-testid={dataTestId} data-testid={dataTestId}
> >
<div className="flex items-baseline gap-3"> <div className="flex items-center gap-3">
{children} {children}
<div className={classes} data-testid="toggle-icon-wrapper"> <div className={classes} data-testid="toggle-icon-wrapper">
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} /> <VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />

View File

@ -1,13 +1,18 @@
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDialogStore } from '@vegaprotocol/wallet-react'; import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
export const ConnectToVega = () => { export const ConnectToVega = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const openVegaWalletDialog = useDialogStore((store) => store.open); const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
return ( return (
<Button <Button
onClick={openVegaWalletDialog} onClick={() => {
openVegaWalletDialog();
}}
data-testid="connect-to-vega-wallet-btn" data-testid="connect-to-vega-wallet-btn"
variant="primary" variant="primary"
> >

View File

@ -1,8 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { type ReactNode } from 'react';
interface HeadingProps { interface HeadingProps {
title?: ReactNode; title?: string;
centerContent?: boolean; centerContent?: boolean;
marginTop?: boolean; marginTop?: boolean;
marginBottom?: boolean; marginBottom?: boolean;

View File

@ -1,44 +0,0 @@
import {
useLinks,
DApp,
CONSOLE_REWARDS_PAGE,
} from '@vegaprotocol/environment';
import {
ExternalLink,
Intent,
NotificationBanner,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Trans } from 'react-i18next';
import { useMatch } from 'react-router-dom';
import Routes from '../../routes/routes';
import { type ReactNode } from 'react';
const ConsoleRewardsLink = ({ children }: { children: ReactNode }) => {
const consoleLink = useLinks(DApp.Console);
return (
<ExternalLink
href={consoleLink(CONSOLE_REWARDS_PAGE)}
className="underline inline-flex gap-1 items-center"
title="Rewards in Console"
>
<span>{children}</span>
<VegaIcon size={12} name={VegaIconNames.OPEN_EXTERNAL} />
</ExternalLink>
);
};
export const RewardsMovedNotification = () => {
const onRewardsPage = useMatch(Routes.REWARDS);
if (!onRewardsPage) return null;
return (
<NotificationBanner intent={Intent.Warning}>
<Trans
i18nKey="rewardsMovedNotification"
components={[<ConsoleRewardsLink>Console</ConsoleRewardsLink>]}
/>
</NotificationBanner>
);
};

View File

@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { AnnouncementBanner } from '@vegaprotocol/announcements'; import { AnnouncementBanner } from '@vegaprotocol/announcements';
import { Nav } from '../nav'; import { Nav } from '../nav';
@ -10,7 +10,6 @@ import {
ProtocolUpgradeProposalNotification, ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit'; import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { RewardsMovedNotification } from '../notifications/rewards-moved-notification';
interface AppLayoutProps { interface AppLayoutProps {
children: ReactNode; children: ReactNode;
@ -46,10 +45,8 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
const NotificationsContainer = () => { const NotificationsContainer = () => {
const { isReadOnly, pubKey, disconnect } = useVegaWallet(); const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return ( return (
<div data-testid="banners"> <div data-testid="banners">
<RewardsMovedNotification />
<ProtocolUpgradeProposalNotification <ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING} mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/> />

View File

@ -1,8 +1,8 @@
import { Children, type ReactNode } from 'react'; import React from 'react';
export interface TemplateSidebarProps { export interface TemplateSidebarProps {
children: ReactNode; children: React.ReactNode;
sidebar: ReactNode; sidebar: React.ReactNode[];
} }
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) { export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
@ -12,9 +12,9 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
{children} {children}
</main> </main>
<aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700"> <aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700">
{Children.map(sidebar, (child, i) => ( {sidebar.map((Component, i) => (
<section className="mb-4 last:mb-0" key={i}> <section className="mb-4 last:mb-0" key={i}>
{child} {Component}
</section> </section>
))} ))}
</aside> </aside>

View File

@ -1,5 +1,5 @@
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -10,7 +10,9 @@ interface VegaWalletContainerProps {
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => { export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const openVegaWalletDialog = useDialogStore((store) => store.open); const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
if (!pubKey) { if (!pubKey) {
return ( return (

View File

@ -9,7 +9,7 @@ import Routes from '../../routes/routes';
export const RiskMessage = () => { export const RiskMessage = () => {
return ( return (
<> <>
<div className="bg-vega-light-100 dark:bg-vega-dark-100 p-6"> <div className="bg-vega-light-100 dark:bg-vega-dark-100 p-6 mb-6">
<ul className="list-[square] ml-4"> <ul className="list-[square] ml-4">
<li> <li>
{t( {t(
@ -23,7 +23,7 @@ export const RiskMessage = () => {
</li> </li>
</ul> </ul>
</div> </div>
<p> <p className="mb-8">
{t( {t(
'By using the Vega Governance App, you acknowledge that you have read and understood the' 'By using the Vega Governance App, you acknowledge that you have read and understood the'
)}{' '} )}{' '}

View File

@ -1,39 +1,25 @@
import { import {
ConnectDialogWithRiskAck, VegaConnectDialog,
useDialogStore, VegaManageDialog,
} from '@vegaprotocol/wallet-react'; ViewAsDialog,
} from '@vegaprotocol/wallet';
import { import {
AppStateActionType, AppStateActionType,
useAppState, useAppState,
} from '../../contexts/app-state/app-state-context'; } from '../../contexts/app-state/app-state-context';
import { useConnectors } from '../../lib/vega-connectors';
import { RiskMessage } from './risk-message'; import { RiskMessage } from './risk-message';
import { VegaManageDialog } from '../manage-dialog';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
export const VegaWalletDialogs = () => { export const VegaWalletDialogs = () => {
const { VEGA_ENV } = useEnvironment();
const { appState, appDispatch } = useAppState(); const { appState, appDispatch } = useAppState();
const [riskAccepted, setRiskAccepted] = useLocalStorage( const connectors = useConnectors();
'vega_wallet_risk_accepted'
);
const vegaWalletDialogOpen = useDialogStore((store) => store.isOpen);
const setVegaWalletDialog = useDialogStore((store) => store.set);
return ( return (
<> <>
<ConnectDialogWithRiskAck <VegaConnectDialog
open={vegaWalletDialogOpen} connectors={connectors}
onChange={setVegaWalletDialog} riskMessage={<RiskMessage />}
riskAccepted={
VEGA_ENV === Networks.TESTNET ? riskAccepted === 'true' : true
}
riskAckContent={<RiskMessage />}
onRiskAccepted={() => setRiskAccepted('true')}
onRiskRejected={() => {
setRiskAccepted('false');
setVegaWalletDialog(false);
}}
/> />
<VegaManageDialog <VegaManageDialog
dialogOpen={appState.vegaWalletManageOverlay} dialogOpen={appState.vegaWalletManageOverlay}
setDialogOpen={(open) => setDialogOpen={(open) =>
@ -43,6 +29,8 @@ export const VegaWalletDialogs = () => {
}) })
} }
/> />
<ViewAsDialog connector={connectors.view} />
</> </>
); );
}; };

View File

@ -4,13 +4,14 @@ import keyBy from 'lodash/keyBy';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ENV } from '../../config';
import noIcon from '../../images/token-no-icon.png'; import noIcon from '../../images/token-no-icon.png';
import vegaBlack from '../../images/vega_black.png'; import vegaBlack from '../../images/vega_black.png';
import vegaVesting from '../../images/vega_vesting.png'; import vegaVesting from '../../images/vega_vesting.png';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { type WalletCardAssetProps } from '../wallet-card'; import { type WalletCardAssetProps } from '../wallet-card';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useContracts } from '../../contexts/contracts/contracts-context'; import { useContracts } from '../../contexts/contracts/contracts-context';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { import {
@ -36,6 +37,7 @@ export const usePollForDelegations = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const client = useApolloClient(); const client = useApolloClient();
const { delegationsPagination } = ENV;
const [delegations, setDelegations] = React.useState< const [delegations, setDelegations] = React.useState<
WalletDelegationFieldsFragment[] WalletDelegationFieldsFragment[]
>([]); >([]);
@ -66,9 +68,11 @@ export const usePollForDelegations = () => {
query: DelegationsDocument, query: DelegationsDocument,
variables: { variables: {
partyId: pubKey, partyId: pubKey,
delegationsPagination: { delegationsPagination: delegationsPagination
first: 50, ? {
}, first: Number(delegationsPagination),
}
: undefined,
}, },
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}) })
@ -232,14 +236,14 @@ export const usePollForDelegations = () => {
// will just continue to fail // will just continue to fail
clearInterval(interval); clearInterval(interval);
}); });
}, 20000); }, 1000);
} }
return () => { return () => {
clearInterval(interval); clearInterval(interval);
mounted = false; mounted = false;
}; };
}, [client, decimals, pubKey, t, vegaToken.address]); }, [delegationsPagination, client, decimals, pubKey, t, vegaToken.address]);
return { delegations, currentStakeAvailable, delegatedNodes, accounts }; return { delegations, currentStakeAvailable, delegatedNodes, accounts };
}; };

View File

@ -1,11 +1,11 @@
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit'; import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/environment'; import { ExternalLinks } from '@vegaprotocol/environment';
import { useConnect } from '@vegaprotocol/wallet-react'; import { useViewAsDialog } from '@vegaprotocol/wallet';
export const VegaWalletPrompt = () => { export const VegaWalletPrompt = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { connect } = useConnect(); const setViewAsDialog = useViewAsDialog((state) => state.setOpen);
return ( return (
<> <>
<h3 className="mt-4 mb-2">{t('getWallet')}</h3> <h3 className="mt-4 mb-2">{t('getWallet')}</h3>
@ -16,7 +16,7 @@ export const VegaWalletPrompt = () => {
<ButtonLink <ButtonLink
className="text-neutral-500" className="text-neutral-500"
data-testid="view-as-user" data-testid="view-as-user"
onClick={() => connect('viewParty')} onClick={() => setViewAsDialog(true)}
> >
{t('viewAsParty')} {t('viewAsParty')}
</ButtonLink> </ButtonLink>

View File

@ -25,7 +25,7 @@ import {
} from '../wallet-card'; } from '../wallet-card';
import { VegaWalletPrompt } from './vega-wallet-prompt'; import { VegaWalletPrompt } from './vega-wallet-prompt';
import { usePollForDelegations } from './hooks'; import { usePollForDelegations } from './hooks';
import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button, ButtonLink } from '@vegaprotocol/ui-toolkit'; import { Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { toBigNum } from '@vegaprotocol/utils'; import { toBigNum } from '@vegaprotocol/utils';
import { usePendingBalancesStore } from '../../hooks/use-pending-balances-manager'; import { usePendingBalancesStore } from '../../hooks/use-pending-balances-manager';
@ -34,17 +34,16 @@ import omit from 'lodash/omit';
export const VegaWallet = () => { export const VegaWallet = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { status, pubKey, pubKeys } = useVegaWallet(); const { pubKey, pubKeys } = useVegaWallet();
const pubKeyObj = useMemo(() => { const pubKeyObj = useMemo(() => {
return pubKeys?.find((pk) => pk.publicKey === pubKey); return pubKeys?.find((pk) => pk.publicKey === pubKey);
}, [pubKey, pubKeys]); }, [pubKey, pubKeys]);
const child = const child = !pubKeys ? (
status === 'connected' ? ( <VegaWalletNotConnected />
<VegaWalletConnected vegaKeys={pubKeys.map((pk) => pk.publicKey)} /> ) : (
) : ( <VegaWalletConnected vegaKeys={pubKeys.map((pk) => pk.publicKey)} />
<VegaWalletNotConnected /> );
);
return ( return (
<section className="vega-wallet" data-testid="vega-wallet"> <section className="vega-wallet" data-testid="vega-wallet">
@ -76,7 +75,9 @@ export const VegaWallet = () => {
const VegaWalletNotConnected = () => { const VegaWalletNotConnected = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const openVegaWalletDialog = useDialogStore((store) => store.open); const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
return ( return (
<> <>
<Button <Button

View File

@ -65,6 +65,7 @@ export const ENV = {
docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'), docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'),
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'), ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'), localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
rest: windowOrDefault('NX_VEGA_REST_URL'), rest: windowOrDefault('NX_VEGA_REST_URL'),
addresses: addresses:
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks], ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],

View File

@ -111,4 +111,3 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
</ContractsContext.Provider> </ContractsContext.Provider>
); );
}; };
ContractsProvider.displayName = 'ContractsProvider';

View File

@ -7,7 +7,7 @@ import { useWeb3React } from '@web3-react/core';
export const useListenForStakingEvents = ( export const useListenForStakingEvents = (
contract: Contract | undefined, contract: Contract | undefined,
vegaPublicKey: string | undefined, vegaPublicKey: string | null,
numberOfConfirmations: number numberOfConfirmations: number
) => { ) => {
const { account } = useWeb3React(); const { account } = useWeb3React();

View File

@ -1,6 +1,6 @@
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { toBigNum } from '@vegaprotocol/utils'; import { toBigNum } from '@vegaprotocol/utils';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEthereumConfig } from '@vegaprotocol/web3'; import { useEthereumConfig } from '@vegaprotocol/web3';
import React from 'react'; import React from 'react';

View File

@ -1,81 +0,0 @@
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { TELEMETRY_ON } from '../components/telemetry-dialog/telemetry-dialog';
import { useEnvironment } from '@vegaprotocol/environment';
import { ENV } from '../config';
import { isPartyNotFoundError } from '../lib/party';
export const useSentryInit = () => {
const { VEGA_ENV, GIT_COMMIT_HASH, GIT_BRANCH } = useEnvironment();
const [telemetryOn] = useLocalStorage(TELEMETRY_ON);
useEffect(() => {
if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({
dsn: ENV.dsn,
tracesSampleRate: 0.1,
enabled: true,
environment: VEGA_ENV,
release: GIT_COMMIT_HASH,
beforeSend(event, hint) {
const error = hint?.originalException;
const errorIsString = typeof error === 'string';
const errorIsObject = error instanceof Error;
const requestUrl = event.request?.url;
const transaction = event.transaction;
if (
(errorIsString && isPartyNotFoundError({ message: error })) ||
(errorIsObject && isPartyNotFoundError(error))
) {
// This error is caused by a pubkey making an API request before
// it has interacted with the chain. This isn't needed in Sentry.
return null;
}
const updatedRequest =
requestUrl && requestUrl.includes('/claim?')
? { ...event.request, url: removeQueryParams(requestUrl) }
: event.request;
const updatedTransaction =
transaction && transaction.includes('/claim?')
? removeQueryParams(transaction)
: transaction;
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
if (
breadcrumb.type === 'navigation' &&
breadcrumb.data?.to?.includes('/claim?')
) {
return {
...breadcrumb,
data: {
...breadcrumb.data,
to: removeQueryParams(breadcrumb.data.to),
},
};
}
return breadcrumb;
});
return {
...event,
request: updatedRequest,
transaction: updatedTransaction,
breadcrumbs: updatedBreadcrumbs ?? event.breadcrumbs,
};
},
});
Sentry.setTag('branch', GIT_BRANCH);
Sentry.setTag('commit', GIT_COMMIT_HASH);
} else {
Sentry.close();
}
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
};
const removeQueryParams = (url: string) => {
return url.split('?')[0];
};

View File

@ -1,41 +0,0 @@
import { useMemo } from 'react';
import {
InjectedConnector,
JsonRpcConnector,
SnapConnector,
ViewPartyConnector,
createConfig,
fairground,
stagnet,
mainnet,
} from '@vegaprotocol/wallet';
import { useEnvironment } from '@vegaprotocol/environment';
export const useVegaWalletConfig = () => {
const { VEGA_ENV, VEGA_URL, VEGA_WALLET_URL } = useEnvironment();
return useMemo(() => {
if (!VEGA_ENV || !VEGA_URL || !VEGA_WALLET_URL) return;
const injected = new InjectedConnector();
const jsonRpc = new JsonRpcConnector({
url: VEGA_WALLET_URL,
});
const snap = new SnapConnector({
node: new URL(VEGA_URL).origin,
snapId: 'npm:@vegaprotocol/snap',
version: '1.0.1',
});
const viewParty = new ViewPartyConnector();
const config = createConfig({
chains: [mainnet, fairground, stagnet],
defaultChainId: fairground.id,
connectors: [injected, snap, jsonRpc, viewParty],
});
return config;
}, [VEGA_ENV, VEGA_URL, VEGA_WALLET_URL]);
};

View File

@ -31,7 +31,7 @@ i18n
load: 'languageOnly', load: 'languageOnly',
debug: isInDev, debug: isInDev,
// have a common namespace used around the full app // have a common namespace used around the full app
ns: ['governance', 'wallet', 'wallet-react'], ns: ['governance'],
defaultNS: 'governance', defaultNS: 'governance',
keySeparator: false, // we use content as keys keySeparator: false, // we use content as keys
nsSeparator: false, nsSeparator: false,

View File

@ -20,7 +20,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.noNodes, multisigStatus: MultisigStatus.noNodes,
showMultisigStatusError: true, showMultisigStatusError: true,
zeroScoreNodes: [],
}); });
}); });
@ -36,7 +35,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.correct, multisigStatus: MultisigStatus.correct,
showMultisigStatusError: false, showMultisigStatusError: false,
zeroScoreNodes: [],
}); });
}); });
@ -52,22 +50,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.nodeNeedsRemoving, multisigStatus: MultisigStatus.nodeNeedsRemoving,
showMultisigStatusError: true, showMultisigStatusError: true,
zeroScoreNodes: [
{
id: '1',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
{
id: '2',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
],
}); });
}); });
@ -83,15 +65,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.nodeNeedsAdding, multisigStatus: MultisigStatus.nodeNeedsAdding,
showMultisigStatusError: true, showMultisigStatusError: true,
zeroScoreNodes: [
{
id: '1',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
],
}); });
}); });
}); });

View File

@ -1,8 +1,5 @@
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
PreviousEpochQuery,
ValidatorNodeFragment,
} from '../routes/staking/__generated__/PreviousEpoch';
export enum MultisigStatus { export enum MultisigStatus {
'correct' = 'correct', 'correct' = 'correct',
@ -20,15 +17,12 @@ export const getMultisigStatusInfo = (
previousEpochData?.epoch.validatorsConnection?.edges previousEpochData?.epoch.validatorsConnection?.edges
); );
const zeroScore = (node: ValidatorNodeFragment) => const hasZero = allNodesInPreviousEpoch.some(
Number(node.rewardScore?.multisigScore) === 0; (node) => Number(node?.rewardScore?.multisigScore) === 0
const oneScore = (node: ValidatorNodeFragment) => );
Number(node.rewardScore?.multisigScore) === 1; const hasOne = allNodesInPreviousEpoch.some(
(node) => Number(node?.rewardScore?.multisigScore) === 1
const hasZero = allNodesInPreviousEpoch.some(zeroScore); );
const hasOne = allNodesInPreviousEpoch.some(oneScore);
const zeroScoreNodes = allNodesInPreviousEpoch.filter(zeroScore);
if (hasZero && hasOne) { if (hasZero && hasOne) {
// If any individual node has 0 it means that node is missing from the multisig and needs to be added // If any individual node has 0 it means that node is missing from the multisig and needs to be added
@ -44,6 +38,5 @@ export const getMultisigStatusInfo = (
return { return {
showMultisigStatusError: status !== MultisigStatus.correct, showMultisigStatusError: status !== MultisigStatus.correct,
multisigStatus: status, multisigStatus: status,
zeroScoreNodes,
}; };
}; };

View File

@ -0,0 +1,30 @@
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useMemo } from 'react';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
const urlParams = new URLSearchParams(window.location.search);
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = new SnapConnector(DEFAULT_SNAP_ID);
export const useConnectors = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
return useMemo(
() => ({
injected,
jsonRpc,
view,
snap: featureFlags.METAMASK_SNAPS ? snap : undefined,
}),
[featureFlags.METAMASK_SNAPS]
);
};

View File

@ -5,6 +5,7 @@ import {
ProposalState, ProposalState,
VoteValue, VoteValue,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { import {
generateNoVotes, generateNoVotes,
@ -15,7 +16,9 @@ import { ProposalHeader, NewTransferSummary } from './proposal-header';
import { import {
lastWeek, lastWeek,
nextWeek, nextWeek,
mockWalletContext,
createUserVoteQueryMock, createUserVoteQueryMock,
networkParamsQueryMock,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote'; import { VoteState } from '../vote-details/use-user-vote';
@ -42,12 +45,14 @@ const renderComponent = (
render( render(
<AppStateProvider> <AppStateProvider>
<BrowserRouter> <BrowserRouter>
<MockedProvider mocks={mocks}> <MockedProvider mocks={[networkParamsQueryMock, ...mocks]}>
<ProposalHeader <VegaWalletContext.Provider value={mockWalletContext}>
proposal={proposal} <ProposalHeader
isListItem={isListItem} proposal={proposal}
voteState={voteState} isListItem={isListItem}
/> voteState={voteState}
/>
</VegaWalletContext.Provider>
</MockedProvider> </MockedProvider>
</BrowserRouter> </BrowserRouter>
</AppStateProvider> </AppStateProvider>
@ -155,7 +160,7 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
/Update to market: MarketId/ 'Update to market ID: MarketId'
); );
}); });

View File

@ -36,9 +36,6 @@ import { type Proposal, type BatchProposal } from '../../types';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals'; import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns'; import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
import { type ProposalNode } from '../proposal/proposal-utils';
const ProposalTypeTags = ({ const ProposalTypeTags = ({
proposal, proposal,
@ -55,8 +52,11 @@ const ProposalTypeTags = ({
if (proposal.__typename === 'BatchProposal') { if (proposal.__typename === 'BatchProposal') {
return ( return (
<div data-testid="proposal-type"> <div data-testid="proposal-type" className="flex gap-1">
<ProposalInfoLabel variant="secondary">BatchProposal</ProposalInfoLabel> {proposal.subProposals?.map((subProposal, i) => {
if (!subProposal?.terms) return null;
return <ProposalTypeTag key={i} terms={subProposal.terms} />;
})}
</div> </div>
); );
} }
@ -138,77 +138,50 @@ const ProposalDetails = ({
); );
} }
case 'UpdateMarketState': { case 'UpdateMarketState': {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.market.id)
);
return ( return (
<span> <span>
{featureFlags.UPDATE_MARKET_STATE && {featureFlags.UPDATE_MARKET_STATE &&
terms.change?.market?.id && terms.change?.market?.id &&
terms.change.updateType ? ( terms.change.updateType ? (
<> <>
<span>{t(terms.change.updateType)}: </span> {t(terms.change.updateType)}:{' '}
<span className="inline-flex gap-2"> {truncateMiddle(terms.change.market.id)}
<span className="break-all">
<MarketName marketId={terms.change.market.id} />
</span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.change.market.id}
description={t('copyId')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<Link
className="inline-block px-1"
to={marketPageLink}
target="_blank"
>
<VegaIcon
size={20}
name={VegaIconNames.OPEN_EXTERNAL}
/>
</Link>
</Tooltip>
</span>
</span>
</> </>
) : null} ) : null}
</span> </span>
); );
} }
case 'UpdateMarket': { case 'UpdateMarket': {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.marketId)
);
return ( return (
<> <>
<span>{t('UpdateToMarket')}: </span> <span>{t('UpdateToMarket')}:</span>{' '}
<span className="inline-flex items-start gap-2"> <span className="inline-flex items-start gap-2">
<span className="break-all"> <span className="break-all">{terms.change.marketId} </span>
<MarketName marketId={terms.change.marketId} />
</span>
<span className="inline-flex items-end gap-0"> <span className="inline-flex items-end gap-0">
<CopyWithTooltip <CopyWithTooltip
text={terms.change.marketId} text={terms.change.marketId}
description={t('copyId')} description={t('copyToClipboard')}
> >
<button className="inline-block px-1"> <button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} /> <VegaIcon size={20} name={VegaIconNames.COPY} />
</button> </button>
</CopyWithTooltip> </CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center"> <Tooltip description={t('OpenInConsole')} align="center">
<Link <button
className="inline-block px-1" className="inline-block px-1"
target="_blank" onClick={() => {
to={marketPageLink} const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(
':marketId',
// @ts-ignore ts doesn't like this field even though its already a string above???
terms.change.marketId
)
);
window.open(marketPageLink, '_blank');
}}
> >
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} /> <VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</Link> </button>
</Tooltip> </Tooltip>
</span> </span>
</span> </span>
@ -251,7 +224,7 @@ const ProposalDetails = ({
}} }}
components={{ components={{
// @ts-ignore children passed by i18next // @ts-ignore children passed by i18next
lozenge: <Lozenge className="text-xs" />, lozenge: <Lozenge />,
}} }}
/> />
); );
@ -313,18 +286,12 @@ const ProposalDetails = ({
{proposal.subProposals.map((p, i) => { {proposal.subProposals.map((p, i) => {
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<li <li key={i}>
key={i} <div>{renderDetails(p.terms)}</div>
className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 items-center" <SubProposalStateText
> state={proposal.state}
<Indicator indicator={i + 1} /> enactmentDatetime={p.terms.enactmentDatetime}
<span> />
<div>{renderDetails(p.terms)}</div>
<SubProposalStateText
state={proposal.state}
enactmentDatetime={p.terms.enactmentDatetime}
/>
</span>
</li> </li>
); );
})} })}
@ -541,12 +508,10 @@ const BatchProposalStateText = ({
export const ProposalHeader = ({ export const ProposalHeader = ({
proposal, proposal,
restData,
isListItem = true, isListItem = true,
voteState, voteState,
}: { }: {
proposal: Proposal | BatchProposal; proposal: Proposal | BatchProposal;
restData?: ProposalNode | null;
isListItem?: boolean; isListItem?: boolean;
voteState?: VoteState | null; voteState?: VoteState | null;
}) => { }) => {
@ -598,7 +563,7 @@ export const ProposalHeader = ({
)} )}
</div> </div>
<ProposalDetails proposal={proposal} /> <ProposalDetails proposal={proposal} />
<VoteBreakdown proposal={proposal} restData={restData} /> <VoteBreakdown proposal={proposal} />
</> </>
); );
}; };

View File

@ -3,8 +3,16 @@ import { useTranslation } from 'react-i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import {
type BatchProposalFieldsFragment,
type ProposalFieldsFragment,
} from '../../__generated__/Proposals';
export const ProposalJson = ({ proposal }: { proposal?: unknown }) => { export const ProposalJson = ({
proposal,
}: {
proposal: ProposalFieldsFragment | BatchProposalFieldsFragment;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
@ -18,7 +26,7 @@ export const ProposalJson = ({ proposal }: { proposal?: unknown }) => {
<SubHeading title={t('proposalJson')} /> <SubHeading title={t('proposalJson')} />
</CollapsibleToggle> </CollapsibleToggle>
{showDetails && <SyntaxHighlighter size="smaller" data={proposal} />} {showDetails && <SyntaxHighlighter data={proposal} />}
</section> </section>
); );
}; };

View File

@ -5,11 +5,6 @@ import {
} from './proposal-market-changes'; } from './proposal-market-changes';
import type { JsonValue } from '../../../../components/json-diff'; import type { JsonValue } from '../../../../components/json-diff';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('applyImmutableKeysFromEarlierVersion', () => { describe('applyImmutableKeysFromEarlierVersion', () => {
it('returns an empty object if any argument is not an object or null', () => { it('returns an empty object if any argument is not an object or null', () => {
const earlierVersion: JsonValue = null; const earlierVersion: JsonValue = null;
@ -62,21 +57,21 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
describe('ProposalMarketChanges', () => { describe('ProposalMarketChanges', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { getByTestId } = render( const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} /> <ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
); );
expect(getByTestId('proposal-market-changes')).toBeInTheDocument(); expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
}); });
it('JsonDiff is not visible when showChanges is false', () => { it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render( const { queryByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} /> <ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
); );
expect(queryByTestId('json-diff')).not.toBeInTheDocument(); expect(queryByTestId('json-diff')).not.toBeInTheDocument();
}); });
it('JsonDiff is visible when showChanges is true', async () => { it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} /> <ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
); );
fireEvent.click(getByTestId('proposal-market-changes-toggle')); fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument(); expect(getByTestId('json-diff')).toBeInTheDocument();

View File

@ -1,24 +1,14 @@
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set'; import set from 'lodash/set';
import get from 'lodash/get'; import get from 'lodash/get';
import compact from 'lodash/compact'; import { JsonDiff } from '../../../../components/json-diff';
import orderBy from 'lodash/orderBy';
import { JsonDiff, type JsonValue } from '../../../../components/json-diff';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { import type { JsonValue } from '../../../../components/json-diff';
useFetchProposal, import { useFetch } from '@vegaprotocol/react-helpers';
useFetchProposals, import { ENV } from '../../../../config';
flatten,
isBatchProposalNode,
isSingleProposalNode,
type ProposalNode,
type SingleProposalData,
type SubProposalData,
} from '../proposal/proposal-utils';
import { MarketName } from '../proposal/market-name';
const immutableKeys = [ const immutableKeys = [
'decimalPlaces', 'decimalPlaces',
@ -28,8 +18,8 @@ const immutableKeys = [
]; ];
export const applyImmutableKeysFromEarlierVersion = ( export const applyImmutableKeysFromEarlierVersion = (
earlierVersion: unknown, earlierVersion: JsonValue,
updatedVersion: unknown updatedVersion: JsonValue
) => { ) => {
if ( if (
typeof earlierVersion !== 'object' || typeof earlierVersion !== 'object' ||
@ -45,8 +35,7 @@ export const applyImmutableKeysFromEarlierVersion = (
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values // Overwrite the immutable keys in the updatedVersionCopy with the earlier values
immutableKeys.forEach((key) => { immutableKeys.forEach((key) => {
const earlier = get(earlierVersion, key); set(updatedVersionCopy, key, get(earlierVersion, key));
if (earlier) set(updatedVersionCopy, key, earlier);
}); });
return updatedVersionCopy; return updatedVersionCopy;
@ -54,84 +43,49 @@ export const applyImmutableKeysFromEarlierVersion = (
interface ProposalMarketChangesProps { interface ProposalMarketChangesProps {
marketId: string; marketId: string;
/** This are the changes from proposal */ updatedProposal: JsonValue;
updateProposalNode: ProposalNode | null;
indicator?: number;
} }
export const ProposalMarketChanges = ({ export const ProposalMarketChanges = ({
marketId, marketId,
updateProposalNode, updatedProposal,
indicator,
}: ProposalMarketChangesProps) => { }: ProposalMarketChangesProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showChanges, setShowChanges] = useState(false); const [showChanges, setShowChanges] = useState(false);
const { data: originalProposalData } = useFetchProposal({ const {
proposalId: marketId, state: { data },
}); } = useFetch(`${ENV.rest}governance?proposalId=${marketId}`, undefined, true);
const { data: enactedProposalsData } = useFetchProposals({ const {
proposalState: 'STATE_ENACTED', state: { data: enactedProposalData },
proposalType: 'TYPE_UPDATE_MARKET', } = useFetch(
}); `${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
let updateProposal: SingleProposalData | SubProposalData | undefined; true
if (isBatchProposalNode(updateProposalNode)) {
updateProposal = updateProposalNode.proposals.find(
(p, i) =>
p.terms.updateMarket?.marketId === marketId &&
(indicator != null ? i === indicator - 1 : true)
);
}
if (isSingleProposalNode(updateProposalNode)) {
updateProposal = updateProposalNode.proposal;
}
// this should get the proposal before the current one
const enactedUpdateMarketProposals = orderBy(
compact(
flatten(enactedProposalsData).filter((enacted) => {
const related = enacted.terms.updateMarket?.marketId === marketId;
const notCurrent =
enacted.id !== updateProposal?.id ||
('batchId' in enacted && enacted.batchId !== updateProposal.id);
const beforeCurrent =
Number(enacted.terms.enactmentTimestamp) <
Number(updateProposal?.terms.enactmentTimestamp);
return related && notCurrent && beforeCurrent;
})
),
[(proposal) => Number(proposal.terms.enactmentTimestamp)],
'desc'
); );
const latestEnactedProposal = // @ts-ignore no types here :-/
enactedUpdateMarketProposals.length > 0 const enacted = enactedProposalData?.connection?.edges
? enactedUpdateMarketProposals[0] .filter(
: undefined; // @ts-ignore no type here
({ node }) => node?.proposal?.terms?.updateMarket?.marketId === marketId
)
// @ts-ignore no type here
.sort((a, b) => {
return (
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
);
});
let originalProposal; const latestEnactedProposal = enacted?.length
if (isBatchProposalNode(originalProposalData)) { ? enacted[enacted.length - 1]
originalProposal = originalProposalData.proposals.find( : undefined;
(proposal) => proposal.id === marketId && proposal.terms.newMarket != null
);
}
if (isSingleProposalNode(originalProposalData)) {
originalProposal = originalProposalData.proposal;
}
// LEFT SIDE: update market proposal enacted just before this one const originalProposal =
// or original new market proposal // @ts-ignore no types with useFetch TODO: check this is good
const left = data?.data?.proposal?.terms?.newMarket?.changes;
latestEnactedProposal?.terms.updateMarket?.changes ||
originalProposal?.terms.newMarket?.changes;
// RIGHT SIDE: this update market proposal
const right = applyImmutableKeysFromEarlierVersion(
left,
updateProposal?.terms.updateMarket?.changes
);
return ( return (
<section data-testid="proposal-market-changes"> <section data-testid="proposal-market-changes">
@ -140,18 +94,25 @@ export const ProposalMarketChanges = ({
setToggleState={setShowChanges} setToggleState={setShowChanges}
dataTestId={'proposal-market-changes-toggle'} dataTestId={'proposal-market-changes-toggle'}
> >
<SubHeading <SubHeading title={t('updatesToMarket')} />
title={
<>
{t('UpdateToMarket')}: <MarketName marketId={marketId} truncate />
</>
}
/>
</CollapsibleToggle> </CollapsibleToggle>
{showChanges && ( {showChanges && (
<div className="mb-6 bg-vega-cdark-900 p-2 rounded-lg"> <div className="mb-6">
<JsonDiff left={left as JsonValue} right={right as JsonValue} /> <JsonDiff
left={latestEnactedProposal || originalProposal}
right={
latestEnactedProposal
? applyImmutableKeysFromEarlierVersion(
latestEnactedProposal,
updatedProposal
)
: applyImmutableKeysFromEarlierVersion(
originalProposal,
updatedProposal
)
}
/>
</div> </div>
)} )}
</section> </section>

View File

@ -2,11 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { ProposalUpdateMarketState } from './proposal-update-market-state'; import { ProposalUpdateMarketState } from './proposal-update-market-state';
import { MarketUpdateType } from '@vegaprotocol/types'; import { MarketUpdateType } from '@vegaprotocol/types';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('<ProposalUpdateMarketState />', () => { describe('<ProposalUpdateMarketState />', () => {
const suspendProposal = { const suspendProposal = {
__typename: 'UpdateMarketState' as const, __typename: 'UpdateMarketState' as const,

View File

@ -9,8 +9,6 @@ import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals'; import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
import { MarketUpdateTypeMapping } from '@vegaprotocol/types';
import { MarketName } from '../proposal/market-name';
interface ProposalUpdateMarketStateProps { interface ProposalUpdateMarketStateProps {
change: UpdateMarketStatesFragment | null; change: UpdateMarketStatesFragment | null;
@ -21,18 +19,16 @@ export const ProposalUpdateMarketState = ({
}: ProposalUpdateMarketStateProps) => { }: ProposalUpdateMarketStateProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
let market;
let isTerminate = false;
if (!change || change.__typename !== 'UpdateMarketState') { if (!change) {
return null; return null;
} }
const market = change?.market; if (change.__typename === 'UpdateMarketState') {
const isTerminate = market = change?.market;
change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE'; isTerminate = change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
let toggleTitle = t(change.updateType);
if (toggleTitle.length === 0) {
toggleTitle = t('MarketDetails');
} }
return ( return (
@ -42,13 +38,7 @@ export const ProposalUpdateMarketState = ({
setToggleState={setShowDetails} setToggleState={setShowDetails}
dataTestId="proposal-market-data-toggle" dataTestId="proposal-market-data-toggle"
> >
<SubHeading <SubHeading title={t('MarketDetails')} />
title={
<>
{toggleTitle}: <MarketName marketId={market?.id} />
</>
}
/>
</CollapsibleToggle> </CollapsibleToggle>
{showDetails && ( {showDetails && (
@ -59,12 +49,6 @@ export const ProposalUpdateMarketState = ({
{t('marketId')} {t('marketId')}
{market?.id} {market?.id}
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow>
{t('State')}
<span className="bg-vega-green-650 px-1">
{MarketUpdateTypeMapping[change.updateType]}
</span>
</KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
{t('marketName')} {t('marketName')}
{market?.tradableInstrument?.instrument?.name} {market?.tradableInstrument?.instrument?.name}

View File

@ -1,46 +0,0 @@
import classNames from 'classnames';
// rainbow-ish order
const COLOURS = ['red', 'pink', 'orange', 'yellow', 'green', 'blue', 'purple'];
const getColour = (indicator: number, max = COLOURS.length) => {
const available =
max < COLOURS.length ? COLOURS.slice(COLOURS.length - max) : COLOURS;
const tiers = Object.keys(available).length;
let index = Math.abs(indicator - 1);
if (indicator >= tiers) {
index = index % tiers;
}
return available[index];
};
export const getStyle = (indicator: number, max = COLOURS.length) =>
classNames({
'bg-vega-yellow-400 before:bg-vega-yellow-400':
'yellow' === getColour(indicator, max),
'bg-vega-green-400 before:bg-vega-green-400':
'green' === getColour(indicator, max),
'bg-vega-blue-400 before:bg-vega-blue-400':
'blue' === getColour(indicator, max),
'bg-vega-purple-400 before:bg-vega-purple-400':
'purple' === getColour(indicator, max),
'bg-vega-pink-400 before:bg-vega-pink-400':
'pink' === getColour(indicator, max),
'bg-vega-orange-400 before:bg-vega-orange-400':
'orange' === getColour(indicator, max),
'bg-vega-red-400 before:bg-vega-red-400':
'red' === getColour(indicator, max),
'bg-vega-clight-600 before:bg-vega-clight-600':
'none' === getColour(indicator, max),
});
export const getIndicatorStyle = (indicator: number) =>
classNames(
'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8 w-7 text-center',
'text-border-1',
getStyle(indicator),
// Comment below if you want to remove the "chevron"
'relative mr-[11px] z-1',
'before:absolute before:z-0 before:top-1 before:right-[-11px] before:rounded-sm',
"before:w-[22.62px] before:h-[22.62px] before:rotate-45 before:content-['']"
);

View File

@ -1,9 +0,0 @@
import { getIndicatorStyle } from './colours';
export const Indicator = ({ indicator }: { indicator: number }) => (
<div className={getIndicatorStyle(indicator)}>
<span className="absolute top-0 left-0 p-1 w-full text-center z-1">
{indicator}
</span>
</div>
);

View File

@ -1,21 +0,0 @@
import { useMarketInfoQuery } from '@vegaprotocol/markets';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const MarketName = ({
marketId,
truncate,
}: {
marketId?: string;
truncate?: boolean;
}) => {
const { data } = useMarketInfoQuery({
variables: {
marketId: marketId || '',
},
skip: !marketId,
});
const id = truncate ? truncateMiddle(marketId || '') : marketId;
return <span>{data?.market?.tradableInstrument.instrument.code || id}</span>;
};

View File

@ -1,4 +1,3 @@
import { Trans, useTranslation } from 'react-i18next';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals'; import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { type Proposal, type BatchProposal } from '../../types'; import { type Proposal, type BatchProposal } from '../../types';
import { ListAsset } from '../list-asset'; import { ListAsset } from '../list-asset';
@ -13,29 +12,21 @@ import {
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers'; import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { ProposalUpdateMarketState } from '../proposal-update-market-state'; import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details'; import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
import { type ProposalNode } from './proposal-utils';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import { Indicator } from './indicator';
import { SubHeading } from '../../../../components/heading';
export const ProposalChangeDetails = ({ export const ProposalChangeDetails = ({
proposal, proposal,
terms, terms,
restData, restData,
indicator,
}: { }: {
proposal: Proposal | BatchProposal; proposal: Proposal | BatchProposal;
terms: ProposalTermsFieldsFragment; terms: ProposalTermsFieldsFragment;
restData: ProposalNode | null; // eslint-disable-next-line
indicator?: number; restData: any;
}) => { }) => {
const { t } = useTranslation();
let details = null;
switch (terms.change.__typename) { switch (terms.change.__typename) {
case 'NewAsset': { case 'NewAsset': {
if (proposal.id && terms.change.source.__typename === 'ERC20') { if (proposal.id && terms.change.source.__typename === 'ERC20') {
details = ( return (
<div> <div>
<ListAsset <ListAsset
assetId={proposal.id} assetId={proposal.id}
@ -46,64 +37,62 @@ export const ProposalChangeDetails = ({
</div> </div>
); );
} }
break; return null;
} }
case 'UpdateAsset': { case 'UpdateAsset': {
if (proposal.id) { if (proposal.id) {
details = ( return (
<ProposalAssetDetails <ProposalAssetDetails
change={terms.change} change={terms.change}
assetId={terms.change.assetId} assetId={terms.change.assetId}
/> />
); );
} }
break; return null;
} }
case 'NewMarket': { case 'NewMarket': {
if (proposal.id) { if (proposal.id) {
details = <ProposalMarketData proposalId={proposal.id} />; return <ProposalMarketData proposalId={proposal.id} />;
} }
break; return null;
} }
case 'UpdateMarket': { case 'UpdateMarket': {
if (proposal.id) { if (proposal.id) {
details = ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<ProposalMarketData proposalId={proposal.id} /> <ProposalMarketData proposalId={proposal.id} />
<ProposalMarketChanges <ProposalMarketChanges
indicator={indicator}
marketId={terms.change.marketId} marketId={terms.change.marketId}
updateProposalNode={restData} updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes
}
/> />
</div> </div>
); );
} }
break; return null;
} }
case 'NewTransfer': { case 'NewTransfer': {
if (proposal.id) { if (proposal.id) {
details = <ProposalTransferDetails proposalId={proposal.id} />; return <ProposalTransferDetails proposalId={proposal.id} />;
} }
break; return null;
} }
case 'CancelTransfer': { case 'CancelTransfer': {
if (proposal.id) { if (proposal.id) {
details = <ProposalCancelTransferDetails proposalId={proposal.id} />; return <ProposalCancelTransferDetails proposalId={proposal.id} />;
} }
break; return null;
} }
case 'UpdateMarketState': { case 'UpdateMarketState': {
details = <ProposalUpdateMarketState change={terms.change} />; return <ProposalUpdateMarketState change={terms.change} />;
break;
} }
case 'UpdateReferralProgram': { case 'UpdateReferralProgram': {
details = <ProposalReferralProgramDetails change={terms.change} />; return <ProposalReferralProgramDetails change={terms.change} />;
break;
} }
case 'UpdateVolumeDiscountProgram': { case 'UpdateVolumeDiscountProgram': {
details = <ProposalVolumeDiscountProgramDetails change={terms.change} />; return <ProposalVolumeDiscountProgramDetails change={terms.change} />;
break;
} }
case 'UpdateNetworkParameter': { case 'UpdateNetworkParameter': {
if ( if (
@ -111,48 +100,18 @@ export const ProposalChangeDetails = ({
terms.change.networkParameter.key === terms.change.networkParameter.key ===
'rewards.activityStreak.benefitTiers' 'rewards.activityStreak.benefitTiers'
) { ) {
details = <ProposalUpdateBenefitTiers change={terms.change} />; return <ProposalUpdateBenefitTiers change={terms.change} />;
} else {
details = (
<div className="mb-4">
<SubHeading title={t(terms.change.__typename as string)} />
<span>
<Trans
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
values={{
key: terms.change.networkParameter.key,
value: terms.change.networkParameter.value,
}}
components={{
// @ts-ignore children passed by i18next
lozenge: <Lozenge />,
}}
/>
</span>
</div>
);
} }
break; return null;
} }
case 'NewFreeform': case 'NewFreeform':
case 'NewSpotMarket': case 'NewSpotMarket':
case 'UpdateSpotMarket': case 'UpdateSpotMarket': {
return null;
}
default: { default: {
break; return null;
} }
} }
if (indicator != null && details != null) {
details = (
<div className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 mb-3">
<div className="w-10">
<Indicator indicator={indicator} />
</div>
<div>{details}</div>
</div>
);
}
return details;
}; };

View File

@ -1,283 +0,0 @@
import compact from 'lodash/compact';
import { ENV } from '../../../../config';
import { useEffect, useState } from 'react';
type Maybe<T> = T | null | undefined;
type ProposalState =
| 'STATE_UNSPECIFIED'
| 'STATE_FAILED'
| 'STATE_OPEN'
| 'STATE_PASSED'
| 'STATE_REJECTED'
| 'STATE_DECLINED'
| 'STATE_ENACTED'
| 'STATE_WAITING_FOR_NODE_VOTE';
type ProposalType =
| 'TYPE_UNSPECIFIED'
| 'TYPE_ALL'
| 'TYPE_NEW_MARKET'
| 'TYPE_UPDATE_MARKET'
| 'TYPE_NETWORK_PARAMETERS'
| 'TYPE_NEW_ASSET'
| 'TYPE_NEW_FREE_FORM'
| 'TYPE_UPDATE_ASSET'
| 'TYPE_NEW_SPOT_MARKET'
| 'TYPE_UPDATE_SPOT_MARKET'
| 'TYPE_NEW_TRANSFER'
| 'TYPE_CANCEL_TRANSFER'
| 'TYPE_UPDATE_MARKET_STATE'
| 'TYPE_UPDATE_REFERRAL_PROGRAM'
| 'TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM';
type ProposalNodeType = 'TYPE_SINGLE_OR_UNSPECIFIED' | 'TYPE_BATCH';
type ProposalData = {
id: string;
rationale: {
description: string;
title: string;
};
state: ProposalState;
timestamp: string;
};
type Terms = {
cancelTransfer?: { changes: unknown };
enactmentTimestamp: string;
newAsset?: { changes: unknown };
newFreeform: object;
newMarket?: { changes: unknown };
newSpotMarket?: { changes: unknown };
newTransfer?: { changes: unknown };
updateAsset?: { assetId: string; changes: unknown };
updateMarket?: { marketId: string; changes: unknown };
updateMarketState?: {
changes: {
marketId: string;
price: string;
updateType:
| 'MARKET_STATE_UPDATE_TYPE_UNSPECIFIED'
| 'MARKET_STATE_UPDATE_TYPE_TERMINATE'
| 'MARKET_STATE_UPDATE_TYPE_SUSPEND'
| 'MARKET_STATE_UPDATE_TYPE_RESUME';
};
};
updateNetworkParameter?: { changes: unknown };
updateReferralProgram?: { changes: unknown };
updateSpotMarket?: { marketId: string; changes: unknown };
updateVolumeDiscountProgram?: { changes: unknown };
};
export type SingleProposalData = ProposalData & {
terms: Terms & {
closingTimestamp: string;
validationTimestamp: string;
};
};
type BatchProposalData = ProposalData & {
batchTerms: {
changes: Terms[];
};
};
export type SubProposalData = SingleProposalData & {
batchId: string;
};
export type ProposalNode = {
proposal: ProposalData;
proposalType: ProposalNodeType;
proposals: SubProposalData[];
yes?: [
{
partyId: string;
elsPerMarket?: [
{
marketId: string;
els: string;
}
];
}
];
no?: [
{
partyId: string;
elsPerMarket?: [
{
marketId: string;
els: string;
}
];
}
];
};
type SingleProposalNode = ProposalNode & {
proposal: SingleProposalData;
proposalType: 'TYPE_SINGLE_OR_UNSPECIFIED';
proposals: [];
};
type BatchProposalNode = ProposalNode & {
proposal: BatchProposalData;
proposalType: 'TYPE_BATCH';
};
export const isProposalNode = (node: unknown): node is ProposalNode =>
Boolean(
typeof node === 'object' &&
node &&
'proposal' in node &&
typeof node.proposal === 'object' &&
node?.proposal &&
'id' in node.proposal &&
node?.proposal?.id
);
export const isSingleProposalNode = (
node: Maybe<ProposalNode>
): node is SingleProposalNode =>
Boolean(
node &&
node?.proposalType === 'TYPE_SINGLE_OR_UNSPECIFIED' &&
node?.proposal
);
export const isBatchProposalNode = (
node: Maybe<ProposalNode>
): node is BatchProposalNode =>
Boolean(
node &&
node?.proposalType === 'TYPE_BATCH' &&
node?.proposal &&
'batchTerms' in node.proposal &&
node?.proposals?.length > 0
);
// this includes also batch proposals with `updateMarket`s 👍
const PROPOSALS_ENDPOINT = `${ENV.rest}governances?proposalState=:proposalState&proposalType=:proposalType`;
// this can be queried also by sub proposal id as `proposalId` and it will
// return full batch proposal data with all of its sub proposals including
// the requested one inside `proposals` array.
const PROPOSAL_ENDPOINT = `${ENV.rest}governance?proposalId=:proposalId`;
export const getProposals = async ({
proposalState,
proposalType,
}: {
proposalState: ProposalState;
proposalType: ProposalType;
}) => {
try {
const response = await fetch(
PROPOSALS_ENDPOINT.replace(':proposalState', proposalState).replace(
':proposalType',
proposalType
)
);
if (response.ok) {
const data = await response.json();
if (
data &&
'connection' in data &&
data.connection &&
'edges' in data.connection &&
data.connection.edges?.length > 0
) {
const nodes = compact(
data.connection.edges.map((e: { node?: object }) => e?.node)
).filter(isProposalNode);
return nodes;
}
}
} catch {
// NOOP - ignore errors
}
return [];
};
export const getProposal = async ({ proposalId }: { proposalId: string }) => {
try {
const response = await fetch(
PROPOSAL_ENDPOINT.replace(':proposalId', proposalId)
);
if (response.ok) {
const data = await response.json();
if (data && 'data' in data && isProposalNode(data.data)) {
return data.data as ProposalNode;
}
}
} catch (err) {
// NOOP - ignore errors
}
return null;
};
export const useFetchProposal = ({ proposalId }: { proposalId?: string }) => {
const [data, setData] = useState<ProposalNode | null>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const cb = async () => {
if (!proposalId) return;
setLoading(true);
const data = await getProposal({ proposalId });
setLoading(false);
if (data) {
setData(data);
}
};
cb();
}, [proposalId]);
return { data, loading };
};
export const useFetchProposals = ({
proposalState,
proposalType,
}: {
proposalState: ProposalState;
proposalType: ProposalType;
}) => {
const [data, setData] = useState<ProposalNode[]>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const cb = async () => {
setLoading(true);
const data = await getProposals({ proposalState, proposalType });
setLoading(false);
if (data) {
setData(data);
}
};
cb();
}, [proposalState, proposalType]);
return { data, loading };
};
export const flatten = (
nodes: ProposalNode[]
): (SingleProposalData | SubProposalData)[] => {
const flattenNodes = [];
for (const node of nodes) {
if (isSingleProposalNode(node)) {
flattenNodes.push(node.proposal);
}
if (isBatchProposalNode(node)) {
for (const sub of node.proposals) {
flattenNodes.push(sub);
}
}
}
return flattenNodes;
};

View File

@ -1,12 +1,12 @@
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import { type VegaWalletConfig } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals'; import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal'; import { Proposal } from './proposal';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { type Proposal as IProposal } from '../../types'; import { type Proposal as IProposal } from '../../types';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
jest.mock('@vegaprotocol/network-parameters', () => ({ jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@vegaprotocol/network-parameters'), ...jest.requireActual('@vegaprotocol/network-parameters'),
@ -24,44 +24,45 @@ jest.mock('@vegaprotocol/network-parameters', () => ({
error: null, error: null,
})), })),
})); }));
jest.mock('../proposal-detail-header/proposal-header', () => ({ jest.mock('../proposal-detail-header/proposal-header', () => ({
ProposalHeader: () => <div data-testid="proposal-header"></div>, ProposalHeader: () => <div data-testid="proposal-header"></div>,
})); }));
jest.mock('../proposal-change-table', () => ({ jest.mock('../proposal-change-table', () => ({
ProposalChangeTable: () => <div data-testid="proposal-change-table"></div>, ProposalChangeTable: () => <div data-testid="proposal-change-table"></div>,
})); }));
jest.mock('../proposal-json', () => ({ jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>, ProposalJson: () => <div data-testid="proposal-json"></div>,
})); }));
jest.mock('../list-asset', () => ({ jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>, ListAsset: () => <div data-testid="proposal-list-asset"></div>,
})); }));
jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
}));
jest.mock('../vote-details', () => ({
UserVote: () => <div data-testid="user-vote"></div>,
}));
jest.mock('./proposal-change-details', () => ({ jest.mock('./proposal-change-details', () => ({
ProposalChangeDetails: () => <div data-testid="proposal-change-details" />, ProposalChangeDetails: () => (
<div data-testid="proposal-change-details"></div>
),
})); }));
const vegaWalletConfig: VegaWalletConfig = {
network: 'TESTNET',
vegaUrl: 'https://vega.xyz',
vegaWalletServiceUrl: 'https://wallet.vega.xyz',
links: {
explorer: 'explorer',
concepts: 'concepts',
chromeExtensionUrl: 'chrome',
mozillaExtensionUrl: 'mozilla',
},
chainId: 'VEGA_CHAIN_ID',
};
const renderComponent = (proposal: IProposal) => { const renderComponent = (proposal: IProposal) => {
return render( render(
<MemoryRouter> <MemoryRouter>
<MockedProvider> <MockedProvider>
<MockedWalletProvider> <VegaWalletProvider config={vegaWalletConfig}>
<AppStateProvider> <Proposal restData={{}} proposal={proposal} />
<Proposal restData={null} proposal={proposal} /> </VegaWalletProvider>
</AppStateProvider>
</MockedWalletProvider>
</MockedProvider> </MockedProvider>
</MemoryRouter> </MemoryRouter>
); );

View File

@ -8,22 +8,20 @@ import { ProposalJson } from '../proposal-json';
import { UserVote } from '../vote-details'; import { UserVote } from '../vote-details';
import Routes from '../../../routes'; import Routes from '../../../routes';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { type ProposalNode } from './proposal-utils';
import { useVoteSubmit } from '@vegaprotocol/proposals'; import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote'; import { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal as IProposal, type BatchProposal } from '../../types'; import { type Proposal as IProposal, type BatchProposal } from '../../types';
import { ProposalChangeDetails } from './proposal-change-details'; import { ProposalChangeDetails } from './proposal-change-details';
import { type JsonValue } from 'type-fest';
export interface ProposalProps { export interface ProposalProps {
proposal: IProposal | BatchProposal; proposal: IProposal | BatchProposal;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: ProposalNode | null; restData: any;
} }
export const Proposal = ({ proposal, restData }: ProposalProps) => { export const Proposal = ({ proposal, restData }: ProposalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { submit, finalizedVote, transaction } = useVoteSubmit(); const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote); const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
return ( return (
@ -48,7 +46,6 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<ProposalHeader <ProposalHeader
proposal={proposal} proposal={proposal}
restData={restData}
isListItem={false} isListItem={false}
voteState={voteState} voteState={voteState}
/> />
@ -61,7 +58,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<ProposalDescription description={proposal.rationale.description} /> <ProposalDescription description={proposal.rationale.description} />
</div> </div>
<div className="mb-4 flex flex-col gap-0"> <div className="mb-4">
{proposal.__typename === 'Proposal' ? ( {proposal.__typename === 'Proposal' ? (
<ProposalChangeDetails <ProposalChangeDetails
proposal={proposal} proposal={proposal}
@ -73,7 +70,6 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<ProposalChangeDetails <ProposalChangeDetails
indicator={i + 1}
key={i} key={i}
proposal={proposal} proposal={proposal}
terms={p.terms} terms={p.terms}
@ -89,6 +85,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<UserVote <UserVote
proposal={proposal} proposal={proposal}
submit={submit} submit={submit}
dialog={Dialog}
transaction={transaction} transaction={transaction}
voteState={voteState} voteState={voteState}
voteDatetime={voteDatetime} voteDatetime={voteDatetime}
@ -97,7 +94,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div> </div>
<div className="mb-6"> <div className="mb-6">
<ProposalJson proposal={restData?.proposal as unknown as JsonValue} /> <ProposalJson proposal={restData?.data?.proposal} />
</div> </div>
</section> </section>
); );

View File

@ -1,14 +1,15 @@
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import { mockWalletContext } from '../../test-helpers/mocks';
const renderComponent = (id: string) => const renderComponent = (id: string) =>
render( render(
<Router> <Router>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails id={id} /> <ProposalsListItemDetails id={id} />
</AppStateProvider> </VegaWalletContext.Provider>
</Router> </Router>
); );

View File

@ -206,7 +206,7 @@ export const ProposalsList = ({
/> />
)} )}
<section className="-mx-4 p-4 mb-8 bg-vega-cdark-900 rounded-l-sm"> <section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} /> <SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 || {sortedProposals.open.length > 0 ||

View File

@ -1,32 +1,25 @@
import { act, render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { import { VegaWalletContext } from '@vegaprotocol/wallet';
MockedWalletProvider,
mockConfig,
} from '@vegaprotocol/wallet-react/testing';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { ProposalFormSubmit } from './proposal-form-submit'; import { ProposalFormSubmit } from './proposal-form-submit';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
const renderComponent = (isSubmitting: boolean) => { const renderComponent = (
context: VegaWalletContextShape,
isSubmitting: boolean
) => {
render( render(
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={context}>
<ProposalFormSubmit isSubmitting={isSubmitting} /> <ProposalFormSubmit isSubmitting={isSubmitting} />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
); );
}; };
describe('Proposal Form Submit', () => { describe('Proposal Form Submit', () => {
const pubKey = { publicKey: '123456__123456', name: 'test' };
afterEach(() => {
act(() => {
mockConfig.reset();
});
});
it('should display connection message and button if wallet not connected', () => { it('should display connection message and button if wallet not connected', () => {
renderComponent(false); renderComponent({ pubKey: null } as VegaWalletContextShape, false);
expect( expect(
screen.getByText('Connect your wallet to submit a proposal') screen.getByText('Connect your wallet to submit a proposal')
@ -37,22 +30,28 @@ describe('Proposal Form Submit', () => {
}); });
it('should display submit button if wallet is connected', () => { it('should display submit button if wallet is connected', () => {
mockConfig.store.setState({ const pubKey = { publicKey: '123456__123456', name: 'test' };
pubKey: pubKey.publicKey, renderComponent(
keys: [pubKey], {
}); pubKey: pubKey.publicKey,
renderComponent(false); pubKeys: [pubKey],
} as VegaWalletContextShape,
false
);
expect(screen.getByTestId('proposal-submit')).toHaveTextContent( expect(screen.getByTestId('proposal-submit')).toHaveTextContent(
'Submit proposal' 'Submit proposal'
); );
}); });
it('should display submitting button text if wallet is connected and submitting', () => { it('should display submitting button text if wallet is connected and submitting', () => {
mockConfig.store.setState({ const pubKey = { publicKey: '123456__123456', name: 'test' };
pubKey: pubKey.publicKey, renderComponent(
keys: [pubKey], {
}); pubKey: pubKey.publicKey,
renderComponent(true); pubKeys: [pubKey],
} as VegaWalletContextShape,
true
);
expect(screen.getByTestId('proposal-submit')).toHaveTextContent( expect(screen.getByTestId('proposal-submit')).toHaveTextContent(
'Submitting proposal' 'Submitting proposal'
); );

View File

@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { VegaWalletContainer } from '../../../../components/vega-wallet-container'; import { VegaWalletContainer } from '../../../../components/vega-wallet-container';
interface ProposalFormSubmitProps { interface ProposalFormSubmitProps {

View File

@ -1,24 +1,19 @@
import { import {
VegaTransactionDialog,
getProposalDialogIcon, getProposalDialogIcon,
getProposalDialogIntent, getProposalDialogIntent,
useGetProposalDialogTitle, useGetProposalDialogTitle,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import type { import type { ProposalEventFieldsFragment } from '@vegaprotocol/proposals';
ProposalEventFieldsFragment, import type { DialogProps } from '@vegaprotocol/proposals';
VegaTxState,
} from '@vegaprotocol/proposals';
interface ProposalFormTransactionDialogProps { interface ProposalFormTransactionDialogProps {
finalizedProposal: ProposalEventFieldsFragment | null; finalizedProposal: ProposalEventFieldsFragment | null;
transaction: VegaTxState; TransactionDialog: (props: DialogProps) => JSX.Element;
onChange: (open: boolean) => void;
} }
export const ProposalFormTransactionDialog = ({ export const ProposalFormTransactionDialog = ({
finalizedProposal, finalizedProposal,
transaction, TransactionDialog,
onChange,
}: ProposalFormTransactionDialogProps) => { }: ProposalFormTransactionDialogProps) => {
const title = useGetProposalDialogTitle(finalizedProposal?.state); const title = useGetProposalDialogTitle(finalizedProposal?.state);
// Render a custom complete UI if the proposal was rejected otherwise // Render a custom complete UI if the proposal was rejected otherwise
@ -29,16 +24,13 @@ export const ProposalFormTransactionDialog = ({
return ( return (
<div data-testid="proposal-transaction-dialog"> <div data-testid="proposal-transaction-dialog">
<VegaTransactionDialog <TransactionDialog
title={title} title={title}
intent={getProposalDialogIntent(finalizedProposal?.state)} intent={getProposalDialogIntent(finalizedProposal?.state)}
icon={getProposalDialogIcon(finalizedProposal?.state)} icon={getProposalDialogIcon(finalizedProposal?.state)}
content={{ content={{
Complete: completeContent, Complete: completeContent,
}} }}
transaction={transaction}
isOpen={transaction.dialogOpen}
onChange={onChange}
/> />
</div> </div>
); );

View File

@ -1,8 +1,10 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { import {
lastWeek, lastWeek,
mockWalletContext,
networkParamsQueryMock, networkParamsQueryMock,
nextWeek, nextWeek,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
@ -46,7 +48,9 @@ const renderComponent = (
render( render(
<Router> <Router>
<MockedProvider mocks={mocks}> <MockedProvider mocks={mocks}>
<VoteBreakdown proposal={proposal} /> <VegaWalletContext.Provider value={mockWalletContext}>
<VoteBreakdown proposal={proposal} />
</VegaWalletContext.Provider>
</MockedProvider> </MockedProvider>
</Router> </Router>
); );
@ -56,7 +60,6 @@ describe('VoteBreakdown', () => {
jest.useFakeTimers(); jest.useFakeTimers();
jest.setSystemTime(0); jest.setSystemTime(0);
}); });
afterAll(() => { afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
}); });

View File

@ -15,9 +15,6 @@ import {
type VoteFieldsFragment, type VoteFieldsFragment,
} from '../../__generated__/Proposals'; } from '../../__generated__/Proposals';
import { useBatchVoteInformation } from '../../hooks/use-vote-information'; import { useBatchVoteInformation } from '../../hooks/use-vote-information';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
import { type ProposalNode } from '../proposal/proposal-utils';
export const CompactVotes = ({ number }: { number: BigNumber }) => ( export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber <CompactNumber
@ -42,9 +39,8 @@ const VoteProgress = ({
children, children,
}: VoteProgressProps) => { }: VoteProgressProps) => {
const containerClasses = classNames( const containerClasses = classNames(
'relative h-2 rounded-md overflow-hidden', 'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
// 'border border-vega-dark-300', colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
colourfulBg ? 'bg-vega-red' : 'bg-vega-dark-200'
); );
const progressClasses = classNames( const progressClasses = classNames(
@ -53,19 +49,17 @@ const VoteProgress = ({
); );
const textClasses = classNames( const textClasses = classNames(
'w-full flex items-center justify-start text-white text-sm pb-1' 'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
); );
return ( return (
<div> <div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
/>
<div className={textClasses}>{children}</div> <div className={textClasses}>{children}</div>
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
/>
</div>
</div> </div>
); );
}; };
@ -84,22 +78,14 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
<div data-testid={testId}> <div data-testid={testId}>
{reached ? ( {reached ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<VegaIcon <VegaIcon name={VegaIconNames.TICK} size={20} />
name={VegaIconNames.TICK}
className="text-vega-green"
size={20}
/>
<span> <span>
{threshold.toString()}% {text} {t('met')} {threshold.toString()}% {text} {t('met')}
</span> </span>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<VegaIcon <VegaIcon name={VegaIconNames.CROSS} size={20} />
name={VegaIconNames.CROSS}
className="text-vega-red"
size={20}
/>
<span> <span>
{threshold.toString()}% {text} {t('not met')} {threshold.toString()}% {text} {t('not met')}
</span> </span>
@ -111,64 +97,24 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
export const VoteBreakdown = ({ export const VoteBreakdown = ({
proposal, proposal,
restData,
}: { }: {
proposal: Proposal | BatchProposal; proposal: Proposal | BatchProposal;
restData?: ProposalNode | null;
}) => { }) => {
if (proposal.__typename === 'Proposal') { if (proposal.__typename === 'Proposal') {
return <VoteBreakdownNormal proposal={proposal} />; return <VoteBreakdownNormal proposal={proposal} />;
} }
if (proposal.__typename === 'BatchProposal') { if (proposal.__typename === 'BatchProposal') {
return <VoteBreakdownBatch proposal={proposal} restData={restData} />; return <VoteBreakdownBatch proposal={proposal} />;
} }
return null; return null;
}; };
const VoteBreakdownBatch = ({ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
proposal,
restData,
}: {
proposal: BatchProposal;
restData?: ProposalNode | null;
}) => {
const [fullBreakdown, setFullBreakdown] = useState(false); const [fullBreakdown, setFullBreakdown] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const yesELS =
restData?.yes?.reduce((all, y) => {
if (y.elsPerMarket) {
y.elsPerMarket.forEach((item) => {
const share = Number(item.els);
if (all[item.marketId]) {
all[item.marketId].push(share);
} else {
all[item.marketId] = [share];
}
return all;
});
}
return all;
}, {} as Record<string, number[]>) || {};
const noELS =
restData?.no?.reduce((all, y) => {
if (y.elsPerMarket) {
y.elsPerMarket.forEach((item) => {
const share = Number(item.els);
if (all[item.marketId]) {
all[item.marketId].push(share);
} else {
all[item.marketId] = [share];
}
return all;
});
}
return all;
}, {} as Record<string, number[]>) || {};
const voteInfo = useBatchVoteInformation({ const voteInfo = useBatchVoteInformation({
terms: compact( terms: compact(
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : [] proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
@ -205,7 +151,7 @@ const VoteBreakdownBatch = ({
<p className="flex gap-2 m-0 items-center"> <p className="flex gap-2 m-0 items-center">
<VegaIcon <VegaIcon
name={VegaIconNames.CROSS} name={VegaIconNames.CROSS}
className="text-vega-red" className="text-vega-pink"
size={20} size={20}
/> />
{t( {t(
@ -230,13 +176,10 @@ const VoteBreakdownBatch = ({
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<VoteBreakdownBatchSubProposal <VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i} key={i}
proposal={proposal} proposal={proposal}
votes={proposal.votes} votes={proposal.votes}
terms={p.terms} terms={p.terms}
yesELS={yesELS}
noELS={noELS}
/> />
); );
})} })}
@ -270,7 +213,7 @@ const VoteBreakdownBatch = ({
<p className="flex gap-2 m-0 items-center"> <p className="flex gap-2 m-0 items-center">
<VegaIcon <VegaIcon
name={VegaIconNames.CROSS} name={VegaIconNames.CROSS}
className="text-vega-red" className="text-vega-pink"
size={20} size={20}
/> />
{t('Proposal failed: {{count}} of {{total}} proposals passed', { {t('Proposal failed: {{count}} of {{total}} proposals passed', {
@ -292,13 +235,10 @@ const VoteBreakdownBatch = ({
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<VoteBreakdownBatchSubProposal <VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i} key={i}
proposal={proposal} proposal={proposal}
votes={proposal.votes} votes={proposal.votes}
terms={p.terms} terms={p.terms}
yesELS={yesELS}
noELS={noELS}
/> />
); );
})} })}
@ -315,62 +255,28 @@ const VoteBreakdownBatchSubProposal = ({
proposal, proposal,
votes, votes,
terms, terms,
indicator,
yesELS,
noELS,
}: { }: {
proposal: BatchProposal; proposal: BatchProposal;
votes: VoteFieldsFragment; votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment; terms: ProposalTermsFieldsFragment;
indicator?: number;
yesELS: Record<string, number[]>;
noELS: Record<string, number[]>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const voteInfo = useVoteInformation({
votes,
terms,
});
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN; const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket'; const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
let marketId = undefined;
if (terms?.change?.__typename === 'UpdateMarket') {
marketId = terms.change.marketId;
}
if (terms?.change?.__typename === 'UpdateMarketState') {
marketId = terms.change.market.id;
}
const voteInfo = useVoteInformation({
votes,
terms,
// yes votes ELS for this specific proposal (market)
yesELS: marketId ? yesELS[marketId] : undefined,
// no votes ELS for this specific proposal (market)
noELS: marketId ? noELS[marketId] : undefined,
});
const marketName = marketId ? (
<>
: <MarketName marketId={marketId} />
</>
) : null;
const indicatorElement = indicator && <Indicator indicator={indicator} />;
return ( return (
<div className="mb-6"> <div>
<div className="flex items-center gap-3 mb-3"> <h4>{t(terms.change.__typename)}</h4>
{indicatorElement} <VoteBreakDownUI
<h4> voteInfo={voteInfo}
{t(terms.change.__typename)} {marketName} isProposalOpen={isProposalOpen}
</h4> isUpdateMarket={isUpdateMarket}
</div> />
<div className="rounded-sm bg-vega-dark-100 p-3">
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
</div>
</div> </div>
); );
}; };
@ -385,13 +291,11 @@ const VoteBreakdownNormal = ({ proposal }: { proposal: Proposal }) => {
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket'; const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
return ( return (
<div className="mb-6"> <VoteBreakDownUI
<VoteBreakDownUI voteInfo={voteInfo}
voteInfo={voteInfo} isProposalOpen={isProposalOpen}
isProposalOpen={isProposalOpen} isUpdateMarket={isUpdateMarket}
isUpdateMarket={isUpdateMarket} />
/>
</div>
); );
}; };
@ -418,6 +322,7 @@ const VoteBreakDownUI = ({
noPercentage, noPercentage,
noLPPercentage, noLPPercentage,
yesPercentage, yesPercentage,
yesLPPercentage,
yesTokens, yesTokens,
noTokens, noTokens,
totalEquityLikeShareWeight, totalEquityLikeShareWeight,
@ -430,7 +335,6 @@ const VoteBreakDownUI = ({
majorityLPMet, majorityLPMet,
willPassByTokenVote, willPassByTokenVote,
willPassByLPVote, willPassByLPVote,
lpVoteWeight,
} = voteInfo; } = voteInfo;
const participationThresholdProgress = BigNumber.min( const participationThresholdProgress = BigNumber.min(
@ -455,13 +359,13 @@ const VoteBreakDownUI = ({
'flex justify-between flex-wrap gap-6' 'flex justify-between flex-wrap gap-6'
); );
const sectionClasses = classNames('min-w-[300px] flex-1 flex-grow'); const sectionClasses = classNames('min-w-[300px] flex-1 flex-grow');
const headingClasses = classNames('mb-2 text-sm text-white font-bold'); const headingClasses = classNames('mb-2 text-vega-dark-400');
const progressDetailsClasses = classNames( const progressDetailsClasses = classNames(
'flex justify-between flex-wrap mt-2 text-sm' 'flex justify-between flex-wrap mt-2 text-sm'
); );
return ( return (
<div> <div className="mb-6">
{isProposalOpen && ( {isProposalOpen && (
<div <div
data-testid="vote-status" data-testid="vote-status"
@ -478,7 +382,7 @@ const VoteBreakDownUI = ({
<VegaIcon <VegaIcon
name={VegaIconNames.CROSS} name={VegaIconNames.CROSS}
size={20} size={20}
className="text-vega-red" className="text-vega-pink"
/> />
)} )}
</span> </span>
@ -494,13 +398,103 @@ const VoteBreakDownUI = ({
<p className="m-0"> <p className="m-0">
<Trans <Trans
i18nKey={'Currently expected to <0>fail</0>'} i18nKey={'Currently expected to <0>fail</0>'}
components={[<span className="text-vega-red" />]} components={[<span className="text-vega-pink" />]}
/> />
</p> </p>
)} )}
</div> </div>
)} )}
{isUpdateMarket && (
<div className="mb-4">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section
className={sectionClasses}
data-testid="lp-majority-breakdown"
>
<VoteProgress
percentageFor={yesLPPercentage}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(1)}%</button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<span>
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
</Tooltip>
</span>
</div>
</div>
</section>
<section
className={sectionClasses}
data-testid="lp-participation-breakdown"
>
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
</Tooltip>
</div>
</div>
</section>
</div>
</div>
)}
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>} {isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
<div className={sectionWrapperClasses}> <div className={sectionWrapperClasses}>
<section <section
@ -600,97 +594,6 @@ const VoteBreakDownUI = ({
</div> </div>
</section> </section>
</div> </div>
{/** Liquidity provider vote */}
{isUpdateMarket && (
<div className="mt-3">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section
className={sectionClasses}
data-testid="lp-majority-breakdown"
>
<VoteProgress
percentageFor={lpVoteWeight}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{lpVoteWeight.toFixed(defaultDP)}%</span>
}
>
<button>{lpVoteWeight.toFixed(1)}%</button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<span>
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
</Tooltip>
</span>
</div>
</div>
</section>
<section
className={sectionClasses}
data-testid="lp-participation-breakdown"
>
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
</Tooltip>
</div>
</div>
</section>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import { captureMessage } from '@sentry/minimal'; import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types'; import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote'; import { useUserVoteQuery } from './__generated__/Vote';

View File

@ -1,19 +1,20 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit'; import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega'; import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons'; import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type VoteValue } from '@vegaprotocol/types'; import { type VoteValue } from '@vegaprotocol/types';
import { type VegaTxState } from '@vegaprotocol/proposals'; import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
import { type VoteState } from './use-user-vote'; import { type VoteState } from './use-user-vote';
import { type Proposal, type BatchProposal } from '../../types'; import { type Proposal, type BatchProposal } from '../../types';
interface UserVoteProps { interface UserVoteProps {
proposal: Proposal | BatchProposal; proposal: Proposal | BatchProposal;
transaction: VegaTxState; transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>; submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null; voteState: VoteState | null;
voteDatetime: Date | null; voteDatetime: Date | null;
} }
@ -22,6 +23,7 @@ export const UserVote = ({
proposal, proposal,
submit, submit,
transaction, transaction,
dialog,
voteState, voteState,
voteDatetime, voteDatetime,
}: UserVoteProps) => { }: UserVoteProps) => {
@ -54,6 +56,7 @@ export const UserVote = ({
className="flex" className="flex"
submit={submit} submit={submit}
transaction={transaction} transaction={transaction}
dialog={dialog}
/> />
) )
) : ( ) : (

View File

@ -2,20 +2,33 @@ import { render, screen } from '@testing-library/react';
import { VoteTransactionDialog } from './vote-transaction-dialog'; import { VoteTransactionDialog } from './vote-transaction-dialog';
import { VoteState } from './use-user-vote'; import { VoteState } from './use-user-vote';
import { VegaTxStatus } from '@vegaprotocol/proposals'; import { VegaTxStatus } from '@vegaprotocol/proposals';
import { ConnectorErrors, unknownError } from '@vegaprotocol/wallet';
describe('VoteTransactionDialog', () => { describe('VoteTransactionDialog', () => {
const mockTransactionDialog = jest.fn(({ title, content }) => (
<div>
<div>{title}</div>
<div>{content?.Complete}</div>
</div>
));
it('renders without crashing', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByTestId('vote-transaction-dialog')).toBeInTheDocument();
});
it('renders with txRequested title when voteState is Requested', () => { it('renders with txRequested title when voteState is Requested', () => {
render( render(
<VoteTransactionDialog <VoteTransactionDialog
voteState={VoteState.Requested} voteState={VoteState.Requested}
transaction={{ transaction={null}
error: null, TransactionDialog={mockTransactionDialog}
txHash: null,
signature: null,
status: VegaTxStatus.Requested,
dialogOpen: true,
}}
/> />
); );
@ -26,13 +39,8 @@ describe('VoteTransactionDialog', () => {
render( render(
<VoteTransactionDialog <VoteTransactionDialog
voteState={VoteState.Pending} voteState={VoteState.Pending}
transaction={{ transaction={null}
error: null, TransactionDialog={mockTransactionDialog}
txHash: null,
signature: null,
status: VegaTxStatus.Pending,
dialogOpen: true,
}}
/> />
); );
@ -43,13 +51,8 @@ describe('VoteTransactionDialog', () => {
render( render(
<VoteTransactionDialog <VoteTransactionDialog
voteState={VoteState.Yes} // or any other state other than Requested or Pending voteState={VoteState.Yes} // or any other state other than Requested or Pending
transaction={{ transaction={null}
error: null, TransactionDialog={mockTransactionDialog}
txHash: null,
signature: null,
status: VegaTxStatus.Complete,
dialogOpen: true,
}}
/> />
); );
@ -62,18 +65,17 @@ describe('VoteTransactionDialog', () => {
<VoteTransactionDialog <VoteTransactionDialog
voteState={VoteState.Failed} voteState={VoteState.Failed}
transaction={{ transaction={{
error: unknownError(), error: { message: 'Custom error test message', name: 'blah' },
txHash: null, txHash: null,
signature: null, signature: null,
status: VegaTxStatus.Error, status: VegaTxStatus.Error,
dialogOpen: true, dialogOpen: false,
}} }}
TransactionDialog={mockTransactionDialog}
/> />
); );
expect( expect(screen.getByText('Custom error test message')).toBeInTheDocument();
screen.getByText(ConnectorErrors.unknown.message)
).toBeInTheDocument();
}); });
it('renders default error message when voteState is failed and no error message exists on the tx', () => { it('renders default error message when voteState is failed and no error message exists on the tx', () => {
@ -84,9 +86,10 @@ describe('VoteTransactionDialog', () => {
error: null, error: null,
txHash: null, txHash: null,
signature: null, signature: null,
status: VegaTxStatus.Complete, status: VegaTxStatus.Error,
dialogOpen: true, dialogOpen: false,
}} }}
TransactionDialog={mockTransactionDialog}
/> />
); );
@ -97,13 +100,8 @@ describe('VoteTransactionDialog', () => {
render( render(
<VoteTransactionDialog <VoteTransactionDialog
voteState={VoteState.Yes} voteState={VoteState.Yes}
transaction={{ transaction={null}
error: null, TransactionDialog={mockTransactionDialog}
txHash: null,
signature: null,
status: VegaTxStatus.Default,
dialogOpen: true,
}}
/> />
); );

View File

@ -1,77 +1,120 @@
import { act, fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { VoteButtons, type VoteButtonsProps } from './vote-buttons'; import { VoteButtons } from './vote-buttons';
import { VoteState } from './use-user-vote'; import { VoteState } from './use-user-vote';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedProvider } from '@apollo/react-testing'; import { MockedProvider } from '@apollo/react-testing';
import { VegaTxStatus } from '@vegaprotocol/proposals';
import {
MockedWalletProvider,
mockConfig,
} from '@vegaprotocol/wallet-react/testing';
describe('Vote buttons', () => { describe('Vote buttons', () => {
const key = { publicKey: '0x123', name: 'key 1' }; it('should render successfully', () => {
const transaction = { const { baseElement } = render(
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
dialogOpen: false,
};
const props = {
voteState: VoteState.NotCast,
voteDatetime: null,
proposalState: ProposalState.STATE_OPEN,
proposalId: null,
minVoterBalance: null,
spamProtectionMinTokens: null,
currentStakeAvailable: new BigNumber(1),
submit: () => Promise.resolve(),
transaction,
};
const renderComponent = (testProps?: Partial<VoteButtonsProps>) => {
return render(
<AppStateProvider> <AppStateProvider>
<MockedProvider> <MockedProvider>
<MockedWalletProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons {...props} {...testProps} /> <VoteButtons
</MockedWalletProvider> voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider> </MockedProvider>
</AppStateProvider> </AppStateProvider>
); );
};
beforeEach(() => {
mockConfig.store.setState({ pubKey: key.publicKey, keys: [key] });
});
afterEach(() => {
act(() => {
mockConfig.reset();
});
});
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy(); expect(baseElement).toBeTruthy();
}); });
it('should explain that voting is closed if the proposal is not open', () => { it('should explain that voting is closed if the proposal is not open', () => {
renderComponent({ proposalState: ProposalState.STATE_PASSED }); render(
<AppStateProvider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_PASSED}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(screen.getByText('Voting has ended.')).toBeTruthy(); expect(screen.getByText('Voting has ended.')).toBeTruthy();
}); });
it('should provide a connect wallet prompt if no pubkey', () => { it('should provide a connect wallet prompt if no pubkey', () => {
mockConfig.reset(); const mockWalletNoPubKeyContext = {
renderComponent(); pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
} as unknown as VegaWalletContextShape;
render(
<AppStateProvider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletNoPubKeyContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(screen.getByTestId('connect-wallet')).toBeTruthy(); expect(screen.getByTestId('connect-wallet')).toBeTruthy();
}); });
it('should tell the user they need tokens if their current stake is 0', () => { it('should tell the user they need tokens if their current stake is 0', () => {
renderComponent({ currentStakeAvailable: new BigNumber(0) }); render(
<AppStateProvider>
<MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance={null}
spamProtectionMinTokens={null}
currentStakeAvailable={new BigNumber(0)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect( expect(
screen.getByText( screen.getByText(
'You need some VEGA tokens to participate in governance.' 'You need some VEGA tokens to participate in governance.'
@ -80,10 +123,26 @@ describe('Vote buttons', () => {
}); });
it('should tell the user of the minimum requirements if they have some, but not enough tokens', () => { it('should tell the user of the minimum requirements if they have some, but not enough tokens', () => {
renderComponent({ render(
minVoterBalance: '2000000000000000000', <AppStateProvider>
spamProtectionMinTokens: '1000000000000000000', <MockedProvider>
}); <VegaWalletContext.Provider value={mockWalletContext}>
<VoteButtons
voteState={VoteState.NotCast}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect( expect(
screen.getByText( screen.getByText(
'You must have at least 2 VEGA associated to vote on this proposal' 'You must have at least 2 VEGA associated to vote on this proposal'
@ -92,23 +151,51 @@ describe('Vote buttons', () => {
}); });
it('should show you voted if vote state is correct, and if the proposal is still open, it will display a change vote button', () => { it('should show you voted if vote state is correct, and if the proposal is still open, it will display a change vote button', () => {
renderComponent({ render(
voteState: VoteState.Yes, <AppStateProvider>
minVoterBalance: '2000000000000000000', <MockedProvider>
spamProtectionMinTokens: '1000000000000000000', <VegaWalletContext.Provider value={mockWalletContext}>
currentStakeAvailable: new BigNumber(10), <VoteButtons
}); voteState={VoteState.Yes}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
expect(screen.getByTestId('you-voted')).toBeInTheDocument(); expect(screen.getByTestId('you-voted')).toBeInTheDocument();
expect(screen.getByTestId('change-vote-button')).toBeInTheDocument(); expect(screen.getByTestId('change-vote-button')).toBeInTheDocument();
}); });
it('should allow you to change your vote', () => { it('should allow you to change your vote', () => {
renderComponent({ render(
voteState: VoteState.No, <AppStateProvider>
minVoterBalance: '2000000000000000000', <MockedProvider>
spamProtectionMinTokens: '1000000000000000000', <VegaWalletContext.Provider value={mockWalletContext}>
currentStakeAvailable: new BigNumber(10), <VoteButtons
}); voteState={VoteState.No}
voteDatetime={null}
proposalState={ProposalState.STATE_OPEN}
proposalId={null}
minVoterBalance="2000000000000000000"
spamProtectionMinTokens="1000000000000000000"
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</AppStateProvider>
);
fireEvent.click(screen.getByTestId('change-vote-button')); fireEvent.click(screen.getByTestId('change-vote-button'));
expect(screen.getByTestId('vote-buttons')).toBeInTheDocument(); expect(screen.getByTestId('vote-buttons')).toBeInTheDocument();
}); });

View File

@ -1,7 +1,7 @@
import { format } from 'date-fns'; import { format } from 'date-fns';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { import {
AsyncRenderer, AsyncRenderer,
Button, Button,
@ -17,7 +17,7 @@ import { VoteState } from './use-user-vote';
import { ProposalMinRequirements, ProposalUserAction } from '../shared'; import { ProposalMinRequirements, ProposalUserAction } from '../shared';
import { VoteTransactionDialog } from './vote-transaction-dialog'; import { VoteTransactionDialog } from './vote-transaction-dialog';
import { useVoteButtonsQuery } from './__generated__/Stake'; import { useVoteButtonsQuery } from './__generated__/Stake';
import type { VegaTxState } from '@vegaprotocol/proposals'; import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
import { filterAcceptableGraphqlErrors } from '../../../../lib/party'; import { filterAcceptableGraphqlErrors } from '../../../../lib/party';
import { import {
NetworkParams, NetworkParams,
@ -32,7 +32,8 @@ interface VoteButtonsContainerProps {
proposalId: string | null; proposalId: string | null;
proposalState: ProposalState; proposalState: ProposalState;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>; submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState; transaction: VegaTxState | null;
dialog: (props: DialogProps) => JSX.Element;
className?: string; className?: string;
} }
@ -135,16 +136,17 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
); );
}; };
export interface VoteButtonsProps { interface VoteButtonsProps {
voteState: VoteState | null; voteState: VoteState | null;
voteDatetime: Date | null; voteDatetime: Date | null;
proposalState: ProposalState;
proposalId: string | null; proposalId: string | null;
proposalState: ProposalState;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState | null;
dialog: (props: DialogProps) => JSX.Element;
currentStakeAvailable: BigNumber; currentStakeAvailable: BigNumber;
minVoterBalance: string | null; minVoterBalance: string | null;
spamProtectionMinTokens: string | null; spamProtectionMinTokens: string | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState;
} }
export const VoteButtons = ({ export const VoteButtons = ({
@ -157,10 +159,13 @@ export const VoteButtons = ({
spamProtectionMinTokens, spamProtectionMinTokens,
submit, submit,
transaction, transaction,
dialog: Dialog,
}: VoteButtonsProps) => { }: VoteButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const openVegaWalletDialog = useDialogStore((store) => store.open); const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const [changeVote, setChangeVote] = React.useState(false); const [changeVote, setChangeVote] = React.useState(false);
const proposalVotable = useMemo( const proposalVotable = useMemo(
() => () =>
@ -179,7 +184,11 @@ export const VoteButtons = ({
if (!pubKey) { if (!pubKey) {
return ( return (
<div data-testid="connect-wallet"> <div data-testid="connect-wallet">
<ButtonLink onClick={openVegaWalletDialog}> <ButtonLink
onClick={() => {
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')} {t('connectVegaWallet')}
</ButtonLink>{' '} </ButtonLink>{' '}
{t('toVote')} {t('toVote')}
@ -292,7 +301,11 @@ export const VoteButtons = ({
</p> </p>
) )
)} )}
<VoteTransactionDialog voteState={voteState} transaction={transaction} /> <VoteTransactionDialog
voteState={voteState}
transaction={transaction}
TransactionDialog={Dialog}
/>
</> </>
); );
}; };

View File

@ -1,13 +1,11 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { VoteState } from './use-user-vote'; import { VoteState } from './use-user-vote';
import { import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
VegaTransactionDialog,
type VegaTxState,
} from '@vegaprotocol/proposals';
interface VoteTransactionDialogProps { interface VoteTransactionDialogProps {
voteState: VoteState; voteState: VoteState;
transaction: VegaTxState; transaction: VegaTxState | null;
TransactionDialog: (props: DialogProps) => JSX.Element;
} }
const dialogTitle = (voteState: VoteState): string | undefined => { const dialogTitle = (voteState: VoteState): string | undefined => {
@ -24,23 +22,22 @@ const dialogTitle = (voteState: VoteState): string | undefined => {
export const VoteTransactionDialog = ({ export const VoteTransactionDialog = ({
voteState, voteState,
transaction, transaction,
TransactionDialog,
}: VoteTransactionDialogProps) => { }: VoteTransactionDialogProps) => {
// Render a custom message if the voting fails otherwise // Render a custom message if the voting fails otherwise
// pass undefined so that the default vega transaction dialog UI gets used // pass undefined so that the default vega transaction dialog UI gets used
const customMessage = const customMessage =
voteState === VoteState.Failed ? ( voteState === VoteState.Failed ? (
<p>{transaction.error?.message || t('voteError')}</p> <p>{transaction?.error?.message || t('voteError')}</p>
) : undefined; ) : undefined;
return ( return (
<div data-testid="vote-transaction-dialog"> <div data-testid="vote-transaction-dialog">
<VegaTransactionDialog <TransactionDialog
title={dialogTitle(voteState)} title={dialogTitle(voteState)}
transaction={transaction}
content={{ content={{
Complete: customMessage, Complete: customMessage,
}} }}
isOpen={transaction.dialogOpen}
/> />
</div> </div>
); );

View File

@ -273,74 +273,4 @@ describe('use-vote-information', () => {
expect(current?.willPassByTokenVote).toEqual(false); expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true); expect(current?.willPassByLPVote).toEqual(true);
}); });
it('mainnet recreation: only yes LP votes equal passing', () => {
const yesVotes = 0;
const noVotes = 70;
const yesEquityLikeShareWeight = '0.21';
const noEquityLikeShareWeight = '0';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(
yesVotes,
fixedTokenValue,
yesEquityLikeShareWeight
),
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
},
});
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true);
});
it('mainnet recreation: mixed yes and no LP votes equal failing', () => {
const yesVotes = 0;
const noVotes = 70;
const yesEquityLikeShareWeight = '0.21';
const noEquityLikeShareWeight = '0.22';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(
yesVotes,
fixedTokenValue,
yesEquityLikeShareWeight
),
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
},
});
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(false);
});
}); });

View File

@ -8,18 +8,13 @@ import {
type VoteFieldsFragment, type VoteFieldsFragment,
} from '../__generated__/Proposals'; } from '../__generated__/Proposals';
import { type ProposalChangeType } from '../types'; import { type ProposalChangeType } from '../types';
import sum from 'lodash/sum';
export const useVoteInformation = ({ export const useVoteInformation = ({
votes, votes,
terms, terms,
yesELS,
noELS,
}: { }: {
votes: VoteFieldsFragment; votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment; terms: ProposalTermsFieldsFragment;
yesELS?: number[];
noELS?: number[];
}) => { }) => {
const { const {
appState: { totalSupply, decimals }, appState: { totalSupply, decimals },
@ -36,9 +31,7 @@ export const useVoteInformation = ({
paramsForChange, paramsForChange,
votes, votes,
totalSupply, totalSupply,
decimals, decimals
yesELS,
noELS
); );
}; };
@ -79,11 +72,7 @@ const getVoteData = (
}, },
votes: ProposalFieldsFragment['votes'], votes: ProposalFieldsFragment['votes'],
totalSupply: BigNumber, totalSupply: BigNumber,
decimals: number, decimals: number
/** A list of ELS yes votes */
yesELS?: number[],
/** A list if ELS no votes */
noELS?: number[]
) => { ) => {
const requiredMajorityPercentage = params.requiredMajority const requiredMajorityPercentage = params.requiredMajority
? new BigNumber(params.requiredMajority).times(100) ? new BigNumber(params.requiredMajority).times(100)
@ -97,31 +86,17 @@ const getVoteData = (
addDecimal(votes.no.totalTokens ?? 0, decimals) addDecimal(votes.no.totalTokens ?? 0, decimals)
); );
let noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight const noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
? new BigNumber(0) ? new BigNumber(0)
: new BigNumber(votes.no.totalEquityLikeShareWeight).times(100); : new BigNumber(votes.no.totalEquityLikeShareWeight).times(100);
// there's no meaningful `totalEquityLikeShareWeight` in batch proposals,
// it has to be deduced from `elsPerMarket` of `no` votes of given proposal
// data. (by REST DATA)
if (noELS != null) {
const noTotalELS = sum(noELS);
noEquityLikeShareWeight = new BigNumber(noTotalELS).times(100);
}
const yesTokens = new BigNumber( const yesTokens = new BigNumber(
addDecimal(votes.yes.totalTokens ?? 0, decimals) addDecimal(votes.yes.totalTokens ?? 0, decimals)
); );
let yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight const yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
? new BigNumber(0) ? new BigNumber(0)
: new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100); : new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100);
// there's no meaningful `totalEquityLikeShareWeight` in batch proposals,
// it has to be deduced from `elsPerMarket` of `yes` votes of given proposal
// data. (by REST DATA)
if (noELS != null) {
const yesTotalELS = sum(yesELS);
yesEquityLikeShareWeight = new BigNumber(yesTotalELS).times(100);
}
const totalTokensVoted = yesTokens.plus(noTokens); const totalTokensVoted = yesTokens.plus(noTokens);
@ -148,19 +123,15 @@ const getVoteData = (
totalSupply.multipliedBy(params.requiredParticipation) totalSupply.multipliedBy(params.requiredParticipation)
); );
const lpVoteWeight = yesEquityLikeShareWeight
.dividedBy(totalEquityLikeShareWeight)
.multipliedBy(100);
const participationLPMet = params.requiredParticipationLP const participationLPMet = params.requiredParticipationLP
? lpVoteWeight.isGreaterThan(params.requiredParticipationLP) ? totalEquityLikeShareWeight.isGreaterThan(params.requiredParticipationLP)
: false; : false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo( const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
requiredMajorityPercentage requiredMajorityPercentage
); );
const majorityLPMet = lpVoteWeight.isGreaterThanOrEqualTo( const majorityLPMet = yesLPPercentage.isGreaterThanOrEqualTo(
requiredMajorityLPPercentage requiredMajorityLPPercentage
); );
@ -178,12 +149,14 @@ const getVoteData = (
const willPassByLPVote = const willPassByLPVote =
participationLPMet && participationLPMet &&
lpVoteWeight.isGreaterThanOrEqualTo(requiredMajorityLPPercentage); new BigNumber(yesLPPercentage).isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
let willPass = false; let willPass = false;
if (changeType === 'UpdateMarket' || changeType === 'UpdateMarketState') { if (changeType === 'UpdateMarket' || changeType === 'UpdateMarketState') {
willPass = willPassByTokenVote || willPassByLPVote; willPass = willPassByTokenVote && willPassByLPVote;
} else { } else {
willPass = willPassByTokenVote; willPass = willPassByTokenVote;
} }
@ -209,7 +182,6 @@ const getVoteData = (
totalLPTokensPercentage, totalLPTokensPercentage,
willPassByTokenVote, willPassByTokenVote,
willPassByLPVote, willPassByLPVote,
lpVoteWeight: lpVoteWeight.isNaN() ? new BigNumber(0) : lpVoteWeight,
yesVotes: new BigNumber(votes.yes.totalNumber ?? 0), yesVotes: new BigNumber(votes.yes.totalNumber ?? 0),
noVotes: new BigNumber(votes.no.totalNumber ?? 0), noVotes: new BigNumber(votes.no.totalNumber ?? 0),
totalVotes: new BigNumber(votes.yes.totalNumber ?? 0).plus( totalVotes: new BigNumber(votes.yes.totalNumber ?? 0).plus(

View File

@ -1,16 +1,17 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { Proposal } from '../components/proposal'; import { Proposal } from '../components/proposal';
import { ProposalNotFound } from '../components/proposal-not-found'; import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from '../__generated__/Proposals'; import { useProposalQuery } from '../__generated__/Proposals';
import { useFetchProposal } from '../components/proposal/proposal-utils';
export const ProposalContainer = () => { export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>(); const params = useParams<{ proposalId: string }>();
const { data: restData, loading: restLoading } = useFetchProposal({ const {
proposalId: params.proposalId, state: { data: restData, loading: restLoading, error: restError },
}); } = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
const { data, loading, error } = useProposalQuery({ const { data, loading, error } = useProposalQuery({
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
@ -25,7 +26,7 @@ export const ProposalContainer = () => {
return ( return (
<AsyncRenderer <AsyncRenderer
loading={Boolean(loading || restLoading)} loading={Boolean(loading || restLoading)}
error={error} error={error || restError}
data={{ data={{
...data, ...data,
...(restData ? { restData } : {}), ...(restData ? { restData } : {}),

View File

@ -1,12 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProposeFreeform } from './propose-freeform'; import { ProposeFreeform } from './propose-freeform';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
jest.mock('@vegaprotocol/environment', () => ({ jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'), ...jest.requireActual('@vegaprotocol/environment'),
@ -71,19 +72,18 @@ const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
}, },
}; };
const renderComponent = () => { const renderComponent = () =>
return render( render(
<Router> <Router>
<MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}> <MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}>
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposeFreeform /> <ProposeFreeform />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
</MockedProvider> </MockedProvider>
</Router> </Router>
); );
};
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form // Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory. // components are tested in their own directory.

View File

@ -51,8 +51,7 @@ export const ProposeFreeform = () => {
watch, watch,
trigger, trigger,
} = useForm<FreeformProposalFormFields>(); } = useForm<FreeformProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const assembleProposal = (fields: FreeformProposalFormFields) => { const assembleProposal = (fields: FreeformProposalFormFields) => {
const isVoteDeadlineAtMinimum = const isVoteDeadlineAtMinimum =
@ -170,8 +169,7 @@ export const ProposeFreeform = () => {
<ProposalFormDownloadJson downloadJson={viewJson} /> <ProposalFormDownloadJson downloadJson={viewJson} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -1,12 +1,13 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { ProposeNetworkParameter } from './propose-network-parameter'; import { ProposeNetworkParameter } from './propose-network-parameter';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
jest.mock('@vegaprotocol/environment', () => ({ jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'), ...jest.requireActual('@vegaprotocol/environment'),
@ -71,27 +72,26 @@ const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
}, },
}; };
const renderComponent = () => { const renderComponent = () =>
return render( render(
<Router> <Router>
<MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}> <MockedProvider mocks={[updateMarketNetworkParamsQueryMock]}>
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposeNetworkParameter /> <ProposeNetworkParameter />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
</MockedProvider> </MockedProvider>
</Router> </Router>
); );
};
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form // Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory. // components are tested in their own directory.
describe('Propose Network Parameter', () => { describe('Propose Network Parameter', () => {
it('should render successfully', () => { it('should render successfully', async () => {
const { baseElement } = renderComponent(); const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy(); await expect(baseElement).toBeTruthy();
}); });
it('should render the correct title', async () => { it('should render the correct title', async () => {

View File

@ -91,8 +91,7 @@ export const ProposeNetworkParameter = () => {
watch, watch,
trigger, trigger,
} = useForm<NetworkParameterProposalFormFields>(); } = useForm<NetworkParameterProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const selectedParamEntry = params const selectedParamEntry = params
? Object.entries(params).find(([key]) => key === selectedNetworkParam) ? Object.entries(params).find(([key]) => key === selectedNetworkParam)
@ -313,8 +312,7 @@ export const ProposeNetworkParameter = () => {
<ProposalFormDownloadJson downloadJson={viewJson} /> <ProposalFormDownloadJson downloadJson={viewJson} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -1,12 +1,13 @@
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { mockWalletContext } from '../../test-helpers/mocks';
import { ProposeNewAsset } from './propose-new-asset'; import { ProposeNewAsset } from './propose-new-asset';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
jest.mock('@vegaprotocol/environment', () => ({ jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'), ...jest.requireActual('@vegaprotocol/environment'),
@ -71,27 +72,26 @@ const newAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
}, },
}; };
const renderComponent = () => { const renderComponent = () =>
return render( render(
<Router> <Router>
<MockedProvider mocks={[newAssetNetworkParamsQueryMock]}> <MockedProvider mocks={[newAssetNetworkParamsQueryMock]}>
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposeNewAsset /> <ProposeNewAsset />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
</MockedProvider> </MockedProvider>
</Router> </Router>
); );
};
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form // Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory. // components are tested in their own directory.
describe('Propose New Asset', () => { describe('Propose New Asset', () => {
it('should render successfully', () => { it('should render successfully', async () => {
const { baseElement } = renderComponent(); const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy(); await expect(baseElement).toBeTruthy();
}); });
it('should render the title', async () => { it('should render the title', async () => {

View File

@ -64,8 +64,7 @@ export const ProposeNewAsset = () => {
watch, watch,
trigger, trigger,
} = useForm<NewAssetProposalFormFields>(); } = useForm<NewAssetProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const assembleProposal = (fields: NewAssetProposalFormFields) => { const assembleProposal = (fields: NewAssetProposalFormFields) => {
const isVoteDeadlineAtMinimum = doesValueEquateToParam( const isVoteDeadlineAtMinimum = doesValueEquateToParam(
@ -233,8 +232,7 @@ export const ProposeNewAsset = () => {
<ProposalFormDownloadJson downloadJson={viewJson} /> <ProposalFormDownloadJson downloadJson={viewJson} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -1,12 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProposeNewMarket } from './propose-new-market'; import { ProposeNewMarket } from './propose-new-market';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
jest.mock('@vegaprotocol/environment', () => ({ jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'), ...jest.requireActual('@vegaprotocol/environment'),
@ -71,27 +72,26 @@ const newMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
}, },
}; };
const renderComponent = () => { const renderComponent = () =>
return render( render(
<Router> <Router>
<MockedProvider mocks={[newMarketNetworkParamsQueryMock]}> <MockedProvider mocks={[newMarketNetworkParamsQueryMock]}>
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposeNewMarket /> <ProposeNewMarket />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
</MockedProvider> </MockedProvider>
</Router> </Router>
); );
};
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form // Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory. // components are tested in their own directory.
describe('Propose New Market', () => { describe('Propose New Market', () => {
it('should render successfully', () => { it('should render successfully', async () => {
const { baseElement } = renderComponent(); const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy(); await expect(baseElement).toBeTruthy();
}); });
it('should render the form components', async () => { it('should render the form components', async () => {

View File

@ -62,8 +62,7 @@ export const ProposeNewMarket = () => {
watch, watch,
trigger, trigger,
} = useForm<NewMarketProposalFormFields>(); } = useForm<NewMarketProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const assembleProposal = (fields: NewMarketProposalFormFields) => { const assembleProposal = (fields: NewMarketProposalFormFields) => {
const isVoteDeadlineAtMinimum = doesValueEquateToParam( const isVoteDeadlineAtMinimum = doesValueEquateToParam(
@ -215,8 +214,7 @@ export const ProposeNewMarket = () => {
<ProposalFormDownloadJson downloadJson={viewJson} /> <ProposalFormDownloadJson downloadJson={viewJson} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -3,6 +3,8 @@ import type { MockedResponse } from '@apollo/client/testing';
import { addHours, getTime } from 'date-fns'; import { addHours, getTime } from 'date-fns';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { ProposeRaw } from './propose-raw'; import { ProposeRaw } from './propose-raw';
import { ProposalEventDocument } from '@vegaprotocol/proposals'; import { ProposalEventDocument } from '@vegaprotocol/proposals';
@ -10,11 +12,6 @@ import type { ProposalEventSubscription } from '@vegaprotocol/proposals';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import {
MockedWalletProvider,
mockConfig,
} from '@vegaprotocol/wallet-react/testing';
import { userRejectedError } from '@vegaprotocol/wallet';
const paramsDelay = 20; const paramsDelay = 20;
@ -106,15 +103,23 @@ describe('Raw proposal form', () => {
}, },
delay: 300, delay: 300,
}; };
const setup = () => { const setup = (mockSendTx = jest.fn()) => {
return render( return render(
<AppStateProvider> <AppStateProvider>
<MockedProvider <MockedProvider
mocks={[rawProposalNetworkParamsQueryMock, mockProposalEvent]} mocks={[rawProposalNetworkParamsQueryMock, mockProposalEvent]}
> >
<MockedWalletProvider> <VegaWalletContext.Provider
value={
{
pubKey,
sendTx: mockSendTx,
links: { explorer: 'explorer' },
} as unknown as VegaWalletContextShape
}
>
<ProposeRaw /> <ProposeRaw />
</MockedWalletProvider> </VegaWalletContext.Provider>
</MockedProvider> </MockedProvider>
</AppStateProvider> </AppStateProvider>
); );
@ -122,22 +127,15 @@ describe('Raw proposal form', () => {
beforeAll(() => { beforeAll(() => {
jest.useFakeTimers(); jest.useFakeTimers();
mockConfig.store.setState({ status: 'connected', pubKey: '0x123' });
}); });
afterAll(() => { afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
mockConfig.reset();
});
afterEach(() => {
jest.clearAllMocks();
}); });
it('handles validation', async () => { it('handles validation', async () => {
const mockSendTx = jest.spyOn(mockConfig, 'sendTransaction'); const mockSendTx = jest.fn().mockReturnValue(Promise.resolve());
setup(mockSendTx);
setup();
expect(await screen.findByTestId('proposal-submit')).toBeTruthy(); expect(await screen.findByTestId('proposal-submit')).toBeTruthy();
await act(async () => { await act(async () => {
@ -164,25 +162,20 @@ describe('Raw proposal form', () => {
}); });
it('sends the transaction', async () => { it('sends the transaction', async () => {
const mockSendTx = jest const mockSendTx = jest.fn().mockReturnValue(
.spyOn(mockConfig, 'sendTransaction') new Promise((resolve) => {
.mockReturnValue( setTimeout(
new Promise((resolve) => { () =>
setTimeout( resolve({
() => transactionHash: 'tx-hash',
resolve({ signature:
transactionHash: 'tx-hash', 'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909',
signature: }),
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909', 100
sentAt: new Date().toISOString(), );
receivedAt: new Date().toISOString(), })
}), );
100 setup(mockSendTx);
);
})
);
setup();
await act(async () => { await act(async () => {
jest.advanceTimersByTime(paramsDelay); jest.advanceTimersByTime(paramsDelay);
@ -213,12 +206,8 @@ describe('Raw proposal form', () => {
fireEvent.click(screen.getByTestId('proposal-submit')); fireEvent.click(screen.getByTestId('proposal-submit'));
}); });
expect(mockSendTx).toHaveBeenCalledWith({ expect(mockSendTx).toHaveBeenCalledWith(pubKey, {
publicKey: pubKey, proposalSubmission: JSON.parse(inputJSON),
sendingMode: 'TYPE_SYNC',
transaction: {
proposalSubmission: JSON.parse(inputJSON),
},
}); });
expect(screen.getByTestId('dialog-title')).toHaveTextContent( expect(screen.getByTestId('dialog-title')).toHaveTextContent(
@ -243,12 +232,12 @@ describe('Raw proposal form', () => {
}); });
it('can be rejected by the user', async () => { it('can be rejected by the user', async () => {
jest.spyOn(mockConfig, 'sendTransaction').mockReturnValue( const mockSendTx = jest.fn().mockReturnValue(
new Promise((_, reject) => { new Promise((resolve) => {
setTimeout(() => reject(userRejectedError()), 100); setTimeout(() => resolve(null), 100);
}) })
); );
setup(); setup(mockSendTx);
await act(async () => { await act(async () => {
jest.advanceTimersByTime(paramsDelay); jest.advanceTimersByTime(paramsDelay);

View File

@ -52,8 +52,7 @@ export const ProposeRaw = () => {
handleSubmit, handleSubmit,
formState: { isSubmitting, errors }, formState: { isSubmitting, errors },
} = useForm<RawProposalFormFields>(); } = useForm<RawProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const hasError = Boolean(errors.rawProposalData?.message); const hasError = Boolean(errors.rawProposalData?.message);
@ -153,8 +152,7 @@ export const ProposeRaw = () => {
<ProposalFormSubmit isSubmitting={isSubmitting} /> <ProposalFormSubmit isSubmitting={isSubmitting} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -1,12 +1,13 @@
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { mockWalletContext } from '../../test-helpers/mocks';
import { ProposeUpdateAsset } from './propose-update-asset'; import { ProposeUpdateAsset } from './propose-update-asset';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
jest.mock('@vegaprotocol/environment', () => ({ jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'), ...jest.requireActual('@vegaprotocol/environment'),
@ -71,27 +72,26 @@ const updateAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
}, },
}; };
const renderComponent = () => { const renderComponent = () =>
return render( render(
<Router> <Router>
<MockedProvider mocks={[updateAssetNetworkParamsQueryMock]}> <MockedProvider mocks={[updateAssetNetworkParamsQueryMock]}>
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposeUpdateAsset /> <ProposeUpdateAsset />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
</MockedProvider> </MockedProvider>
</Router> </Router>
); );
};
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form // Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory. // components are tested in their own directory.
describe('Propose Update Asset', () => { describe('Propose Update Asset', () => {
it('should render successfully', () => { it('should render successfully', async () => {
const { baseElement } = renderComponent(); const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy(); await expect(baseElement).toBeTruthy();
}); });
it('should render the title', async () => { it('should render the title', async () => {

View File

@ -62,8 +62,7 @@ export const ProposeUpdateAsset = () => {
watch, watch,
trigger, trigger,
} = useForm<UpdateAssetProposalFormFields>(); } = useForm<UpdateAssetProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const assembleProposal = (fields: UpdateAssetProposalFormFields) => { const assembleProposal = (fields: UpdateAssetProposalFormFields) => {
const isVoteDeadlineAtMinimum = doesValueEquateToParam( const isVoteDeadlineAtMinimum = doesValueEquateToParam(
@ -219,8 +218,7 @@ export const ProposeUpdateAsset = () => {
<ProposalFormDownloadJson downloadJson={viewJson} /> <ProposalFormDownloadJson downloadJson={viewJson} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -2,14 +2,15 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter as Router } from 'react-router-dom'; import { MemoryRouter as Router } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { mockWalletContext } from '../../test-helpers/mocks';
import { ProposeUpdateMarket } from './propose-update-market'; import { ProposeUpdateMarket } from './propose-update-market';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { ProposalMarketsQueryQuery } from './__generated__/UpdateMarket'; import type { ProposalMarketsQueryQuery } from './__generated__/UpdateMarket';
import { ProposalMarketsQueryDocument } from './__generated__/UpdateMarket'; import { ProposalMarketsQueryDocument } from './__generated__/UpdateMarket';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing';
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = { const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
request: { request: {
@ -216,30 +217,29 @@ const marketQueryMock: MockedResponse<ProposalMarketsQueryQuery> = {
}, },
}; };
const renderComponent = () => { const renderComponent = () =>
return render( render(
<MockedProvider <MockedProvider
mocks={[updateMarketNetworkParamsQueryMock, marketQueryMock]} mocks={[updateMarketNetworkParamsQueryMock, marketQueryMock]}
addTypename={false} addTypename={false}
> >
<Router> <Router>
<MockedWalletProvider> <AppStateProvider>
<AppStateProvider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposeUpdateMarket /> <ProposeUpdateMarket />
</AppStateProvider> </VegaWalletContext.Provider>
</MockedWalletProvider> </AppStateProvider>
</Router> </Router>
</MockedProvider> </MockedProvider>
); );
};
// Note: form submission is tested in propose-raw.spec.tsx. Reusable form // Note: form submission is tested in propose-raw.spec.tsx. Reusable form
// components are tested in their own directory. // components are tested in their own directory.
describe('Propose Update Market', () => { describe('Propose Update Market', () => {
it('should render successfully', () => { it('should render successfully', async () => {
const { baseElement } = renderComponent(); const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy(); await expect(baseElement).toBeTruthy();
}); });
it('should render the title', async () => { it('should render the title', async () => {

View File

@ -109,8 +109,7 @@ export const ProposeUpdateMarket = () => {
watch, watch,
trigger, trigger,
} = useForm<UpdateMarketProposalFormFields>(); } = useForm<UpdateMarketProposalFormFields>();
const { finalizedProposal, transaction, submit, setTransaction } = const { finalizedProposal, submit, Dialog } = useProposalSubmit();
useProposalSubmit();
const assembleProposal = (fields: UpdateMarketProposalFormFields) => { const assembleProposal = (fields: UpdateMarketProposalFormFields) => {
const isVoteDeadlineAtMinimum = doesValueEquateToParam( const isVoteDeadlineAtMinimum = doesValueEquateToParam(
@ -324,8 +323,7 @@ export const ProposeUpdateMarket = () => {
<ProposalFormDownloadJson downloadJson={viewJson} /> <ProposalFormDownloadJson downloadJson={viewJson} />
<ProposalFormTransactionDialog <ProposalFormTransactionDialog
finalizedProposal={finalizedProposal} finalizedProposal={finalizedProposal}
transaction={transaction} TransactionDialog={Dialog}
onChange={(open) => setTransaction({ dialogOpen: open })}
/> />
</form> </form>
</div> </div>

View File

@ -1,17 +1,28 @@
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
import type { VoteValue } from '@vegaprotocol/types'; import type { VoteValue } from '@vegaprotocol/types';
import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote'; import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote';
import { UserVoteDocument } from '../components/vote-details/__generated__/Vote'; import { UserVoteDocument } from '../components/vote-details/__generated__/Vote';
import faker from 'faker'; import faker from 'faker';
import { type Key } from '@vegaprotocol/wallet';
export const mockPubkey: Key = { export const mockPubkey: PubKey = {
publicKey: '0x123', publicKey: '0x123',
name: 'test key 1', name: 'test key 1',
}; };
export const mockWalletContext = {
pubKey: mockPubkey.publicKey,
pubKeys: [mockPubkey],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
} as unknown as VegaWalletContextShape;
const mockEthereumConfig = { const mockEthereumConfig = {
network_id: '3', network_id: '3',
chain_id: '3', chain_id: '3',

View File

@ -1,11 +1,13 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDialogStore } from '@vegaprotocol/wallet-react'; import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../components/heading'; import { SubHeading } from '../../components/heading';
export const ConnectToSeeRewards = () => { export const ConnectToSeeRewards = () => {
const openVegaWalletDialog = useDialogStore((store) => store.open); const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { t } = useTranslation(); const { t } = useTranslation();
const classes = classNames( const classes = classNames(

View File

@ -1,10 +1,11 @@
import { useMemo, useState } from 'react'; import { useMemo, useEffect, useState, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards'; import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useRewardsQuery } from '../home/__generated__/Rewards'; import { useRewardsQuery } from '../home/__generated__/Rewards';
import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { ENV } from '../../../config';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table'; import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list'; import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { calculateEpochOffset } from '../../../lib/epoch-pagination'; import { calculateEpochOffset } from '../../../lib/epoch-pagination';
@ -26,16 +27,22 @@ export const EpochIndividualRewards = ({
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { param: marketCreationQuantumMultiple } = useNetworkParam( const { param: marketCreationQuantumMultiple } = useNetworkParam(
'rewards_marketCreationQuantumMultiple' 'rewards_marketCreationQuantumMultiple'
); );
const { data, loading, error } = useRewardsQuery({ const { data, loading, error, refetch } = useRewardsQuery({
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
variables: { variables: {
partyId: pubKey || '', partyId: pubKey || '',
...calculateEpochOffset({ epochId, page, size: EPOCHS_PAGE_SIZE }), fromEpoch: epochId - EPOCHS_PAGE_SIZE,
delegationsPagination: { first: 50 }, toEpoch: epochId,
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
}, },
skip: !pubKey, skip: !pubKey,
}); });
@ -63,6 +70,36 @@ export const EpochIndividualRewards = ({
}); });
}, [data?.party, epochId, epochRewardSummaries, page, rewards]); }, [data?.party, epochId, epochRewardSummaries, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
partyId: pubKey || '',
...calculateEpochOffset({ epochId, page, size: EPOCHS_PAGE_SIZE }),
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
});
setPage(targetPage);
},
[epochId, page, refetch, delegationsPagination, pubKey]
);
const prevEpochIdRef = useRef<number | null>(null);
useEffect(() => {
if (prevEpochIdRef.current === null) {
prevEpochIdRef.current = epochId;
} else if (epochId !== prevEpochIdRef.current) {
// When the epoch changes, we want to refetch the data to update the current page
refetchData();
}
prevEpochIdRef.current = epochId;
}, [epochId, refetchData]);
// Workarounds for the error handling of AsyncRenderer // Workarounds for the error handling of AsyncRenderer
const filteredErrors = filterAcceptableGraphqlErrors(error); const filteredErrors = filterAcceptableGraphqlErrors(error);
const filteredData = data || []; const filteredData = data || [];
@ -94,10 +131,10 @@ export const EpochIndividualRewards = ({
isLoading={loading} isLoading={loading}
hasPrevPage={page > 1} hasPrevPage={page > 1}
hasNextPage={page < totalPages} hasNextPage={page < totalPages}
onBack={() => setPage((x) => x - 1)} onBack={() => refetchData(page - 1)}
onNext={() => setPage((x) => x + 1)} onNext={() => refetchData(page + 1)}
onFirst={() => setPage(1)} onFirst={() => refetchData(1)}
onLast={() => setPage(totalPages)} onLast={() => refetchData(totalPages)}
> >
{t('Page')} {page} {t('Page')} {page}
</Pagination> </Pagination>

View File

@ -23,6 +23,36 @@ describe('generateEpochIndividualRewardsList', () => {
epoch: { id: '2' }, epoch: { id: '2' },
}; };
const reward3: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP', name: 'GBP', decimals: 7 },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward4: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '1' },
};
const reward5: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '150',
percentageOfTotal: '0.15',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '3' },
};
const rewardWrongType: RewardFieldsFragment = { const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE, rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50', amount: '50',
@ -90,10 +120,26 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0', amount: '0',
percentageOfTotal: '0', percentageOfTotal: '0',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: { [AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100', amount: '100',
percentageOfTotal: '0.1', percentageOfTotal: '0.1',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
}, },
}, },
], ],
@ -101,7 +147,7 @@ describe('generateEpochIndividualRewardsList', () => {
}); });
it('should return an array sorted by epoch descending', () => { it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2]; const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList({ const result1 = generateEpochIndividualRewardsList({
rewards, rewards,
epochId: 2, epochId: 2,
@ -111,7 +157,7 @@ describe('generateEpochIndividualRewardsList', () => {
expect(result1[0].epoch).toEqual(2); expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1); expect(result1[1].epoch).toEqual(1);
const reorderedRewards = [reward2, reward1]; const reorderedRewards = [reward4, reward3, reward2, reward1];
const result2 = generateEpochIndividualRewardsList({ const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards, rewards: reorderedRewards,
epochId: 2, epochId: 2,
@ -124,7 +170,7 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => { it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here // Just sanity checking the whole structure here
const rewards = [reward1, reward2]; const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList({ const result = generateEpochIndividualRewardsList({
rewards, rewards,
epochId: 2, epochId: 2,
@ -135,6 +181,37 @@ describe('generateEpochIndividualRewardsList', () => {
{ {
epoch: 2, epoch: 2,
rewards: [ rewards: [
{
asset: 'GBP',
totalAmount: '200',
decimals: 7,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{ {
asset: 'EUR', asset: 'EUR',
totalAmount: '50', totalAmount: '50',
@ -144,10 +221,26 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0', amount: '0',
percentageOfTotal: '0', percentageOfTotal: '0',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: { [AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50', amount: '50',
percentageOfTotal: '0.05', percentageOfTotal: '0.05',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
}, },
}, },
], ],
@ -157,17 +250,33 @@ describe('generateEpochIndividualRewardsList', () => {
rewards: [ rewards: [
{ {
asset: 'USD', asset: 'USD',
totalAmount: '100', totalAmount: '200',
decimals: 6, decimals: 6,
rewardTypes: { rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: { [AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0', amount: '0',
percentageOfTotal: '0', percentageOfTotal: '0',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: { [AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100', amount: '100',
percentageOfTotal: '0.1', percentageOfTotal: '0.1',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
}, },
}, },
], ],
@ -176,7 +285,7 @@ describe('generateEpochIndividualRewardsList', () => {
}); });
it('returns data correctly for the requested epoch range', () => { it('returns data correctly for the requested epoch range', () => {
const rewards = [reward1, reward2]; const rewards = [reward1, reward2, reward3, reward4, reward5];
const resultPageOne = generateEpochIndividualRewardsList({ const resultPageOne = generateEpochIndividualRewardsList({
rewards, rewards,
epochId: 3, epochId: 3,
@ -188,11 +297,74 @@ describe('generateEpochIndividualRewardsList', () => {
expect(resultPageOne).toEqual([ expect(resultPageOne).toEqual([
{ {
epoch: 3, epoch: 3,
rewards: [], rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '150',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '150',
percentageOfTotal: '0.15',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
}, },
{ {
epoch: 2, epoch: 2,
rewards: [ rewards: [
{
asset: 'GBP',
totalAmount: '200',
decimals: 7,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{ {
asset: 'EUR', asset: 'EUR',
totalAmount: '50', totalAmount: '50',
@ -202,10 +374,26 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0', amount: '0',
percentageOfTotal: '0', percentageOfTotal: '0',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: { [AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50', amount: '50',
percentageOfTotal: '0.05', percentageOfTotal: '0.05',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
}, },
}, },
], ],
@ -226,17 +414,33 @@ describe('generateEpochIndividualRewardsList', () => {
rewards: [ rewards: [
{ {
asset: 'USD', asset: 'USD',
totalAmount: '100', totalAmount: '200',
decimals: 6, decimals: 6,
rewardTypes: { rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: { [AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0', amount: '0',
percentageOfTotal: '0', percentageOfTotal: '0',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: { [AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100', amount: '100',
percentageOfTotal: '0.1', percentageOfTotal: '0.1',
}, },
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
}, },
}, },
], ],

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards'; import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
@ -23,17 +23,37 @@ export const EpochTotalRewards = ({ currentEpoch }: EpochTotalRewardsProps) => {
'rewards_marketCreationQuantumMultiple' 'rewards_marketCreationQuantumMultiple'
); );
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { data, loading, error } = useEpochAssetsRewardsQuery({ const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
variables: { variables: {
epochRewardSummariesFilter: calculateEpochOffset({ epochRewardSummariesFilter: {
epochId, fromEpoch: epochId - EPOCHS_PAGE_SIZE,
page: page, },
size: EPOCHS_PAGE_SIZE,
}),
}, },
}); });
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
epochRewardSummariesFilter: calculateEpochOffset({
epochId,
page: targetPage,
size: EPOCHS_PAGE_SIZE,
}),
});
setPage(targetPage);
},
[epochId, page, refetch]
);
useEffect(() => {
// when the epoch changes, we want to refetch the data to update the current page
if (data) {
refetchData();
}
}, [epochId, data, refetchData]);
const epochTotalRewardSummaries = const epochTotalRewardSummaries =
generateEpochTotalRewardsList({ generateEpochTotalRewardsList({
data, data,
@ -65,10 +85,10 @@ export const EpochTotalRewards = ({ currentEpoch }: EpochTotalRewardsProps) => {
isLoading={loading} isLoading={loading}
hasPrevPage={page > 1} hasPrevPage={page > 1}
hasNextPage={page < totalPages} hasNextPage={page < totalPages}
onBack={() => setPage((x) => x - 1)} onBack={() => refetchData(page - 1)}
onNext={() => setPage((x) => x + 1)} onNext={() => refetchData(page + 1)}
onFirst={() => setPage(1)} onFirst={() => refetchData(1)}
onLast={() => setPage(totalPages)} onLast={() => refetchData(totalPages)}
> >
{t('Page')} {page} {t('Page')} {page}
</Pagination> </Pagination>

View File

@ -149,6 +149,38 @@ describe('generateEpochAssetRewardsList', () => {
amount: '0', amount: '0',
}, },
], ],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]), ]),
totalAmount: '123', totalAmount: '123',
}, },
@ -259,8 +291,40 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100', amount: '100',
}, },
], ],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]), ]),
totalAmount: '100', totalAmount: '223',
}, },
], ],
]), ]),
@ -270,7 +334,66 @@ describe('generateEpochAssetRewardsList', () => {
'2', '2',
{ {
epoch: 2, epoch: 2,
assetRewards: new Map(), assetRewards: new Map([
[
'1',
{
assetId: '1',
decimals: 18,
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '5',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '5',
},
],
]),
}, },
], ],
]) ])
@ -366,7 +489,66 @@ describe('generateEpochAssetRewardsList', () => {
'2', '2',
{ {
epoch: 2, epoch: 2,
assetRewards: new Map(), assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
decimals: 18,
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '33',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '33',
},
],
]),
}, },
], ],
[ [
@ -396,6 +578,38 @@ describe('generateEpochAssetRewardsList', () => {
amount: '15', amount: '15',
}, },
], ],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]), ]),
totalAmount: '15', totalAmount: '15',
}, },
@ -442,8 +656,40 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100', amount: '100',
}, },
], ],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]), ]),
totalAmount: '100', totalAmount: '223',
}, },
], ],
]), ]),

Some files were not shown because too many files have changed in this diff Show More