Compare commits

..
1307 changed files with 28900 additions and 76615 deletions
+1 -3
View File
@@ -73,11 +73,9 @@
"error", "error",
{ {
"prefer": "type-imports", "prefer": "type-imports",
"disallowTypeAnnotations": true, "disallowTypeAnnotations": true
"fixStyle": "inline-type-imports"
} }
], ],
"@typescript-eslint/no-useless-constructor": 0,
"curly": ["error", "multi-line"] "curly": ["error", "multi-line"]
} }
}, },
-1
View File
@@ -1,4 +1,3 @@
* text eol=lf * text eol=lf
*.png binary *.png binary
*.ico binary *.ico binary
*.woff2 binary
+6 -6
View File
@@ -34,7 +34,7 @@ jobs:
${{ runner.os }}-cache-node-modules- ${{ runner.os }}-cache-node-modules-
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
@@ -57,7 +57,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions # https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
@@ -96,7 +96,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: yarn cache: yarn
@@ -128,7 +128,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: yarn cache: yarn
@@ -182,7 +182,7 @@ jobs:
run: | run: |
if [[ "${{ github.base_ref }}" == "develop" ]]; then if [[ "${{ github.base_ref }}" == "develop" ]]; then
echo "e2e-needed=true" >> $GITHUB_OUTPUT echo "e2e-needed=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.base_ref }}" == "main" ]]; then elif [[ "${{ github.base_ref }}" == "main" ]]; then
echo "e2e-needed=true" >> $GITHUB_OUTPUT echo "e2e-needed=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref_name }}" == *"release/"* ]]; then elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref_name }}" == *"release/"* ]]; then
echo "e2e-needed=true" >> $GITHUB_OUTPUT echo "e2e-needed=true" >> $GITHUB_OUTPUT
@@ -205,7 +205,7 @@ jobs:
console-e2e: console-e2e:
needs: [build-sources, check-e2e-needed] needs: [build-sources, check-e2e-needed]
name: '(CI) trading e2e python' name: '(CI) console python'
uses: ./.github/workflows/console-test-run.yml uses: ./.github/workflows/console-test-run.yml
secrets: inherit secrets: inherit
if: needs.check-e2e-needed.outputs.run-tests == 'true' && contains(needs.build-sources.outputs.projects, 'trading') if: needs.check-e2e-needed.outputs.run-tests == 'true' && contains(needs.build-sources.outputs.projects, 'trading')
+24 -64
View File
@@ -10,7 +10,7 @@ on:
inputs: inputs:
console-test-branch: console-test-branch:
type: choice type: choice
description: 'main: v0.73.5, develop: v0.73.5' description: 'main: v0.72.14, develop: v0.73.0-preview7'
options: options:
- main - main
- develop - develop
@@ -19,7 +19,7 @@ jobs:
create-docker-image: create-docker-image:
name: Create docker image for console-test name: Create docker image for console-test
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
timeout-minutes: 45 timeout-minutes: 20
steps: steps:
#---------------------------------------------- #----------------------------------------------
# check-out frontend-monorepo # check-out frontend-monorepo
@@ -32,9 +32,9 @@ jobs:
# cache node modules # cache node modules
#---------------------------------------------- #----------------------------------------------
- name: setup node - name: setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version: '16'
cache: yarn cache: yarn
- name: Cache node modules - name: Cache node modules
@@ -138,7 +138,7 @@ jobs:
name: run-tests name: run-tests
runs-on: 8-cores runs-on: 8-cores
needs: [create-docker-image, console-test-branch] needs: [create-docker-image, console-test-branch]
timeout-minutes: 45 timeout-minutes: 20
steps: steps:
#---------------------------------------------- #----------------------------------------------
# load docker image # load docker image
@@ -153,19 +153,25 @@ jobs:
run: | run: |
docker load --input /tmp/console-image.tar docker load --input /tmp/console-image.tar
docker image ls -a docker image ls -a
#---------------------------------------------- #----------------------------------------------
# check-out frontend-monorepo # check-out tests repo
#---------------------------------------------- #----------------------------------------------
- name: Checkout frontend-monorepo - name: Checkout console test repo
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: ${{ inputs.github-sha || github.sha }} repository: vegaprotocol/console-test
#---------------------------------------------- ref: ${{ needs.console-test-branch.outputs.console-branch }}
# get vega version
#---------------------------------------------- - name: Load console test envs
- name: Set VEGA_VERSION from .env id: console-test-env
id: set_vega_version uses: falti/dotenv-action@v1.0.4
run: echo "VEGA_VERSION=$(grep VEGA_VERSION apps/trading/e2e/.env | cut -d '=' -f2)" >> $GITHUB_ENV with:
path: '.env.${{ needs.console-test-branch.outputs.console-branch }}'
export-variables: true
keys-case: upper
log-variables: true
#---------------------------------------------- #----------------------------------------------
# ----- Setup python ----- # ----- Setup python -----
#---------------------------------------------- #----------------------------------------------
@@ -188,25 +194,22 @@ jobs:
#---------------------------------------------- #----------------------------------------------
- name: Install dependencies - name: Install dependencies
run: poetry install --no-interaction --no-root run: poetry install --no-interaction --no-root
working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# install vega binaries # install vega binaries
#---------------------------------------------- #----------------------------------------------
- name: Install vega binaries - name: Install vega binaries
run: poetry run python -m vega_sim.tools.load_binaries --force --version ${{ env.VEGA_VERSION }} run: poetry run python -m vega_sim.tools.load_binaries --force --version ${{ env.VEGA_VERSION }}
working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# install playwrightworking-directory: apps/trading/e2e # install playwright
#---------------------------------------------- #----------------------------------------------
- name: install playwright - name: install playwright
run: poetry run playwright install --with-deps chromium run: poetry run playwright install --with-deps chromium
working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# run tests # run tests
#---------------------------------------------- #----------------------------------------------
- name: Run tests - name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45 run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# upload traces # upload traces
#---------------------------------------------- #----------------------------------------------
@@ -215,7 +218,7 @@ jobs:
if: always() if: always()
with: with:
name: playwright-trace name: playwright-trace
path: apps/trading/e2e/traces/ path: ./traces/
retention-days: 15 retention-days: 15
#---------------------------------------------- #----------------------------------------------
# ----- upload logs ----- # ----- upload logs -----
@@ -227,46 +230,3 @@ jobs:
name: worker-logs name: worker-logs
path: ./logs/ path: ./logs/
retention-days: 15 retention-days: 15
#----------------------------------------------
# ----- upload market-sim logs -----
#----------------------------------------------
- name: Prepare and Zip market-sim-logs
if: always()
run: |
parent_dir="/tmp/market-sim-logs"
echo "Creating parent directory at $parent_dir"
mkdir -p "$parent_dir"
echo "Waiting for vega-sim-* folders to be created..."
sleep 10 # Waits 10 seconds to ensure all folders are created
echo "Before searching for vega-sim-* folders in /tmp..."
folders=$(find /tmp -mindepth 1 -type d -name 'vega-sim-*' -print) || echo "Find command failed with exit code $?"
echo "After searching for vega-sim-* folders in /tmp..."
if [ -z "$folders" ]; then
echo "No vega-sim-* folders found."
exit 0
fi
echo "Moving vega-sim-* folders to $parent_dir"
echo "$folders" | xargs -I {} mv {} "$parent_dir/"
echo "Checking if $parent_dir is not empty..."
if [ "$(ls -A $parent_dir)" ]; then
echo "Zipping the parent directory..."
zip -r market-sim-logs.zip "$parent_dir" && echo "Zip file created successfully."
else
echo "$parent_dir is empty. No zip file created."
exit 0
fi
shell: /usr/bin/bash -e {0}
- name: Upload market-sim-logs
uses: actions/upload-artifact@v3
if: always()
with:
name: market-sim-logs
path: market-sim-logs.zip
retention-days: 15
+3 -3
View File
@@ -18,11 +18,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Use Node.js 20 - name: Use Node.js 16
id: Node id: Node
uses: actions/setup-node@v4 uses: actions/setup-node@v2
with: with:
node-version-file: '.nvmrc' node-version: 16.15.1
- name: Run Cypress tests - name: Run Cypress tests
uses: cypress-io/github-action@v4 uses: cypress-io/github-action@v4
-8
View File
@@ -57,14 +57,6 @@ jobs:
path: './frontend-monorepo' path: './frontend-monorepo'
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: './frontend-monorepo/.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
cache-dependency-path: './frontend-monorepo/yarn.lock'
# Restore node_modules from cache if possible # Restore node_modules from cache if possible
- name: Restore node_modules from cache - name: Restore node_modules from cache
id: cache-node-modules id: cache-node-modules
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions # https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
+3 -3
View File
@@ -18,13 +18,13 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version: 16
- name: Install dependencies - name: Install dependencies
run: | run: |
rm package.json rm package.json
npm install --no-save @commitlint/cli @commitlint/config-conventional @commitlint/config-nx-scopes nx npm install --no-save @commitlint/cli @commitlint/config-conventional @commitlint/config-nx-scopes nx
- name: Check PR title - name: Check PR title
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions # https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions # https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
+1 -9
View File
@@ -48,15 +48,7 @@ cypress.env.json
# Next.js # Next.js
.next .next
# cypress #cypress
/apps/**/cypress/reports/ /apps/**/cypress/reports/
/apps/**/cypress/downloads/ /apps/**/cypress/downloads/
/apps/**/fixtures/wallet/node** /apps/**/fixtures/wallet/node**
# apps/trading/e2e
__pycache__/
apps/trading/e2e/logs/
apps/trading/e2e/.pytest_cache/
apps/trading/e2e/traces/
.nx/
-3
View File
@@ -1,8 +1,5 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
# Auto-format all files
yarn nx format:write
# Lint all staged files # Lint all staged files
yarn lint-staged yarn lint-staged
+3 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
# Lint all staged files - this brings more value as pre-commit # Lint all staged files
# yarn nx format:check yarn nx format:check
# Test all projects with changes # Test all projects with changes
# yarn nx affected -t test --exclude trading yarn nx affected -t test --exclude trading
+1 -1
View File
@@ -1 +1 @@
20.9.0 16.20.2
-13
View File
@@ -1,7 +1,6 @@
# Add files here to ignore them from prettier formatting # Add files here to ignore them from prettier formatting
/dist /dist
/dist-result
/coverage /coverage
__generated__ __generated__
__generated___ __generated___
@@ -9,15 +8,3 @@ __generated___
apps/static/src/assets/devnet-tranches.json apps/static/src/assets/devnet-tranches.json
apps/static/src/assets/mainnet-tranches.json apps/static/src/assets/mainnet-tranches.json
apps/static/src/assets/testnet-tranches.json apps/static/src/assets/testnet-tranches.json
/apps/**/cypress/reports/
/apps/**/cypress/downloads/
/.nx/cache
# apps/trading/e2e
__pycache__/
apps/trading/e2e/logs/
apps/trading/e2e/.pytest_cache/
apps/trading/e2e/traces/
.pytest_cache/
+1 -2
View File
@@ -1,4 +1,3 @@
* @vegaprotocol/frontend * @vegaprotocol/frontend
apps/ @vegaprotocol/frontend-qa * @vegaprotocol/frontend-qa
libs/ @vegaprotocol/frontend-qa
*.graphql @vegaprotocol/core *.graphql @vegaprotocol/core
+6 -6
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev). This repository is managed using [Nx](https://nx.dev).
## 🔎 Applications in this repo # 🔎 Applications in this repo
### [Block explorer](./apps/explorer) ### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract. The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
## 🧱 Libraries in this repo # 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit) ### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities. Generic react helpers that can be used across multiple applications, along with other utilities.
## 💻 Develop # 💻 Develop
### Set up ### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more. Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
## 🐋 Hosting a console # 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions). To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -136,7 +136,7 @@ The [`docker`](./docker) subfolder has some docker configurations for easily set
Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build). The multistage builds ensures consistent CPU architecture and build toolchains are used so that the result will be identical. Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build). The multistage builds ensures consistent CPU architecture and build toolchains are used so that the result will be identical.
```bash ```bash
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=20.9.1 --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile . docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=16.5.1 --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
``` ```
### Computing ipfs-hash of the build ### Computing ipfs-hash of the build
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
``` ```
## 📑 License # 📑 License
[MIT](./LICENSE) [MIT](./LICENSE)
+1 -3
View File
@@ -4,9 +4,7 @@
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": { "rules": {}
"cypress/unsafe-to-chain-command": 0
}
} }
] ]
} }
+1 -1
View File
@@ -17,7 +17,7 @@
} }
}, },
"lint": { "lint": {
"executor": "@nx/eslint:lint", "executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"], "outputs": ["{options.outputFile}"],
"options": { "options": {
"lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"] "lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"]
@@ -39,6 +39,7 @@ context('Market page', { tags: '@regression' }, function () {
cy.contains('Test market 1').click(); cy.contains('Test market 1').click();
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1'); cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
cy.validate_element_from_table('Name', 'Test market 1'); cy.validate_element_from_table('Name', 'Test market 1');
cy.validate_element_from_table('Market ID', this.createdMarketId);
cy.validate_element_from_table('Trading Mode', 'Opening auction'); cy.validate_element_from_table('Trading Mode', 'Opening auction');
cy.validate_element_from_table('Market Decimal Places', '5'); cy.validate_element_from_table('Market Decimal Places', '5');
cy.validate_element_from_table('Position Decimal Places', '5'); cy.validate_element_from_table('Position Decimal Places', '5');
@@ -0,0 +1,68 @@
context('Oracle page', { tags: '@smoke' }, () => {
describe('Verify elements on page', () => {
before('create market and navigate to oracle page', () => {
cy.createMarket();
cy.visit('/oracles');
});
it('should see oracle data', () => {
cy.getByTestId('oracle-details').should('have.length.at.least', 2);
cy.getByTestId('oracle-details')
.should('exist')
.eq(0)
.within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('th').should('have.text', 'ID');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/oracles/');
});
cy.get('tr')
.eq(1)
.within(() => {
cy.get('th').should('have.text', 'Type');
cy.get('td').should('have.text', 'External data');
});
cy.get('tr')
.eq(2)
.within(() => {
cy.get('th').should('have.text', 'Signer');
cy.getByTestId('keytype').should('have.text', 'Vega');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/parties/');
});
cy.get('tr')
.eq(3)
.within(() => {
cy.get('th').should('have.text', 'Settlement for');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/markets/');
});
cy.get('tr')
.eq(4)
.within(() => {
cy.get('th').should('have.text', 'Matched data');
cy.get('td').should('have.text', '❌');
});
cy.get('details')
.eq(0)
.within(() => {
cy.contains('Filter').click();
cy.get('.language-json').should('exist');
});
cy.get('details')
.eq(1)
.within(() => {
cy.contains('JSON').click();
cy.get('.language-json').should('exist');
});
});
});
});
});
@@ -44,7 +44,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.getByTestId('icon-cross').click(); cy.getByTestId('icon-cross').click();
}); });
it('Proposal page displayed on mobile', function () { it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market'; const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle(); cy.common_switch_to_mobile_and_click_toggle();
@@ -55,7 +55,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
}); });
}); });
it.skip('Able to view new asset proposal', function () { it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal'; const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody(); const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody); cy.VegaWalletSubmitProposal(newAssetProposalBody);
+2
View File
@@ -1,4 +1,5 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.rocks NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.rocks
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_ENV=STAGNET1 NX_VEGA_ENV=STAGNET1
@@ -8,6 +9,7 @@ NX_VEGA_WALLET_URL=http://localhost:1789
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
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.rocks/rest NX_BLOCK_EXPLORER=https://be.stagnet1.vega.rocks/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.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_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks
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
+1
View File
@@ -1,5 +1,6 @@
# App configuration variables # App configuration variables
NX_VEGA_ENV=CUSTOM NX_VEGA_ENV=CUSTOM
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_VEGA_EXPLORER_URL=/ NX_VEGA_EXPLORER_URL=/
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
+1
View File
@@ -2,6 +2,7 @@
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_VEGA_ENV=DEVNET NX_VEGA_ENV=DEVNET
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.governance.vega.xyz NX_VEGA_GOVERNANCE_URL=https://dev.governance.vega.xyz
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
+1
View File
@@ -4,6 +4,7 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste
NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_ENV=MAINNET NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest NX_BLOCK_EXPLORER=https://be.vega.community/rest
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz NX_VEGA_GOVERNANCE_URL=https://governance.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_EXPLORER_URL=https://explorer.vega.xyz/ NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
+1
View File
@@ -4,6 +4,7 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-inter
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
NX_VEGA_ENV=MAINNET_MIRROR NX_VEGA_ENV=MAINNET_MIRROR
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks
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_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/ NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/
+1
View File
@@ -4,6 +4,7 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-inter
NX_VEGA_ENV=TESTNET NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf
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_EXPLORER_URL=https://explorer.fairground.wtf NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
+1
View File
@@ -4,6 +4,7 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/ NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
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_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
+1
View File
@@ -3,6 +3,7 @@ NX_TENDERMINT_URL=http://localhost:26607/
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM NX_VEGA_ENV=CUSTOM
NX_BLOCK_EXPLORER= NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_VEGA_EXPLORER_URL=/ NX_VEGA_EXPLORER_URL=/
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
+4
View File
@@ -0,0 +1,4 @@
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
+19 -3
View File
@@ -53,7 +53,7 @@
} }
}, },
"lint": { "lint": {
"executor": "@nx/eslint:lint", "executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"], "outputs": ["{options.outputFile}"],
"options": { "options": {
"lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"] "lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"]
@@ -63,14 +63,30 @@
"executor": "@nx/jest:jest", "executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/apps/explorer"], "outputs": ["{workspaceRoot}/coverage/apps/explorer"],
"options": { "options": {
"jestConfig": "apps/explorer/jest.config.ts" "jestConfig": "apps/explorer/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
} }
}, },
"generate-types": { "generate-types": {
"executor": "nx:run-commands", "executor": "nx:run-commands",
"options": { "options": {
"commands": [ "commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.73.1/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types" "npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.72.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
"build-netlify": {
"executor": "nx:run-commands",
"options": {
"commands": [
"cp apps/explorer/netlify.toml netlify.toml",
"nx build explorer"
] ]
} }
}, },
+16 -25
View File
@@ -1,7 +1,6 @@
import '../i18n';
import { import {
AppFailure,
NetworkLoader, NetworkLoader,
NodeFailure,
NodeGuard, NodeGuard,
NodeSwitcherDialog, NodeSwitcherDialog,
useEnvironment, useEnvironment,
@@ -12,8 +11,7 @@ import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-web
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client'; import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { useRouterConfig } from './routes/router-config'; import { router } from './routes/router-config';
import { createBrowserRouter } from 'react-router-dom';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Suspense } from 'react'; import { Suspense } from 'react';
@@ -30,27 +28,20 @@ function App() {
); );
return ( return (
<TendermintWebsocketProvider> <TendermintWebsocketProvider>
<Suspense fallback={splashLoading}> <NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}> <NodeGuard
<NodeGuard skeleton={<div>{t('Loading')}</div>}
skeleton={<div>{t('Loading')}</div>} failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
failure={ >
<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} /> <Suspense fallback={splashLoading}>
} <RouterProvider router={router} fallbackElement={splashLoading} />
> </Suspense>
<Suspense fallback={splashLoading}> </NodeGuard>
<RouterProvider <NodeSwitcherDialog
router={createBrowserRouter(useRouterConfig())} open={nodeSwitcherOpen}
fallbackElement={splashLoading} setOpen={setNodeSwitcherOpen}
/> />
</Suspense> </NetworkLoader>
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader>
</Suspense>
</TendermintWebsocketProvider> </TendermintWebsocketProvider>
); );
} }
@@ -1,5 +1,5 @@
import { useAssetDataProvider } from '@vegaprotocol/assets'; import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { AssetLink } from '../links'; import { AssetLink } from '../links';
export type AssetBalanceProps = { export type AssetBalanceProps = {
@@ -23,12 +23,12 @@ const AssetBalance = ({
const label = const label =
!loading && asset && asset.decimals !loading && asset && asset.decimals
? addDecimalsFixedFormatNumber(price, asset.decimals) ? addDecimalsFormatNumber(price, asset.decimals)
: price; : price;
return ( return (
<div className="inline-block"> <div className="inline-block">
<span className="font-mono">{label}</span>{' '} <span>{label}</span>{' '}
{showAssetLink && asset?.id ? ( {showAssetLink && asset?.id ? (
<AssetLink showAssetSymbol={showAssetSymbol} assetId={assetId} /> <AssetLink showAssetSymbol={showAssetSymbol} assetId={assetId} />
) : null} ) : null}
@@ -1,30 +1,24 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import type { AssetFieldsFragment } from '@vegaprotocol/assets';
useAssetTypeMapping, import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
useAssetStatusMapping,
type AssetFieldsFragment,
} from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { type AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
import { type VegaICellRendererParams } from '@vegaprotocol/datagrid'; import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react'; import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { type ColDef } from 'ag-grid-community'; import type { RowClickedEvent, ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = { type AssetsTableProps = {
data: AssetFieldsFragment[] | null; data: AssetFieldsFragment[] | null;
}; };
export const AssetsTable = ({ data }: AssetsTableProps) => { export const AssetsTable = ({ data }: AssetsTableProps) => {
const assetTypeMapping = useAssetTypeMapping();
const assetStatusMapping = useAssetStatusMapping();
const navigate = useNavigate(); const navigate = useNavigate();
const ref = useRef<AgGridReact>(null); const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => { const showColumnsOnDesktop = () => {
ref.current?.api.setColumnsVisible( ref.current?.columnApi.setColumnsVisible(
['id', 'type', 'status'], ['id', 'type', 'status'],
window.innerWidth > BREAKPOINT_MD window.innerWidth > BREAKPOINT_MD
); );
@@ -53,14 +47,14 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
field: 'source.__typename', field: 'source.__typename',
hide: window.innerWidth < BREAKPOINT_MD, hide: window.innerWidth < BREAKPOINT_MD,
valueFormatter: ({ value }: { value?: string }) => valueFormatter: ({ value }: { value?: string }) =>
value ? assetTypeMapping[value].value : '', value ? AssetTypeMapping[value].value : '',
}, },
{ {
headerName: t('Status'), headerName: t('Status'),
field: 'status', field: 'status',
hide: window.innerWidth < BREAKPOINT_MD, hide: window.innerWidth < BREAKPOINT_MD,
valueFormatter: ({ value }: { value?: string }) => valueFormatter: ({ value }: { value?: string }) =>
value ? assetStatusMapping[value].value : '', value ? AssetStatusMapping[value].value : '',
}, },
{ {
colId: 'actions', colId: 'actions',
@@ -75,7 +69,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) => }: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? ( value ? (
<ButtonLink <ButtonLink
onClick={() => { onClick={(e) => {
navigate(value); navigate(value);
}} }}
> >
@@ -86,7 +80,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
), ),
}, },
], ],
[navigate, assetStatusMapping, assetTypeMapping] [navigate]
); );
return ( return (
@@ -14,7 +14,7 @@ import { Routes } from '../../routes/route-names';
import { NetworkSwitcher } from '@vegaprotocol/environment'; import { NetworkSwitcher } from '@vegaprotocol/environment';
import type { Navigable } from '../../routes/router-config'; import type { Navigable } from '../../routes/router-config';
import { isNavigable } from '../../routes/router-config'; import { isNavigable } from '../../routes/router-config';
import { useRouterConfig } from '../../routes/router-config'; import { routerConfig } from '../../routes/router-config';
import { useMemo } from 'react'; import { useMemo } from 'react';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { Search } from '../search'; import { Search } from '../search';
@@ -26,7 +26,6 @@ const routeToNavigationItem = (r: Navigable) => (
); );
export const Header = () => { export const Header = () => {
const routerConfig = useRouterConfig();
const isHome = Boolean(useMatch(Routes.HOME)); const isHome = Boolean(useMatch(Routes.HOME));
const pages = routerConfig[0].children || []; const pages = routerConfig[0].children || [];
const mainItems = compact( const mainItems = compact(
@@ -0,0 +1,34 @@
import React from 'react';
import { DATA_SOURCES } from '../../../config';
import Hash from '../hash';
export enum EthExplorerLinkTypes {
block = 'block',
address = 'address',
tx = 'tx',
}
export type EthExplorerLinkProps = Partial<typeof HTMLAnchorElement> & {
id: string;
type: EthExplorerLinkTypes;
};
export const EthExplorerLink = ({
id,
type,
...props
}: EthExplorerLinkProps) => {
const link = `${DATA_SOURCES.ethExplorerUrl}/${type}/${id}`;
return (
<a
className="underline external font-mono"
target="_blank"
rel="noopener noreferrer"
{...props}
href={link}
>
<Hash text={id} />
</a>
);
};
@@ -1,34 +0,0 @@
import type { ChainIdMapping } from './external-chain';
import { SUPPORTED_CHAIN_IDS, SUPPORTED_CHAIN_LABELS } from './external-chain';
export const SUPPORTED_CHAIN_ICON_URLS: ChainIdMapping = {
'1': '/assets/chain-eth-logo.svg',
'100': '/assets/chain-gno-logo.svg',
'42161': '/assets/chain-arb-logo.svg',
'11155111': '/assets/chain-eth-logo.svg',
};
export type ExternalChainIconProps = {
chainId?: string;
};
export const ExternalChainIcon = ({
// If chainID is not provided, default to a non-existent chain
chainId = '-1',
}: ExternalChainIconProps) => {
if (SUPPORTED_CHAIN_IDS.includes(chainId)) {
const url = SUPPORTED_CHAIN_ICON_URLS[chainId];
const alt = SUPPORTED_CHAIN_LABELS[chainId];
return (
<img
src={url}
className="inline-block w-4 h-4 mr-1 dark:invert"
alt={alt}
title={alt}
/>
);
} else {
return null;
}
};
@@ -1,38 +0,0 @@
export type ChainIdMapping = {
[K in typeof SUPPORTED_CHAIN_IDS[number]]: string;
};
export const SUPPORTED_CHAIN_IDS: string[] = ['1', '100', '42161', '11155111'];
export const SUPPORTED_CHAIN_LABELS: ChainIdMapping = {
'1': 'Ethereum',
'100': 'Gnosis',
'42161': 'Arbitrum',
'11155111': 'Sepolia',
};
export function getExternalExplorerLink(chainId: string, type: string) {
if (SUPPORTED_CHAIN_IDS.includes(chainId)) {
switch (chainId) {
case '1':
return 'https://etherscan.io';
case '100':
return 'https://gnosisscan.io';
case '42161':
return 'https://arbiscan.io';
case '11155111':
return 'https://sepolia.etherscan.io';
default:
return '#';
}
} else {
return '#';
}
}
export function getExternalChainLabel(chainId?: string) {
if (chainId && SUPPORTED_CHAIN_IDS.includes(chainId)) {
return SUPPORTED_CHAIN_LABELS[chainId];
} else {
return 'Custom Chain';
}
}
@@ -1,41 +0,0 @@
import Hash from '../hash';
import { getExternalExplorerLink } from './external-chain';
import { ExternalChainIcon } from './external-chain-icon';
export enum EthExplorerLinkTypes {
block = 'block',
address = 'address',
tx = 'tx',
}
export type ExternalExplorerLinkProps = Partial<typeof HTMLAnchorElement> & {
id: string;
type: EthExplorerLinkTypes;
// Defaults to Ethereum Mainnet, as chain support was added late
chain?: string;
code?: boolean;
};
export const ExternalExplorerLink = ({
id,
type,
chain = '1',
code = false,
...props
}: ExternalExplorerLinkProps) => {
const link = `${getExternalExplorerLink(chain, type)}/${type}/${id}${
code ? '#code' : ''
}`;
return (
<a
className="underline external font-mono"
target="_blank"
rel="noopener noreferrer"
{...props}
href={link}
>
<ExternalChainIcon chainId={chain} />
<Hash text={id} />
</a>
);
};
@@ -1,6 +1,5 @@
export type HashProps = React.HTMLProps<HTMLSpanElement> & { export type HashProps = {
text: string; text: string;
truncate?: boolean;
}; };
/** /**
@@ -8,16 +7,10 @@ export type HashProps = React.HTMLProps<HTMLSpanElement> & {
* are broken when they need to wrap. This will remove the need * are broken when they need to wrap. This will remove the need
* for a lot of the overflow scrolling that currently exists. * for a lot of the overflow scrolling that currently exists.
*/ */
const Hash = ({ text, truncate = false }: HashProps) => { const Hash = ({ text }: HashProps) => {
const h = truncate ? text.slice(0, 6) : text;
return ( return (
<code <code className="break-all font-mono" style={{ wordWrap: 'break-word' }}>
title={text} {text}
className="break-all font-mono"
style={{ wordWrap: 'break-word' }}
>
{h}
</code> </code>
); );
}; };
@@ -2,5 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
export { default as PartyLink } from './party-link/party-link'; export { default as PartyLink } from './party-link/party-link';
export { default as NodeLink } from './node-link/node-link'; export { default as NodeLink } from './node-link/node-link';
export { default as MarketLink } from './market-link/market-link'; export { default as MarketLink } from './market-link/market-link';
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
export * from './asset-link/asset-link'; export * from './asset-link/asset-link';
@@ -3,7 +3,6 @@ query ExplorerMarket($id: ID!) {
id id
decimalPlaces decimalPlaces
positionDecimalPlaces positionDecimalPlaces
state
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@@ -23,5 +22,6 @@ query ExplorerMarket($id: ID!) {
} }
} }
} }
state
} }
} }
@@ -17,7 +17,6 @@ export const ExplorerMarketDocument = gql`
id id
decimalPlaces decimalPlaces
positionDecimalPlaces positionDecimalPlaces
state
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@@ -37,6 +36,7 @@ export const ExplorerMarketDocument = gql`
} }
} }
} }
state
} }
} }
`; `;
@@ -63,9 +63,6 @@ describe('Market link component', () => {
product: { product: {
__typename: 'Future', __typename: 'Future',
quoteName: 'dai', quoteName: 'dai',
settlementAsset: {
decimals: 8,
},
}, },
}, },
}, },
@@ -91,6 +88,6 @@ describe('Market link component', () => {
}; };
const res = render(renderComponent('123', [mock])); const res = render(renderComponent('123', [mock]));
expect(await res.findByTitle('123')).toBeInTheDocument(); expect(await res.findByText('123')).toBeInTheDocument();
}); });
}); });
@@ -23,7 +23,6 @@ const MarketLink = ({
}: MarketLinkProps) => { }: MarketLinkProps) => {
const { data, error, loading } = useExplorerMarketQuery({ const { data, error, loading } = useExplorerMarketQuery({
variables: { id }, variables: { id },
fetchPolicy: 'cache-first',
}); });
let label = <span>{id}</span>; let label = <span>{id}</span>;
@@ -1,30 +0,0 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
parameter: string;
};
/**
* Links a given network parameter to the relevant page and anchor on the page
*/
const NetworkParameterLink = ({
parameter,
...props
}: NetworkParameterLinkProps) => {
return (
<Link
className="underline"
{...props}
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
>
<Hash text={parameter} />
</Link>
);
};
export default NetworkParameterLink;
@@ -1,57 +0,0 @@
import OracleLink, { getStatusString } from './oracle-link';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
describe('getStatusString', () => {
it("returns 'Unknown' for undefined status", () => {
expect(getStatusString(undefined)).toBe('Unknown');
});
it('returns the correct string for a known status', () => {
expect(getStatusString('STATUS_ACTIVE')).toBe('Active');
expect(getStatusString('STATUS_DEACTIVATED')).toBe('Deactivated');
});
});
describe('OracleLink', () => {
it('renders the truncated Oracle ID', () => {
const id = '123456789';
render(
<MockedProvider>
<MemoryRouter>
<OracleLink id={id} />
</MemoryRouter>
</MockedProvider>
);
const idElement = screen.getByText(id.slice(0, 6));
expect(idElement).toBeInTheDocument();
expect(idElement).toHaveAttribute('title', id);
});
it('renders the Oracle status', () => {
const id = '123';
const status = 'STATUS_ACTIVE';
render(
<MockedProvider>
<MemoryRouter>
<OracleLink id={id} status={status} data-testid="link" />
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('link')).toHaveAttribute('data-status', 'Active');
});
it('renders the Oracle data indicator', () => {
const id = '123';
const hasSeenOracleReports = true;
render(
<MockedProvider>
<MemoryRouter>
<OracleLink id={id} hasSeenOracleReports={hasSeenOracleReports} />
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('oracle-data-indicator')).toBeInTheDocument();
});
});
@@ -3,93 +3,20 @@ import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import Hash from '../hash'; import Hash from '../hash';
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
DataSourceSpecStatus,
DataSourceSpecStatusMapping,
} from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
/**
* Returns a human-readable string for the given status, or a meaningful
* default if the status is unrecognised
* @param status string status
*/
export function getStatusString(status: string | undefined): string {
if (status && status in DataSourceSpecStatus) {
return DataSourceSpecStatusMapping[status as DataSourceSpecStatus];
}
return t('Unknown');
}
export type OracleLinkProps = Partial<ComponentProps<typeof Link>> & { export type OracleLinkProps = Partial<ComponentProps<typeof Link>> & {
// The Oracle ID
id: string; id: string;
// If available, the oracle status
status?: string;
// If the oracle has corresponding data in the OracleDataConnection
hasSeenOracleReports?: boolean;
}; };
/** const OracleLink = ({ id, ...props }: OracleLinkProps) => {
* Given an Oracle ID, renders a data-dense link to the Oracle page. Data density is achieved by:
* - Colour coding the link based on the Oracle's status
* - Showing a small indicator if the Oracle has matched data
* - Showing a tooltip with the Oracle's status and whether it has matched data
*
* @returns
*/
export const OracleLink = ({
id,
status,
hasSeenOracleReports = false,
...props
}: OracleLinkProps) => {
const bgColour =
status === 'STATUS_ACTIVE'
? 'bg-yellow-100 hover:bg-yellow-200 border-yellow-200 dark:bg-yellow-200 dark:border-yellow-200 dark:text-gray-900 dark:border-yellow-300'
: 'bg-gray-100 hover:bg-gray-200 border-gray-200';
const indicatorColour =
status === 'STATUS_ACTIVE'
? 'bg-yellow-300 hover:bg-yellow-500 dark:bg-yellow-500'
: 'bg-gray-300 hover:bg-gray-500';
const description = (
<div>
<p>
<strong>{`Status: `}</strong>
{getStatusString(status)}
</p>
<p>
<strong>{`Matched data: `}</strong>
{hasSeenOracleReports ? (
<VegaIcon name={VegaIconNames.TICK} />
) : (
<VegaIcon name={VegaIconNames.CROSS} />
)}
</p>
</div>
);
return ( return (
<Tooltip description={description}> <Link
<Link className="underline font-mono"
className={`pl-2 pr-2 font-mono dark:text-black ${bgColour} rounded-sm border-solid border-2 relative`} {...props}
{...props} to={`/${Routes.ORACLES}/${id}`}
to={`/${Routes.ORACLES}/${id}`} >
data-status={getStatusString(status)} <Hash text={id} />
> </Link>
<Hash text={id} truncate={true} />
{hasSeenOracleReports ? (
<strong
data-testid="oracle-data-indicator"
className={`absolute top-0 right-0 w-1 h-full font-thin ${indicatorColour}`}
title="Oracle has matched data"
></strong>
) : null}
</Link>
</Tooltip>
); );
}; };
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
} }
return ( return (
<span> <span className="whitespace-nowrap">
{useName && <Icon size={4} name="cube" className="mr-2" />} {useName && <Icon size={4} name="cube" className="mr-2" />}
<Link <Link
className="underline font-mono" className="underline font-mono"
@@ -1,11 +1,9 @@
query ExplorerProposal($id: ID!) { query ExplorerProposal($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
... on Proposal { id
id rationale {
rationale { title
title description
description
}
} }
} }
} }
@@ -8,18 +8,16 @@ export type ExplorerProposalQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null }; export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export const ExplorerProposalDocument = gql` export const ExplorerProposalDocument = gql`
query ExplorerProposal($id: ID!) { query ExplorerProposal($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
... on Proposal { id
id rationale {
rationale { title
title description
description
}
} }
} }
} }
@@ -3,11 +3,7 @@ import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import ProposalLink from './proposal-link'; import ProposalLink from './proposal-link';
import { import { ExplorerProposalDocument } from './__generated__/Proposal';
ExplorerProposalDocument,
type ExplorerProposalQuery,
type ExplorerProposalQueryVariables,
} from './__generated__/Proposal';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
function renderComponent(id: string, mocks: MockedResponse[]) { function renderComponent(id: string, mocks: MockedResponse[]) {
@@ -27,10 +23,7 @@ describe('Proposal link component', () => {
}); });
it('Renders the ID on error', async () => { it('Renders the ID on error', async () => {
const mock: MockedResponse< const mock = {
ExplorerProposalQuery,
ExplorerProposalQueryVariables
> = {
request: { request: {
query: ExplorerProposalDocument, query: ExplorerProposalDocument,
variables: { variables: {
@@ -47,22 +40,17 @@ describe('Proposal link component', () => {
}); });
it('Renders the proposal title when the query returns a result', async () => { it('Renders the proposal title when the query returns a result', async () => {
const proposalId = '123'; const mock = {
const mock: MockedResponse<
ExplorerProposalQuery,
ExplorerProposalQueryVariables
> = {
request: { request: {
query: ExplorerProposalDocument, query: ExplorerProposalDocument,
variables: { variables: {
id: proposalId, id: '123',
}, },
}, },
result: { result: {
data: { data: {
proposal: { proposal: {
__typename: 'Proposal', id: '123',
id: proposalId,
rationale: { rationale: {
title: 'test-title', title: 'test-title',
description: 'test description', description: 'test description',
@@ -72,16 +60,13 @@ describe('Proposal link component', () => {
}, },
}; };
const res = render(renderComponent(proposalId, [mock])); const res = render(renderComponent('123', [mock]));
expect(res.getByText(proposalId)).toBeInTheDocument(); expect(res.getByText('123')).toBeInTheDocument();
expect(await res.findByText('test-title')).toBeInTheDocument(); expect(await res.findByText('test-title')).toBeInTheDocument();
}); });
it('Leaves the proposal id when the market is not found', async () => { it('Leaves the proposal id when the market is not found', async () => {
const mock: MockedResponse< const mock = {
ExplorerProposalQuery,
ExplorerProposalQueryVariables
> = {
request: { request: {
query: ExplorerProposalDocument, query: ExplorerProposalDocument,
variables: { variables: {
@@ -1,11 +1,7 @@
import { import { useExplorerProposalQuery } from './__generated__/Proposal';
useExplorerProposalQuery,
type ExplorerProposalQuery,
} from './__generated__/Proposal';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ENV } from '../../../config/env'; import { ENV } from '../../../config/env';
import Hash from '../hash'; import Hash from '../hash';
export type ProposalLinkProps = { export type ProposalLinkProps = {
id: string; id: string;
text?: string; text?: string;
@@ -20,13 +16,8 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
variables: { id }, variables: { id },
}); });
const proposal = data?.proposal as Extract<
ExplorerProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const base = ENV.dataSources.governanceUrl; const base = ENV.dataSources.governanceUrl;
const label = proposal?.rationale?.title || id; const label = data?.proposal?.rationale.title || id;
return ( return (
<ExternalLink href={`${base}/proposals/${id}`}> <ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,7 +1,6 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/markets'; import type { MarketInfoWithData } from '@vegaprotocol/markets';
import { import {
LiquidationStrategyInfoPanel,
LiquidityPriceRangeInfoPanel, LiquidityPriceRangeInfoPanel,
LiquiditySLAParametersInfoPanel, LiquiditySLAParametersInfoPanel,
MarginScalingFactorsPanel, MarginScalingFactorsPanel,
@@ -95,8 +94,6 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
</> </>
) )
)} )}
<h2 className={headerClassName}>{t('Liquidation strategy')}</h2>
<LiquidationStrategyInfoPanel market={market} />
<h2 className={headerClassName}>{t('Liquidity monitoring')}</h2> <h2 className={headerClassName}>{t('Liquidity monitoring')}</h2>
<LiquidityMonitoringParametersInfoPanel market={market} /> <LiquidityMonitoringParametersInfoPanel market={market} />
<h2 className={headerClassName}>{t('Liquidity price range')}</h2> <h2 className={headerClassName}>{t('Liquidity price range')}</h2>
@@ -2,18 +2,18 @@ import { useMemo } from 'react';
import { getAsset, type MarketFieldsFragment } from '@vegaprotocol/markets'; import { getAsset, type MarketFieldsFragment } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { type AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { type ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
import { import type {
type VegaICellRendererParams, VegaICellRendererParams,
type VegaValueGetterParams, VegaValueGetterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react'; import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { MarketStateMapping } from '@vegaprotocol/types'; import { MarketStateMapping } from '@vegaprotocol/types';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { type RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
type MarketsTableProps = { type MarketsTableProps = {
@@ -29,7 +29,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const showColumnsOnDesktop = () => { const showColumnsOnDesktop = () => {
gridRef.current?.api.setColumnsVisible( gridRef.current?.columnApi.setColumnsVisible(
['id', 'state', 'asset'], ['id', 'state', 'asset'],
window.innerWidth > BREAKPOINT_MD window.innerWidth > BREAKPOINT_MD
); );
@@ -1,5 +1,9 @@
import { render, waitFor } from '@testing-library/react'; import { render, waitFor } from '@testing-library/react';
import { NestedDataList, sortNestedDataByChildren } from './nested-data-list'; import {
BORDER_COLOURS,
NestedDataList,
sortNestedDataByChildren,
} from './nested-data-list';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
const mockData = { const mockData = {
@@ -57,6 +61,38 @@ describe('NestedDataList', () => {
expect(parent[0].querySelector('li')).toHaveClass('pl-4 border-l-4 pt-2'); expect(parent[0].querySelector('li')).toHaveClass('pl-4 border-l-4 pt-2');
}); });
it('should repeat the border colours in the correct order', () => {
const colourMockData = {
t0: {
t1: {
t2: {
t3: {
t4: {
t5: {
t6: {
t7: {
t8: {
hello: 'world',
},
},
},
},
},
},
},
},
},
};
const tree = render(<NestedDataList data={colourMockData} />);
const { getByTestId } = tree;
for (let i = 0; i < 8; i++) {
const item = getByTestId(`T${i}`);
const expected = BORDER_COLOURS.light[i % 5];
expect(item.style.borderColor.toUpperCase()).toBe(expected);
}
});
it('should sort the data by values with children', () => { it('should sort the data by values with children', () => {
const mockData = { const mockData = {
nonce: '5980890939790185837', nonce: '5980890939790185837',
@@ -17,7 +17,7 @@ function renderAmendOrderDetails(
mocks: MockedResponse[] mocks: MockedResponse[]
) { ) {
return render( return render(
<MockedProvider mocks={mocks}> <MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter> <MemoryRouter>
<AmendOrderDetails version={version} id={id} amend={amend} /> <AmendOrderDetails version={version} id={id} amend={amend} />
</MemoryRouter> </MemoryRouter>
@@ -44,18 +44,18 @@ function renderExistingAmend(
orderByID: { orderByID: {
__typename: 'Order', __typename: 'Order',
id: '123', id: '123',
type: Schema.OrderType.TYPE_LIMIT, type: 'GTT',
status: Schema.OrderStatus.STATUS_ACTIVE, status: Schema.OrderStatus.STATUS_ACTIVE,
version: version ? version.toString() : '', version: version,
createdAt: '123', createdAt: '123',
updatedAt: '456', updatedAt: '456',
expiresAt: '789', expiresAt: '789',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
price: '200', price: '200',
side: Schema.Side.SIDE_BUY, side: 'BUY',
peggedOrder: null, peggedOrder: null,
remaining: '99', remaining: '99',
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_NOT_FOUND, rejectionReason: 'rejection',
reference: '123', reference: '123',
size: '100', size: '100',
party: { party: {
@@ -65,9 +65,9 @@ function renderExistingAmend(
market: { market: {
__typename: 'Market', __typename: 'Market',
id: '789', id: '789',
state: Schema.MarketState.STATE_ACTIVE, state: 'STATUS_ACTIVE',
positionDecimalPlaces: 2, positionDecimalPlaces: 2,
decimalPlaces: 0, decimalPlaces: '5',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
name: 'test', name: 'test',
@@ -94,18 +94,18 @@ function renderExistingAmend(
orderByID: { orderByID: {
__typename: 'Order', __typename: 'Order',
id: '123', id: '123',
type: Schema.OrderType.TYPE_LIMIT, type: 'GTT',
status: Schema.OrderStatus.STATUS_ACTIVE, status: Schema.OrderStatus.STATUS_ACTIVE,
version: '100', version: 100,
createdAt: '123', createdAt: '123',
updatedAt: '456', updatedAt: '456',
expiresAt: '789', expiresAt: '789',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
peggedOrder: null, peggedOrder: null,
price: '200', price: '200',
side: Schema.Side.SIDE_BUY, side: 'BUY',
remaining: '99', remaining: '99',
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_NOT_FOUND, rejectionReason: 'rejection',
reference: '123', reference: '123',
size: '200', size: '200',
party: { party: {
@@ -114,10 +114,10 @@ function renderExistingAmend(
}, },
market: { market: {
__typename: 'Market', __typename: 'Market',
id: '8888', id: 'amend-to-order-latest-version',
state: Schema.MarketState.STATE_ACTIVE, state: 'STATUS_ACTIVE',
positionDecimalPlaces: 2, positionDecimalPlaces: 2,
decimalPlaces: 0, decimalPlaces: '5',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
name: 'amend-to-order-latest-version-test', name: 'amend-to-order-latest-version-test',
@@ -136,13 +136,13 @@ function renderExistingAmend(
request: { request: {
query: ExplorerMarketDocument, query: ExplorerMarketDocument,
variables: { variables: {
id: '8888', id: '789',
}, },
}, },
result: { result: {
data: { data: {
market: { market: {
id: '8888', id: '789',
decimalPlaces: 5, decimalPlaces: 5,
positionDecimalPlaces: 2, positionDecimalPlaces: 2,
state: 'irrelevant-test-data', state: 'irrelevant-test-data',
@@ -152,9 +152,6 @@ function renderExistingAmend(
product: { product: {
__typename: 'Future', __typename: 'Future',
quoteName: 'dai', quoteName: 'dai',
settlementAsset: {
decimals: 8,
},
}, },
}, },
}, },
@@ -219,10 +216,12 @@ describe('Amend order details', () => {
it('Fetches latest version when version is not specified', async () => { it('Fetches latest version when version is not specified', async () => {
const amend: Amend = { const amend: Amend = {
price: '123', price: '-7879',
}; };
const res = renderExistingAmend('123', undefined, amend); const res = renderExistingAmend('123', undefined, amend);
expect(await res.findByText('test-label')).toBeInTheDocument(); expect(
await res.findByText('amend-to-order-latest-version')
).toBeInTheDocument();
}); });
}); });
@@ -46,10 +46,10 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
<div className={wrapperClasses}> <div className={wrapperClasses}>
<div className="mb-12 lg:mb-0"> <div className="mb-12 lg:mb-0">
<div className="relative block rounded-lg px-3 py-6 md:px-6 lg:-mr-7"> <div className="relative block rounded-lg px-3 py-6 md:px-6 lg:-mr-7">
<h2 className="display-5 mb-4 text-3xl font-bold"> <h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')} {t('Order not found')}
</h2> </h2>
<p className="mb-12 text-gray-500"> <p className="text-gray-500 mb-12">
{t('No order created from this transaction')} {t('No order created from this transaction')}
</p> </p>
</div> </div>
@@ -68,24 +68,24 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
<div className={wrapperClasses}> <div className={wrapperClasses}>
<div className="mb-12 lg:mb-0"> <div className="mb-12 lg:mb-0">
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7"> <div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
<h2 className="display-5 mb-4 text-3xl font-bold"> <h2 className="text-3xl font-bold mb-4 display-5">
{t('Edits to ')} {t('Edits to ')}
{sideText[o.side]} {sideText[o.side]}
{t(' order')} {t(' order')}
</h2> </h2>
<div className="mb-4 text-gray-500"> <p className="text-gray-500 mb-4">
In <MarketLink id={o.market.id} />, updated at{' '} In <MarketLink id={o.market.id} />, updated at{' '}
<Time date={o.updatedAt} />. <Time date={o.updatedAt} />.
</div> </p>
<div className="grid gap-x-6 md:grid-cols-4"> <div className="grid md:grid-cols-4 gap-x-6">
{amend.sizeDelta && amend.sizeDelta !== '0' ? ( {amend.sizeDelta && amend.sizeDelta !== '0' ? (
<div className="mb-12 md:mb-0"> <div className="mb-12 md:mb-0">
<h2 className="text-dark mb-4 text-2xl font-bold"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('New size')} {t('New size')}
</h2> </h2>
<h5 <h5
className={`mb-0 text-lg font-medium capitalize text-gray-500 ${getSideDeltaColour( className={`text-lg font-medium text-gray-500 mb-0 capitalize ${getSideDeltaColour(
amend.sizeDelta amend.sizeDelta
)}`} )}`}
> >
@@ -96,10 +96,10 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
{amend.price && amend.price !== '0' ? ( {amend.price && amend.price !== '0' ? (
<div className=""> <div className="">
<h2 className="text-dark mb-4 text-2xl font-bold"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('New price')} {t('New price')}
</h2> </h2>
<h5 className="mb-0 text-lg font-medium text-gray-500"> <h5 className="text-lg font-medium text-gray-500 mb-0">
<PriceInMarket price={amend.price} marketId={o.market.id} /> <PriceInMarket price={amend.price} marketId={o.market.id} />
</h5> </h5>
</div> </div>
@@ -108,10 +108,10 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
{amend.peggedReference && {amend.peggedReference &&
amend.peggedReference !== 'PEGGED_REFERENCE_UNSPECIFIED' ? ( amend.peggedReference !== 'PEGGED_REFERENCE_UNSPECIFIED' ? (
<div className=""> <div className="">
<h2 className="text-dark mb-4 text-2xl font-bold"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('New reference')} {t('New reference')}
</h2> </h2>
<h5 className="mb-0 text-lg font-medium text-gray-500"> <h5 className="text-lg font-medium text-gray-500 mb-0">
{peggedReference[amend.peggedReference]} {peggedReference[amend.peggedReference]}
</h5> </h5>
</div> </div>
@@ -119,10 +119,10 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
{amend.peggedOffset ? ( {amend.peggedOffset ? (
<div className=""> <div className="">
<h2 className="text-dark mb-4 text-2xl font-bold"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('New offset')} {t('New offset')}
</h2> </h2>
<h5 className="mb-0 text-lg font-medium text-gray-500"> <h5 className="text-lg font-medium text-gray-500 mb-0">
{amend.peggedOffset} {amend.peggedOffset}
</h5> </h5>
</div> </div>
@@ -6,7 +6,6 @@ import { Time } from '../time';
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels'; import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market'; import SizeInMarket from '../size-in-market/size-in-market';
import { TxOrderPeggedReference } from '../txs/details/order/tx-order-peg'; import { TxOrderPeggedReference } from '../txs/details/order/tx-order-peg';
import { OrderTypeMapping } from '@vegaprotocol/types';
export interface DeterministicOrderDetailsProps { export interface DeterministicOrderDetailsProps {
id: string; id: string;
@@ -60,7 +59,7 @@ const DeterministicOrderDetails = ({
const o = data.orderByID; const o = data.orderByID;
return ( return (
<div className={wrapperClasses}> <div className={wrapperClasses}>
<div className="mb-0"> <div className="mb-12 lg:mb-0">
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7"> <div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
<h2 className="text-3xl font-bold mb-4 display-5"> <h2 className="text-3xl font-bold mb-4 display-5">
<abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2"> <abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2">
@@ -70,7 +69,7 @@ const DeterministicOrderDetails = ({
<span className="mx-5 text-base">@</span> <span className="mx-5 text-base">@</span>
<PriceInMarket price={o.price} marketId={o.market.id} /> <PriceInMarket price={o.price} marketId={o.market.id} />
</h2> </h2>
<p className="text-gray-400 dark:text-gray-600"> <p className="text-gray-200">
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />. In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
</p> </p>
{o.peggedOrder ? ( {o.peggedOrder ? (
@@ -84,14 +83,15 @@ const DeterministicOrderDetails = ({
/> />
</p> </p>
) : null} ) : null}
{o.reference ? ( {o.reference ? (
<p className="text-gray-500 mt-4"> <p className="text-gray-500 mt-4">
<span>{t('Reference')}</span>: {o.reference} <span>{t('Reference')}</span>: {o.reference}
</p> </p>
) : null} ) : null}
<div className="grid grid-cols-2 md:grid-cols-5 gap-x-6 mt-4"> <div className="grid md:grid-cols-4 gap-x-6 mt-4">
<div className="mb-6 md:mb-0"> <div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('Status')} {t('Status')}
</h2> </h2>
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize"> <h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
@@ -99,33 +99,21 @@ const DeterministicOrderDetails = ({
</h5> </h5>
</div> </div>
<div className="mb-6 md:mb-0"> <div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4"> <h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
{t('Size')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0"> <h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.size} marketId={o.market.id} /> <SizeInMarket size={o.size} marketId={o.market.id} />
</h5> </h5>
</div> </div>
<div className="mb-6 md:mb-0"> <div className="">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('Version')} {t('Version')}
</h2> </h2>
<h5 className="text-lg font-medium text-gray-500 mb-0"> <h5 className="text-lg font-medium text-gray-500 mb-0">
{o.version} {o.version}
</h5> </h5>
</div> </div>
{o.type ? (
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
{t('Type')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{OrderTypeMapping[o.type]}
</h5>
</div>
) : null}
</div> </div>
</div> </div>
</div> </div>
@@ -1,15 +1,14 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals'; import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
import { import type {
type VegaICellRendererParams, VegaICellRendererParams,
type VegaValueFormatterParams, VegaValueFormatterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { type ColDef } from 'ag-grid-community'; import type { RowClickedEvent, ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { import {
@@ -44,11 +43,11 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const showColumnsOnDesktop = () => { const showColumnsOnDesktop = () => {
gridRef.current?.api.setColumnsVisible( gridRef.current?.columnApi.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'], ['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD window.innerWidth > BREAKPOINT_MD
); );
gridRef.current?.api.setColumnWidth( gridRef.current?.columnApi.setColumnWidth(
'actions', 'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80 window.innerWidth > BREAKPOINT_MD ? 221 : 80
); );
@@ -106,7 +105,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
? new BigNumber(0) ? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted); : yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return ( return (
<div className="flex h-full items-center justify-center pt-2 uppercase"> <div className="flex items-center justify-center h-full pt-2 uppercase">
<VoteProgress <VoteProgress
threshold={requiredMajorityPercentage} threshold={requiredMajorityPercentage}
progress={yesPercentage} progress={yesPercentage}
@@ -1,59 +0,0 @@
import { useState } from 'react';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import {
CopyWithTooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
interface SignatureProps {
signature: BlockExplorerTransactionResult['signature'];
}
const valueClass =
'font-mono px-2.5 py-0.5 text-xs max-w-[200px] cursor-pointer';
const valueClassClosed = 'text-ellipsis overflow-hidden';
const valueClassOpen = 'break-words text-left';
/**
* Viewer component for a vega signature. Featuers copy and pasting, truncation
*
* @param signature
*/
export const Signature = ({ signature }: SignatureProps) => {
const [isOpen, setIsOpen] = useState(false);
if (!signature || !signature.value || !signature.version || !signature.algo) {
return null;
}
return (
<div className="inline-flex border rounded signature-component relative pr-[20px]">
<div
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
title={`${signature.algo}`}
>
<span>v{signature.version}</span>
</div>
<div
className={
isOpen
? `${valueClass} ${valueClassOpen}`
: `${valueClass} ${valueClassClosed}`
}
>
<CopyWithTooltip text={signature.value}>
<span title={signature.value}>{signature.value}</span>
</CopyWithTooltip>
</div>
<button
onClick={() => setIsOpen(!isOpen)}
className="absolute top-[-3px] right-0 pr-2"
title={t('Show full signature')}
>
<VegaIcon name={isOpen ? VegaIconNames.EYE_OFF : VegaIconNames.EYE} />
</button>
</div>
);
};
@@ -22,9 +22,5 @@ export const TimeAgo = ({ date, ...props }: TimeAgoProps) => {
return <>{t('Date unknown')}</>; return <>{t('Date unknown')}</>;
} }
return ( return <span {...props}>{t(`${distanceToNow} ago`)}</span>;
<span {...props} title={date} className="underline decoration-dotted">
{t(`${distanceToNow} ago`)}
</span>
);
}; };
@@ -15,7 +15,6 @@ import isUndefined from 'lodash/isUndefined';
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response'; import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
import { TxDetailsChainEventWithdrawal } from './tx-erc20-withdrawal'; import { TxDetailsChainEventWithdrawal } from './tx-erc20-withdrawal';
import { TxDetailsChainEventErc20AssetDelist } from './tx-erc20-asset-delist'; import { TxDetailsChainEventErc20AssetDelist } from './tx-erc20-asset-delist';
import { TxDetailsContractCall } from './tx-contract-call';
interface ChainEventProps { interface ChainEventProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -39,7 +38,7 @@ export const ChainEvent = ({ txData }: ChainEventProps) => {
return null; return null;
} }
const { builtin, erc20, erc20Multisig, stakingEvent, contractCall } = const { builtin, erc20, erc20Multisig, stakingEvent } =
txData.command.chainEvent; txData.command.chainEvent;
// Builtin Asset events // Builtin Asset events
@@ -141,10 +140,6 @@ export const ChainEvent = ({ txData }: ChainEventProps) => {
} }
} }
if (contractCall) {
return <TxDetailsContractCall contractCall={contractCall} />;
}
// If we hit this return, tx-shared-details should give a basic overview // If we hit this return, tx-shared-details should give a basic overview
return null; return null;
}; };
@@ -1,26 +0,0 @@
import { decodeEthCallResult } from './tx-contract-call';
import { base64 } from 'ethers/lib/utils';
import { defaultAbiCoder } from '@ethersproject/abi';
import { BigNumber } from '@ethersproject/bignumber';
describe('decodeEthCallResult', () => {
it('should decode contractData correctly (mocked)', () => {
const mockContractData = base64.encode(
defaultAbiCoder.encode(['int256'], [BigNumber.from(123)])
);
const result = decodeEthCallResult(mockContractData);
expect(result).toBe('123');
});
it('should decode contractData correctly (known data)', () => {
const mockContractData = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADH8cueyY=';
const result = decodeEthCallResult(mockContractData);
expect(result).toBe('3435020581670');
});
it('should return "-" when an error occurs', () => {
const mockContractData = 'invalid_data';
const result = decodeEthCallResult(mockContractData);
expect(result).toBe('-');
});
});
@@ -1,102 +0,0 @@
import { TableCell, TableRow } from '../../../table';
import { t } from '@vegaprotocol/i18n';
import {
ExternalExplorerLink,
EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link';
import { getExternalChainLabel } from '../../../links/external-explorer-link/external-chain';
import type { components } from '../../../../../types/explorer';
import { defaultAbiCoder, base64 } from 'ethers/lib/utils';
import { BigNumber } from 'ethers';
import OracleLink from '../../../links/oracle-link/oracle-link';
import { useExplorerOracleSpecByIdQuery } from '../../../../routes/oracles/__generated__/Oracles';
import { OracleEthSource } from '../../../../routes/oracles/components/oracle-eth-source';
/**
* Decodes the b64/ABIcoded result from an eth cal
* @param data
* @returns
*/
export function decodeEthCallResult(contractData: string): string {
try {
const rawResult = defaultAbiCoder.decode(
['int256'],
base64.decode(contractData)
);
// Finally, convert the resulting BigNumber in to a string
const res = BigNumber.from(rawResult[0]).toString();
return res;
} catch (e) {
return '-';
}
}
interface TxDetailsContractCallProps {
contractCall: components['schemas']['vegaEthContractCallEvent'];
}
export const TxDetailsContractCall = ({
contractCall,
}: TxDetailsContractCallProps) => {
const { data } = useExplorerOracleSpecByIdQuery({
variables: {
id: contractCall.specId || '1',
},
});
if (!contractCall) {
return null;
}
const chainLabel = getExternalChainLabel(contractCall.sourceChainId);
return (
<>
{contractCall.specId && (
<TableRow modifier="bordered">
<TableCell>{t('Oracle')}</TableCell>
<TableCell>
<OracleLink
id={contractCall.specId}
hasSeenOracleReports={true}
status={data?.oracleSpec?.dataSourceSpec.spec.status || '-'}
/>
</TableCell>
</TableRow>
)}
{contractCall.blockHeight && (
<TableRow modifier="bordered">
<TableCell>
{chainLabel} {t('block')}
</TableCell>
<TableCell>
<ExternalExplorerLink
chain={contractCall.sourceChainId}
id={contractCall.blockHeight}
type={EthExplorerLinkTypes.block}
/>
</TableCell>
</TableRow>
)}
{data?.oracleSpec?.dataSourceSpec && (
<OracleEthSource
chain={contractCall.sourceChainId}
sourceType={data.oracleSpec.dataSourceSpec.spec.data.sourceType}
/>
)}
{contractCall.error && (
<TableRow modifier="bordered">
<TableCell>{t('Call error')}</TableCell>
<TableCell>{contractCall.error}</TableCell>
</TableRow>
)}
{contractCall.result && (
<TableRow modifier="bordered">
<TableCell>{t('Result')}</TableCell>
<TableCell>{decodeEthCallResult(contractCall.result)}</TableCell>
</TableRow>
)}
</>
);
};
@@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { AssetLink } from '../../../links'; import { AssetLink } from '../../../links';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventErc20AssetLimitsUpdatedProps { interface TxDetailsChainEventErc20AssetLimitsUpdatedProps {
assetLimitsUpdated: components['schemas']['vegaERC20AssetLimitsUpdated']; assetLimitsUpdated: components['schemas']['vegaERC20AssetLimitsUpdated'];
@@ -42,7 +42,7 @@ export const TxDetailsChainEventErc20AssetLimitsUpdated = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('ERC20 asset')}</TableCell> <TableCell>{t('ERC20 asset')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={assetLimitsUpdated.sourceEthereumAddress} id={assetLimitsUpdated.sourceEthereumAddress}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { AssetLink } from '../../../links'; import { AssetLink } from '../../../links';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventErc20AssetListProps { interface TxDetailsChainEventErc20AssetListProps {
assetList: components['schemas']['vegaERC20AssetList']; assetList: components['schemas']['vegaERC20AssetList'];
@@ -32,7 +32,7 @@ export const TxDetailsChainEventErc20AssetList = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Source')}</TableCell> <TableCell>{t('Source')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={assetList.assetSource} id={assetList.assetSource}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { AssetLink, PartyLink } from '../../../links'; import { AssetLink, PartyLink } from '../../../links';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventProps { interface TxDetailsChainEventProps {
deposit: components['schemas']['vegaERC20Deposit']; deposit: components['schemas']['vegaERC20Deposit'];
@@ -36,7 +36,7 @@ export const TxDetailsChainEventDeposit = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Source')}</TableCell> <TableCell>{t('Source')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={deposit.sourceEthereumAddress} id={deposit.sourceEthereumAddress}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { AssetLink } from '../../../links'; import { AssetLink } from '../../../links';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventWithdrawalProps { interface TxDetailsChainEventWithdrawalProps {
withdrawal: components['schemas']['vegaERC20Withdrawal']; withdrawal: components['schemas']['vegaERC20Withdrawal'];
@@ -35,7 +35,7 @@ export const TxDetailsChainEventWithdrawal = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Recipient')}</TableCell> <TableCell>{t('Recipient')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={withdrawal.targetEthereumAddress} id={withdrawal.targetEthereumAddress}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { PartyLink } from '../../../links'; import { PartyLink } from '../../../links';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventStakeDepositProps { interface TxDetailsChainEventStakeDepositProps {
deposit: components['schemas']['vegaStakeDeposited']; deposit: components['schemas']['vegaStakeDeposited'];
@@ -39,7 +39,7 @@ export const TxDetailsChainEventStakeDeposit = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Source')}</TableCell> <TableCell>{t('Source')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={deposit.ethereumAddress} id={deposit.ethereumAddress}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -3,9 +3,9 @@ import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { PartyLink } from '../../../links'; import { PartyLink } from '../../../links';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventStakeRemoveProps { interface TxDetailsChainEventStakeRemoveProps {
remove: components['schemas']['vegaStakeRemoved']; remove: components['schemas']['vegaStakeRemoved'];
@@ -39,7 +39,7 @@ export const TxDetailsChainEventStakeRemove = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Source')}</TableCell> <TableCell>{t('Source')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={remove.ethereumAddress} id={remove.ethereumAddress}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -3,9 +3,9 @@ import { t } from '@vegaprotocol/i18n';
import { TableRow, TableCell } from '../../../table'; import { TableRow, TableCell } from '../../../table';
import type { components } from '../../../../../types/explorer'; import type { components } from '../../../../../types/explorer';
import { import {
ExternalExplorerLink, EthExplorerLink,
EthExplorerLinkTypes, EthExplorerLinkTypes,
} from '../../../links/external-explorer-link/external-explorer-link'; } from '../../../links/eth-explorer-link/eth-explorer-link';
interface TxDetailsChainEventStakeTotalSupplyProps { interface TxDetailsChainEventStakeTotalSupplyProps {
update: components['schemas']['vegaStakeTotalSupply']; update: components['schemas']['vegaStakeTotalSupply'];
@@ -38,7 +38,7 @@ export const TxDetailsChainEventStakeTotalSupply = ({
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Source')}</TableCell> <TableCell>{t('Source')}</TableCell>
<TableCell> <TableCell>
<ExternalExplorerLink <EthExplorerLink
id={update.tokenAddress} id={update.tokenAddress}
type={EthExplorerLinkTypes.address} type={EthExplorerLinkTypes.address}
/> />
@@ -0,0 +1,6 @@
query ExplorerSettlementAssetForMarket($id: ID!) {
market(id: $id) {
id
decimalPlaces
}
}
@@ -0,0 +1,49 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerSettlementAssetForMarketQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerSettlementAssetForMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number } | null };
export const ExplorerSettlementAssetForMarketDocument = gql`
query ExplorerSettlementAssetForMarket($id: ID!) {
market(id: $id) {
id
decimalPlaces
}
}
`;
/**
* __useExplorerSettlementAssetForMarketQuery__
*
* To run a query within a React component, call `useExplorerSettlementAssetForMarketQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerSettlementAssetForMarketQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerSettlementAssetForMarketQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerSettlementAssetForMarketQuery(baseOptions: Apollo.QueryHookOptions<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>(ExplorerSettlementAssetForMarketDocument, options);
}
export function useExplorerSettlementAssetForMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>(ExplorerSettlementAssetForMarketDocument, options);
}
export type ExplorerSettlementAssetForMarketQueryHookResult = ReturnType<typeof useExplorerSettlementAssetForMarketQuery>;
export type ExplorerSettlementAssetForMarketLazyQueryHookResult = ReturnType<typeof useExplorerSettlementAssetForMarketLazyQuery>;
export type ExplorerSettlementAssetForMarketQueryResult = Apollo.QueryResult<ExplorerSettlementAssetForMarketQuery, ExplorerSettlementAssetForMarketQueryVariables>;
@@ -0,0 +1,131 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { Side } from '@vegaprotocol/types';
import type { LiquidityOrder } from '@vegaprotocol/types';
import { PeggedReference } from '@vegaprotocol/types';
import { LiquidityProvisionDetailsRow } from './liquidity-provision-details-row';
import type { VegaSide } from './liquidity-provision-details-row';
describe('LiquidityProvisionDetails component', () => {
function renderComponent(
order: LiquidityOrder,
side: VegaSide,
normaliseProportionsTo: number,
marketId: string
) {
return render(
<MockedProvider>
<table>
<tbody data-testid="container">
<LiquidityProvisionDetailsRow
order={order}
marketId={marketId}
normaliseProportionsTo={normaliseProportionsTo}
side={side}
/>
</tbody>
</table>
</MockedProvider>
);
}
it('renders null for an order with no proportion', () => {
const mockOrder = {
offset: '1',
reference: PeggedReference.PEGGED_REFERENCE_MID,
};
const res = renderComponent(
mockOrder as LiquidityOrder,
Side.SIDE_BUY,
100,
'123'
);
expect(res.getByTestId('container')).toBeEmptyDOMElement();
});
it('renders null for a null order', () => {
const res = renderComponent(
null as unknown as LiquidityOrder,
Side.SIDE_BUY,
100,
'123'
);
expect(res.getByTestId('container')).toBeEmptyDOMElement();
});
it('renders a row when the order is as expected', () => {
const mockOrder = {
offset: '1',
proportion: 20,
reference: PeggedReference.PEGGED_REFERENCE_MID,
};
const res = renderComponent(
mockOrder as LiquidityOrder,
Side.SIDE_BUY,
100,
'123'
);
// Row test ids and keys are based on the side, reference and proportion
expect(res.getByTestId('SIDE_BUY-20-1')).toBeInTheDocument();
expect(res.getByText('+1')).toBeInTheDocument();
expect(res.getByText('Mid')).toBeInTheDocument();
expect(res.getByText('20%')).toBeInTheDocument();
});
it('normalises offsets when normaliseToProportion is not 100', () => {
const mockOrder = {
offset: '1',
proportion: 20,
reference: PeggedReference.PEGGED_REFERENCE_BEST_BID,
};
const res = renderComponent(
mockOrder as LiquidityOrder,
Side.SIDE_SELL,
50,
'123'
);
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
expect(res.getByTestId('SIDE_SELL-40-1')).toBeInTheDocument();
expect(res.getByText('-1')).toBeInTheDocument();
expect(res.getByText('Best Bid')).toBeInTheDocument();
expect(res.getByText('40%')).toBeInTheDocument();
});
it('handles a missing offset gracefully (should not happen)', () => {
const mockOrder = {
proportion: 20,
reference: PeggedReference.PEGGED_REFERENCE_BEST_BID,
};
const res = renderComponent(
mockOrder as LiquidityOrder,
Side.SIDE_SELL,
50,
'123'
);
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
expect(res.getByTestId('SIDE_SELL-40-')).toBeInTheDocument();
expect(res.getByText('-')).toBeInTheDocument();
});
it('handles a missing reference gracefully (should not happen)', () => {
const mockOrder = {
offset: '1',
proportion: 20,
};
const res = renderComponent(
mockOrder as LiquidityOrder,
Side.SIDE_SELL,
50,
'123'
);
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
expect(res.getByTestId('SIDE_SELL-40-1')).toBeInTheDocument();
expect(res.getByText('40%')).toBeInTheDocument();
expect(res.getByText('-')).toBeInTheDocument();
});
});
@@ -0,0 +1,73 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../../types/explorer';
import { TableRow } from '../../../../table';
import { LiquidityProvisionOffset } from './liquidity-provision-offset';
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
export type VegaSide = components['schemas']['vegaSide'];
export type LiquidityProvisionOrder =
components['schemas']['vegaLiquidityOrder'];
export const LiquidityReferenceLabel: Record<VegaPeggedReference, string> = {
PEGGED_REFERENCE_BEST_ASK: t('Best Ask'),
PEGGED_REFERENCE_BEST_BID: t('Best Bid'),
PEGGED_REFERENCE_MID: t('Mid'),
PEGGED_REFERENCE_UNSPECIFIED: '-',
};
export type LiquidityProvisionDetailsRowProps = {
order?: LiquidityProvisionOrder;
marketId?: string;
side: VegaSide;
// If this is
normaliseProportionsTo: number;
};
/**
*
* Note: offset is formatted by settlement asset on the market, assuming that is available
* Note: Due to the mix of references (MID vs BEST_X), it's not possible to correctly order
* the orders by their actual distance from a midpoint. This would require us knowing
* the best bid (now or at placement) and the mid. Getting the data for *now* would be
* misleading for LP submissions in the past. There is no API for getting <mid />
* at the time of a transaction.
*/
export function LiquidityProvisionDetailsRow({
normaliseProportionsTo,
order,
side,
marketId,
}: LiquidityProvisionDetailsRowProps) {
if (!order || !order.proportion) {
return null;
}
const proportion =
normaliseProportionsTo === 100
? order.proportion
: Math.round((order.proportion / normaliseProportionsTo) * 100);
const key = `${side}-${proportion}-${order.offset ? order.offset : ''}`;
return (
<TableRow modifier="bordered" key={key} data-testid={key}>
<td className="text-right px-2">
{order.offset && marketId ? (
<LiquidityProvisionOffset
offset={order.offset}
side={side}
marketId={marketId}
/>
) : (
'-'
)}
</td>
<td className="text-center">
{order.reference ? LiquidityReferenceLabel[order.reference] : '-'}
</td>
<td className="text-center">{proportion}%</td>
</TableRow>
);
}
@@ -0,0 +1,22 @@
import { render } from '@testing-library/react';
import { LiquidityProvisionMid } from './liquidity-provision-mid';
describe('LiquidityProvisionMid component', () => {
function renderComponent() {
return render(
<table>
<tbody data-testid="container">
<LiquidityProvisionMid />
</tbody>
</table>
);
}
it('renders a basic row that spans the whole table', () => {
const res = renderComponent();
const display = res.getByTestId('mid-display');
expect(res.getByTestId('mid')).toBeInTheDocument();
expect(display).toBeInTheDocument();
expect(display).toHaveAttribute('colspan', '3');
});
});
@@ -0,0 +1,17 @@
import { TableRow } from '../../../../table';
/**
* In a LiquidityProvision table, this row is the midpoint. Above our LP orders on the
* buy side, below are LP orders on the sell side. This component simply divides them.
*
* There is no API that can give us the mid price when the order was created, and even
* if there was it isn't clear that would be appropriate for this centre row. So instead
* it's a simple divider.
*/
export function LiquidityProvisionMid() {
return (
<TableRow modifier="bordered" data-testid="mid">
<td data-testid="mid-display" colSpan={3} className="bg-white"></td>
</TableRow>
);
}
@@ -0,0 +1,67 @@
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { ExplorerSettlementAssetForMarketDocument } from '../__generated__/Explorer-settlement-asset';
import type { ExplorerSettlementAssetForMarketQuery } from '../__generated__/Explorer-settlement-asset';
import type { VegaSide } from './liquidity-provision-details-row';
import {
getFormattedOffset,
LiquidityProvisionOffset,
} from './liquidity-provision-offset';
const decimalsMock: ExplorerSettlementAssetForMarketQuery = {
market: {
id: '123',
__typename: 'Market',
decimalPlaces: 5,
},
};
describe('LiquidityProvisionOffset component', () => {
function renderComponent(
offset: string,
side: VegaSide,
marketId: string,
mocks: MockedResponse[]
) {
return render(
<MockedProvider mocks={mocks}>
<LiquidityProvisionOffset
offset={offset}
side={side}
marketId={marketId}
/>
</MockedProvider>
);
}
it('renders a simple row before market data comes in', () => {
const res = renderComponent('1', 'SIDE_BUY', '123', []);
expect(res.getByText('+1')).toBeInTheDocument();
});
it('replaces unformatted with formatted if the market data comes in', () => {
const mock = {
request: {
query: ExplorerSettlementAssetForMarketDocument,
variables: {
id: '123',
},
result: {
data: decimalsMock,
},
},
};
const res = renderComponent('1', 'SIDE_BUY', '123', [mock]);
expect(res.getByText('+1')).toBeInTheDocument();
});
it('getFormattedOffset returns the unformatted offset if there is not enough data', () => {
const res = getFormattedOffset('1', {});
expect(res).toEqual('1');
});
it('getFormattedOffset decimal formats a number if it comes in with market data', () => {
const res = getFormattedOffset('1', decimalsMock);
expect(res).toEqual('0.00001');
});
});
@@ -0,0 +1,59 @@
import { useExplorerSettlementAssetForMarketQuery } from '../__generated__/Explorer-settlement-asset';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import type { ExplorerSettlementAssetForMarketQuery } from '../__generated__/Explorer-settlement-asset';
import type { VegaSide } from './liquidity-provision-details-row';
export type LiquidityProvisionOffsetProps = {
side: VegaSide;
offset: string;
marketId: string;
};
/**
* Correctly formats an LP's offset according to the market settlement decimal places.
* Initially this will appear unformatted, then when the query loads in the proper formatted
* value will be displayed
*
* @see getFormattedOffset
*/
export function LiquidityProvisionOffset({
side,
offset,
marketId,
}: LiquidityProvisionOffsetProps) {
const { data } = useExplorerSettlementAssetForMarketQuery({
variables: {
id: marketId,
},
});
// getFormattedOffset handles missing results/loading states
const formattedOffset = getFormattedOffset(offset, data);
const label = side === 'SIDE_BUY' ? '+' : '-';
const className = side === 'SIDE_BUY' ? 'text-vega-green' : 'text-vega-pink';
return <span className={className}>{`${label}${formattedOffset}`}</span>;
}
/**
* Does the work of formatting the number now we have the market decimal places.
* If no market data is assigned (i.e. during loading, or if the market doesn't exist)
* this function will return the unformatted number
*
* @see LiquidityProvisionOffset
* @param data the result of a ExplorerSettlementAssetForMarketQuery
* @param offset the unformatted offset
* @returns string the offset of this lp order formatted with the settlement decimal places
*/
export function getFormattedOffset(
offset: string,
data?: ExplorerSettlementAssetForMarketQuery
) {
const decimals = data?.market?.decimalPlaces;
if (!decimals) {
return offset;
}
return addDecimalsFormatNumber(offset, decimals);
}
@@ -0,0 +1,209 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import type { LiquidityOrder } from '@vegaprotocol/types';
import { PeggedReference } from '@vegaprotocol/types';
import type { LiquiditySubmission } from '../tx-liquidity-submission';
import {
LiquidityProvisionDetails,
sumProportions,
} from './liquidity-provision-details';
function mockProportion(proportion: number): LiquidityOrder {
return {
proportion,
reference: PeggedReference.PEGGED_REFERENCE_MID,
offset: '1',
};
}
describe('sumProportions function', () => {
it('returns 0 if the side is undefined', () => {
const side: LiquidityOrder[] = undefined as unknown as LiquidityOrder[];
const res = sumProportions(side);
expect(res).toEqual(0);
});
it('returns 0 if the side is empty', () => {
const side: LiquidityOrder[] = [];
const res = sumProportions(side);
expect(res).toEqual(0);
});
it('sums 1 item correctly (under 100%)', () => {
const side: LiquidityOrder[] = [mockProportion(10)];
const res = sumProportions(side);
expect(res).toEqual(10);
});
it('sums 2 item correctly (exactly 100%)', () => {
const side: LiquidityOrder[] = [mockProportion(50), mockProportion(50)];
const res = sumProportions(side);
expect(res).toEqual(100);
});
it('sums 3 item correctly to over 100%', () => {
const side: LiquidityOrder[] = [
mockProportion(20),
mockProportion(40),
mockProportion(50),
];
const res = sumProportions(side);
expect(res).toEqual(110);
});
});
describe('LiquidityProvisionDetails component', () => {
function renderComponent(provision: LiquiditySubmission) {
return render(
<MockedProvider>
<LiquidityProvisionDetails provision={provision} />
</MockedProvider>
);
}
it('handles an LP with no buys or sells by returning empty (should never happen)', () => {
const mock: LiquiditySubmission = {};
const res = renderComponent(mock);
expect(res.container).toBeEmptyDOMElement();
});
it('handles an LP with no sells by just rendering buys', () => {
const mock: LiquiditySubmission = {
marketId: '123',
buys: [
{
offset: '1',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
{
offset: '2',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
],
};
const res = renderComponent(mock);
expect(res.getByText('Price offset')).toBeInTheDocument();
expect(res.getByText('Price reference')).toBeInTheDocument();
expect(res.getByText('Proportion')).toBeInTheDocument();
expect(res.getByTestId('SIDE_BUY-50-1')).toBeInTheDocument();
expect(res.getByTestId('SIDE_BUY-50-2')).toBeInTheDocument();
});
it('handles an LP with no buys by just rendering sells', () => {
const mock: LiquiditySubmission = {
marketId: '123',
sells: [
{
offset: '1',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
{
offset: '2',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
],
};
const res = renderComponent(mock);
expect(res.getByText('Price offset')).toBeInTheDocument();
expect(res.getByText('Price reference')).toBeInTheDocument();
expect(res.getByText('Proportion')).toBeInTheDocument();
expect(res.getByTestId('SIDE_SELL-50-1')).toBeInTheDocument();
expect(res.getByTestId('SIDE_SELL-50-2')).toBeInTheDocument();
});
it('handles an LP with sells by just rendering buys', () => {
const mock: LiquiditySubmission = {
marketId: '123',
buys: [
{
offset: '1',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
{
offset: '2',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
],
};
const res = renderComponent(mock);
expect(res.getByText('Price offset')).toBeInTheDocument();
expect(res.getByText('Price reference')).toBeInTheDocument();
expect(res.getByText('Proportion')).toBeInTheDocument();
expect(res.getByTestId('SIDE_BUY-50-1')).toBeInTheDocument();
expect(res.getByTestId('SIDE_BUY-50-2')).toBeInTheDocument();
});
it('handles an LP with both sides', () => {
const mock: LiquiditySubmission = {
marketId: '123',
buys: [
{
offset: '1',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
{
offset: '2',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
],
sells: [
{
offset: '4',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
{
offset: '2',
proportion: 50,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
],
};
const res = renderComponent(mock);
expect(res.getByText('Price offset')).toBeInTheDocument();
expect(res.getByText('Price reference')).toBeInTheDocument();
expect(res.getByText('Proportion')).toBeInTheDocument();
expect(res.getByTestId('SIDE_BUY-50-1')).toBeInTheDocument();
expect(res.getByTestId('SIDE_BUY-50-2')).toBeInTheDocument();
expect(res.getByTestId('SIDE_SELL-50-4')).toBeInTheDocument();
expect(res.getByTestId('SIDE_SELL-50-2')).toBeInTheDocument();
});
it('normalises proportions when they do not total 100%', () => {
const mock: LiquiditySubmission = {
marketId: '123',
buys: [
{
offset: '1',
proportion: 25,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
{
offset: '2',
proportion: 30,
reference: PeggedReference.PEGGED_REFERENCE_MID,
},
],
};
const res = renderComponent(mock);
expect(res.getByText('45%')).toBeInTheDocument();
expect(res.getByText('55%')).toBeInTheDocument();
});
});
@@ -0,0 +1,94 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import type { LiquiditySubmission } from '../tx-liquidity-submission';
import { TableRow } from '../../../table';
import { LiquidityProvisionMid } from './components/liquidity-provision-mid';
import { LiquidityProvisionDetailsRow } from './components/liquidity-provision-details-row';
import { Side } from '@vegaprotocol/types';
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
export type LiquidityProvisionOrder =
components['schemas']['vegaLiquidityOrder'];
export const LiquidityReferenceLabel: Record<VegaPeggedReference, string> = {
PEGGED_REFERENCE_BEST_ASK: t('Best Ask'),
PEGGED_REFERENCE_BEST_BID: t('Best Bid'),
PEGGED_REFERENCE_MID: t('Mid'),
PEGGED_REFERENCE_UNSPECIFIED: '-',
};
/**
* Given a side of a liquidity provision order, returns the total
* It should be 100%, but it isn't always and if it isn't the proportion
* reported for each order should be scaled
*
* @returns number
*/
export function sumProportions(
side: LiquiditySubmission['buys'] | LiquiditySubmission['sells']
): number {
if (!side || side.length === 0) {
return 0;
}
return side.reduce((total, o) => total + (o.proportion || 0), 0);
}
export type LiquidityProvisionDetailsProps = {
provision: LiquiditySubmission;
};
/**
* Renders a table displaying all buys and sells in this LP. It is valid for there
* to be no buys or sells.
*
* It might seem logical to turn proportions in to values based on the total commitment
* but based on the current API structure it is awkward, and given that non-LP orders
* will change the amount that is actually deployed vs assigned to a level, we decided
* not to bother going down that route.
*/
export function LiquidityProvisionDetails({
provision,
}: LiquidityProvisionDetailsProps) {
if (!provision.buys?.length && !provision.sells?.length) {
return null;
}
// We need to do some additional calcs if these aren't both 100
const buyTotal = sumProportions(provision.buys);
const sellTotal = sumProportions(provision.sells);
return (
<table>
<thead>
<TableRow modifier="bordered">
<th className="px-2 pb-1">{t('Price offset')}</th>
<th className="px-2 pb-1">{t('Price reference')}</th>
<th className="px-2 pb-1">{t('Proportion')}</th>
</TableRow>
</thead>
<tbody>
{provision.buys?.map((b, i) => (
<LiquidityProvisionDetailsRow
order={b}
marketId={provision.marketId}
side={Side.SIDE_BUY}
key={`SIDE_BUY-${i}`}
normaliseProportionsTo={buyTotal}
/>
))}
<LiquidityProvisionMid />
{provision.sells?.map((s, i) => (
<LiquidityProvisionDetailsRow
order={s}
marketId={provision.marketId}
side={Side.SIDE_SELL}
key={`SIDE_SELL-${i}`}
normaliseProportionsTo={sellTotal}
/>
))}
</tbody>
</table>
);
}
@@ -1,20 +1,11 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import { TableCell, TableRow } from '../../../table'; import { TableCell, TableRow } from '../../../table';
import type { VegaPeggedReference } from '../liquidity-provision/liquidity-provision-details';
import { Side, PeggedReferenceMapping } from '@vegaprotocol/types'; import { Side, PeggedReferenceMapping } from '@vegaprotocol/types';
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market'; import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market'; import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
export type VegaPeggedReference = components['schemas']['vegaPeggedReference'];
export const LiquidityReferenceLabel: Record<VegaPeggedReference, string> = {
PEGGED_REFERENCE_BEST_ASK: t('Best Ask'),
PEGGED_REFERENCE_BEST_BID: t('Best Bid'),
PEGGED_REFERENCE_MID: t('Mid'),
PEGGED_REFERENCE_UNSPECIFIED: '-',
};
export interface TxDetailsOrderProps { export interface TxDetailsOrderProps {
offset: string; offset: string;
reference: VegaPeggedReference; reference: VegaPeggedReference;
@@ -1,14 +1,7 @@
query ExplorerProposalStatus($id: ID!) { query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
... on Proposal { id
id state
state rejectionReason
rejectionReason
}
... on BatchProposal {
id
state
rejectionReason
}
} }
} }
@@ -8,22 +8,15 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null }; export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql` export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) { query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
... on Proposal { id
id state
state rejectionReason
rejectionReason
}
... on BatchProposal {
id
state
rejectionReason
}
} }
} }
`; `;
@@ -1,257 +0,0 @@
import { render, screen } from '@testing-library/react';
import { BatchItem } from './batch-item';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { components } from '../../../../../types/explorer';
type Item = components['schemas']['vegaBatchProposalTermsChange'];
describe('BatchItem', () => {
it('Renders "Unknown proposal type" by default', () => {
const item = {};
render(<BatchItem item={item} />);
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
});
it('Renders "Unknown proposal type" for unknown items', () => {
const item = {
newLochNessMonster: {
location: 'Loch Ness',
},
} as unknown as Item;
render(<BatchItem item={item} />);
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
});
it('Renders "New spot market"', () => {
const item = {
newSpotMarket: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New spot market')).toBeInTheDocument();
});
it('Renders "Cancel transfer"', () => {
const item = {
cancelTransfer: {
changes: {
transferId: 'transfer123',
},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
expect(screen.getByText('transf')).toBeInTheDocument();
});
it('Renders "Cancel transfer" without an id', () => {
const item = {
cancelTransfer: {
changes: {},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
});
it('Renders "New freeform"', () => {
const item = {
newFreeform: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New freeform proposal')).toBeInTheDocument();
});
it('Renders "New market"', () => {
const item = {
newMarket: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New market')).toBeInTheDocument();
});
it('Renders "New transfer"', () => {
const item = {
newTransfer: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New transfer')).toBeInTheDocument();
});
it('Renders "Update asset" with assetId', () => {
const item = {
updateAsset: {
assetId: 'asset123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update asset')).toBeInTheDocument();
expect(screen.getByText('asset123')).toBeInTheDocument();
});
it('Renders "Update asset" even if assetId is not set', () => {
const item = {
updateAsset: {
assetId: undefined,
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update asset')).toBeInTheDocument();
});
it('Renders "Update market state" with marketId', () => {
const item = {
updateMarketState: {
changes: {
marketId: 'market123',
},
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market state')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update market state" even if marketId is not set', () => {
const item = {
updateMarketState: {
changes: {
marketId: undefined,
},
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market state')).toBeInTheDocument();
});
it('Renders "Update network parameter" with parameter', () => {
const item = {
updateNetworkParameter: {
changes: {
key: 'parameter123',
},
},
};
render(
<MockedProvider>
<MemoryRouter>
<BatchItem item={item} />
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
expect(screen.getByText('parameter123')).toBeInTheDocument();
});
it('Renders "Update network parameter" even if parameter is not set', () => {
const item = {
updateNetworkParameter: {
changes: {
key: undefined,
},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
});
it('Renders "Update referral program"', () => {
const item = {
updateReferralProgram: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update referral program')).toBeInTheDocument();
});
it('Renders "Update spot market" with marketId', () => {
const item = {
updateSpotMarket: {
marketId: 'market123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update spot market')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update spot market" even if marketId is not set', () => {
const item = {
updateSpotMarket: {
marketId: undefined,
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update spot market')).toBeInTheDocument();
});
it('Renders "Update market" with marketId', () => {
const item = {
updateMarket: {
marketId: 'market123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update market" even if marketId is not set', () => {
const item = {
updateMarket: {
marketId: undefined,
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market')).toBeInTheDocument();
});
it('Renders "Update volume discount program"', () => {
const item = {
updateVolumeDiscountProgram: {},
};
render(<BatchItem item={item} />);
expect(
screen.getByText('Update volume discount program')
).toBeInTheDocument();
});
});
@@ -1,87 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links';
import type { components } from '../../../../../types/explorer';
import Hash from '../../../links/hash';
type Item = components['schemas']['vegaBatchProposalTermsChange'];
export interface BatchItemProps {
item: Item;
}
/**
* Produces a one line summary for an item in a batch proposal. Could
* easily be adapted to summarise individual proposals, but there is no
* place for that yet.
*
* Details (like IDs) should be shown and linked if available, but handled
* if not available. This is adequate as the ProposalSummary component contains
* a JSON viewer for the full proposal.
*/
export const BatchItem = ({ item }: BatchItemProps) => {
if (item.cancelTransfer) {
const transferId = item?.cancelTransfer?.changes?.transferId || false;
return (
<span>
{t('Cancel transfer')}&nbsp;
{transferId && (
<Hash className="ml-1" truncate={true} text={transferId} />
)}
</span>
);
} else if (item.newFreeform) {
return <span>{t('New freeform proposal')}</span>;
} else if (item.newMarket) {
return <span>{t('New market')}</span>;
} else if (item.newSpotMarket) {
return <span>{t('New spot market')}</span>;
} else if (item.newTransfer) {
return <span>{t('New transfer')}</span>;
} else if (item.updateAsset) {
const assetId = item?.updateAsset?.assetId || false;
return (
<span>
{t('Update asset')}
{assetId && <AssetLink className="ml-1" assetId={assetId} />}
</span>
);
} else if (item.updateMarket) {
const marketId = item?.updateMarket?.marketId || false;
return (
<span>
{t('Update market')}{' '}
{marketId && <MarketLink className="ml-1" id={marketId} />}
</span>
);
} else if (item.updateMarketState) {
const marketId = item?.updateMarketState?.changes?.marketId || false;
return (
<span>
{t('Update market state')}
{marketId && <MarketLink className="ml-1" id={marketId} />}
</span>
);
} else if (item.updateNetworkParameter) {
const param = item?.updateNetworkParameter?.changes?.key || false;
return (
<span>
{t('Update network parameter')}
{param && <NetworkParameterLink className="ml-1" parameter={param} />}
</span>
);
} else if (item.updateReferralProgram) {
return <span>{t('Update referral program')}</span>;
} else if (item.updateSpotMarket) {
const marketId = item?.updateSpotMarket?.marketId || '';
return (
<span>
{t('Update spot market')}
<MarketLink className="ml-1" id={marketId} />
</span>
);
} else if (item.updateVolumeDiscountProgram) {
return <span>{t('Update volume discount program')}</span>;
}
return <span>{t('Unknown proposal type')}</span>;
};
@@ -14,18 +14,16 @@ export function format(date: string | undefined, def: string) {
return new Date().toLocaleDateString() || def; return new Date().toLocaleDateString() || def;
} }
type Proposal = Extract< export function getDate(
ExplorerProposalStatusQuery['proposal'], data: ExplorerProposalStatusQuery | undefined,
{ __typename?: 'Proposal' } terms: Terms
>; ): string {
export function getDate(proposal: Proposal | undefined, terms: Terms): string {
const DEFAULT = t('Unknown'); const DEFAULT = t('Unknown');
if (!proposal?.state) { if (!data?.proposal?.state) {
return DEFAULT; return DEFAULT;
} }
switch (proposal.state) { switch (data.proposal.state) {
case 'STATE_DECLINED': case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`; return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED': case 'STATE_ENACTED':
@@ -64,11 +62,9 @@ export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
}, },
}); });
const proposal = data?.proposal as Proposal;
return ( return (
<Lozenge className="font-sans text-xs float-right"> <Lozenge className="font-sans text-xs float-right">
{getDate(proposal, terms)} {getDate(data, terms)}
</Lozenge> </Lozenge>
); );
}; };
@@ -2,8 +2,17 @@ import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit'; import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal'; import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal'; import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import type * as Apollo from '@apollo/client';
import type * as Types from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps { interface ProposalStatusIconProps {
id: string; id: string;
} }
@@ -20,38 +29,29 @@ type IconAndLabel = {
* @param data a data result from useExplorerProposalStatusQuery * @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name * @returns Icon name
*/ */
export function useIconAndLabelForStatus(id: string): IconAndLabel { export function getIconAndLabelForStatus(
const { data, loading, error } = useExplorerProposalStatusQuery({ res: ProposalQueryResult
variables: { ): IconAndLabel {
id,
},
});
const proposal = data?.proposal as Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const DEFAULT: IconAndLabel = { const DEFAULT: IconAndLabel = {
icon: 'error', icon: 'error',
label: t('Proposal state unknown'), label: t('Proposal state unknown'),
}; };
if (loading) { if (res.loading) {
return { return {
icon: 'more', icon: 'more',
label: t('Loading data'), label: t('Loading data'),
}; };
} }
if (!data?.proposal || error) { if (!res?.data?.proposal || res.error) {
return { return {
icon: 'error', icon: 'error',
label: error?.message || DEFAULT.label, label: res.error?.message || DEFAULT.label,
}; };
} }
switch (proposal.state) { switch (res.data.proposal.state) {
case 'STATE_DECLINED': case 'STATE_DECLINED':
return { return {
icon: 'stop', icon: 'stop',
@@ -99,7 +99,13 @@ export function useIconAndLabelForStatus(id: string): IconAndLabel {
/** /**
*/ */
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => { export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = useIconAndLabelForStatus(id); const { icon, label } = getIconAndLabelForStatus(
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
return ( return (
<div className="float-left mr-3"> <div className="float-left mr-3">
@@ -1,4 +1,6 @@
import type { ProposalTerms } from '../tx-proposal';
import { useState } from 'react'; import { useState } from 'react';
import type { components } from '../../../../../types/explorer';
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
import ProposalLink from '../../../links/proposal-link/proposal-link'; import ProposalLink from '../../../links/proposal-link/proposal-link';
import truncate from 'lodash/truncate'; import truncate from 'lodash/truncate';
@@ -7,12 +9,7 @@ import ReactMarkdown from 'react-markdown';
import { ProposalDate } from './proposal-date'; import { ProposalDate } from './proposal-date';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { ProposalTerms } from '../tx-proposal';
import type { components } from '../../../../../types/explorer';
import { BatchItem } from './batch-item';
type Rationale = components['schemas']['vegaProposalRationale']; type Rationale = components['schemas']['vegaProposalRationale'];
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
type ProposalTermsDialog = { type ProposalTermsDialog = {
open: boolean; open: boolean;
@@ -24,7 +21,6 @@ interface ProposalSummaryProps {
id: string; id: string;
rationale?: Rationale; rationale?: Rationale;
terms?: ProposalTerms; terms?: ProposalTerms;
batch?: Batch;
} }
/** /**
@@ -35,7 +31,6 @@ export const ProposalSummary = ({
id, id,
rationale, rationale,
terms, terms,
batch,
}: ProposalSummaryProps) => { }: ProposalSummaryProps) => {
const [dialog, setDialog] = useState<ProposalTermsDialog>({ const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false, open: false,
@@ -77,18 +72,6 @@ export const ProposalSummary = ({
</ReactMarkdown> </ReactMarkdown>
</div> </div>
)} )}
{batch && (
<section className="pt-2 text-sm leading-tight my-3">
<h2 className="text-lg pb-1">{t('Changes')}</h2>
<ol>
{batch.map((change, index) => (
<li className="ml-4 list-decimal" key={`batch-${index}`}>
<BatchItem item={change} />
</li>
))}
</ol>
</section>
)}
<div className="pt-5"> <div className="pt-5">
<button className="underline max-md:hidden mr-5" onClick={openDialog}> <button className="underline max-md:hidden mr-5" onClick={openDialog}>
{t('View terms')} {t('View terms')}
@@ -1,54 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerReferralCodeOwnerQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerReferralCodeOwnerQuery = { __typename?: 'Query', referralSets: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', createdAt: any, updatedAt: any, referrer: string } } | null> } };
export const ExplorerReferralCodeOwnerDocument = gql`
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
`;
/**
* __useExplorerReferralCodeOwnerQuery__
*
* To run a query within a React component, call `useExplorerReferralCodeOwnerQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerReferralCodeOwnerQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerReferralCodeOwnerQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerReferralCodeOwnerQuery(baseOptions: Apollo.QueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
}
export function useExplorerReferralCodeOwnerLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
}
export type ExplorerReferralCodeOwnerQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerQuery>;
export type ExplorerReferralCodeOwnerLazyQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerLazyQuery>;
export type ExplorerReferralCodeOwnerQueryResult = Apollo.QueryResult<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>;
@@ -1,11 +0,0 @@
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
@@ -1,95 +0,0 @@
import { render, waitFor } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { ReferralCodeOwner } from './referral-code-owner';
import type { ReferralCodeOwnerProps } from './referral-code-owner';
import { ExplorerReferralCodeOwnerDocument } from './__generated__/code-owner';
const renderComponent = (
props: ReferralCodeOwnerProps,
mocks: MockedResponse[]
) => {
return render(
<MockedProvider mocks={mocks}>
<MemoryRouter>
<ReferralCodeOwner {...props} />
</MemoryRouter>
</MockedProvider>
);
};
describe('ReferralCodeOwner', () => {
it('should render loading state', () => {
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
expect(getByText('Loading...')).toBeInTheDocument();
});
it('should render error state', async () => {
const errorMessage = 'Error fetching referrer: ABC123';
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
error: new Error('nope'),
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
await waitFor(() => {
expect(getByText(errorMessage)).toBeInTheDocument();
});
});
it('should render link to referring party', async () => {
const referrerId = 'DEF789';
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
result: {
data: {
referralSets: {
edges: [
{
node: {
__typename: 'ReferralSet',
referrer: referrerId,
createdAt: '123',
updatedAt: '456',
},
},
],
},
},
},
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
await waitFor(() => {
expect(getByText(referrerId)).toBeInTheDocument();
});
});
});
@@ -1,26 +0,0 @@
import { TableCell } from '../../../table';
import { useExplorerReferralCodeOwnerQuery } from './__generated__/code-owner';
import { PartyLink } from '../../../links';
export interface ReferralCodeOwnerProps {
code: string;
}
/**
* Render the owner of a referral code
*/
export const ReferralCodeOwner = ({ code }: ReferralCodeOwnerProps) => {
const { data, error, loading } = useExplorerReferralCodeOwnerQuery({
variables: {
id: code,
},
});
const referrer = data?.referralSets.edges[0]?.node.referrer || '';
return (
<TableCell>
{loading && 'Loading...'}
{error && `Error fetching referrer: ${code}`}
{referrer.length > 0 && <PartyLink id={referrer} />}
</TableCell>
);
};
@@ -1,93 +0,0 @@
import { render } from '@testing-library/react';
import { ReferralTeam } from './team';
import type { CreateReferralSet } from './team';
import { MockedProvider } from '@apollo/client/testing';
describe('ReferralTeam', () => {
const team = {
name: 'Test Team',
teamUrl: 'https://example.com/team',
avatarUrl: 'https://example.com/avatar',
closed: false,
};
const mockTx: CreateReferralSet = {
team,
};
const mockId = '123456';
const mockCreator = 'JohnDoe';
it('should render the team name', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Test Team')).toBeInTheDocument();
});
it('should render the team ID', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Id')).toBeInTheDocument();
expect(getByText(mockId)).toBeInTheDocument();
});
it('should render the creator', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Creator')).toBeInTheDocument();
expect(getByText(mockCreator)).toBeInTheDocument();
});
it('should render the team URL', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Team URL')).toBeInTheDocument();
expect(getByText(team.teamUrl)).toBeInTheDocument();
});
it('should render the avatar URL', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Avatar')).toBeInTheDocument();
expect(getByText(team.avatarUrl)).toBeInTheDocument();
});
it('should render the open status as a tick if closed is falsy', () => {
const { getByTestId } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByTestId('open-yes')).toBeInTheDocument();
});
it('should render the open status as a cross if it is truthy', () => {
const m = {
team: {
closed: true,
},
};
const { getByTestId } = render(
<MockedProvider>
<ReferralTeam tx={m} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByTestId('open-no')).toBeInTheDocument();
});
});
@@ -1,73 +0,0 @@
import {
VegaIcon,
Icon,
KeyValueTable,
KeyValueTableRow,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
import Hash from '../../../links/hash';
import { t } from 'i18next';
import { PartyLink } from '../../../links';
export type CreateReferralSet = components['schemas']['v1CreateReferralSet'];
export type ReferralTeam = CreateReferralSet['team'];
export interface ReferralTeamProps {
tx: CreateReferralSet;
id: string;
creator: string;
}
/**
* Renders the details for a team in a CreateReferralSet or UpdateReferralSet transaction.
*
* Intentionally does not render the avatar image or link to the team url.
*/
export const ReferralTeam = ({ tx, id, creator }: ReferralTeamProps) => {
if (!tx.team) {
return null;
}
return (
<section>
<div className="inline-block mr-2 leading-none">
<VegaIcon name={VegaIconNames.TEAM} />
</div>
{tx.team.name && (
<h3 className="inline-block leading-loose">{tx.team.name}</h3>
)}
<div className="min-w-fit max-w-2xl block">
<KeyValueTable>
<KeyValueTableRow>
{t('Id')}
<Hash text={id} truncate={false} />
</KeyValueTableRow>
<KeyValueTableRow>
{t('Creator')}
<PartyLink id={creator} truncate={false} />
</KeyValueTableRow>
{tx.team.teamUrl && (
<KeyValueTableRow>
{t('Team URL')}
<Hash text={tx.team.teamUrl} truncate={false} />
</KeyValueTableRow>
)}
{tx.team.avatarUrl && (
<KeyValueTableRow>
{t('Avatar')}
<Hash text={tx.team.avatarUrl} truncate={false} />
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('Open')}
<span data-testid={!tx.team.closed ? 'open-yes' : 'open-no'}>
{!tx.team.closed ? <Icon name="tick" /> : <Icon name="cross" />}
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
</section>
);
};
@@ -9,7 +9,6 @@ import { Time } from '../../../time';
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code'; import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
import { TxDataView } from '../../tx-data-view'; import { TxDataView } from '../../tx-data-view';
import Hash from '../../../links/hash'; import Hash from '../../../links/hash';
import { Signature } from '../../../signature/signature';
interface TxDetailsSharedProps { interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -27,8 +26,7 @@ export const sharedHeaderProps = {
className: 'align-top', className: 'align-top',
}; };
// The incoming type field is usually the right thing to show. Exceptions are listed here const Labels: Record<BlockExplorerTransactionResult['type'], string> = {
const LabelOverrides: Record<BlockExplorerTransactionResult['type'], string> = {
'Stop Orders Submission': 'Stop Order', 'Stop Orders Submission': 'Stop Order',
'Stop Orders Cancellation': 'Cancel Stop Order', 'Stop Orders Cancellation': 'Cancel Stop Order',
}; };
@@ -51,7 +49,7 @@ export const TxDetailsShared = ({
const time: string = blockData?.result.block.header.time || ''; const time: string = blockData?.result.block.header.time || '';
const height: string = blockData?.result.block.header.height || txData.block; const height: string = blockData?.result.block.header.height || txData.block;
const type = LabelOverrides[txData.type] || txData.type; const type = Labels[txData.type] || txData.type;
return ( return (
<> <>
@@ -77,12 +75,6 @@ export const TxDetailsShared = ({
<BlockLink height={height} /> <BlockLink height={height} />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Signature')}</TableCell>
<TableCell>
<Signature signature={txData.signature} />
</TableCell>
</TableRow>
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Time')}</TableCell> <TableCell {...sharedHeaderProps}>{t('Time')}</TableCell>
<TableCell> <TableCell>
@@ -33,15 +33,6 @@ const AccountType: Record<AccountTypes, string> = {
ACCOUNT_TYPE_HOLDING: 'Holding', ACCOUNT_TYPE_HOLDING: 'Holding',
ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution', ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution',
ACCOUNT_TYPE_LP_LIQUIDITY_FEES: 'LP Liquidity Fees', ACCOUNT_TYPE_LP_LIQUIDITY_FEES: 'LP Liquidity Fees',
ACCOUNT_TYPE_NETWORK_TREASURY: 'Network Treasury',
ACCOUNT_TYPE_VESTING_REWARDS: 'Vesting Rewards',
ACCOUNT_TYPE_VESTED_REWARDS: 'Vested Rewards',
ACCOUNT_TYPE_REWARD_AVERAGE_POSITION: 'Reward Average Position',
ACCOUNT_TYPE_REWARD_RELATIVE_RETURN: 'Reward Relative Return',
ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY: 'Reward Return Volatility',
ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING: 'Reward Validator Ranking',
ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD: 'Pending Fee Referral Reward',
ACCOUNT_TYPE_ORDER_MARGIN: 'Order Margin',
}; };
interface TransferParticipantsProps { interface TransferParticipantsProps {

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