Compare commits

..
2492 changed files with 187445 additions and 136101 deletions
-4
View File
@@ -3,7 +3,3 @@ apps/**/node_modules/*
tmp/*
.dockerignore
dockerfiles
node_modules
.git
.github
.vscode
+3 -2
View File
@@ -14,7 +14,8 @@ What we need to achieve and who for
## Tasks
- [ ]
- [ ]
- [ ] What do we need to do first
- [ ] and then what?
- [ ] Etc.
## Additional details / background info
-21
View File
@@ -1,21 +0,0 @@
---
name: Release
about: A template to outline the steps needed to for a successful release of our frontend apps
title: 'Release [add dapp version]-core-[add core version]'
labels:
assignees: ''
---
### Tasks
- [ ] Review [link to core release](xxx)
- [ ] Tag frontend-monorepo
- [ ] Create release and generate release notes
- [ ] Run `@smoke` tests
- [ ] Run `@regression` tests
- [ ] Run `@slow` tests
- [ ] Explorative testing of key flows
- [ ] Set `release/[network]` to tagged commit
- [ ] Verify builds (on Netlify and Fleek) are successful
- [ ] Verify build has been deployed
- [ ] Smoke testing on deployed app
+4 -7
View File
@@ -22,14 +22,11 @@ So that
## Tasks
- [ ] UX (if needed)
- [ ] Design (if needed)
- [ ] Explore and sketch
- [ ] Team and stakeholder review
- [ ] Specs reviewed and created or adjusted
- [ ] Implementation
- [ ] Testing (unit and/or e2e)
- [ ] Code review
- [ ] QA review
- [ ] Visual Design
- [ ] Team review
- [ ] Etc.
## Sketch
@@ -1,18 +1,6 @@
outputs:
token:
description: 'api-token of wallet'
value: ${{ steps.generate-api-token.outputs.api-token }}
runs:
using: 'composite'
steps:
- name: Vegacapsule version
shell: bash
run: vegacapsule version
- name: Vega wallet version
shell: bash
run: vega wallet software version
- name: Start nomad
shell: bash
run: vegacapsule nomad &
@@ -20,32 +8,3 @@ runs:
- name: Bootstrap network
shell: bash
run: vegacapsule network bootstrap --config-path=./frontend-monorepo/vegacapsule/config.hcl --force
- name: Initialize wallet
shell: bash
run: vega wallet init -f --home ~/.vegacapsule/testnet/wallet
- name: Import wallet
shell: bash
run: vega wallet import -w capsule_wallet --recovery-phrase-file ./frontend-monorepo/vegacapsule/recovery -p ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet
- name: Generate second public key
shell: bash
run: vega wallet key generate -w capsule_wallet -p ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet
- name: Import network
shell: bash
run: vega wallet network import --force --from-file ./frontend-monorepo/vegacapsule/wallet-config.toml --home ~/.vegacapsule/testnet/wallet
- name: Init api-token
shell: bash
run: vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file ./frontend-monorepo/vegacapsule/passphrase
- name: Generate api-token
id: generate-api-token
shell: bash
run: echo api-token=$(vega wallet api-token generate --wallet-name capsule_wallet --tokens-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --wallet-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet | grep -Eo '[a-zA-Z0-9]{64}') >> $GITHUB_OUTPUT
- name: Start service using capsule network
shell: bash
run: vega wallet service run -n DV --load-tokens --tokens-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet &
@@ -0,0 +1,38 @@
inputs:
passphrase:
description: 'Wallet password'
outputs:
token:
description: 'api-token of wallet'
value: ${{ steps.generate-api-token.outputs.api-token }}
runs:
using: 'composite'
steps:
- name: Initialize wallet
shell: bash
run: vega wallet init -f --home ~/.vegacapsule/testnet/wallet
- name: Import wallet
shell: bash
run: vega wallet import -w capsule_wallet --recovery-phrase-file ./frontend-monorepo/vegacapsule/recovery -p ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet
- name: Generate second public key
shell: bash
run: vega wallet key generate -w capsule_wallet -p ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet
- name: Import network
shell: bash
run: vega wallet network import --force --from-file ./frontend-monorepo/vegacapsule/wallet-config.toml --home ~/.vegacapsule/testnet/wallet
- name: Init api-token
shell: bash
run: vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file ./frontend-monorepo/vegacapsule/passphrase
- name: Generate api-token
id: generate-api-token
shell: bash
run: echo api-token=$(vega wallet api-token generate --wallet-name capsule_wallet --tokens-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --wallet-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet | grep -Eo '[a-zA-Z0-9]{64}') >> $GITHUB_OUTPUT
- name: Start service using capsule network
shell: bash
run: vega wallet service run -n DV --load-tokens --tokens-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet &
@@ -0,0 +1,48 @@
inputs:
recovery:
description: 'Recovery phrase'
passphrase:
description: 'Wallet password'
outputs:
token:
description: 'api-token of wallet'
value: ${{ steps.generate-api-token.outputs.api-token }}
runs:
using: 'composite'
steps:
- name: Create passphrase
shell: bash
run: echo "${{ inputs.passphrase }}" > ./passphrase
- name: Create recovery
shell: bash
run: echo "${{ inputs.recovery }}" > ./recovery
- name: Initialize wallet
shell: bash
run: vega wallet init -f --home ~/.vegacapsule/testnet/wallet
- name: Import wallet
shell: bash
run: vega wallet import -w UI_Trading_Test --recovery-phrase-file ./recovery -p ./passphrase --home ~/.vegacapsule/testnet/wallet
- name: Create public key 2
shell: bash
run: vega wallet key generate -w UI_Trading_Test -p ./passphrase --home ~/.vegacapsule/testnet/wallet
- name: Import network
shell: bash
run: vega wallet network import --from-url="https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml" --force --home ~/.vegacapsule/testnet/wallet
- name: Init api-token
shell: bash
run: vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file passphrase
- name: Generate api-token
id: generate-api-token
shell: bash
run: echo api-token=$(vega wallet api-token generate --wallet-name UI_Trading_Test --tokens-passphrase-file passphrase --wallet-passphrase-file passphrase --home ~/.vegacapsule/testnet/wallet | grep -Eo '[a-zA-Z0-9]{64}') >> $GITHUB_OUTPUT
- name: Start service using stagnet3 network
shell: bash
run: vega wallet service run -n stagnet3 --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet &
@@ -1,82 +0,0 @@
name: After Release
on:
release:
types: [published]
jobs:
after-release:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: resolve ipfs hashes for release
run: |
echo "Name: ${{ github.event.release.name }}"
echo "Description: ${{ github.event.release.body }}"
echo "Tag: ${{ github.event.release.tag_name }}"
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
echo "Commit: $commit"
until docker pull vegaprotocol/trading:$commit; do
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
echo IPFS_V0=$(cat ipfs-hash) >> $GITHUB_ENV
echo IPFS_V1=$(ipfs cid format -v 1 -b base32 $(cat ipfs-hash)) >> $GITHUB_ENV
- name: Edit Release
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
body: |
___
# Deployments
* https://explorer.vega.xyz
* https://governance.vega.xyz
# IPFS releases
The IPFS hash of this release of the Trading app is:
CIDv0: ${{ env.IPFS_V0 }}
CIDv1: ${{ env.IPFS_V1 }}
You can always access the latest IPFS release by visiting [console.vega.xyz](https://console.vega.xyz).
You can also access Trading directly from an IPFS gateway.
BEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).
Your settings are not remembered across different URLs.
IPFS gateways:
https://${{ env.IPFS_V1 }}.ipfs.dweb.link/
https://${{ env.IPFS_V1 }}.ipfs.cf-ipfs.com/
ipfs://${{ env.IPFS_V0 }}/
+1 -1
View File
@@ -13,7 +13,7 @@ env:
jobs:
add_issue:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: 'Add issue to project board'
run: |
-16
View File
@@ -1,16 +0,0 @@
name: 'Check if branch is shorter than 52 chars'
on: pull_request
jobs:
branch-naming-rules:
runs-on: ubuntu-latest
steps:
# echo "branches that are longer than 51 chars can't be parsed by kubernetes to create previews. Each app has prefix of it's name like: 'governance-' (12 chars), what leaves 51 max branch length"
# current parsable length: $( git rev-parse --abbrev-ref HEAD | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | wc -c)
- uses: deepakputhraya/action-branch-name@master
with:
# regex: '([a-z])+\/([a-z])+' # Regex the branch should match. This example enforces grouping
# allowed_prefixes: 'feature,stable,fix' # All branches should start with the given prefix
# ignore: master,develop # Ignore exactly matching branch names from convention
min_length: 1 # Min length of the branch name
max_length: 51 # Max length of the branch name
@@ -0,0 +1,52 @@
name: Cypress tests -- manual trigger
# This workflow runs the frontend tests against chosen branch
on:
workflow_dispatch:
inputs:
project:
description: 'Project'
required: true
type: choice
options:
- console-lite-e2e
- explorer-e2e
- liquidity-provision-dashboard-e2e
- stats-e2e
- token-e2e
- trading-e2e
tags:
description: 'Test tags to run'
required: true
type: string
default: '@smoke, @regression, @slow'
skip-nx-cache:
description: 'Add --skip-nx-cache to cypress test'
required: false
type: boolean
default: false
jobs:
manual:
name: Run Cypress tests -- manual trigger
runs-on: ubuntu-latest
steps:
- name: Set tags
run: echo TAGS="--env.grepTags '[ ${{ github.event.inputs.tags }} ]'" >> $GITHUB_ENV
- name: Set --skip-nx-cache flag
if: ${{ github.event.inputs.skip-nx-cache == 'true' }}
run: echo SKIP_NX_CACHE="--skip-nx-cache" >> $GITHUB_ENV
outputs:
skip-cache: ${{env.SKIP_NX_CACHE}}
tags: ${{env.TAGS}}
dispatch:
needs: manual
uses: ./.github/workflows/tests-dispatcher.yml
secrets: inherit
with:
project: ${{ inputs.project }}
skip-cache: ${{needs.manual.outputs.skip-cache}}
tags: ${{needs.manual.outputs.tags}}
@@ -0,0 +1,17 @@
name: Cypress tests -- night run
# This workflow runs the frontend tests against latest develop of the core to preempt breaking changes
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
jobs:
nightly:
uses: ./.github/workflows/tests-dispatcher.yml
secrets: inherit
with:
project: '[console-lite-e2e, explorer-e2e, liquidity-provision-dashboard-e2e, stats-e2e, token-e2e, trading-e2e]'
tags: --env.grepTags '[ @smoke, @regression, @slow ]'
night-run: true
+61
View File
@@ -0,0 +1,61 @@
name: Cypress tests - PR
on:
push:
branches:
- develop
- main
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
pr:
runs-on: ubuntu-latest
steps:
# Checkout front ends
- name: Checkout frontend mono repo
uses: actions/checkout@v2
with:
fetch-depth: 0
path: './frontend-monorepo'
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
# Check SHAs
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
working-directory: frontend-monorepo
main-branch-name: ${{ github.base_ref || github.ref_name }}
set-environment-variables-for-job: true
# See affected projects
- name: See affected apps
run: echo AFFECTED=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects) >> $GITHUB_ENV
working-directory: frontend-monorepo
outputs:
projects: ${{ env.AFFECTED }}
dispatch:
needs: pr
uses: ./.github/workflows/tests-dispatcher.yml
secrets: inherit
with:
project: ${{ needs.pr.outputs.projects }}
tags: "--env.grepTags='[ @smoke, @regression ]'"
-293
View File
@@ -1,293 +0,0 @@
name: CI/CD
on:
push:
branches:
- release/*
- develop
- main
# uncomment pull_request and comment pull_request_target to test CI changes against feature branch not target branch (develop)
# pull_request:
pull_request_target:
types:
- opened
- ready_for_review
- reopened
- edited
- synchronize
jobs:
node-modules:
runs-on: ubuntu-22.04
name: 'Cache yarn modules'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Cache node modules
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# comment out "restore-keys" if you need to rebuild yarn from 0
restore-keys: |
${{ runner.os }}-cache-node-modules-
- name: Setup node
uses: actions/setup-node@v3
if: steps.cache.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
lint-pr-title:
needs: node-modules
if: ${{ github.event_name == 'pull_request' }}
name: Verify PR title
uses: ./.github/workflows/lint-pr.yml
secrets: inherit
lint-test-build:
timeout-minutes: 60
needs: node-modules
runs-on: ubuntu-22.04
name: '(CI) lint + unit test + build'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v3
with:
main-branch-name: develop
- name: Check formatting
run: yarn nx format:check
- name: Lint affected
run: yarn nx affected:lint --max-warnings=0
- name: Build affected spec
run: yarn nx affected --target=build-spec
- name: Test affected
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
# See affected apps
- name: See affected apps
run: |
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
echo ">>>> debug"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo "Affected: ${affected}"
echo "Branch slug: ${branch_slug}"
echo ">>>> eof debug"
projects_array=()
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
# parse if affected is any of three main applications, if none - use all of them
if echo "$affected" | grep -q governance; then
echo "Governance is affected"
projects_array+=("governance")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
fi
if echo "$affected" | grep -q trading; then
echo "Trading is affected"
projects_array+=("trading")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
fi
if echo "$affected" | grep -q explorer; then
echo "Explorer is affected"
projects_array+=("explorer")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
if [[ ${#projects_array[@]} -eq 0 ]]; then
projects_array=("governance" "trading" "explorer")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
# applications parsed before this loop are applicable for running e2e-tests
projects_e2e_array=()
for project in "${projects_array[@]}"; do
projects_e2e_array+=("${project}-e2e")
done
# all applications below this loop are not applicable for running e2e-test
# check if pull request event to deploy tools
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects_array+=("multisig-signer")
fi
# those apps deploy only from develop to mainnet
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on s3"
projects_array+=("multisig-signer")
fi
if echo "$affected" | grep -q static; then
echo "static is affected"
echo "Deploying static on s3"
projects_array+=("static")
fi
if echo "$affected" | grep -q ui-toolkit; then
echo "ui-toolkit is affected"
echo "Deploying ui-toolkit on s3"
projects_array+=("ui-toolkit")
fi
fi
echo "Projects: ${projects_array[@]}"
echo "Projects E2E: ${projects_e2e_array[@]}"
projects_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_array[@]}")
projects_e2e_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_e2e_array[@]}")
echo PROJECTS_E2E=$projects_e2e_json >> $GITHUB_ENV
echo PROJECTS=$projects_json >> $GITHUB_ENV
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }}
preview_governance: ${{ env.PREVIEW_GOVERNANCE }}
preview_trading: ${{ env.PREVIEW_TRADING }}
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
preview_tools: ${{ env.PREVIEW_TOOLS }}
cypress:
needs: lint-test-build
name: '(CI) cypress'
if: ${{ needs.lint-test-build.outputs.projects-e2e != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
tags: '@smoke @regression'
publish-dist:
needs: lint-test-build
name: '(CD) publish dist'
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
uses: ./.github/workflows/publish-dist.yml
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects }}
dist-check:
runs-on: ubuntu-latest
needs:
- publish-dist
- lint-test-build
if: ${{ github.event_name == 'pull_request' }}
timeout-minutes: 60
name: '(CD) comment preview links'
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: Previews
- name: Wait for deployments
run: |
# https://stackoverflow.com/questions/3183444/check-for-valid-link-url
regex='(https?|ftp|file)://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]'
if [[ "${{ needs.lint-test-build.outputs.preview_governance }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_governance }}"; do
echo "waiting for governance preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_explorer }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_explorer }}"; do
echo "waiting for explorer preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_trading }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_trading }}"; do
echo "waiting for trading preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_tools }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_tools }}"; do
echo "waiting for tools preview"
sleep 5
done
fi
- name: Create comment
uses: peter-evans/create-or-update-comment@v3
if: ${{ steps.fc.outputs.comment-id == 0 }}
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Previews:
* governance: ${{ needs.lint-test-build.outputs.preview_governance }}
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
* trading: ${{ needs.lint-test-build.outputs.preview_trading }}
* tools: ${{ needs.lint-test-build.outputs.preview_tools }}
# Report single result at the end, to avoid mess with required checks in PR
cypress-check:
name: '(CI) cypress - check'
if: ${{ always() }}
needs: cypress
runs-on: ubuntu-22.04
steps:
- run: |
result="${{ needs.cypress.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi
@@ -0,0 +1,69 @@
name: Cypress - console-lite
on:
workflow_call:
inputs:
trigger:
required: true
type: string
default: 'false'
skip-cache:
required: false
type: string
tags:
required: false
type: string
jobs:
console-lite-e2e:
if: ${{ inputs.trigger == 'true' }}
runs-on: self-hosted
steps:
- name: Vega version
run: vega version
# Checkout front ends
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
path: './frontend-monorepo'
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
######
## Setup a Vega wallet for our user
######
- name: Set up Vegawallet
id: setup-vega
uses: ./frontend-monorepo/.github/actions/setup-vegawallet
with:
recovery: ${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
# To make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
- name: Run Cypress tests
run: npx nx run console-lite-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
working-directory: frontend-monorepo
env:
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
+106
View File
@@ -0,0 +1,106 @@
name: Cypress - explorer
on:
workflow_call:
inputs:
trigger:
required: true
type: string
default: 'false'
skip-cache:
required: false
type: string
tags:
required: false
type: string
night-run:
required: false
type: boolean
default: false
jobs:
explorer-e2e:
if: ${{ inputs.trigger == 'true' }}
runs-on: self-hosted
timeout-minutes: 30
steps:
- name: Vega version
run: vega version
- name: Vegacapsule version
run: vegacapsule version
# Checkout front ends
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
path: './frontend-monorepo'
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
#######
## Build and run Vegacapsule network
#######
- name: Build and run Vegacapsule network
uses: ./frontend-monorepo/.github/actions/run-vegacapsule
######
## Setup a Vega wallet for our user
######
- name: Set up Vegawallet for docker
id: setup-vega
uses: ./frontend-monorepo/.github/actions/setup-vegawallet-docker
with:
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
######
## Run some tests
######
# To make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
- name: Run Cypress tests
run: npx nx run explorer-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
working-directory: frontend-monorepo
env:
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
CYPRESS_NIGHTLY_RUN: ${{ inputs.night-run }}
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
######
## Upload logs
######
# Artifact path is not valid: /ganache-1/capsule-logscolletor.stderr-2022-12-22T10:20:30Z.log. Contains the following character: Colon :
- name: Rename files to allow archive
if: ${{ always() }}
run: |
while read -r file; do
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
- uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: logs
path: /home/runner/.vegacapsule/testnet/logs
@@ -0,0 +1,47 @@
name: Cypress - liquidity provision dashboard
on:
workflow_call:
inputs:
trigger:
required: true
type: string
default: 'false'
jobs:
liquidity-provision-dashboard-e2e:
timeout-minutes: 10
if: ${{ inputs.trigger == 'true' }}
runs-on: self-hosted
steps:
# Checkout front ends
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
path: './frontend-monorepo'
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
# To make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
- name: Run Cypress tests
run: npx nx run liquidity-provision-dashboard-e2e:e2e --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
working-directory: frontend-monorepo
env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
+2 -7
View File
@@ -13,17 +13,11 @@ on:
jobs:
cypress-run:
name: Run Cypress Trading tests -- live environment
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- name: Run Cypress tests
uses: cypress-io/github-action@v4
with:
@@ -33,4 +27,5 @@ jobs:
config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,33 +0,0 @@
name: Cypress tests -- manual trigger
# This workflow runs the frontend tests against chosen branch
on:
workflow_dispatch:
inputs:
project:
description: 'Project'
required: true
type: choice
options:
- explorer-e2e
- governance-e2e
- trading-e2e
tags:
description: 'Test tags to run'
required: true
type: string
default: '@smoke @regression @slow'
skip-nx-cache:
description: 'Skip NX cache'
required: false
type: boolean
default: false
jobs:
manual:
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: '["${{inputs.project}}"]'
skip-cache: ${{inputs.skip-nx-cache}}
tags: ${{inputs.tags}}
-14
View File
@@ -1,14 +0,0 @@
name: Cypress tests -- night run
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
jobs:
nightly:
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
tags: '@smoke @regression @slow'
+47
View File
@@ -0,0 +1,47 @@
name: Cypress - stats
on:
workflow_call:
inputs:
trigger:
required: true
type: string
default: 'false'
jobs:
stats-e2e:
runs-on: self-hosted
if: ${{ inputs.trigger == 'true' }}
timeout-minutes: 10
steps:
# Checkout front ends
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
path: './frontend-monorepo'
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
# To make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
- name: Run Cypress tests
run: npx nx run stats-e2e:e2e --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
working-directory: frontend-monorepo
env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
@@ -1,31 +1,30 @@
name: (CI) Cypress Run
name: Cypress - token
on:
workflow_call:
inputs:
projects:
trigger:
required: true
type: string
default: 'false'
skip-cache:
required: false
type: boolean
type: string
tags:
required: false
type: string
jobs:
e2e:
strategy:
fail-fast: false
matrix:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 100
token-e2e:
if: ${{ inputs.trigger == 'true' }}
runs-on: self-hosted
timeout-minutes: 60
steps:
# Checks if skip cache was requested
- name: Set skip-nx-cache flag
if: ${{ inputs.skip-cache == true }}
run: echo "SKIP_CACHE=--skip-nx-cache" >> $GITHUB_ENV
- name: Vega version
run: vega version
- name: Vegacapsule version
run: vegacapsule version
# Checkout front ends
- name: Checkout frontend mono repo
@@ -33,7 +32,6 @@ jobs:
with:
fetch-depth: 0
path: './frontend-monorepo'
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
@@ -42,35 +40,46 @@ jobs:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock', 'frontend-monorepo/package.json') }}
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
# Make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
#######
## Build and run Vegacapsule network
#######
######
## Setup Vegacapsule and Vega wallet
######
- name: Run Vegacapsule network and Vega wallet
id: setup-vega
- name: Build and run Vegacapsule network
uses: ./frontend-monorepo/.github/actions/run-vegacapsule
######
## Setup a Vega wallet for our user
######
- name: Set up Vegawallet for docker
id: setup-vega
uses: ./frontend-monorepo/.github/actions/setup-vegawallet-docker
with:
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
######
## Run some tests
######
# To make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
- name: Run Cypress tests
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}"
run: npx nx run token-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
working-directory: frontend-monorepo
env:
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
######
@@ -85,18 +94,8 @@ jobs:
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
- name: Print logs files
- uses: actions/upload-artifact@v2
if: ${{ always() }}
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
- uses: actions/upload-artifact@v3
if: ${{ failure() }}
with:
name: logs-${{ matrix.project }}
name: logs
path: /home/runner/.vegacapsule/testnet/logs
- uses: actions/upload-artifact@v3
if: ${{ failure() }}
with:
name: test-report-${{ matrix.project }}
path: frontend-monorepo/apps/${{ matrix.project }}/cypress/reports
+70
View File
@@ -0,0 +1,70 @@
name: Cypress - trading
on:
workflow_call:
inputs:
trigger:
required: true
type: string
default: 'false'
skip-cache:
required: false
type: string
tags:
required: false
type: string
jobs:
trading-e2e:
if: ${{ inputs.trigger == 'true' }}
timeout-minutes: 30
runs-on: self-hosted
steps:
- name: Vega version
run: vega version
# Checkout front ends
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
path: './frontend-monorepo'
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: |
frontend-monorepo/node_modules
/home/runner/.cache/Cypress
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
# Install frontend dependencies
- name: Install root dependencies
run: yarn install --frozen-lockfile
working-directory: frontend-monorepo
######
## Setup a Vega wallet for our user
######
- name: Set up Vegawallet
id: setup-vega
uses: ./frontend-monorepo/.github/actions/setup-vegawallet
with:
recovery: ${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
# To make sure that all Cypress binaries are installed properly
- name: Install cypress bins
run: yarn cypress install
working-directory: frontend-monorepo
- name: Run Cypress tests
run: npx nx run trading-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
working-directory: frontend-monorepo
env:
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
+9 -12
View File
@@ -8,25 +8,22 @@ on:
jobs:
master:
name: Generate Queries
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
uses: actions/checkout@v2
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.14.0
- name: Install root dependencies
run: yarn install --frozen-lockfile
run: yarn install
- name: Generate queries
run: node ./scripts/get-queries.js
- uses: actions/upload-artifact@v2
with:
name: queries
-31
View File
@@ -1,31 +0,0 @@
---
name: Verify PR title
on:
workflow_call:
jobs:
lint_pr:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+21
View File
@@ -0,0 +1,21 @@
---
name: Verify PR title
on:
pull_request:
types: [opened, ready_for_review, reopened, edited, synchronize]
jobs:
lint_pr:
runs-on: ubuntu-latest
steps:
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.14.0
- name: Install commitlint cli and config
run: npm install @commitlint/cli @commitlint/config-conventional
- name: Create config
run: echo "module.exports = {extends:['@commitlint/config-conventional']};" > commitlint.config.js
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
+32
View File
@@ -0,0 +1,32 @@
name: Generate tranches
on:
schedule:
- cron: '0 */6 * * *'
jobs:
master:
name: Generate Queries
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Generate queries
run: node ./scripts/get-tranches.js
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: 'chore: update tranches'
commit_options: '--no-verify --signoff'
skip_fetch: true
skip_checkout: true
-315
View File
@@ -1,315 +0,0 @@
name: (CD) Publish docker + s3
on:
workflow_call:
inputs:
projects:
required: true
type: string
jobs:
publish-dist:
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- name: Check out code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Set up QEMU
id: quemu
uses: docker/setup-qemu-action@v2
- name: Available platforms
run: echo ${{ steps.qemu.outputs.platforms }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Define dist variables
if: ${{ github.event_name == 'push' }}
run: |
envName=''
domain="vega.rocks"
bucketName=''
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet1"
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
envName="mainnet"
bucketName="tools.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "static" ]]; then
envName="mainnet"
bucketName="static.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
envName="mainnet"
bucketName="ui.vega.rocks"
fi
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
envName="mainnet"
fi
if [[ "${envName}" = "mainnet" ]]; then
domain="vega.xyz"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
elif [[ "${envName}" = "testnet" ]]; then
domain="fairground.wtf"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
fi
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${envName}.${domain}"
fi
echo "bucket name: ${bucketName}"
echo "env name: ${envName}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
echo ENV_NAME=${envName} >> $GITHUB_ENV
- name: Build local dist
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
if [ "${{ matrix.app }}" = "trading" ]; then
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
DIST_LOCATION=dist/apps/trading/exported
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit
else
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
tree dist-result
- name: Build and export to local Docker
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
file: docker/node-outside-docker.Dockerfile
load: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest
if: ${{ github.event_name == 'pull_request' }}
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image
run: |
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash > ${{ matrix.app }}-ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html'
- name: Publish dist as docker image (ghcr)
uses: docker/build-push-action@v3
continue-on-error: true
id: ghcr-push
if: ${{ github.event_name == 'pull_request' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- name: Publish dist as docker image (docker hub)
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
- name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3
if: ${{ steps.ghcr-push.outcome == 'failure' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- name: Publish dist as docker image (docker hub - retry)
uses: docker/build-push-action@v3
if: ${{ steps.dockerhub-push.outcome == 'failure' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
with:
args: --acl private --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ env.BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-1'
SOURCE_DIR: 'dist-result'
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' }}
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Trigger fleek deployment
# release to ipfs happens only on mainnet (represented by main branch) for trading
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
run: |
if echo ${{ github.ref }} | grep -q main; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
elif echo ${{ github.ref }} | grep -q release/testnet; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fi
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
path: 'ipfs-redirect'
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update interstitial page to point to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
# set CID
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
(
cd ipfs-redirect
# configure git
git status
cat .git/config
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
# update CID files
if echo ${{ github.ref }} | grep -q main; then
echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt
elif echo ${{ github.ref }} | grep -q release/testnet; then
echo $new_hash > cidv0-fairground.txt
echo $new_cid > cidv1-fairground.txt
git add cidv0-fairground.txt cidv1-fairground.txt
fi
# create commit
commit_msg="Automated hash update from ${{ github.ref }}"
git commit -m "$commit_msg"
git push -u origin "main"
)
@@ -0,0 +1,126 @@
name: Publish docker containers
'on':
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
publish:
description: 'Publish tag to Docker Hub & GitHub Registry'
required: false
type: boolean
default: false
tag:
description: 'Git Tag to build and publish'
required: false
type: string
default: ''
apps:
description: 'Applications to build and publish'
required: false
type: choice
options:
- '["explorer", "token", "trading"]'
- '["explorer"]'
- '["token"]'
- '["trading"]'
archs:
description: 'Architecture to build and publish'
required: false
type: choice
options:
- linux/amd64, linux/arm64
- linux/amd64
- linux/arm64
jobs:
master:
strategy:
fail-fast: false
matrix:
app: ${{ fromJson(inputs.apps || '["explorer", "token", "trading"]') }}
name: Build the ${{ matrix.app }} image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
with:
ref: ${{ inputs.tag }}
- name: Set up QEMU
id: quemu
uses: docker/setup-qemu-action@v2
- name: Available platforms
run: echo ${{ steps.qemu.outputs.platforms }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine Docker Image tag
id: tags
run: |
hash=$(git rev-parse HEAD|cut -b1-8)
versionTag=${{ inputs.tag || startsWith(github.ref, 'refs/tags/') && github.ref_name || '${hash}' }}
echo ::set-output name=version::${versionTag}
echo ::set-output name=npmVersion::$(cat dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }} | grep FROM | head -n 1 | awk '{print $2}' | cut -d ':' -f 2 | cut -d '-' -f 1 )
- name: Print config
run: |
git rev-parse --verify HEAD
git status
echo "inputs.tag=${{ inputs.tag }}"
echo "inputs.publish=${{ inputs.publish }}"
echo "inputs.apps=${{ inputs.apps }}"
echo "inputs.archs=${{ inputs.archs }}"
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
- uses: actions/setup-node@v3
with:
node-version: ${{ steps.tags.outputs.npmVersion }}
- name: Build frontend dists
run: |
yarn --verbose --pure-lockfile
yarn nx ${{ matrix.app =='trading' && 'export' || 'build' }} ${{ matrix.app }} --pure-lockfile
- name: Build and export to local Docker
uses: docker/build-push-action@v3
with:
context: .
push: false
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: APP=${{ matrix.app }}
load: true
tags: vegaprotocol/${{ matrix.app }}:local
- name: Sanity check docker image
run: |
docker run --rm vegaprotocol/${{ matrix.app }}:local cat .env
docker run --rm vegaprotocol/${{ matrix.app }}:local ls -lah
- name: Build and push to DockerHub
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
push: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: APP=${{ matrix.app }}
platforms: ${{ inputs.archs || 'linux/amd64, linux/arm64' }}
tags: |
vegaprotocol/${{ matrix.app }}:latest
vegaprotocol/${{ matrix.app }}:${{ steps.tags.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
-48
View File
@@ -1,48 +0,0 @@
name: Publish libs to npm
on:
workflow_dispatch:
inputs:
project:
description: 'Monorepo project to publish'
required: true
type: choice
options:
- announcements
- ui-toolkit
- react-helpers
- tailwindcss-config
- types
- utils
- i18n
jobs:
publish:
name: Build & Publish - Tag
runs-on: ubuntu-22.04
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn nx build ${{inputs.project}}
- name: Publish project to @vegaprotocol
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: dist/libs/${{inputs.project}}/package.json
access: public
-84
View File
@@ -1,84 +0,0 @@
name: 'Rollback console'
on:
workflow_dispatch:
inputs:
version:
description: 'Version that should be set on rollback'
required: true
type: string
jobs:
rollback:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Retag mainnet
run: |
docker pull vegaprotocol/trading:${{ inputs.version }}
docker tag vegaprotocol/trading:${{ inputs.version }} vegaprotocol/trading:mainnet
docker push vegaprotocol/trading:mainnet
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
- name: Trigger fleek deployment
run: |
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
path: 'ipfs-redirect'
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update console.vega.xyz DNS to redirect to the new console
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
new_hash=$(cat ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
(
cd ipfs-redirect
git status
branch_name="rollback-to-$new_hash"
git checkout -b "$branch_name"
commit_msg="hash rollback to $new_hash"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "$branch_name" --force-with-lease
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
)
+46
View File
@@ -0,0 +1,46 @@
name: Unit tests & build
on:
push:
branches:
- develop
- main
pull_request:
jobs:
pr:
name: Test and lint - PR
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: ${{ github.base_ref }}
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.14.0
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Check formatting
run: yarn nx format:check
- name: Lint affected
run: yarn nx affected:lint --max-warnings=0
- name: Test affected
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build
- name: Build affected spec
run: yarn nx affected --target=build-spec
+61
View File
@@ -0,0 +1,61 @@
on:
workflow_call:
inputs:
project:
required: true
type: string
skip-cache:
required: false
type: string
tags:
required: false
type: string
night-run:
required: false
type: boolean
jobs:
run-console-lite-e2e:
uses: ./.github/workflows/cypress-console-lite-e2e.yml
secrets: inherit
with:
trigger: ${{ contains(inputs.project, 'console-lite-e2e') || contains(inputs.project, 'console-lite') }}
skip-cache: ${{ inputs.skip-cache }}
tags: ${{ inputs.tags }}
run-explorer-e2e:
uses: ./.github/workflows/cypress-explorer-e2e.yml
secrets: inherit
with:
trigger: ${{ contains(inputs.project, 'explorer-e2e') || contains(inputs.project, 'explorer') }}
skip-cache: ${{ inputs.skip-cache }}
tags: ${{ inputs.tags }}
night-run: ${{ inputs.night-run }}
run-liquidity-e2e:
uses: ./.github/workflows/cypress-liquidity-provision-dashboard-e2e.yml
secrets: inherit
with:
trigger: ${{ contains(inputs.project, 'liquidity-provision-dashboard-e2e') || contains(inputs.project, 'liquidity-provision-dashboard') }}
run-stats-e2e:
uses: ./.github/workflows/cypress-stats-e2e.yml
secrets: inherit
with:
trigger: ${{ contains(inputs.project, 'stats-e2e') || contains(inputs.project, 'stats') }}
run-token-e2e:
uses: ./.github/workflows/cypress-token-e2e.yml
secrets: inherit
with:
trigger: ${{ contains(inputs.project, 'token-e2e') || contains(inputs.project, 'token') }}
skip-cache: ${{ inputs.skip-cache }}
tags: ${{ inputs.tags }}
run-trading-e2e:
uses: ./.github/workflows/cypress-trading-e2e.yml
secrets: inherit
with:
trigger: ${{ contains(inputs.project, 'trading-e2e') || contains(inputs.project, 'trading') }}
skip-cache: ${{ inputs.skip-cache }}
tags: ${{ inputs.tags }}
-6
View File
@@ -2,7 +2,6 @@
# compiled output
/dist
/dist-result
/tmp
/out-tsc
/tools/executors/**/*.js
@@ -47,8 +46,3 @@ cypress.env.json
# Next.js
.next
#cypress
/apps/**/cypress/reports/
/apps/**/cypress/downloads/
/apps/**/fixtures/wallet/node**
+1 -1
View File
@@ -1 +1 @@
16.15.1
16.14.0
+1
View File
@@ -7,4 +7,5 @@ __generated___
apps/static/src/assets/devnet-tranches.json
apps/static/src/assets/mainnet-tranches.json
apps/static/src/assets/stagnet3-tranches.json
apps/static/src/assets/testnet-tranches.json
Vendored
+19 -1
View File
@@ -1,2 +1,20 @@
@Library('vega-shared-library') _
runApprobation ignoreFailure: false, frontendBranch: env.BRANCH_NAME, type: 'frontend'
def commitHash = 'UNKNOWN'
pipeline {
agent any
options {
skipDefaultCheckout true
parallelsAlwaysFailFast()
}
stages {
stage('approbation') {
steps {
sh 'printenv'
checkout scm
runApprobation ignoreFailure: false, frontendBranch: env.BRANCH_NAME, type: 'frontend'
}
}
}
}
-20
View File
@@ -1,20 +0,0 @@
.PHONY: latest-release
latest-release:
gh release list | head -n 1 | awk '{print $1}'
.PHONY: show-latest-release
show-latest-release:
gh release view `gh release list | head -n 1 | awk '{print $1}'`
.PHONY: recalculate-ipfs
recalculate-ipfs:
echo "ipfs hash inside the image"
docker run --rm ${TAG} cat /ipfs-hash
echo "recalculating ipfs hash"
docker run --rm ${TAG} ipfs add -r /usr/share/nginx/html
.PHONY: eject-ipfs-hash
unpack:
docker create --name=dist ${TAG}
docker cp dist:/usr/share/nginx/html dist
docker rm dist
+18 -57
View File
@@ -22,6 +22,10 @@ The utility dApp for interacting with the Vega token and using its' utility. Thi
The block explorer for the Vega network, showing details of raw chain states and the state of markets on the Vega network.
### [Stats](./apps/stats)
An application for the status of the Vega network. Showing block height and other network activity.
### [Static](./apps/static)
Hosting for static content being shared across apps, for example fonts.
@@ -72,7 +76,7 @@ Run `nx serve my-app` for a dev server. Navigate to the port specified in `app/<
In order to generate the schemas for your GraphQL queries, you can run `GRAPHQL_SCHEMA_PATH=[YOUR SCHEMA FILE / API URL HERE] nx run types:generate`.
```bash
export GRAPHQL_SCHEMA_PATH=https://api.n07.testnet.vega.xyz/graphql
export GRAPHQL_SCHEMA_PATH=https://api.n11.testnet.vega.xyz/graphql
yarn nx run types:generate
```
@@ -103,68 +107,25 @@ 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.
# 🐋 Hosting a console
# Docker & Vegacapsule
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).
## Docker
In order to run a container on port 3000:
The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
```
In order to run a container:
```bash
docker run -p 3000:80 [TAG]
```
## Build instructions
The [`docker`](./docker) subfolder has some docker configurations for easily setting up your own hosted version of console either for the web, or ready for pinning on IPFS
### nx build outside the docker
Packaging prepared dist into [`nginx`](https://hub.docker.com/_/nginx)([server configuration](./nginx/nginx.conf)) docker image involves building the application on docker host machine from source.
As a prerequisite you need to perform build of `dist` directory and move its content for specific application to `dist-result` directory. Use following script to do it with a single command:
```bash
./docker/prepare-dist.sh
```
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile docker/node-outside-docker.Dockerfile . --tag=[TAG]
```
### nx build inside the docker
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) example.
```bash
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=$(cat .nvmrc) --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
```
### Computing ipfs-hash of the build
At the moment this feature is important only for `trading` (console) releases.
Each docker build finishes with hash calculation for dist directory. Resulting hash is added to file named as `/ipfs-hash`. Once docker image is produced you can run following commad to display ipfs-hash:
```bash
make recalculate-ipfs TAG=vegaprotocol/trading:{YOUR_VERSION}
```
**updating hash:** recompiling dist directory (even if there are no changed to source code) results in different hash computed by ipfs command.
### Verifying ipfs-hash of existing current application version
An IPFS CID will be attached to every [release](https://github.com/vegaprotocol/frontend-monorepo/releases). If you are intending to pin an application on IPFS, you can check that your build matches by running the following steps:
1. Show latest release by runnning: `make latest-release`. You need to configure [`gh`](https://cli.github.com/) for this step to work, otherwise please provide release manually from [github](https://github.com/vegaprotocol/frontend-monorepo/releases) or [dockerhub](https://hub.docker.com/r/vegaprotocol/trading)
2. Set RELEASE environment variable to value that you want to validate: `export RELEASE=$(make latest-release)` or `export RELEASE=vXX.XX.XX`
3. Set TAG environment variable to image that you want to validate: `export TAG=vegaprotocol/trading:$RELEASE`
4. Download docker image with the desired release `docker pull $TAG`.
5. Recalculate hash: `make recalculate-ipfs`
6. You should see exactly same hash produced by ipfs command as one placed with the release notes: `make show-latest-release`
7. If you want to extract dist from docker image to your local filesystem you can run following command: `make unpack`
8. Now `dist` directory contains valid application build. **it is not possible to calculate same ipfs hash on files that are result of copy operation**
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
## Config
@@ -204,7 +165,7 @@ In order to setup and run vegawallet for e2e capsule tests, in a separate termin
bash setup-vegawallet.sh
```
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/governance-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/token-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
+10
View File
@@ -0,0 +1,10 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
NX_VEGA_ENV=STAGNET3
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789
CYPRESS_VEGA_ENV=STAGNET3
CYPRESS_VEGA_WALLET_API_TOKEN=
+45
View File
@@ -0,0 +1,45 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
projectId: 'et4snf',
e2e: {
setupNodeEvents(on, config) {
require('@cypress/grep/src/plugin')(config);
return config;
},
baseUrl: 'http://localhost:4200',
fileServerFolder: '.',
fixturesFolder: false,
specPattern: './src/integration/*.ts',
excludeSpecPattern: '**/*.js',
modifyObstructiveCode: false,
supportFile: './src/support/index.js',
video: false,
videoUploadOnPasses: false,
videosFolder: '../../dist/cypress/apps/console-lite-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/console-lite-e2e/screenshots',
chromeWebSecurity: false,
viewportWidth: 1440,
viewportHeight: 900,
},
env: {
ETHEREUM_PROVIDER_URL:
'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
VEGA_PUBLIC_KEY:
'47836c253520d2661bf5bed6339c0de08fd02cf5d4db0efee3b4373f20c7d278',
VEGA_PUBLIC_KEY2:
'1a18cdcaaa4f44a57b35a4e9b77e0701c17a476f2b407620f8c17371740cf2e4',
TRUNCATED_VEGA_PUBLIC_KEY: '47836c…c7d278',
TRUNCATED_VEGA_PUBLIC_KEY2: '1a18cd…0cf2e4',
ETHEREUM_WALLET_ADDRESS: '0x265Cc6d39a1B53d0d92068443009eE7410807158',
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
tsConfig: 'tsconfig.json',
TAGS: 'not @todo and not @ignore and not @manual',
TRADING_TEST_VEGA_WALLET_PASSPHRASE: '123',
ETH_WALLET_MNEMONIC:
'ugly gallery notice network true range brave clarify flat logic someone chunk',
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
grepOmitFiltered: true,
},
});
+1
View File
@@ -0,0 +1 @@
declare module '*.scss';
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/console-lite-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "@nrwl/cypress:cypress",
"options": {
"cypressConfig": "apps/console-lite-e2e/cypress.config.js",
"devServerTarget": "console-lite:serve"
},
"configurations": {
"production": {
"devServerTarget": "console-lite:serve:production"
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/console-lite-e2e/**/*.{js,ts}"]
}
},
"build": {
"executor": "@nrwl/workspace:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/console-lite-e2e/"
}
}
},
"tags": [],
"implicitDependencies": ["console-lite"]
}
@@ -0,0 +1,10 @@
describe('simple trading app', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockConsole();
cy.visit('/');
});
it('render', () => {
cy.get('#root').should('exist');
});
});
@@ -0,0 +1,30 @@
describe('console lite header', { tags: '@smoke' }, () => {
beforeEach(() => {
window.localStorage.setItem('theme', 'dark');
cy.visit('/');
});
it('logo should linked home', () => {
cy.get('span').contains('Markets').click();
cy.location('pathname').should('eq', '/markets');
cy.getByTestId('header').find('a').click();
cy.location('pathname').should('eq', '/');
});
it('theme switcher should switch theme', () => {
cy.get('#root').children().eq(0).as('Container');
cy.get('@Container').should('have.css', 'background-color', 'rgb(8, 8, 8)');
cy.getByTestId('theme-switcher').click();
cy.get('@Container').should(
'have.css',
'background-color',
'rgb(255, 255, 255)'
);
});
it('wallet connector should open a dialog', () => {
cy.get('[role="dialog"]').should('not.exist');
cy.getByTestId('connect-vega-wallet').click();
cy.get('[role="dialog"]').should('be.visible');
});
});
@@ -0,0 +1,139 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { MarketsQuery } from '@vegaprotocol/market-list';
import { marketCandlesQuery, marketsQuery } from '@vegaprotocol/mock';
describe('market list', { tags: '@smoke' }, () => {
describe('simple url', () => {
beforeEach(() => {
cy.mockConsole();
cy.visit('/markets');
});
it('selects menus', () => {
cy.get('[aria-label="Sidebar Navigation Menu"] [aria-current]').should(
'have.text',
'Markets'
);
cy.getByTestId('state-trigger').should('have.text', 'Active');
cy.get('[aria-label="Future"]').click();
cy.get('[data-testid="market-assets-menu"] a.active').should(
'have.text',
'All'
);
});
it('navigation should make possibly shortest url', () => {
cy.location('pathname').should('equal', '/markets');
cy.getByTestId('state-trigger').click();
cy.get('[role=menuitemcheckbox]').contains('All').click();
cy.location('pathname').should('equal', '/markets/all');
cy.get('[aria-label="Future"]').click();
cy.location('pathname').should('eq', '/markets/all/Future');
let asset = '';
cy.getByTestId('market-assets-menu')
.children()
.then((children) => {
if (children.length > 1) {
asset = children[1].innerText;
if (asset) {
cy.wrap(children[1]).click();
cy.location('pathname').should(
'match',
new RegExp(`/markets/all/Future/${asset}`, 'i')
);
cy.get('a').contains('All Markets').click();
cy.location('pathname').should('eq', '/markets/all');
}
}
});
cy.getByTestId('state-trigger').click();
cy.get('[role=menuitemcheckbox]').contains('Active').click();
cy.location('pathname').should('equal', '/markets');
});
});
describe('url params should select filters', () => {
beforeEach(() => {
cy.mockConsole();
});
it('suspended status', () => {
cy.visit('/markets/Suspended');
cy.getByTestId('state-trigger').should('have.text', 'Suspended');
});
it('last asset (if exists)', () => {
cy.visit('/markets');
cy.wait('@Markets').then((filters) => {
const data: MarketsQuery | undefined = filters?.response?.body?.data;
if (data?.marketsConnection?.edges.length) {
const asset =
data?.marketsConnection?.edges[0].node.tradableInstrument.instrument
.product.settlementAsset.symbol;
cy.visit(`/markets/Suspended/Future/${asset}`);
cy.getByTestId('market-assets-menu')
.find('a.active')
.should('have.text', asset);
}
});
});
it('Future product', () => {
cy.visit('/markets/Suspended/Future');
cy.getByTestId('market-products-menu')
.find('a.active')
.should('have.text', 'Future');
});
});
describe('long list of results should be handled properly', () => {
beforeEach(() => {
cy.mockConsole();
cy.viewport(1440, 900);
const market = marketsQuery().marketsConnection?.edges[0];
const edges = new Array(1000)
.fill('')
.map(() => Object.assign({}, market));
cy.mockGQL((req) => {
aliasGQLQuery(
req,
'Markets',
marketsQuery({ marketsConnection: { edges } })
);
aliasGQLQuery(req, 'MarketsCandles', marketCandlesQuery());
});
performance.mark('start-1k');
cy.visit('/markets');
cy.wait('@Markets');
});
it('handles 1000 markets', () => {
cy.get('.ag-center-cols-container', { timeout: 50000 }).then(() => {
performance.mark('end-1k');
performance.measure('load-1k', 'start-1k', 'end-1k');
const measure = performance.getEntriesByName('load-1k')[0];
expect(measure.duration).lte(20000);
cy.log(`Ag-grid 1k load took ${measure.duration} milliseconds.`);
cy.get('.ag-root').should('have.attr', 'aria-rowcount', '1001');
cy.get('.ag-center-cols-container')
.find('[role="row"]')
.its('length')
.then((length) => expect(length).to.be.closeTo(20, 3));
cy.get('.ag-cell-label-container').eq(4).click();
cy.get('body').then(($body) => {
for (let i = 0; i < 15; i++) {
cy.wrap($body).realPress('Tab', { pressDelay: 100 });
}
});
cy.focused().parent('.ag-row').should('have.attr', 'row-index', '14');
cy.get('.ag-center-cols-container')
.find('[role="row"]')
.its('length')
.then((length) => expect(length).to.be.closeTo(26, 2));
});
});
});
});
@@ -0,0 +1,84 @@
const marketId = 'market-0';
const marketName = 'ACTIVE MARKET';
describe('market selector', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockConsole();
cy.visit(`/trading/${marketId}`);
cy.connectVegaWallet();
cy.wait('@Markets');
});
it('should be properly rendered', () => {
cy.get('input[placeholder="Search"]').should('have.value', marketName);
cy.getByTestId('arrow-button').click();
cy.getByTestId('market-pane').should('be.visible');
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.first()
.should('contain.text', marketName);
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.first()
.click();
cy.getByTestId('market-pane').should('not.be.visible');
});
it('typing should change list', () => {
cy.get('input[placeholder="Search"]').type('{backspace}');
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.should('have.length.at.least', 1);
cy.get('input[placeholder="Search"]').clear();
cy.get('input[placeholder="Search"]').type('ACTIVE');
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.should('have.length', 1);
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.last()
.click();
cy.location('pathname').should('eq', `/trading/${marketId}`);
cy.get('input[placeholder="Search"]').should('have.value', marketName);
});
// constantly failing on ci
it.skip('keyboard navigation should work well', () => {
cy.get('input[placeholder="Search"]').type('{backspace}');
cy.get('input[placeholder="Search"]').clear();
cy.focused().realPress('ArrowDown');
cy.focused().should('contain.text', marketName);
cy.focused().realPress('ArrowDown');
cy.focused().should('contain.text', 'ETHBTC').realPress('Enter');
cy.location('pathname').should('eq', '/trading/ethbtc-quaterly');
cy.get('input[placeholder="Search"]').type('{backspace}');
cy.get('input[placeholder="Search"]').clear();
cy.getByTestId('market-pane').should('be.visible');
cy.get('body').realPress('ArrowDown');
cy.get('body').realPress('Tab');
cy.getByTestId('market-pane').should('not.be.visible');
});
it('mobile view', () => {
cy.viewport('iphone-xr');
cy.visit(`/trading/${marketId}`);
cy.get('[role="dialog"]').should('not.exist');
cy.getByTestId('arrow-button').click();
cy.get('[role="dialog"]').should('be.visible');
cy.get('input[placeholder="Search"]').then((search) => {
cy.wrap(search).clear();
});
cy.getByTestId('market-pane')
.children()
.find('[role="button"]')
.should('have.length', 3);
cy.get('div[role="dialog"]').should('have.class', 'w-screen');
cy.getByTestId('dialog-close').click();
cy.get('input[placeholder="Search"]').should('have.value', marketName);
});
});
@@ -0,0 +1,278 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketQuery, marketsQuery } from '@vegaprotocol/mock';
const marketId =
marketsQuery().marketsConnection?.edges[1].node.id || 'market-1';
const marketOverride = {
market: {
depth: {
lastTrade: {
price: '9893006',
},
},
id: marketId,
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-id-2',
name: 'DAI Name',
symbol: 'tDAI',
},
},
},
},
},
};
describe('Market trade with wallet disconnected', { tags: '@smoke' }, () => {
before(() => {
cy.mockConsole();
cy.visit(`/trading/${marketId}`);
cy.wait('@Market');
console.log('marketId', marketId);
});
it('should not display steps', () => {
cy.getByTestId('trading-connect-wallet')
.find('h3')
.should('have.text', 'Please connect your Vega wallet to make a trade');
cy.getByTestId('trading-connect-wallet')
.find('button')
.should('have.text', 'Connect Vega wallet');
cy.getByTestId('trading-connect-wallet')
.find('a')
.should('have.text', 'https://vega.xyz/wallet');
});
});
describe('Market trade', { tags: '@regression' }, () => {
beforeEach(() => {
cy.mockConsole();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Market', marketQuery(marketOverride));
});
cy.visit(`/trading/${marketId}`);
cy.connectVegaWallet();
cy.wait('@Market');
});
it('side selector should work well', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').should(
'have.text',
'Long'
);
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-1-control [aria-label^="Selected value"]').should(
'have.text',
'Short'
);
});
it('side selector mobile view should work well', () => {
cy.viewport('iphone-xr');
cy.getByTestId('next-button').scrollIntoView().click();
cy.get('button[aria-label="Open long position"]').should(
'have.class',
'selected'
);
cy.get('button[aria-label="Open short position"]').should(
'not.have.class',
'selected'
);
cy.get('button[aria-label="Open short position"]').click();
cy.get('button[aria-label="Open long position"]').should(
'not.have.class',
'selected'
);
cy.get('button[aria-label="Open short position"]').should(
'have.class',
'selected'
);
cy.getByTestId('next-button').scrollIntoView().click();
cy.get('#step-1-control').should(
'contain.html',
'aria-label="Selected value Short"'
);
});
it('size slider should work well', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.get('#step-2-panel').find('[role="slider"]').type('{rightarrow}');
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '2');
});
it('percentage selection should work well', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.getByTestId('max-label').should('have.text', '10');
cy.getByTestId('percentage-selector')
.find('button')
.contains('Max')
.click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '10');
});
it('size input should work well', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel').find('dd').eq(0).find('input').type('{backspace}2');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '2');
cy.get('button').contains('Max').click();
});
it('slippage value should be displayed', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('button').contains('Max').click();
cy.get('#step-2-panel')
.find('dl')
.eq(2)
.find('dd')
.should('have.text', '0.01%');
});
it('allow slippage value to be adjusted', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('button').contains('Max').click();
cy.get('#step-2-panel')
.find('dl')
.eq(2)
.find('dd')
.should('have.text', '0.01%');
cy.get('#step-2-panel').find('dl').eq(2).find('button').click();
cy.get('#input-order-slippage')
.focus()
.type('{backspace}{backspace}{backspace}1');
cy.getByTestId('slippage-dialog').find('button').click();
cy.get('#step-2-panel')
.find('dl')
.eq(2)
.find('dd')
.should('have.text', '1%');
});
it('notional position size should be present', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dd')
.eq(0)
.find('button')
.should('have.text', '1');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel').find('dd').eq(0).find('input').type('{backspace}2');
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
cy.get('#step-2-panel')
.find('dt')
.eq(2)
.should('have.text', 'Est. Position Size (tDAI)');
cy.get('#step-2-panel').find('dd').eq(2).should('have.text', '197.86012');
});
it('total fees should be displayed', () => {
cy.get('#step-1-control [aria-label^="Selected value"]').click();
cy.get('button[aria-label="Open short position"]').click();
cy.get('#step-2-control').click();
cy.get('#step-2-panel')
.find('dt')
.eq(3)
.should('have.text', 'Est. Fees (tDAI)');
cy.get('#step-2-panel')
.find('dd')
.eq(3)
.should('have.text', '3.00 (3.03%)');
});
it('order review should display proper calculations', () => {
cy.get('#step-3-control').click();
cy.getByTestId('review-trade')
.get('#contracts_tooltip_trigger')
.trigger('click')
.realTouch();
cy.get('[data-radix-popper-content-wrapper]').contains(
'The number of contracts determines'
);
cy.get('#step-3-panel').find('dd').eq(1).should('have.text', '1');
cy.get('#step-3-panel').find('dd').eq(2).should('have.text', '98.93006');
cy.get('#step-3-panel')
.find('dd')
.eq(3)
.should('have.text', '3.00 (3.03%)');
cy.get('#step-3-panel')
.find('dd')
.eq(4)
.should('have.text', '45,126.90058');
cy.getByTestId('place-order').should('be.enabled').click();
});
it('info tooltip on mobile view should work well', () => {
cy.viewport('iphone-xr');
cy.get('#step-3-control').click();
// Start from the bottom tooltip to ensure the tooltip above
// can be interacted with
cy.getByTestId('review-trade').get('div.cursor-help').eq(1).realTouch();
cy.get('[data-radix-popper-content-wrapper]').contains(
'The notional size represents the position size'
);
cy.getByTestId('review-trade')
.get('#contracts_tooltip_trigger')
.realTouch();
cy.get('[data-radix-popper-content-wrapper]').contains(
'The number of contracts determines'
);
});
});
@@ -0,0 +1,194 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
assetsQuery,
chainIdQuery,
fillsQuery,
marginsQuery,
marketsDataQuery,
marketsQuery,
ordersQuery,
positionsQuery,
statisticsQuery,
} from '@vegaprotocol/mock';
describe('Portfolio page - wallet', { tags: '@smoke' }, () => {
it('button for wallet connect should work', () => {
cy.visit('/');
cy.get('[href="/portfolio"]').eq(0).click();
cy.getByTestId('trading-connect-wallet').should('be.visible');
cy.connectVegaWallet();
cy.getByTestId('trading-connect-wallet').should('not.exist');
});
});
describe('Portfolio page tabs', { tags: '@smoke' }, () => {
before(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Positions', positionsQuery());
aliasGQLQuery(req, 'Margins', marginsQuery());
aliasGQLQuery(req, 'Markets', marketsQuery());
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
aliasGQLQuery(req, 'Accounts', accountsQuery());
aliasGQLQuery(req, 'Assets', assetsQuery());
});
});
it('certain tabs should exist', () => {
cy.visit('/portfolio');
cy.connectVegaWallet();
cy.getByTestId('assets').click();
cy.location('pathname').should('eq', '/portfolio/assets');
cy.getByTestId('positions').click();
cy.location('pathname').should('eq', '/portfolio/positions');
cy.getByTestId('orders').click();
cy.location('pathname').should('eq', '/portfolio/orders');
cy.getByTestId('fills').click();
cy.location('pathname').should('eq', '/portfolio/fills');
cy.getByTestId('deposits').click();
cy.location('pathname').should('eq', '/portfolio/deposits');
});
describe('Assets view', () => {
before(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Positions', positionsQuery());
aliasGQLQuery(req, 'Margins', marginsQuery());
aliasGQLQuery(req, 'Markets', marketsQuery());
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
aliasGQLQuery(req, 'Accounts', accountsQuery());
aliasGQLQuery(req, 'Assets', assetsQuery());
});
cy.visit('/portfolio/assets');
cy.connectVegaWallet();
});
it('data should be properly rendered', () => {
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
cy.get(
'.ag-center-cols-container [row-id="ACCOUNT_TYPE_GENERAL-asset-id-null"]'
)
.find('button')
.click();
cy.getByTestId('dialog-title').should(
'have.text',
'Asset details - tEURO'
);
cy.getByTestId('dialog-close').click();
});
});
describe('Positions view', () => {
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Positions', positionsQuery());
aliasGQLQuery(req, 'Margins', marginsQuery());
aliasGQLQuery(req, 'Markets', marketsQuery());
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
aliasGQLQuery(req, 'Accounts', accountsQuery());
aliasGQLQuery(req, 'Assets', assetsQuery());
});
cy.visit('/portfolio/positions');
cy.connectVegaWallet();
});
it('data should be properly rendered', () => {
cy.get('.ag-center-cols-container .ag-row').should('have.length', 2);
});
});
describe('Orders view', () => {
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Orders', ordersQuery());
aliasGQLQuery(req, 'Markets', marketsQuery());
});
cy.visit('/portfolio/orders');
cy.connectVegaWallet();
});
it('data should be properly rendered', () => {
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
});
});
describe('Fills view', () => {
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Orders', ordersQuery());
aliasGQLQuery(req, 'Markets', marketsQuery());
aliasGQLQuery(req, 'Fills', fillsQuery());
});
cy.visit('/portfolio/fills');
cy.connectVegaWallet();
});
it('data should be properly rendered', () => {
cy.get('.ag-center-cols-container .ag-row').should('have.length', 4);
});
});
describe('Empty views', () => {
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Positions', { party: null });
aliasGQLQuery(req, 'Accounts', { party: null });
aliasGQLQuery(req, 'Orders', { party: null });
aliasGQLQuery(req, 'Fills', { party: null });
aliasGQLQuery(req, 'Markets', {
marketsConnection: { edges: [], __typename: 'MarketConnection' },
});
aliasGQLQuery(req, 'Assets', {
assetsConnection: { edges: null, __typename: 'AssetsConnection' },
});
aliasGQLQuery(req, 'Margins', marginsQuery());
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
});
cy.visit('/portfolio');
cy.connectVegaWallet();
});
it('"No data to display" should be always displayed', () => {
cy.getByTestId('assets').click();
cy.get('div.flex.items-center.justify-center').should(
'contain.text',
'No data to display'
);
cy.getByTestId('positions').click();
cy.get('div.flex.items-center.justify-center').should(
'contain.text',
'No data to display'
);
cy.getByTestId('orders').click();
cy.get('div.flex.items-center.justify-center').should(
'contain.text',
'No data to display'
);
cy.getByTestId('fills').click();
cy.get('div.flex.items-center.justify-center').should(
'contain.text',
'No data to display'
);
});
});
});
@@ -0,0 +1,56 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
import {
accountsQuery,
assetsQuery,
chainIdQuery,
estimateOrderQuery,
fillsQuery,
marginsQuery,
marketDataQuery,
marketDepthQuery,
marketQuery,
marketsCandlesQuery,
marketsDataQuery,
marketsQuery,
ordersQuery,
positionsQuery,
statisticsQuery,
} from '@vegaprotocol/mock';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
mockConsole(): void;
}
}
}
const mockPage = (req: CyHttpMessages.IncomingHttpRequest) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'Markets', marketsQuery());
aliasGQLQuery(req, 'MarketsCandles', marketsCandlesQuery());
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
aliasGQLQuery(req, 'MarketData', marketDataQuery());
aliasGQLQuery(req, 'Market', marketQuery());
aliasGQLQuery(req, 'MarketTags', {});
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
aliasGQLQuery(req, 'MarketNames', {});
aliasGQLQuery(req, 'MarketDepth', marketDepthQuery());
aliasGQLQuery(req, 'Positions', positionsQuery());
aliasGQLQuery(req, 'Margins', marginsQuery());
aliasGQLQuery(req, 'Accounts', accountsQuery());
aliasGQLQuery(req, 'Assets', assetsQuery());
aliasGQLQuery(req, 'SimpleMarkets', marketsQuery());
aliasGQLQuery(req, 'Orders', ordersQuery());
aliasGQLQuery(req, 'Fills', fillsQuery());
};
export const addMockConsole = () => {
Cypress.Commands.add('mockConsole', () => {
cy.mockGQL(mockPage);
});
};
@@ -0,0 +1,7 @@
import '@vegaprotocol/cypress';
import 'cypress-real-events/support';
import registerCypressGrep from '@cypress/grep';
import { addMockConsole } from './console-mock';
registerCypressGrep();
addMockConsole();
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"sourceMap": false,
"allowSyntheticDefaultImports": true,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node", "cypress-real-events", "@cypress/grep"]
},
"include": ["src/**/*.ts", "src/**/*.js", "./declaration.d.ts", "index.d.ts"]
}
+27
View File
@@ -0,0 +1,27 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
NX_VERSION=$npm_package_version
NX_REPOSITORY_URL=$REPOSITORY_URL
NX_BRANCH=$BRANCH
NX_PULL_REQUEST=$PULL_REQUEST
NX_HEAD=$HEAD
NX_COMMIT_REF=$COMMIT_REF
NX_CONTEXT=$CONTEXT
NX_REVIEW_ID=$REVIEW_ID
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
NX_URL=$URL
NX_DEPLOY_URL=$DEPLOY_URL
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
NX_VEGA_CONFIG_URL="https://static.vega.xyz/assets/stagnet3-network.json"
NX_VEGA_ENV=STAGNET3
NX_VEGA_URL="https://api.stagnet3.vega.xyz/graphql"
NX_VEGA_WALLET_URL=http://localhost:1789
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_NETWORKS={"MAINNET":"https://alpha.console.vega.xyz"}
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
+3
View File
@@ -0,0 +1,3 @@
# App configuration variables
NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_ENV=LOCAL
+8
View File
@@ -0,0 +1,8 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
NX_VEGA_ENV=DEVNET
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
+8
View File
@@ -0,0 +1,8 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_URL=https://api.vega.xyz/query
NX_VEGA_ENV=MAINNET
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
+7
View File
@@ -0,0 +1,7 @@
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
NX_VEGA_ENV=SANDBOX
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
+10
View File
@@ -0,0 +1,10 @@
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.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"STAGNET3\":\"https://stagnet3.console.vega.xyz\"}
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_URL=https://api.n00.stagnet1.vega.xyz/graphql
NX_VEGA_WALLET_URL=http://localhost:1789
+7
View File
@@ -0,0 +1,7 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
NX_VEGA_ENV=STAGNET3
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
+8
View File
@@ -0,0 +1,8 @@
# App configuration variables
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_URL=https://api.n12.testnet.vega.xyz/graphql
NX_VEGA_ENV=TESTNET
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
+12
View File
@@ -0,0 +1,12 @@
/* eslint-disable */
export default {
displayName: 'console-lite',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/console-lite',
setupFilesAfterEnv: ['./setup-tests.ts'],
};
+90
View File
@@ -0,0 +1,90 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/console-lite/src",
"projectType": "application",
"targets": {
"build": {
"executor": "./tools/executors/webpack:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/console-lite",
"index": "apps/console-lite/src/index.html",
"baseHref": "/",
"main": "apps/console-lite/src/main.tsx",
"polyfills": "apps/console-lite/src/polyfills.ts",
"tsConfig": "apps/console-lite/tsconfig.app.json",
"assets": [
"apps/console-lite/src/favicon.ico",
"apps/console-lite/src/assets"
],
"styles": ["apps/console-lite/src/styles.scss"],
"scripts": [],
"webpackConfig": "@nrwl/react/plugins/webpack"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/console-lite/src/environments/environment.ts",
"with": "apps/console-lite/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false
}
}
},
"serve": {
"executor": "./tools/executors/webpack:serve",
"options": {
"buildTarget": "console-lite:build:development",
"hmr": true,
"port": 4001
},
"configurations": {
"production": {
"buildTarget": "console-lite:build:production",
"hmr": false
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/console-lite/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/apps/console-lite"],
"options": {
"jestConfig": "apps/console-lite/jest.config.ts",
"passWithNoTests": true
}
},
"build-netlify": {
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"cp apps/console-lite/netlify.toml netlify.toml",
"nx build console-lite"
]
}
},
"build-spec": {
"executor": "@nrwl/workspace:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/console-lite/tsconfig.spec.json"
}
}
},
"tags": []
}
@@ -1,7 +1,9 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
import { defaultFallbackInView } from 'react-intersection-observer';
import ResizeObserver from 'resize-observer-polyfill';
defaultFallbackInView(true);
global.ResizeObserver = ResizeObserver;
global.DOMRect = class DOMRect {
@@ -23,3 +25,19 @@ global.DOMRect = class DOMRect {
return JSON.stringify(this);
}
};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
jest.setTimeout(30000);
+77
View File
@@ -0,0 +1,77 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
import {
VegaConnectDialog,
VegaManageDialog,
VegaWalletProvider,
} from '@vegaprotocol/wallet';
import { Connectors } from './lib/vega-connectors';
import '../styles.scss';
import { AppLoader } from './components/app-loader';
import Header from './components/header';
import { Main } from './components/main';
import LocalContext from './context/local-context';
import useLocalValues from './hooks/use-local-values';
import type { InMemoryCacheConfig } from '@apollo/client';
function App() {
const localValues = useLocalValues();
const {
vegaWalletDialog,
menu: { setMenuOpen },
} = localValues;
const location = useLocation();
useEffect(() => {
setMenuOpen(false);
}, [location, setMenuOpen]);
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
Market: {
merge: true,
},
Party: {
merge: true,
},
Query: {},
Account: {
keyFields: false,
fields: {
balanceFormatted: {},
},
},
Node: {
keyFields: false,
},
Instrument: {
keyFields: false,
},
},
};
return (
<EnvironmentProvider>
<NetworkLoader cache={cacheConfig}>
<VegaWalletProvider>
<LocalContext.Provider value={localValues}>
<AppLoader>
<div className="max-h-full min-h-full dark:bg-lite-black dark:text-neutral-200 bg-white text-neutral-800 grid grid-rows-[min-content,1fr]">
<Header />
<Main />
<VegaConnectDialog connectors={Connectors} />
<VegaManageDialog
dialogOpen={vegaWalletDialog.manage}
setDialogOpen={vegaWalletDialog.setManage}
/>
</div>
</AppLoader>
</LocalContext.Provider>
</VegaWalletProvider>
</NetworkLoader>
</EnvironmentProvider>
);
}
export default App;
@@ -0,0 +1,19 @@
import { useEagerConnect } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { Connectors } from '../../lib/vega-connectors';
interface AppLoaderProps {
children: ReactNode;
}
/**
* Component to handle any app initialization, startup querys and other things
* that must happen for it can be used
*/
export function AppLoader({ children }: AppLoaderProps) {
// Get keys from vega wallet immediately
useEagerConnect(Connectors);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
@@ -0,0 +1,109 @@
import React, { forwardRef, useCallback, useMemo, useRef } from 'react';
import classNames from 'classnames';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import {
useScreenDimensions,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import type {
GridOptions,
GetRowIdParams,
TabToNextCellParams,
CellKeyDownEvent,
FullWidthCellKeyDownEvent,
IGetRowsParams,
IDatasource,
} from 'ag-grid-community';
import { NO_DATA_MESSAGE } from '../../constants';
import * as constants from '../simple-market-list/constants';
export interface GetRowsParams<T>
extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: T[], lastRow?: number): void;
}
export interface Datasource<T> extends IDatasource {
getRows(params: GetRowsParams<T>): void;
}
interface Props<T> extends GridOptions {
rowData?: T[];
datasource?: Datasource<T>;
handleRowClicked?: (event: { data: T }) => void;
components?: Record<string, unknown>;
classNamesParam?: string | string[];
}
const ConsoleLiteGrid = <T extends { id?: string }>(
{ handleRowClicked, getRowId, classNamesParam, ...props }: Props<T>,
ref?: React.Ref<AgGridReact>
) => {
const { isMobile, screenSize } = useScreenDimensions();
const gridRef = useRef<AgGridReact | null>(null);
const { theme } = useThemeSwitcher();
const handleOnGridReady = useCallback(() => {
(
(ref as React.RefObject<AgGridReact>) || gridRef
).current?.api?.sizeColumnsToFit();
}, [gridRef, ref]);
const onTabToNextCell = useCallback((params: TabToNextCellParams) => {
const {
api,
previousCellPosition: { rowIndex },
} = params;
const rowCount = api.getDisplayedRowCount();
if (rowCount <= rowIndex + 1) {
return null;
}
return { ...params.previousCellPosition, rowIndex: rowIndex + 1 };
}, []);
const getRowIdLocal = useCallback(({ data }: GetRowIdParams) => data.id, []);
const onCellKeyDown = useCallback(
(
params: (CellKeyDownEvent | FullWidthCellKeyDownEvent) & {
event: KeyboardEvent;
}
) => {
const { event: { key = '' } = {}, data } = params;
if (key === 'Enter') {
handleRowClicked?.({ data });
}
},
[handleRowClicked]
);
const shouldSuppressHorizontalScroll = useMemo(() => {
return !isMobile && constants.LARGE_SCREENS.includes(screenSize);
}, [isMobile, screenSize]);
return (
<AgGrid
className={classNames(classNamesParam)}
rowHeight={60}
customThemeParams={
theme === 'dark'
? constants.agGridDarkVariables
: constants.agGridLightVariables
}
onGridReady={handleOnGridReady}
onRowClicked={handleRowClicked}
rowClass={isMobile ? 'mobile' : ''}
rowClassRules={constants.ROW_CLASS_RULES}
ref={ref || gridRef}
overlayNoRowsTemplate={NO_DATA_MESSAGE}
suppressContextMenu
getRowId={getRowId || getRowIdLocal}
suppressMovableColumns
suppressRowTransform
onCellKeyDown={onCellKeyDown}
tabToNextCell={onTabToNextCell}
suppressHorizontalScroll={shouldSuppressHorizontalScroll}
{...props}
/>
);
};
const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as <T>(
p: Props<T> & { ref?: React.Ref<AgGridReact> }
) => React.ReactElement;
export default ConsoleLiteGridForwarder;
@@ -0,0 +1 @@
export { default as ConsoleLiteGrid } from './console-lite-grid';
@@ -0,0 +1,11 @@
query MarketTags($marketId: ID!) {
market(id: $marketId) {
tradableInstrument {
instrument {
metadata {
tags
}
}
}
}
}
@@ -0,0 +1,54 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarketTagsQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type MarketTagsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null } } } } | null };
export const MarketTagsDocument = gql`
query MarketTags($marketId: ID!) {
market(id: $marketId) {
tradableInstrument {
instrument {
metadata {
tags
}
}
}
}
}
`;
/**
* __useMarketTagsQuery__
*
* To run a query within a React component, call `useMarketTagsQuery` and pass it any options that fit your needs.
* When your component renders, `useMarketTagsQuery` 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 } = useMarketTagsQuery({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useMarketTagsQuery(baseOptions: Apollo.QueryHookOptions<MarketTagsQuery, MarketTagsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<MarketTagsQuery, MarketTagsQueryVariables>(MarketTagsDocument, options);
}
export function useMarketTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarketTagsQuery, MarketTagsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<MarketTagsQuery, MarketTagsQueryVariables>(MarketTagsDocument, options);
}
export type MarketTagsQueryHookResult = ReturnType<typeof useMarketTagsQuery>;
export type MarketTagsLazyQueryHookResult = ReturnType<typeof useMarketTagsLazyQuery>;
export type MarketTagsQueryResult = Apollo.QueryResult<MarketTagsQuery, MarketTagsQueryVariables>;
@@ -0,0 +1,36 @@
import React from 'react';
import Video from '../header/video';
import Comet from '../header/comet';
import Star from '../icons/star';
const Baubles = () => {
return (
<aside className="relative right-0 top-0 h-[700px] hidden md:block md:w-1/2 overflow-hidden">
<div className="absolute top-[100px] w-[393px] left-[19%] h-[517px]">
<div className="absolute top-[82px] right-[34px] w-[100px] h-[100px] clip-path-rounded">
<Video />
</div>
<div className="absolute bottom-[100px] left-[59px] w-[200px] h-[200px] clip-path-rounded">
<Video />
</div>
<div className="absolute w-[118px] h-[85px] right-0 bottom-[178px]">
<Comet />
</div>
<div className="absolute w-[118px] h-[82px] left-0 bottom-[120px]">
<Comet />
</div>
<div className="absolute w-[20px] h-[20px] top-0 left-[49px]">
<Star />
</div>
<div className="absolute w-[20px] h-[20px] top-[89px] left-[184px]">
<Star />
</div>
<div className="absolute w-[10px] h-[10px] bottom-0 right-[137px]">
<Star />
</div>
</div>
</aside>
);
};
export default Baubles;
@@ -0,0 +1,61 @@
import React from 'react';
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import type { MarketDealTicketAsset } from '@vegaprotocol/market-list';
import { DealTicketBalance } from './deal-ticket-balance';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
const tDAI: MarketDealTicketAsset = {
__typename: 'Asset',
id: '1',
symbol: 'tDAI',
name: 'TDAI',
decimals: 2,
};
let mockAccountBalance: {
accountBalance: string;
accountDecimals: number | null;
} = { accountBalance: '1000000', accountDecimals: 2 };
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useAccountBalance: jest.fn(() => mockAccountBalance),
}));
describe('DealTicketBalance', function () {
it('should render the balance', () => {
const { getByText, getByRole } = render(
<DealTicketBalance settlementAsset={tDAI} isWalletConnected />,
{ wrapper: MockedProvider }
);
expect(getByRole('complementary')).toHaveAccessibleName('tDAI Balance');
expect(getByText('10,000.00')).toBeInTheDocument();
expect(getByText('tDAI')).toBeInTheDocument();
});
it('should prompt to connect wallet', () => {
const { getByText } = render(
<DealTicketBalance settlementAsset={tDAI} isWalletConnected={false} />,
{ wrapper: MockedProvider }
);
expect(
getByText('Please connect your Vega wallet to see your balance')
).toBeInTheDocument();
});
it('should display zero balance', () => {
mockAccountBalance = { accountBalance: '', accountDecimals: null };
const { getByText } = render(
<DealTicketBalance settlementAsset={tDAI} isWalletConnected={true} />,
{ wrapper: MockedProvider }
);
expect(getByText('No tDAI left to trade')).toBeInTheDocument();
});
});
@@ -0,0 +1,58 @@
import classNames from 'classnames';
import {
addDecimalsFormatNumber,
t,
toBigNum,
} from '@vegaprotocol/react-helpers';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
import { useAccountBalance } from '@vegaprotocol/accounts';
interface DealTicketBalanceProps {
settlementAsset: MarketDealTicket['tradableInstrument']['instrument']['product']['settlementAsset'];
isWalletConnected: boolean;
className?: string;
}
export const DealTicketBalance = ({
settlementAsset,
isWalletConnected,
className = '',
}: DealTicketBalanceProps) => {
const settlementAssetId = settlementAsset?.id;
const settlementAssetSymbol = settlementAsset?.symbol;
const { accountBalance, accountDecimals } =
useAccountBalance(settlementAssetId) || {};
const settlementBalance = toBigNum(accountBalance || 0, accountDecimals || 0);
const formattedNumber =
accountBalance &&
accountDecimals &&
addDecimalsFormatNumber(accountBalance, accountDecimals);
const balance = (
<p className="text-blue text-lg font-semibold">
{!settlementBalance.isZero()
? t(`${formattedNumber}`)
: `No ${settlementAssetSymbol} left to trade`}
</p>
);
const connectWallet = (
<p>{t('Please connect your Vega wallet to see your balance')}</p>
);
const ariaLabel = t(`${settlementAssetSymbol} Balance`);
return (
<aside
aria-label={ariaLabel}
className={classNames('text-right', className)}
>
<div className="inline-block">
<span className="text-blue">{settlementAssetSymbol}</span>
{isWalletConnected ? balance : connectWallet}
</div>
</aside>
);
};
@@ -0,0 +1,75 @@
import { useParams } from 'react-router-dom';
import { DealTicketManager } from '@vegaprotocol/deal-ticket';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { DealTicketSteps } from './deal-ticket-steps';
import { DealTicketBalance } from './deal-ticket-balance';
import Baubles from './baubles-decor';
import ConnectWallet from '../wallet-connector';
import { useMemo } from 'react';
import type {
MarketDataUpdateFieldsFragment,
MarketDealTicket,
} from '@vegaprotocol/market-list';
import { marketDealTicketProvider } from '@vegaprotocol/market-list';
const tempEmptyText = (
<p>{t('Please select a market from the markets page')}</p>
);
export const DealTicketContainer = () => {
const { marketId } = useParams<{ marketId: string }>();
const { pubKey } = useVegaWallet();
const variables = useMemo(
() => ({
marketId: marketId || '',
}),
[marketId]
);
const { data, loading } = useDataProvider<
MarketDealTicket,
MarketDataUpdateFieldsFragment
>({
dataProvider: marketDealTicketProvider,
variables,
skip: !marketId,
});
const loader = <Loader />;
if (marketId && data) {
const balance = (
<DealTicketBalance
className="mb-4"
settlementAsset={
data.tradableInstrument.instrument.product?.settlementAsset
}
isWalletConnected={!!pubKey}
/>
);
const container = (
<DealTicketManager market={data}>
{loading ? loader : balance}
<DealTicketSteps market={data} />
</DealTicketManager>
);
return (
<section className="flex p-4 md:p-6">
<section className="w-full md:w-1/2 md:min-w-[500px]">
{pubKey ? container : <ConnectWallet />}
</section>
<Baubles />
</section>
);
}
return marketId ? (
<Splash>
<p>{t('Could not load market')}</p>
</Splash>
) : (
tempEmptyText
);
};
@@ -0,0 +1,149 @@
import React, { useCallback, useState } from 'react';
import { BigNumber } from 'bignumber.js';
import { t } from '@vegaprotocol/react-helpers';
import {
SliderRoot,
SliderThumb,
SliderTrack,
SliderRange,
FormGroup,
} from '@vegaprotocol/ui-toolkit';
import { InputSetter } from '../input-setter';
interface DealTicketSizeInputProps {
step: number;
min: number;
max: number;
value: number;
onValueChange: (value: number) => void;
positionDecimalPlaces: number;
}
const getSizeLabel = (value: number): string => {
const MIN_LABEL = 'Min';
const MAX_LABEL = 'Max';
if (value === 0) {
return MIN_LABEL;
} else if (value === 100) {
return MAX_LABEL;
}
return `${value}%`;
};
export const DealTicketSizeInput = ({
value,
step,
min,
max,
onValueChange,
positionDecimalPlaces,
}: DealTicketSizeInputProps) => {
const sizeRatios = [0, 25, 50, 75, 100];
const [inputValue, setInputValue] = useState(value);
const onInputValueChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
let value = parseFloat(event.target.value);
const isLessThanMin = value < min;
const isMoreThanMax = value > max;
if (isLessThanMin) {
value = min;
} else if (isMoreThanMax) {
value = max;
}
if (value) {
onValueChange(value);
}
setInputValue(value);
},
[min, max, onValueChange, setInputValue]
);
const onButtonValueChange = (size: number) => {
const newVal = new BigNumber(size)
.decimalPlaces(positionDecimalPlaces)
.toNumber();
onValueChange(newVal);
setInputValue(newVal);
};
const onSliderValueChange = useCallback(
(value: number[]) => {
const val = value[0];
setInputValue(val);
onValueChange(val);
},
[onValueChange]
);
return (
<div>
<div className="flex justify-between text-black dark:text-white mb-2">
<span data-testid="min-label">{min}</span>
<span data-testid="max-label">{max}</span>
</div>
<SliderRoot
className="mb-2"
value={[value]}
onValueChange={onSliderValueChange}
step={step}
min={min}
max={max}
>
<SliderTrack className="bg-lightGrey dark:bg-offBlack">
<SliderRange className="!bg-black dark:!bg-white" />
</SliderTrack>
<SliderThumb />
</SliderRoot>
<div
data-testid="percentage-selector"
className="flex w-full justify-between text-black dark:text-white mb-6"
>
{sizeRatios.map((size, index) => {
const proportionalSize = size ? (size / 100) * max : min;
return (
<button
className="no-underline hover:underline text-blue"
onClick={() => onButtonValueChange(proportionalSize)}
type="button"
key={index}
>
{getSizeLabel(size)}
</button>
);
})}
</div>
<dl className="text-black dark:text-white">
<div className="flex items-center justify-between">
<dt>{t('Contracts')}</dt>
<dd className="flex justify-end w-full">
<FormGroup
hideLabel={true}
label="Enter Size"
labelFor="trade-size-input"
className="mb-1"
>
<InputSetter
id="input-order-size-market"
type="number"
step={step}
min={min}
max={max}
className="w-full"
value={inputValue}
onChange={onInputValueChange}
>
{inputValue}
</InputSetter>
</FormGroup>
</dd>
</div>
</dl>
</div>
);
};
@@ -0,0 +1,54 @@
import { DealTicketEstimates } from '@vegaprotocol/deal-ticket';
import { DealTicketSizeInput } from './deal-ticket-size-input';
interface DealTicketSizeProps {
step: number;
min: number;
max: number;
size: number;
onSizeChange: (value: number) => void;
name: string;
quoteName: string;
price: string;
estCloseOut: string;
estMargin: string;
fees: string;
positionDecimalPlaces: number;
notionalSize: string;
}
export const DealTicketSize = ({
step,
min,
max,
price,
quoteName,
size,
onSizeChange,
estCloseOut,
positionDecimalPlaces,
fees,
notionalSize,
}: DealTicketSizeProps) => {
return max === 0 ? (
<p>Not enough balance to trade</p>
) : (
<div>
<DealTicketSizeInput
step={step}
min={min}
max={max}
value={size}
onValueChange={onSizeChange}
positionDecimalPlaces={positionDecimalPlaces}
/>
<DealTicketEstimates
quoteName={quoteName}
fees={fees}
estCloseOut={estCloseOut}
price={price}
notionalSize={notionalSize}
/>
</div>
);
};
@@ -0,0 +1,112 @@
import React, { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import {
Dialog,
Icon,
Intent,
Tooltip,
TrafficLight,
} from '@vegaprotocol/ui-toolkit';
import { InputSetter } from '../../components/input-setter';
import { IconNames } from '@blueprintjs/icons';
import {
DataTitle,
ValueTooltipRow,
EST_SLIPPAGE,
} from '@vegaprotocol/deal-ticket';
interface DealTicketSlippageProps {
step?: number;
min?: number;
max?: number;
value: number;
onValueChange(value: number): void;
}
export const DealTicketSlippage = ({
value,
step = 0.01,
min = 0,
max = 50,
onValueChange,
}: DealTicketSlippageProps) => {
const [isDialogVisible, setIsDialogVisible] = useState(false);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const numericValue = parseFloat(value);
onValueChange(numericValue);
},
[onValueChange]
);
const toggleDialog = useCallback(() => {
setIsDialogVisible(!isDialogVisible);
}, [isDialogVisible]);
const formLabel = (
<label className="flex items-center mb-1">
<span className="mr-1">{t('Adjust slippage tolerance')}</span>
<Tooltip align="center" description={EST_SLIPPAGE}>
<div className="cursor-help" tabIndex={-1}>
<Icon
name={IconNames.ISSUE}
className="block rotate-180"
ariaLabel={EST_SLIPPAGE}
/>
</div>
</Tooltip>
</label>
);
return (
<>
<Dialog
open={isDialogVisible}
onChange={toggleDialog}
intent={Intent.None}
title={t('Transaction Settings')}
>
<div data-testid="slippage-dialog">
{formLabel}
<InputSetter
id="input-order-slippage"
isInputVisible
hasError={!value}
type="number"
step={step}
min={min}
max={max}
className="w-full"
value={value}
onChange={onChange}
>
{value}%
</InputSetter>
</div>
</Dialog>
<dl className="text-black dark:text-white">
<div className="flex justify-between mb-2">
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
<div className="flex">
<div className="mr-1">
<ValueTooltipRow description={EST_SLIPPAGE}>
<TrafficLight value={value} q1={1} q2={5}>
{value}%
</TrafficLight>
</ValueTooltipRow>
</div>
<button type="button" onClick={toggleDialog}>
<Icon
name={IconNames.COG}
className="block rotate-180"
ariaLabel={t('Override slippage value')}
/>
</button>
</div>
</div>
</dl>
</>
);
};
@@ -0,0 +1,342 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
import {
getDefaultOrder,
useOrderCloseOut,
useOrderMargin,
useMaximumPositionSize,
useCalculateSlippage,
validateAmount,
} from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
import { MarketSelector } from '@vegaprotocol/deal-ticket';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import {
t,
toDecimal,
removeDecimal,
addDecimalsFormatNumber,
addDecimal,
formatNumber,
} from '@vegaprotocol/react-helpers';
import {
useOrderSubmit,
getOrderDialogTitle,
getOrderDialogIntent,
getOrderDialogIcon,
OrderFeedback,
} from '@vegaprotocol/orders';
import { DealTicketSize } from './deal-ticket-size';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
import SideSelector, { SIDE_NAMES } from './side-selector';
import ReviewTrade from './review-trade';
import * as Schema from '@vegaprotocol/types';
import { DealTicketSlippage } from './deal-ticket-slippage';
import { useOrderValidation } from './use-order-validation';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
interface DealTicketMarketProps {
market: MarketDealTicket;
}
export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
const navigate = useNavigate();
const setMarket = useCallback(
(marketId: string) => {
navigate(`/trading/${marketId}`);
},
[navigate]
);
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<OrderSubmissionBody['orderSubmission']>({
mode: 'onChange',
defaultValues: getDefaultOrder(market),
});
const emptyString = ' - ';
const step = toDecimal(market.positionDecimalPlaces);
const order = watch();
const { pubKey } = useVegaWallet();
const { message: invalidText, isDisabled } = useOrderValidation({
market,
order,
fieldErrors: errors,
});
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
const maxTrade = useMaximumPositionSize({
marketId: market.id,
settlementAssetId:
market.tradableInstrument.instrument.product.settlementAsset.id,
price: market?.depth?.lastTrade?.price,
order,
});
const estCloseOut = useOrderCloseOut({
order,
market,
});
const slippage = useCalculateSlippage({ marketId: market.id, order });
const [slippageValue, setSlippageValue] = useState(
slippage ? parseFloat(slippage) : 0
);
const transactionStatus =
transaction.status === VegaTxStatus.Requested ||
transaction.status === VegaTxStatus.Pending
? 'pending'
: 'default';
useEffect(() => {
setSlippageValue(slippage ? parseFloat(slippage) : 0);
}, [slippage]);
const price = useMemo(() => {
if (slippage && market?.depth?.lastTrade?.price) {
const isLong = order.side === Schema.Side.SIDE_BUY;
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
parseFloat(slippage) / 100
);
return new BigNumber(market?.depth?.lastTrade?.price)
.multipliedBy(multiplier)
.toString();
}
return undefined;
}, [market?.depth?.lastTrade?.price, order.side, slippage]);
const estMargin = useOrderMargin({
order,
market,
partyId: pubKey || '',
derivedPrice: price,
});
const formattedPrice =
price && addDecimalsFormatNumber(price, market.decimalPlaces);
const notionalSize = useMemo(() => {
if (price) {
const size = new BigNumber(price).multipliedBy(order.size).toNumber();
return addDecimalsFormatNumber(size, market.decimalPlaces);
}
return null;
}, [market.decimalPlaces, order.size, price]);
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const fees = useMemo(() => {
if (estMargin?.totalFees && notionalSize) {
const percentage = new BigNumber(estMargin?.totalFees)
.dividedBy(notionalSize)
.multipliedBy(100)
.decimalPlaces(2)
.toString();
return `${addDecimalsFormatNumber(
estMargin.totalFees,
assetDecimals
)} (${formatNumber(addDecimal(percentage, assetDecimals), 2)}%)`;
}
return null;
}, [assetDecimals, estMargin?.totalFees, notionalSize]);
const max = useMemo(() => {
return new BigNumber(maxTrade)
.decimalPlaces(market.positionDecimalPlaces)
.toNumber();
}, [market.positionDecimalPlaces, maxTrade]);
useEffect(() => {
setSlippageValue(slippage ? parseFloat(slippage) : 0);
}, [slippage]);
const onSizeChange = useCallback(
(value: number) => {
const newVal = new BigNumber(value)
.decimalPlaces(market.positionDecimalPlaces)
.toString();
const isValid = validateAmount(step, 'Size')(newVal);
if (isValid !== 'step') {
setValue('size', newVal);
}
},
[market.positionDecimalPlaces, setValue, step]
);
const onSlippageChange = useCallback(
(value: number) => {
if (market?.depth?.lastTrade?.price) {
if (value) {
const isLong = order.side === Schema.Side.SIDE_BUY;
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
value / 100
);
const bestAskPrice = new BigNumber(market?.depth?.lastTrade?.price)
.multipliedBy(multiplier)
.decimalPlaces(market.decimalPlaces)
.toString();
setValue('price', bestAskPrice);
if (order.type === Schema.OrderType.TYPE_MARKET) {
setValue('type', Schema.OrderType.TYPE_LIMIT);
}
} else {
setValue('type', Schema.OrderType.TYPE_MARKET);
setValue('price', market?.depth?.lastTrade?.price);
}
}
setSlippageValue(value);
},
[
market.decimalPlaces,
market?.depth?.lastTrade?.price,
order.side,
order.type,
setSlippageValue,
setValue,
]
);
const onSubmit = useCallback(
(order: OrderSubmissionBody['orderSubmission']) => {
if (transactionStatus !== 'pending') {
submit({
...order,
price:
order.price && removeDecimal(order.price, market.decimalPlaces),
size: removeDecimal(order.size, market.positionDecimalPlaces),
});
}
},
[
transactionStatus,
submit,
market.decimalPlaces,
market.positionDecimalPlaces,
]
);
const steps = [
{
label: t('Select Market'),
component: (
<MarketSelector
market={market}
setMarket={setMarket}
ItemRenderer={MarketNameRenderer}
/>
),
value: market.tradableInstrument.instrument.name,
},
{
label: t('Select Direction'),
component: (
<Controller
name="side"
control={control}
render={({ field }) => (
<SideSelector value={field.value} onSelect={field.onChange} />
)}
/>
),
value: SIDE_NAMES[order.side] || '',
},
{
label: t('Choose Position Size'),
component:
max !== null ? (
<>
<DealTicketSize
step={step}
min={step}
max={max}
onSizeChange={onSizeChange}
size={new BigNumber(order.size).toNumber()}
name="size"
price={formattedPrice || emptyString}
positionDecimalPlaces={market.positionDecimalPlaces}
quoteName={
market.tradableInstrument.instrument.product.settlementAsset
.symbol
}
notionalSize={notionalSize || emptyString}
estCloseOut={estCloseOut || emptyString}
fees={fees || emptyString}
estMargin={estMargin?.margin || emptyString}
/>
<DealTicketSlippage
value={slippageValue}
onValueChange={onSlippageChange}
/>
</>
) : (
t('Loading...')
),
value: order.size,
},
{
label: t('Review Trade'),
component: (
<div className="mb-8">
{invalidText && (
<div className="mb-2">
<InputError data-testid="dealticket-error-message">
{invalidText}
</InputError>
</div>
)}
<ReviewTrade
market={market}
isDisabled={isDisabled}
transactionStatus={transactionStatus}
order={order}
estCloseOut={estCloseOut || emptyString}
estMargin={estMargin?.margin || emptyString}
price={formattedPrice || emptyString}
quoteName={
market.tradableInstrument.instrument.product.settlementAsset
.symbol
}
notionalSize={notionalSize || emptyString}
fees={fees || emptyString}
slippage={slippageValue}
/>
<Dialog
title={getOrderDialogTitle(finalizedOrder?.status)}
intent={getOrderDialogIntent(finalizedOrder?.status)}
icon={getOrderDialogIcon(finalizedOrder?.status)}
content={{
Complete: (
<OrderFeedback
transaction={transaction}
order={finalizedOrder}
/>
),
}}
/>
</div>
),
disabled: true,
},
];
return (
<form onSubmit={handleSubmit(onSubmit)} className="px-2 py-4">
<Stepper steps={steps} />
</form>
);
};
@@ -0,0 +1,2 @@
export * from './deal-ticket-container';
export * from './deal-ticket-size';
@@ -0,0 +1,105 @@
import { t } from '@vegaprotocol/react-helpers';
import {
Button,
KeyValueTable,
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { DealTicketEstimates } from '@vegaprotocol/deal-ticket';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { SIDE_NAMES } from './side-selector';
import * as Schema from '@vegaprotocol/types';
import { MarketExpires } from '@vegaprotocol/market-info';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
import { useMarketTagsQuery } from './__generated__/MarketTags';
interface Props {
market: MarketDealTicket;
isDisabled: boolean;
transactionStatus?: string;
order: OrderSubmissionBody['orderSubmission'];
estCloseOut: string;
estMargin: string;
quoteName: string;
price: string;
fees: string;
notionalSize: string;
slippage: number;
}
export default ({
isDisabled,
market,
order,
transactionStatus,
estCloseOut,
quoteName,
fees,
price,
notionalSize,
slippage,
}: Props) => {
const { data: tagsData } = useMarketTagsQuery({
variables: { marketId: market.id },
});
return (
<div className="mb-8 text-black dark:text-white" data-testid="review-trade">
<KeyValueTable>
<KeyValueTableRow noBorder>
<div className="flex flex-none gap-x-2 items-center">
<div
className={classNames(
{
'buyButton dark:buyButtonDark':
order.side === Schema.Side.SIDE_BUY,
'sellButton dark:sellButtonDark':
order.side === Schema.Side.SIDE_SELL,
},
'px-2 py-1 inline text-ui-small'
)}
>
{SIDE_NAMES[order.side]}
</div>
<div>{market.tradableInstrument.instrument.product.quoteName}</div>
<div>
{tagsData?.market?.tradableInstrument.instrument.metadata
.tags && (
<MarketExpires
tags={
tagsData?.market.tradableInstrument.instrument.metadata.tags
}
/>
)}
</div>
</div>
<div className="text-blue">
{`@ ${price} `}
<span className="text-ui-small inline">(EST)</span>
</div>
</KeyValueTableRow>
</KeyValueTable>
<DealTicketEstimates
size={order.size}
quoteName={quoteName}
fees={fees}
estCloseOut={estCloseOut}
notionalSize={notionalSize}
slippage={slippage.toString()}
/>
<div className="mt-12 max-w-sm">
<Button
fill={true}
type="submit"
disabled={transactionStatus === 'pending' || isDisabled}
data-testid="place-order"
rightIcon="arrow-top-right"
>
{transactionStatus === 'pending' ? t('Pending...') : t('Submit')}
</Button>
</div>
</div>
);
};
@@ -0,0 +1,60 @@
import React from 'react';
import classNames from 'classnames';
import { FormGroup } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
interface SideSelectorProps {
value: Schema.Side;
onSelect: (side: Schema.Side) => void;
}
export const SIDE_NAMES: Record<Schema.Side, string> = {
[Schema.Side.SIDE_BUY]: t('Long'),
[Schema.Side.SIDE_SELL]: t('Short'),
};
export default ({ value, onSelect }: SideSelectorProps) => {
return (
<FormGroup
label={t('Direction')}
labelFor="order-side-toggle"
hideLabel={true}
>
<fieldset
className="w-full grid md:grid-cols-2 gap-4"
id="order-side-toggle"
>
<button
aria-label={t('Open long position')}
type="button"
className={classNames(
'px-8 py-2',
'buyButton hover:buyButton dark:buyButtonDark dark:hover:buyButtonDark',
{ selected: value === Schema.Side.SIDE_BUY }
)}
onClick={() => onSelect(Schema.Side.SIDE_BUY)}
>
{t('Long')}
</button>
<button
aria-label={t('Open short position')}
type="button"
className={classNames(
'px-8 py-2',
'sellButton hover:sellButton dark:sellButtonDark dark:hover:sellButtonDark',
{ selected: value === Schema.Side.SIDE_SELL }
)}
onClick={() => onSelect(Schema.Side.SIDE_SELL)}
>
{t('Short')}
</button>
<div className="md:col-span-2 text-black dark:text-white text-ui-small">
{t(
'Trading derivatives allows you to make a profit or loss regardless of whether the market you are trading goes up or down. If you open a "long" position, you will make a profit if the price of your chosen market goes up, and you will make a profit for "short" positions when the price goes down.'
)}
</div>
</fieldset>
</FormGroup>
);
};
@@ -0,0 +1,415 @@
import * as React from 'react';
import { renderHook } from '@testing-library/react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types';
import type { ValidationProps } from './use-order-validation';
import { marketTranslations, useOrderValidation } from './use-order-validation';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
import * as DealTicket from '@vegaprotocol/deal-ticket';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/deal-ticket', () => {
return {
...jest.requireActual('@vegaprotocol/deal-ticket'),
useOrderMarginValidation: jest.fn(),
};
});
type SettlementAsset =
MarketDealTicket['tradableInstrument']['instrument']['product']['settlementAsset'];
const asset: SettlementAsset = {
__typename: 'Asset',
id: 'asset-id',
symbol: 'asset-symbol',
name: 'asset-name',
decimals: 2,
};
const market: MarketDealTicket = {
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
state: Schema.MarketState.STATE_ACTIVE,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: 'instrument-id',
name: 'instrument-name',
code: 'instriment-code',
metadata: {
tags: [],
},
product: {
__typename: 'Future',
quoteName: 'quote-name',
settlementAsset: asset,
dataSourceSpecForTradingTermination: {
id: 'dataSource-id',
},
},
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '100',
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
makerFee: '1',
infrastructureFee: '2',
liquidityFee: '3',
},
},
data: {
__typename: 'MarketData',
bestBidPrice: '1605489971',
bestOfferPrice: '1606823730',
markPrice: '1606823730',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
staticMidPrice: '1606156850',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
marketState: Schema.MarketState.STATE_ACTIVE,
indicativeVolume: '0',
indicativePrice: '0',
bestStaticBidPrice: '1605489971',
bestStaticOfferPrice: '1606823730',
targetStake: '8561302732',
suppliedStake: '727654170336',
auctionStart: null,
auctionEnd: null,
market: { __typename: 'Market', id: 'market-id' },
},
marketTimestamps: {
__typename: 'MarketTimestamps',
close: null,
open: null,
},
};
const defaultWalletContext = {
pubKey: '111111__111111',
pubKeys: [],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
const order = {
type: Schema.OrderType.TYPE_MARKET,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
marketId: 'market-id',
side: Schema.Side.SIDE_BUY,
size: '0.1',
};
const defaultOrder = {
market: { ...market },
step: 0.1,
order: {
...order,
},
};
const ERROR = {
KEY_MISSING: 'No public key selected',
KEY_TAINTED: 'Selected public key has been tainted',
MARKET_SUSPENDED: 'Market is currently suspended',
MARKET_INACTIVE: 'Market is no longer active',
MARKET_WAITING: 'Market is not active yet',
MARKET_CONTINUOUS_LIMIT:
'Only limit orders are permitted when market is in auction',
MARKET_CONTINUOUS_TIF:
'Until the auction ends, you can only place GFA, GTT, or GTC limit orders',
FIELD_SIZE_REQ: 'You need to provide a size',
FIELD_SIZE_MIN: `Size cannot be lower than "${defaultOrder.step}"`,
FIELD_PRICE_REQ: 'You need to provide a price',
FIELD_PRICE_MIN: 'The price cannot be negative',
FIELD_PRICE_STEP_NULL: 'Order sizes must be in whole numbers for this market',
FIELD_PRICE_STEP_DECIMAL: `The size field accepts up to ${market.positionDecimalPlaces} decimal places`,
};
function setup(
props?: Partial<ValidationProps>,
context?: Partial<VegaWalletContextShape>
) {
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context });
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }), {
wrapper: MockedProvider,
});
}
describe('useOrderValidation', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('Returns empty string when given valid data', () => {
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
balance: '0',
margin: '100',
balanceError: false,
});
const { result } = setup();
expect(result.current).toStrictEqual({
isDisabled: false,
message: ``,
section: '',
});
});
it('Returns an error message when no keypair found', () => {
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
balance: '0',
margin: '100',
balanceError: false,
});
const { result } = setup(defaultOrder, { pubKey: null });
expect(result.current).toStrictEqual({
isDisabled: false,
message: ``,
section: '',
});
});
it.each`
state
${Schema.MarketState.STATE_SETTLED}
${Schema.MarketState.STATE_REJECTED}
${Schema.MarketState.STATE_TRADING_TERMINATED}
${Schema.MarketState.STATE_CLOSED}
${Schema.MarketState.STATE_CANCELLED}
`(
'Returns an error message for market state when not accepting orders',
({ state }) => {
const market = {
...defaultOrder.market,
data: { ...defaultOrder.market.data, marketState: state },
};
const { result } = setup({ market });
expect(result.current).toStrictEqual({
isDisabled: true,
message: `This market is ${marketTranslations(
state
)} and not accepting orders`,
section: 'sec-summary',
});
}
);
it.each`
state
${Schema.MarketState.STATE_PENDING}
${Schema.MarketState.STATE_PROPOSED}
`(
'Returns an error message for market state suspended or pending',
({ state }) => {
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
balance: '0',
margin: '100',
balanceError: false,
});
const market = {
...defaultOrder.market,
data: {
...defaultOrder.market.data,
marketState: state,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
},
};
const { result } = setup({
market,
order: {
...order,
type: Schema.OrderType.TYPE_LIMIT,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
},
});
expect(result.current).toStrictEqual({
isDisabled: false,
message: `This market is ${Schema.MarketStateMapping[
state as Schema.MarketState
].toLowerCase()} and only accepting liquidity commitment orders`,
section: 'sec-summary',
});
}
);
it.each`
tradingMode | errorMessage
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
`(
`Returns an error message when trying to submit a non-limit order for a "$tradingMode" market`,
({ tradingMode, errorMessage }) => {
const market = {
...defaultOrder.market,
data: { ...defaultOrder.market.data, marketTradingMode: tradingMode },
};
const { result } = setup({
market,
order: {
...order,
type: Schema.OrderType.TYPE_MARKET,
},
});
expect(result.current.isDisabled).toBeTruthy();
expect(result.current.message).toBe(errorMessage);
}
);
it.each`
tradingMode | orderTimeInForce | errorMessage
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
`(
`Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market`,
({ tradingMode, orderTimeInForce, errorMessage }) => {
const market = {
...defaultOrder.market,
data: { ...defaultOrder.market.data, marketTradingMode: tradingMode },
};
const { result } = setup({
market,
order: {
...order,
type: Schema.OrderType.TYPE_LIMIT,
timeInForce: orderTimeInForce,
},
});
expect(result.current).toStrictEqual({
isDisabled: true,
message: errorMessage,
section: 'sec-force',
});
}
);
it.each`
fieldName | errorType | section | errorMessage
${`size`} | ${`required`} | ${'sec-size'} | ${ERROR.FIELD_SIZE_REQ}
${`size`} | ${`min`} | ${'sec-size'} | ${ERROR.FIELD_SIZE_MIN}
${`price`} | ${`required`} | ${'sec-price'} | ${ERROR.FIELD_PRICE_REQ}
${`price`} | ${`min`} | ${'sec-price'} | ${ERROR.FIELD_PRICE_MIN}
`(
`Returns an error message when the order $fieldName "$errorType" validation fails`,
({ fieldName, errorType, section, errorMessage }) => {
const { result } = setup({
fieldErrors: { [fieldName]: { type: errorType } },
order: {
...order,
type: Schema.OrderType.TYPE_LIMIT,
},
});
expect(result.current).toStrictEqual({
isDisabled: true,
message: errorMessage,
section,
});
}
);
it('Returns an error message when the order size incorrectly has decimal values', () => {
const { result } = setup({
market: { ...market, positionDecimalPlaces: 0 },
fieldErrors: {
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
},
});
expect(result.current).toStrictEqual({
isDisabled: true,
message: ERROR.FIELD_PRICE_STEP_NULL,
section: 'sec-size',
});
});
it('Returns an error message when the order size has more decimals than allowed', () => {
const { result } = setup({
fieldErrors: {
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
},
});
expect(result.current).toStrictEqual({
isDisabled: true,
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
section: 'sec-size',
});
});
it('Returns an error message when the estimated margin is higher than collateral', async () => {
const invalidatedMockValue = {
balance: '100',
margin: '200',
balanceError: true,
};
jest
.spyOn(DealTicket, 'useOrderMarginValidation')
.mockReturnValue(invalidatedMockValue);
const { result } = setup({});
expect(result.current.isDisabled).toBe(false);
const testElement = (
<DealTicket.MarginWarning
margin={invalidatedMockValue.margin}
balance={invalidatedMockValue.balance}
asset={asset}
/>
);
expect((result.current.message as React.ReactElement)?.props).toEqual(
testElement.props
);
expect((result.current.message as React.ReactElement)?.type).toEqual(
testElement.type
);
});
it.each`
state
${Schema.MarketState.STATE_PENDING}
${Schema.MarketState.STATE_PROPOSED}
`(
'Returns error when market state is pending and size is wrong',
({ state }) => {
const market = {
...defaultOrder.market,
data: { ...defaultOrder.market.data, marketState: state },
};
const { result } = setup({
fieldErrors: {
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
},
market,
});
expect(result.current).toStrictEqual({
isDisabled: true,
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
section: 'sec-size',
});
}
);
});
@@ -0,0 +1,384 @@
import type { ReactNode } from 'react';
import type { FieldErrors } from 'react-hook-form';
import { useMemo } from 'react';
import { t, toDecimal } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import {
MarketDataGrid,
compileGridData,
MarginWarning,
isMarketInAuction,
ERROR_SIZE_DECIMAL,
useOrderMarginValidation,
} from '@vegaprotocol/deal-ticket';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
export const DEAL_TICKET_SECTION = {
TYPE: 'sec-type',
SIZE: 'sec-size',
PRICE: 'sec-price',
FORCE: 'sec-force',
EXPIRY: 'sec-expiry',
SUMMARY: 'sec-summary',
};
export const ERROR_EXPIRATION_IN_THE_PAST = 'ERROR_EXPIRATION_IN_THE_PAST';
export type ValidationProps = {
step?: number;
market: MarketDealTicket;
order: OrderSubmissionBody['orderSubmission'];
fieldErrors?: FieldErrors<OrderSubmissionBody['orderSubmission']>;
};
export const marketTranslations = (marketState: Schema.MarketState) => {
switch (marketState) {
case Schema.MarketState.STATE_TRADING_TERMINATED:
return t('terminated');
default:
return t(Schema.MarketStateMapping[marketState]).toLowerCase();
}
};
export type DealTicketSection =
| ''
| typeof DEAL_TICKET_SECTION[keyof typeof DEAL_TICKET_SECTION];
export const useOrderValidation = ({
market,
fieldErrors,
order,
}: ValidationProps): {
message: ReactNode | string;
isDisabled: boolean;
section: DealTicketSection;
} => {
const { pubKey } = useVegaWallet();
const minSize = toDecimal(market.positionDecimalPlaces);
const isInvalidOrderMargin = useOrderMarginValidation({ market, order });
const fieldErrorChecking = useMemo<{
message: ReactNode | string;
isDisabled: boolean;
section: DealTicketSection;
} | null>(() => {
if (fieldErrors?.size?.type || fieldErrors?.price?.type) {
if (fieldErrors?.size?.type === 'required') {
return {
isDisabled: true,
message: t('You need to provide a size'),
section: DEAL_TICKET_SECTION.SIZE,
};
}
if (fieldErrors?.size?.type === 'min') {
return {
isDisabled: true,
message: t(`Size cannot be lower than "${minSize}"`),
section: DEAL_TICKET_SECTION.SIZE,
};
}
if (
fieldErrors?.price?.type === 'required' &&
order.type !== Schema.OrderType.TYPE_MARKET
) {
return {
isDisabled: true,
message: t('You need to provide a price'),
section: DEAL_TICKET_SECTION.PRICE,
};
}
if (
fieldErrors?.price?.type === 'min' &&
order.type !== Schema.OrderType.TYPE_MARKET
) {
return {
isDisabled: true,
message: t(`The price cannot be negative`),
section: DEAL_TICKET_SECTION.PRICE,
};
}
if (
fieldErrors?.size?.type === 'validate' &&
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
) {
if (market.positionDecimalPlaces === 0) {
return {
isDisabled: true,
message: t('Order sizes must be in whole numbers for this market'),
section: DEAL_TICKET_SECTION.SIZE,
};
}
return {
isDisabled: true,
message: t(
`The size field accepts up to ${market.positionDecimalPlaces} decimal places`
),
section: DEAL_TICKET_SECTION.SIZE,
};
}
}
if (
fieldErrors?.expiresAt?.type === 'validate' &&
fieldErrors?.expiresAt.message === ERROR_EXPIRATION_IN_THE_PAST
) {
return {
isDisabled: false,
message: t(
'The expiry date that you have entered appears to be in the past'
),
section: DEAL_TICKET_SECTION.EXPIRY,
};
}
return null;
}, [
fieldErrors?.size?.type,
fieldErrors?.size?.message,
fieldErrors?.price?.type,
fieldErrors?.expiresAt?.type,
fieldErrors?.expiresAt?.message,
order.type,
minSize,
market.positionDecimalPlaces,
]);
const { message, isDisabled, section } = useMemo<{
message: ReactNode | string;
isDisabled: boolean;
section: DealTicketSection;
}>(() => {
if (!pubKey) {
return {
message: t('No public key selected'),
isDisabled: true,
section: DEAL_TICKET_SECTION.SUMMARY,
};
}
if (
[
Schema.MarketState.STATE_SETTLED,
Schema.MarketState.STATE_REJECTED,
Schema.MarketState.STATE_TRADING_TERMINATED,
Schema.MarketState.STATE_CANCELLED,
Schema.MarketState.STATE_CLOSED,
].includes(market.data.marketState)
) {
return {
isDisabled: true,
message: t(
`This market is ${marketTranslations(
market.data.marketState
)} and not accepting orders`
),
section: DEAL_TICKET_SECTION.SUMMARY,
};
}
if (
[
Schema.MarketState.STATE_PROPOSED,
Schema.MarketState.STATE_PENDING,
].includes(market.data.marketState)
) {
if (fieldErrorChecking) {
return fieldErrorChecking;
}
return {
isDisabled: false,
message: t(
`This market is ${marketTranslations(
market.data.marketState
)} and only accepting liquidity commitment orders`
),
section: DEAL_TICKET_SECTION.SUMMARY,
};
}
if (isMarketInAuction(market)) {
if (order.type === Schema.OrderType.TYPE_MARKET) {
if (
market.data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data.trigger ===
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
{t('Only limit orders are permitted when market is in auction')}
</span>
),
section: DEAL_TICKET_SECTION.TYPE,
};
}
if (
market.data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}
{t('Only limit orders are permitted when market is in auction')}
</span>
),
section: DEAL_TICKET_SECTION.TYPE,
};
}
return {
isDisabled: true,
message: t(
'Only limit orders are permitted when market is in auction'
),
section: DEAL_TICKET_SECTION.SUMMARY,
};
}
if (
order.type === Schema.OrderType.TYPE_LIMIT &&
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
].includes(order.timeInForce)
) {
if (
market.data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data.trigger ===
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
{t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
)}
</span>
),
section: DEAL_TICKET_SECTION.FORCE,
};
}
if (
market.data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE
) {
return {
isDisabled: true,
message: (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={
<MarketDataGrid grid={compileGridData(market)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}
{t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
)}
</span>
),
section: DEAL_TICKET_SECTION.FORCE,
};
}
return {
isDisabled: true,
message: t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
),
section: DEAL_TICKET_SECTION.FORCE,
};
}
}
if (fieldErrorChecking) {
return fieldErrorChecking;
}
if (isInvalidOrderMargin.balanceError) {
return {
isDisabled: false,
message: (
<MarginWarning
margin={isInvalidOrderMargin.margin}
balance={isInvalidOrderMargin.balance}
asset={market.tradableInstrument.instrument.product.settlementAsset}
/>
),
section: DEAL_TICKET_SECTION.PRICE,
};
}
if (
[
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
].includes(market.data.marketTradingMode)
) {
return {
isDisabled: false,
message: t(
'Any orders placed now will not trade until the auction ends'
),
section: DEAL_TICKET_SECTION.SUMMARY,
};
}
return {
isDisabled: false,
message: '',
section: '',
};
}, [
pubKey,
market,
fieldErrorChecking,
isInvalidOrderMargin,
order.type,
order.timeInForce,
]);
return { message, isDisabled, section };
};
@@ -0,0 +1,18 @@
import { t } from '@vegaprotocol/react-helpers';
import { Button } from '@vegaprotocol/ui-toolkit';
import { DepositDialog, useDepositDialog } from '@vegaprotocol/deposits';
/**
* Fetches data required for the Deposit page
*/
export const DepositContainer = () => {
const openDepositDialog = useDepositDialog((state) => state.open);
return (
<div>
<DepositDialog />
<Button size="sm" onClick={() => openDepositDialog()}>
{t('Make deposit')}
</Button>
</div>
);
};
@@ -0,0 +1 @@
export * from './deposits';
@@ -0,0 +1,21 @@
import * as React from 'react';
import classNames from 'classnames';
import type { ReactElement } from 'react';
interface Props {
children: ReactElement | ReactElement[];
className?: string;
}
export const DrawerContent = ({ children, className = '' }: Props) => {
const classes = classNames(
'w-full sm:w-full grow-1 overflow-hidden',
className
);
return (
<main aria-label="Page Content" className={classes}>
{children}
</main>
);
};
@@ -0,0 +1,46 @@
import * as React from 'react';
import classNames from 'classnames';
import { ButtonLink, Icon } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
export const enum DRAWER_TOGGLE_VARIANTS {
OPEN = 'open',
CLOSE = 'close',
}
interface Props {
onToggle: React.MouseEventHandler<HTMLButtonElement>;
variant: DRAWER_TOGGLE_VARIANTS.OPEN | DRAWER_TOGGLE_VARIANTS.CLOSE;
className?: string;
}
export const DrawerToggle = ({
onToggle,
variant = DRAWER_TOGGLE_VARIANTS.CLOSE,
className = '',
}: Props) => {
const [iconName, setIconName] = useState(IconNames.MENU);
const classes = classNames('md:hidden', className);
useEffect(() => {
if (variant === DRAWER_TOGGLE_VARIANTS.OPEN) {
setIconName(IconNames.MENU);
} else {
setIconName(IconNames.CROSS);
}
}, [variant]);
const ariaLabel = `${
variant === DRAWER_TOGGLE_VARIANTS.OPEN ? 'Open' : 'Close'
} Sidebar Navigation Menu`;
return (
<span className={classes}>
<ButtonLink aria-label={ariaLabel} onClick={onToggle}>
<Icon name={iconName as IconName} />
</ButtonLink>
</span>
);
};
@@ -0,0 +1,13 @@
import * as React from 'react';
import classNames from 'classnames';
import type { ReactElement } from 'react';
interface Props {
children: ReactElement | ReactElement[];
className?: string;
}
export const DrawerWrapper = ({ children, className = '' }: Props) => {
const classes = classNames('flex dark:bg-lite-black md:flex-row', className);
return <div className={classes}>{children}</div>;
};
@@ -0,0 +1,63 @@
import * as React from 'react';
import classNames from 'classnames';
import type { ReactElement } from 'react';
interface Props {
children?: ReactElement | ReactElement[];
isMenuOpen?: boolean;
onToggle(): void;
rtl?: boolean;
outerClasses?: string;
innerClasses?: string;
}
export const NavigationDrawer = ({
isMenuOpen = false,
onToggle,
children,
rtl,
outerClasses = '',
innerClasses = '',
}: Props) => {
const width = 'w-full md:w-auto md:min-w-[15%] shrink-0';
const position = 'absolute inset-0 h-full z-10 md:static';
const background = 'bg-black/50 dark:bg-white/50';
const flex = 'flex justify-end overflow-hidden';
const joinedClasses = [flex, width, position, background].join(' ');
const outerStyles = classNames(joinedClasses, {
visible: isMenuOpen,
'invisible md:visible': !isMenuOpen,
'flex-row-reverse': !rtl,
[outerClasses]: outerClasses,
});
const translateClose = rtl ? 'translate-x-full' : '-translate-x-full';
const innerStyles = classNames(
'w-3/4 md:w-full bg-white dark:bg-lite-black',
{
'translate-x-0 transition-transform md:transform-none': isMenuOpen,
[`${translateClose} md:transform-none`]: !isMenuOpen,
[innerClasses]: innerClasses,
}
);
return (
<aside aria-label="Sidebar Navigation Menu" className={outerStyles}>
<div
role="presentation"
aria-label="Content Overlay - Click To Close Sidebar Navigation"
className="md:hidden grow h-full"
onClick={onToggle}
/>
<div
role="group"
aria-label="Sidebar Navigation Grouped Content"
className={innerStyles}
>
{children}
</div>
</aside>
);
};
@@ -0,0 +1,4 @@
export * from './drawer';
export * from './drawer-toggle';
export * from './drawer-content';
export * from './drawer-wrapper';
@@ -0,0 +1,48 @@
import React from 'react';
const Comet = () => {
return (
<svg
viewBox="0 0 118 82"
fill="currentColor"
className="max-w-[7.5rem] mx-auto"
>
<g className="fill-black dark:fill-white">
<path d="M44.3791 67.5864H41.5859V70.3796H44.3791V67.5864Z"></path>
<path d="M47.173 64.7932V62H44.3799V64.7932V67.5863H47.173V64.7932Z"></path>
<path d="M41.5863 70.3794H36V73.1726H41.5863V70.3794Z"></path>
<path d="M44.3791 73.1719H41.5859V75.965H44.3791V73.1719Z"></path>
<path d="M47.173 75.9658H44.3799V81.5521H47.173V75.9658Z"></path>
<path d="M49.966 73.1719H47.1729V75.965H49.966V73.1719Z"></path>
<path d="M55.5521 70.3794H49.9658V73.1726H55.5521V70.3794Z"></path>
<path d="M49.966 67.5864H47.1729V70.3796H49.966V67.5864Z"></path>
<path d="M57.1876 60.564H54V63.7515H57.1876V60.564Z"></path>
<path d="M60.3751 57.3765H57.1875V60.564H60.3751V57.3765Z"></path>
<path d="M63.5626 54.1885H60.375V57.3761H63.5626V54.1885Z"></path>
<path d="M66.7501 51.001H63.5625V54.1886H66.7501V51.001Z"></path>
<path d="M69.9376 47.8135H66.75V51.0011H69.9376V47.8135Z"></path>
<path d="M73.1251 44.626H69.9375V47.8136H73.1251V44.626Z"></path>
<path d="M76.3126 41.4385H73.125V44.6261H76.3126V41.4385Z"></path>
<path d="M79.5001 38.251H76.3125V41.4386H79.5001V38.251Z"></path>
<path d="M82.6886 35.0635H79.501V38.2511H82.6886V35.0635Z"></path>
<path d="M85.8761 31.876H82.6885V35.0636H85.8761V31.876Z"></path>
<path d="M89.0636 28.688H85.876V31.8756H89.0636V28.688Z"></path>
<path d="M92.2511 25.5005H89.0635V28.6881H92.2511V25.5005Z"></path>
<path d="M95.4386 22.313H92.251V25.5006H95.4386V22.313Z"></path>
<path d="M98.6261 19.1255H95.4385V22.3131H98.6261V19.1255Z"></path>
<path d="M101.814 15.938H98.626V19.1256H101.814V15.938Z"></path>
<path d="M105.001 12.7505H101.813V15.9381H105.001V12.7505Z"></path>
<path d="M108.189 9.5625H105.001V12.7501H108.189V9.5625Z"></path>
<path d="M111.376 6.375H108.188V9.56258H111.376V6.375Z"></path>
<path d="M114.565 3.1875H111.377V6.37508H114.565V3.1875Z"></path>
<path d="M117.752 0H114.564V3.18758H117.752V0Z"></path>
<path d="M2.79316 20.793H0V23.5861H2.79316V20.793Z"></path>
<path d="M5.58612 18H2.79297V20.7932H5.58612V18Z"></path>
<path d="M5.58612 23.5859H2.79297V26.3791H5.58612V23.5859Z"></path>
<path d="M8.37909 20.793H5.58594V23.5861H8.37909V20.793Z"></path>
</g>
</svg>
);
};
export default Comet;
@@ -0,0 +1,32 @@
import React, { useContext } from 'react';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import Logo from './logo';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import LocalContext from '../../context/local-context';
const Header = () => {
const { updateVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
updateVegaWalletDialog: store.updateVegaWalletDialog,
}));
const {
vegaWalletDialog: { setManage },
} = useContext(LocalContext);
return (
<div
className="dark flex items-stretch pr-6 py-6 bg-black text-neutral-400"
data-testid="header"
>
<Logo />
<div className="flex items-center gap-2 ml-auto relative z-10">
<VegaWalletConnectButton
setConnectDialog={updateVegaWalletDialog}
setManageDialog={setManage}
/>
<ThemeSwitcher className="-my-4" />
</div>
</div>
);
};
export default Header;
@@ -0,0 +1 @@
export { default } from './header';
@@ -0,0 +1,18 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { t } from '@vegaprotocol/react-helpers';
import { VLogo } from '@vegaprotocol/ui-toolkit';
const Logo = () => {
return (
<NavLink
className="mx-6 text-white"
aria-label={t('Go to home page')}
to="/"
>
<VLogo className="mx-6 my-2" />
</NavLink>
);
};
export default Logo;

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