Compare commits

...
33 changed files with 1252 additions and 428 deletions
+28
View File
@@ -0,0 +1,28 @@
---
name: Feature Epic
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'FEATURE EPIC: '
labels: feature-epic
---
## Core Feature
<Name>
## Tasks
- [ ] Define high level requirements
- [ ] Create basic mockups
- [ ] Update "API Requirements" in core spec
- [ ] Update "User-Interface Spec" in relevant front end repo
- [ ] Create detailed user stories using normal template
## High Level Requirements
## Basic Mockups
## Link to API Requirements in Core spec
## Link to User Interface Specs
## Linked User Stories
+1 -1
View File
@@ -1,6 +1,6 @@
# Related issues 🔗 # Related issues 🔗
Closes #[Issue number here] Issue: #[Issue number here]
# Description # Description
@@ -7,7 +7,7 @@ on:
jobs: jobs:
after-release: after-release:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
timeout-minutes: 30 timeout-minutes: 45
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -30,18 +30,20 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Wait for publish to complete
uses: lewagon/wait-on-check-action@v1.3.1
with:
ref: ${{ github.event.release.tag_name }}
check-name: '(CD) publish dist / trading'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- name: resolve ipfs hashes for release - name: resolve ipfs hashes for release
run: | run: |
echo "Name: ${{ github.event.release.name }}" echo "Name: ${{ github.event.release.name }}"
echo "Description: ${{ github.event.release.body }}" echo "Description: ${{ github.event.release.body }}"
echo "Tag: ${{ github.event.release.tag_name }}" echo "Tag: ${{ github.event.release.tag_name }}"
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})" docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
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 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 tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo" export PATH="$PATH:$PWD/kubo"
+1 -1
View File
@@ -6,7 +6,7 @@ name: 'Add Issues To Project Board'
types: types:
- opened - opened
env: env:
GH_TOKEN: ${{ secrets.GH_NEW_CARD_TO_PROJECT }} GH_TOKEN: ${{ secrets.PROJECT_MANAGE_ACTION }}
PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }} PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }}
ISSUE_ID: ${{ github.event.issue.node_id }} ISSUE_ID: ${{ github.event.issue.node_id }}
USER: ${{ github.actor }} USER: ${{ github.actor }}
+31 -103
View File
@@ -5,10 +5,7 @@ on:
branches: branches:
- release/* - release/*
- develop - develop
- main pull_request:
# 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: types:
- opened - opened
- ready_for_review - ready_for_review
@@ -49,7 +46,7 @@ jobs:
lint-pr-title: lint-pr-title:
needs: node-modules needs: node-modules
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} if: ${{ github.event_name == 'pull_request' }}
name: Verify PR title name: Verify PR title
uses: ./.github/workflows/lint-pr.yml uses: ./.github/workflows/lint-pr.yml
secrets: inherit secrets: inherit
@@ -84,6 +81,22 @@ jobs:
with: with:
main-branch-name: develop main-branch-name: develop
# See affected apps
- name: See affected apps
run: |
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 )"
python3 tools/ci/check-affected.py --github-ref="${{ github.ref }}" --branch-slug="$branch_slug" --event-name="${{ github.event_name }}"
- name: Verify script result
run: |
echo "Check outputs from script"
echo "projects: ${{ env.PROJECTS }}"
echo "projects-e2e: ${{ env.PROJECTS_E2E }}"
echo "preview_governance: ${{ env.PREVIEW_GOVERNANCE }}"
echo "preview_trading: ${{ env.PREVIEW_TRADING }}"
echo "preview_explorer: ${{ env.PREVIEW_EXPLORER }}"
echo "preview_tools: ${{ env.PREVIEW_TOOLS }}"
- name: Check formatting - name: Check formatting
run: yarn nx format:check run: yarn nx format:check
@@ -99,100 +112,6 @@ jobs:
- name: Build affected - name: Build affected
run: yarn nx affected:build || (yarn install && yarn nx affected:build) 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: outputs:
projects: ${{ env.PROJECTS }} projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }} projects-e2e: ${{ env.PROJECTS_E2E }}
@@ -201,6 +120,15 @@ jobs:
preview_explorer: ${{ env.PREVIEW_EXPLORER }} preview_explorer: ${{ env.PREVIEW_EXPLORER }}
preview_tools: ${{ env.PREVIEW_TOOLS }} preview_tools: ${{ env.PREVIEW_TOOLS }}
console-e2e:
needs: lint-test-build
name: '(CI) console python'
uses: ./.github/workflows/console-test-run.yml
secrets: inherit
if: ${{ contains(fromJSON(needs.lint-test-build.outputs.projects), 'trading') && github.event_name == 'pull_request' }}
with:
github-sha: ${{ github.event.pull_request.head.sha || github.sha }}
cypress: cypress:
needs: lint-test-build needs: lint-test-build
name: '(CI) cypress' name: '(CI) cypress'
@@ -209,12 +137,12 @@ jobs:
secrets: inherit secrets: inherit
with: with:
projects: ${{ needs.lint-test-build.outputs.projects-e2e }} projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
tags: '@smoke @regression' tags: '@smoke'
publish-dist: publish-dist:
needs: lint-test-build needs: lint-test-build
name: '(CD) publish dist' name: '(CD) publish dist'
# if: ${{ needs.lint-test-build.outputs.projects != '[]' }} if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'vegaprotocol/frontend-monorepo') || github.event_name == 'push' }}
uses: ./.github/workflows/publish-dist.yml uses: ./.github/workflows/publish-dist.yml
secrets: inherit secrets: inherit
with: with:
@@ -225,7 +153,7 @@ jobs:
needs: needs:
- publish-dist - publish-dist
- lint-test-build - lint-test-build
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'vegaprotocol/frontend-monorepo' }}
timeout-minutes: 60 timeout-minutes: 60
name: '(CD) comment preview links' name: '(CD) comment preview links'
steps: steps:
@@ -271,7 +199,7 @@ jobs:
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
body: | body: |
Previews: Previews
* governance: ${{ needs.lint-test-build.outputs.preview_governance }} * governance: ${{ needs.lint-test-build.outputs.preview_governance }}
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }} * explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
* trading: ${{ needs.lint-test-build.outputs.preview_trading }} * trading: ${{ needs.lint-test-build.outputs.preview_trading }}
+142
View File
@@ -0,0 +1,142 @@
name: (CI) Console tests
on:
workflow_call:
inputs:
github-sha:
required: true
type: string
jobs:
run-tests:
name: run-tests
runs-on: 8-cores
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
#----------------------------------------------
- name: Checkout console test repo
uses: actions/checkout@v3
with:
ref: ${{ inputs.github-sha }}
#----------------------------------------------
# cache node modules
#----------------------------------------------
- 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-
#----------------------------------------------
# setup node
#----------------------------------------------
- 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
#----------------------------------------------
# install deps if cache missing
#----------------------------------------------
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
#----------------------------------------------
# build trading
#----------------------------------------------
- name: Build affected spec
run: |
yarn env-cmd -f ./apps/trading/.env.stagnet1 yarn nx export trading
#----------------------------------------------
# run trading server
#----------------------------------------------
- name: Run trading server
run: |
docker run -d -p 80:4200 -v $PWD/docker/nginx.conf:/etc/nginx/conf.d/default.conf -v $PWD/dist/apps/trading/exported:/usr/share/nginx/html nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
sleep 5
docker ps
#----------------------------------------------
# check if container persists between runs
#----------------------------------------------
- name: Check server
run: |
docker ps
#----------------------------------------------
# check-out tests repo
#----------------------------------------------
- name: Checkout console test repo
uses: actions/checkout@v3
with:
repository: vegaprotocol/console-test
path: './console-test'
#----------------------------------------------
# set-up python
#----------------------------------------------
- name: Set up python
id: setup-python
uses: actions/setup-python@v4
with:
python-version: '3.10.11'
#----------------------------------------------
# ----- install & configure poetry -----
#----------------------------------------------
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
virtualenvs-path: console-test/.venv
#----------------------------------------------
# load cached venv if cache exists
#----------------------------------------------
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: console-test/.venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
#----------------------------------------------
# install dependencies if cache does not exist
#----------------------------------------------
- name: Install dependencies
working-directory: ./console-test
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
#----------------------------------------------
# install vega binaries
#----------------------------------------------
- name: Install vega binaries
working-directory: ./console-test
run: poetry run python -m vega_sim.tools.load_binaries --force
#----------------------------------------------
# install playwright
#----------------------------------------------
- name: install playwright
run: poetry run playwright install
working-directory: ./console-test
#----------------------------------------------
# run tests
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest -v -s --numprocesses auto --dist loadfile --durations=20
- name: Check files
run: |
ls -al .
ls -al console-test
#----------------------------------------------
# upload traces
#----------------------------------------------
- name: Upload Playwright Trace
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-trace
path: ./traces/
retention-days: 15
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
project: ${{ fromJSON(inputs.projects) }} project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }} name: ${{ matrix.project }}
runs-on: self-hosted-runner runs-on: self-hosted-runner
timeout-minutes: 100 timeout-minutes: 120
steps: steps:
# Checks if skip cache was requested # Checks if skip cache was requested
- name: Set skip-nx-cache flag - name: Set skip-nx-cache flag
+67 -63
View File
@@ -22,6 +22,45 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha || github.sha }} ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Init variables
run: |
echo IS_PR=false >> $GITHUB_ENV
echo IS_MAINNET_RELEASE=false >> $GITHUB_ENV
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
echo IS_S3_RELEASE=false >> $GITHUB_ENV
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
- name: Is dev image
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
run: |
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
- name: Is PR
if: ${{ github.event_name == 'pull_request' }}
run: |
echo IS_PR=true >> $GITHUB_ENV
- name: Is mainnet release
if: ${{ contains(github.ref, 'release/mainnet') && !contains(github.ref, 'mirror') }}
run: |
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
- name: Is testnet release
if: ${{ contains(github.ref, 'release/testnet') }}
run: |
echo IS_TESTNET_RELEASE=true >> $GITHUB_ENV
- name: Is IPFS Release
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( env.IS_MAINNET_RELEASE == 'true' || env.IS_TESTNET_RELEASE == 'true' ) }}
run: |
echo IS_IPFS_RELEASE=true >> $GITHUB_ENV
- name: Is S3 Release
if: ${{ env.IS_IPFS_RELEASE == 'false' && github.event_name == 'push' }}
run: |
echo IS_S3_RELEASE=true >> $GITHUB_ENV
- name: Set up QEMU - name: Set up QEMU
id: quemu id: quemu
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
@@ -33,7 +72,7 @@ jobs:
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr) - name: Log in to the Container registry (ghcr)
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} if: ${{ env.IS_PR == 'true' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
@@ -42,9 +81,8 @@ jobs:
- name: Log in to the Container registry (docker hub) - name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2 uses: docker/login-action@v2
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }} if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with: with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -65,51 +103,13 @@ jobs:
- name: Define dist variables - name: Define dist variables
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
run: | run: |
envName='' python3 tools/ci/define-dist-variables.py --github-ref="${{ github.ref }}" --app="${{ matrix.app }}"
domain="vega.rocks"
bucketName=''
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then - name: Verify script result
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)" if: ${{ github.event_name == 'push' }}
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then run: |
envName="stagnet1" echo "BUCKET_NAME=${{ env.BUCKET_NAME }}"
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then echo "ENV_NAME=${{ env.ENV_NAME }}"
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 - name: Build local dist
run: | run: |
@@ -124,8 +124,12 @@ jobs:
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit DIST_LOCATION=dist/storybook/ui-toolkit
elif [ "${{ matrix.app }}" = "static" ]; then
yarn nx build static || (yarn install && yarn nx build static)
else else
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }}) $envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
fi
if [[ -z "$DIST_LOCATION" ]]; then
DIST_LOCATION=dist/apps/${{ matrix.app }} DIST_LOCATION=dist/apps/${{ matrix.app }}
fi fi
mv $DIST_LOCATION dist-result mv $DIST_LOCATION dist-result
@@ -145,7 +149,7 @@ jobs:
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest - name: Image digest
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} if: ${{ env.IS_PR == 'true' }}
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image - name: Sanity check docker image
@@ -160,7 +164,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
continue-on-error: true continue-on-error: true
id: ghcr-push id: ghcr-push
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} if: ${{ env.IS_PR == 'true' }}
with: with:
context: . context: .
file: docker/node-outside-docker.Dockerfile file: docker/node-outside-docker.Dockerfile
@@ -175,7 +179,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
continue-on-error: true continue-on-error: true
id: dockerhub-push id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }} if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with: with:
context: . context: .
file: docker/node-outside-docker.Dockerfile file: docker/node-outside-docker.Dockerfile
@@ -185,7 +189,7 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }} ENV_NAME=${{ env.ENV_NAME }}
tags: | tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }} vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }} vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
- name: Publish dist as docker image (ghcr - retry) - name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
@@ -212,13 +216,13 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }} ENV_NAME=${{ env.ENV_NAME }}
tags: | tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }} vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }} vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
# bucket creation in github.com/vegaprotocol/terraform//frontend # bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3 - name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS # 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') ) ) ) }} if: ${{ env.IS_S3_RELEASE == 'true' }}
with: with:
args: --acl private --follow-symlinks --delete args: --acl private --follow-symlinks --delete
env: env:
@@ -229,11 +233,11 @@ jobs:
SOURCE_DIR: 'dist-result' SOURCE_DIR: 'dist-result'
- name: Install aws CLI - name: Install aws CLI
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }} if: ${{ env.IS_S3_RELEASE == 'true' }}
uses: unfor19/install-aws-cli-action@master uses: unfor19/install-aws-cli-action@master
- name: Perform cache invalidation - name: Perform cache invalidation
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }} if: ${{ env.IS_S3_RELEASE == 'true' }}
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -246,16 +250,16 @@ jobs:
- name: Add preview label - name: Add preview label
uses: actions-ecosystem/action-add-labels@v1 uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} if: ${{ env.IS_PR == 'true' }}
with: with:
labels: ${{ matrix.app }}-preview labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }} number: ${{ github.event.number }}
- name: Trigger fleek deployment - name: Trigger fleek deployment
# release to ipfs happens only on mainnet (represented by main branch) for trading # 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') ) }} if: ${{ env.IS_IPFS_RELEASE == 'true' }}
run: | run: |
if echo ${{ github.ref }} | grep -q main; then if [[ "${{ env.IS_MAINNET_RELEASE }}" = "true" ]]; then
# display info about app # display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \ curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -268,7 +272,7 @@ jobs:
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \ -d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql https://api.fleek.co/graphql
elif echo ${{ github.ref }} | grep -q release/testnet; then elif [[ "${{ env.IS_TESTNET_RELEASE }}" = "true" ]]; then
# display info about app # display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \ curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -283,7 +287,7 @@ jobs:
fi fi
- name: Check out ipfs-redirect - name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }} if: ${{ env.IS_IPFS_RELEASE == 'true' }}
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: 'vegaprotocol/ipfs-redirect' repository: 'vegaprotocol/ipfs-redirect'
@@ -292,7 +296,7 @@ jobs:
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update interstitial page to point to the new console - 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') ) }} if: ${{ env.IS_IPFS_RELEASE == 'true' }}
env: env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: | run: |
@@ -314,11 +318,11 @@ jobs:
git config --global user.name "vega-ci-bot" git config --global user.name "vega-ci-bot"
# update CID files # update CID files
if echo ${{ github.ref }} | grep -q main; then if [[ "${{ env.IS_MAINNET_RELEASE }}" = "true" ]]; then
echo $new_hash > cidv0-mainnet.txt echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt git add cidv0-mainnet.txt cidv1-mainnet.txt
elif echo ${{ github.ref }} | grep -q release/testnet; then elif [[ "${{ env.IS_TESTNET_RELEASE }}" = "true" ]]; then
echo $new_hash > cidv0-fairground.txt echo $new_hash > cidv0-fairground.txt
echo $new_cid > cidv1-fairground.txt echo $new_cid > cidv1-fairground.txt
git add cidv0-fairground.txt cidv1-fairground.txt git add cidv0-fairground.txt cidv1-fairground.txt
+1
View File
@@ -15,6 +15,7 @@ on:
- types - types
- utils - utils
- i18n - i18n
- wallet
jobs: jobs:
publish: publish:
@@ -13,12 +13,6 @@ export const VegaWalletDialogs = () => {
<> <>
<VegaConnectDialog <VegaConnectDialog
connectors={Connectors} connectors={Connectors}
onChangeOpen={(open) =>
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: open,
})
}
riskMessage={<RiskMessage />} riskMessage={<RiskMessage />}
/> />
@@ -2,15 +2,18 @@ import {
RestConnector, RestConnector,
JsonRpcConnector, JsonRpcConnector,
ViewConnector, ViewConnector,
InjectedConnector,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
export const rest = new RestConnector(); export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector(); export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address')); export const view = new ViewConnector(urlParams.get('address'));
export const Connectors = { export const Connectors = {
injected,
rest, rest,
jsonRpc, jsonRpc,
view, view,
@@ -8,9 +8,10 @@ import { TxState } from '../../../hooks/transaction-reducer';
import { useTransaction } from '../../../hooks/use-transaction'; import { useTransaction } from '../../../hooks/use-transaction';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { AssociateInfo } from './associate-info'; import { AssociateInfo } from './associate-info';
import { removeDecimal, toBigNum } from '@vegaprotocol/utils'; import { toBigNum } from '@vegaprotocol/utils';
import type { EthereumConfig } from '@vegaprotocol/web3'; import type { EthereumConfig } from '@vegaprotocol/web3';
import { useBalances } from '../../../lib/balances/balances-store'; import { useBalances } from '../../../lib/balances/balances-store';
import { MaxUint256 } from '@ethersproject/constants';
export const WalletAssociate = ({ export const WalletAssociate = ({
perform, perform,
@@ -42,7 +43,7 @@ export const WalletAssociate = ({
} = useTransaction(() => { } = useTransaction(() => {
return token.approve( return token.approve(
ethereumConfig.staking_bridge_contract.address, ethereumConfig.staking_bridge_contract.address,
removeDecimal('1000000', decimals).toString() MaxUint256.toString()
); );
}); });
+1 -1
View File
@@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz NX_VEGA_CONSOLE_URL=https://console.vega.xyz
# TAG name of the current app version - TODO: bump to the latest upon release # TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.21-core-0.71.6 NX_APP_VERSION=v0.20.23-core-0.71.6
+3
View File
@@ -2,10 +2,12 @@ import {
RestConnector, RestConnector,
JsonRpcConnector, JsonRpcConnector,
ViewConnector, ViewConnector,
InjectedConnector,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
export const rest = new RestConnector(); export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector(); export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
let view: ViewConnector; let view: ViewConnector;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -16,6 +18,7 @@ if (typeof window !== 'undefined') {
} }
export const Connectors = { export const Connectors = {
injected,
rest, rest,
jsonRpc, jsonRpc,
view, view,
@@ -6,6 +6,7 @@ import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types'; import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community'; import type { ICellRendererParams } from 'ag-grid-community';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
jest.mock('./liquidation-price', () => ({ jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => ( LiquidationPrice: () => (
@@ -19,7 +20,7 @@ const singleRow: Position = {
assetSymbol: 'BTC', assetSymbol: 'BTC',
averageEntryPrice: '133', averageEntryPrice: '133',
currentLeverage: 1.1, currentLeverage: 1.1,
decimals: 2, decimals: 2, // this is settlementAsset.decimals
quantum: '0.1', quantum: '0.1',
lossSocializationAmount: '0', lossSocializationAmount: '0',
marginAccountBalance: '12345600', marginAccountBalance: '12345600',
@@ -177,12 +178,22 @@ it('displays allocated margin', async () => {
}); });
it('displays realised and unrealised PNL', async () => { it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
);
await act(async () => { await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />); render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual('12.3'); expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual('45.6'); expect(cells[10].textContent).toEqual(expectedUnrealised);
}); });
it('displays close button', async () => { it('displays close button', async () => {
+4 -16
View File
@@ -365,20 +365,14 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data return !data
? undefined ? undefined
: toBigNum( : toBigNum(data.realisedPNL, data.decimals).toNumber();
data.realisedPNL,
data.marketDecimalPlaces
).toNumber();
}, },
valueFormatter: ({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => { }: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data return !data
? '' ? ''
: addDecimalsFormatNumber( : addDecimalsFormatNumber(data.realisedPNL, data.decimals);
data.realisedPNL,
data.marketDecimalPlaces
);
}, },
headerTooltip: t( headerTooltip: t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.' 'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
@@ -396,20 +390,14 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data return !data
? undefined ? undefined
: toBigNum( : toBigNum(data.unrealisedPNL, data.decimals).toNumber();
data.unrealisedPNL,
data.marketDecimalPlaces
).toNumber();
}, },
valueFormatter: ({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) => }: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data !data
? '' ? ''
: addDecimalsFormatNumber( : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
data.unrealisedPNL,
data.marketDecimalPlaces
),
headerTooltip: t( headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.' 'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
), ),
@@ -0,0 +1,7 @@
export const IconChevronLeft = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M10.38 1.62L11.13 2.38L5.5 8L11.13 13.62L10.38 14.38L4 8L10.38 1.62Z" />
</svg>
);
};
@@ -2,6 +2,7 @@ import { IconArrowDown } from './svg-icons/icon-arrow-down';
import { IconArrowRight } from './svg-icons/icon-arrow-right'; import { IconArrowRight } from './svg-icons/icon-arrow-right';
import { IconBreakdown } from './svg-icons/icon-breakdown'; import { IconBreakdown } from './svg-icons/icon-breakdown';
import { IconChevronDown } from './svg-icons/icon-chevron-down'; import { IconChevronDown } from './svg-icons/icon-chevron-down';
import { IconChevronLeft } from './svg-icons/icon-chevron-left';
import { IconChevronUp } from './svg-icons/icon-chevron-up'; import { IconChevronUp } from './svg-icons/icon-chevron-up';
import { IconCopy } from './svg-icons/icon-copy'; import { IconCopy } from './svg-icons/icon-copy';
import { IconCross } from './svg-icons/icon-cross'; import { IconCross } from './svg-icons/icon-cross';
@@ -26,6 +27,7 @@ export enum VegaIconNames {
ARROW_RIGHT = 'arrow-right', ARROW_RIGHT = 'arrow-right',
BREAKDOWN = 'breakdown', BREAKDOWN = 'breakdown',
CHEVRON_DOWN = 'chevron-down', CHEVRON_DOWN = 'chevron-down',
CHEVRON_LEFT = 'chevron-left',
CHEVRON_UP = 'chevron-up', CHEVRON_UP = 'chevron-up',
COPY = 'copy', COPY = 'copy',
CROSS = 'cross', CROSS = 'cross',
@@ -53,6 +55,7 @@ export const VegaIconNameMap: Record<
'arrow-down': IconArrowDown, 'arrow-down': IconArrowDown,
'arrow-right': IconArrowRight, 'arrow-right': IconArrowRight,
'chevron-down': IconChevronDown, 'chevron-down': IconChevronDown,
'chevron-left': IconChevronLeft,
'chevron-up': IconChevronUp, 'chevron-up': IconChevronUp,
'open-external': IconOpenExternal, 'open-external': IconOpenExternal,
'question-mark': IconQuestionMark, 'question-mark': IconQuestionMark,
@@ -1,7 +1,10 @@
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment'; import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Link } from '@vegaprotocol/ui-toolkit'; import { Link } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { VegaConnector } from '../connectors';
import { RestConnector } from '../connectors';
export const ConnectDialogTitle = ({ children }: { children: ReactNode }) => { export const ConnectDialogTitle = ({ children }: { children: ReactNode }) => {
return ( return (
@@ -18,11 +21,35 @@ export const ConnectDialogContent = ({ children }: { children: ReactNode }) => {
return <div>{children}</div>; return <div>{children}</div>;
}; };
export const ConnectDialogFooter = ({ children }: { children?: ReactNode }) => { export const ConnectDialogFooter = ({
connector,
}: {
connector: VegaConnector | undefined;
}) => {
const wrapperClasses = classNames(
'flex justify-center gap-4',
'px-4 md:px-8 pt-4 md:pt-6',
'border-t border-vega-light-200 dark:border-vega-dark-200',
'text-vega-light-400 dark:text-vega-dark-400'
);
const isHostedWalletSelected = connector instanceof RestConnector;
return ( return (
<footer className="flex justify-center gap-4 px-4 md:px-8 pt-4 md:pt-6 -mx-4 md:-mx-8 border-t border-neutral-500 text-neutral-500 dark:text-neutral-400 mt-6"> <footer className={wrapperClasses}>
{children ? ( {isHostedWalletSelected ? (
children <p className="text-center">
{t('For demo purposes get a ')}
<Link
href={ExternalLinks.VEGA_WALLET_HOSTED_URL}
target="_blank"
rel="noreferrer"
>
{t('hosted wallet')}
</Link>
{t(', or for the real experience create a wallet in the ')}
<Link href={ExternalLinks.VEGA_WALLET_URL}>
{t('Vega wallet app')}
</Link>
</p>
) : ( ) : (
<> <>
<Link href={ExternalLinks.VEGA_WALLET_URL}> <Link href={ExternalLinks.VEGA_WALLET_URL}>
@@ -16,6 +16,7 @@ import {
import type { VegaConnectDialogProps } from '..'; import type { VegaConnectDialogProps } from '..';
import { import {
ClientErrors, ClientErrors,
InjectedConnector,
JsonRpcConnector, JsonRpcConnector,
RestConnector, RestConnector,
ViewConnector, ViewConnector,
@@ -24,6 +25,12 @@ import {
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import type { ChainIdQuery } from './__generated__/ChainId'; import type { ChainIdQuery } from './__generated__/ChainId';
import { ChainIdDocument } from './__generated__/ChainId'; import { ChainIdDocument } from './__generated__/ChainId';
import {
mockBrowserWallet,
clearBrowserWallet,
delayedReject,
delayedResolve,
} from '../test-helpers';
const mockUpdateDialogOpen = jest.fn(); const mockUpdateDialogOpen = jest.fn();
const mockCloseVegaDialog = jest.fn(); const mockCloseVegaDialog = jest.fn();
@@ -49,10 +56,12 @@ const INITIAL_KEY = 'some-key';
const rest = new RestConnector(); const rest = new RestConnector();
const jsonRpc = new JsonRpcConnector(); const jsonRpc = new JsonRpcConnector();
const view = new ViewConnector(INITIAL_KEY); const view = new ViewConnector(INITIAL_KEY);
const injected = new InjectedConnector();
const connectors = { const connectors = {
rest, rest,
jsonRpc, jsonRpc,
view, view,
injected,
}; };
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@@ -105,7 +114,7 @@ describe('VegaConnectDialog', () => {
expect(screen.getByTestId('connector-jsonRpc')).toHaveTextContent( expect(screen.getByTestId('connector-jsonRpc')).toHaveTextContent(
'Connect Vega wallet' 'Connect Vega wallet'
); );
expect(screen.getByTestId('connector-hosted')).toHaveTextContent( expect(screen.getByTestId('connector-rest')).toHaveTextContent(
'Hosted Fairground wallet' 'Hosted Fairground wallet'
); );
expect(screen.getByTestId('connector-view')).toHaveTextContent( expect(screen.getByTestId('connector-view')).toHaveTextContent(
@@ -113,6 +122,17 @@ describe('VegaConnectDialog', () => {
); );
}); });
it('displays browser wallet option if detected on window object', async () => {
mockBrowserWallet();
render(generateJSX());
const list = await screen.findByTestId('connectors-list');
expect(list.children).toHaveLength(4);
expect(screen.getByTestId('connector-injected')).toHaveTextContent(
'Connect Web wallet'
);
clearBrowserWallet();
});
describe('RestConnector', () => { describe('RestConnector', () => {
it('connects', async () => { it('connects', async () => {
const spy = jest const spy = jest
@@ -229,17 +249,19 @@ describe('VegaConnectDialog', () => {
beforeEach(() => { beforeEach(() => {
spyOnCheckCompat = jest spyOnCheckCompat = jest
.spyOn(connectors.jsonRpc, 'checkCompat') .spyOn(connectors.jsonRpc, 'checkCompat')
.mockImplementation(() => delayedResolve(true)); .mockImplementation(() => delayedResolve(true, delay));
spyOnGetChainId = jest spyOnGetChainId = jest
.spyOn(connectors.jsonRpc, 'getChainId') .spyOn(connectors.jsonRpc, 'getChainId')
.mockImplementation(() => delayedResolve({ chainID: mockChainId })); .mockImplementation(() =>
delayedResolve({ chainID: mockChainId }, delay)
);
spyOnConnectWallet = jest spyOnConnectWallet = jest
.spyOn(connectors.jsonRpc, 'connectWallet') .spyOn(connectors.jsonRpc, 'connectWallet')
.mockImplementation(() => delayedResolve(null)); .mockImplementation(() => delayedResolve(null, delay));
spyOnConnect = jest spyOnConnect = jest
.spyOn(connectors.jsonRpc, 'connect') .spyOn(connectors.jsonRpc, 'connect')
.mockImplementation(() => .mockImplementation(() =>
delayedResolve([{ publicKey: 'pubkey', name: 'test key 1' }]) delayedResolve([{ publicKey: 'pubkey', name: 'test key 1' }], delay)
); );
}); });
@@ -351,18 +373,6 @@ describe('VegaConnectDialog', () => {
expect(screen.getByText('An unknown error occurred')).toBeInTheDocument(); expect(screen.getByText('An unknown error occurred')).toBeInTheDocument();
}); });
function delayedResolve<T>(result: T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => resolve(result), delay);
});
}
function delayedReject<T>(result: T): Promise<T> {
return new Promise((_, reject) => {
setTimeout(() => reject(result), delay);
});
}
async function selectJsonRpc() { async function selectJsonRpc() {
expect(await screen.findByRole('dialog')).toBeInTheDocument(); expect(await screen.findByRole('dialog')).toBeInTheDocument();
fireEvent.click(await screen.findByTestId('connector-jsonRpc')); fireEvent.click(await screen.findByTestId('connector-jsonRpc'));
@@ -439,4 +449,109 @@ describe('VegaConnectDialog', () => {
}); });
}); });
}); });
describe('InjectedConnector', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
localStorage.clear();
});
afterEach(() => {
clearBrowserWallet();
});
it('connects', async () => {
const delay = 100;
const vegaWindow = {
getChainId: jest.fn(() =>
delayedResolve({ chainID: mockChainId }, delay)
),
connectWallet: jest.fn(() => delayedResolve(null, delay)),
disconnectWallet: jest.fn(() => delayedResolve(undefined, delay)),
listKeys: jest.fn(() =>
delayedResolve(
{
keys: [{ name: 'test key', publicKey: '0x123' }],
},
100
)
),
};
mockBrowserWallet(vegaWindow);
render(generateJSX());
await selectInjected();
// Chain check
expect(screen.getByText('Verifying chain')).toBeInTheDocument();
expect(vegaWindow.getChainId).toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(delay);
});
// Await user connect
expect(screen.getByText('Connecting...')).toBeInTheDocument();
expect(vegaWindow.connectWallet).toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(delay);
});
// Connect (list keys)
expect(vegaWindow.listKeys).toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(delay);
});
expect(screen.getByText('Successfully connected')).toBeInTheDocument();
await act(async () => {
jest.advanceTimersByTime(CLOSE_DELAY);
});
expect(mockCloseVegaDialog).toHaveBeenCalledWith();
});
it('handles invalid chain', async () => {
const delay = 100;
const invalidChain = 'invalid chain';
const vegaWindow = {
getChainId: jest.fn(() =>
delayedResolve({ chainID: invalidChain }, delay)
),
connectWallet: jest.fn(() => delayedResolve(null, delay)),
disconnectWallet: jest.fn(() => delayedResolve(undefined, delay)),
listKeys: jest.fn(() =>
delayedResolve(
{
keys: [{ name: 'test key', publicKey: '0x123' }],
},
100
)
),
};
mockBrowserWallet(vegaWindow);
render(generateJSX());
await selectInjected();
// Chain check
expect(screen.getByText('Verifying chain')).toBeInTheDocument();
expect(vegaWindow.getChainId).toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(delay);
});
expect(screen.getByText('Wrong network')).toBeInTheDocument();
expect(
screen.getByText(
new RegExp(`set your wallet network in your app to "${mockChainId}"`)
)
).toBeInTheDocument();
});
async function selectInjected() {
expect(await screen.findByRole('dialog')).toBeInTheDocument();
fireEvent.click(await screen.findByTestId('connector-injected'));
}
});
}); });
+150 -187
View File
@@ -3,45 +3,50 @@ import {
Button, Button,
Dialog, Dialog,
FormGroup, FormGroup,
Icon,
Input, Input,
Link, VegaIcon,
Loader, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { WalletClientError } from '@vegaprotocol/wallet-client'; import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { VegaConnector } from '../connectors'; import type { VegaConnector } from '../connectors';
import { InjectedConnector } from '../connectors';
import { ViewConnector } from '../connectors'; import { ViewConnector } from '../connectors';
import { JsonRpcConnector, RestConnector } from '../connectors'; import { JsonRpcConnector, RestConnector } from '../connectors';
import { RestConnectorForm } from './rest-connector-form'; import { RestConnectorForm } from './rest-connector-form';
import { JsonRpcConnectorForm } from './json-rpc-connector-form'; import { JsonRpcConnectorForm } from './json-rpc-connector-form';
import { import { Networks, useEnvironment } from '@vegaprotocol/environment';
Networks,
useEnvironment,
ExternalLinks,
} from '@vegaprotocol/environment';
import { import {
ConnectDialogContent, ConnectDialogContent,
ConnectDialogFooter, ConnectDialogFooter,
ConnectDialogTitle, ConnectDialogTitle,
} from './connect-dialog-elements'; } from './connect-dialog-elements';
import type { Status } from '../use-json-rpc-connect'; import type { Status as JsonRpcStatus } from '../use-json-rpc-connect';
import type { Status as InjectedStatus } from '../use-injected-connector';
import { useJsonRpcConnect } from '../use-json-rpc-connect'; import { useJsonRpcConnect } from '../use-json-rpc-connect';
import { ViewConnectorForm } from './view-connector-form'; import { ViewConnectorForm } from './view-connector-form';
import { useChainIdQuery } from './__generated__/ChainId'; import { useChainIdQuery } from './__generated__/ChainId';
import { useVegaWallet } from '../use-vega-wallet'; import { useVegaWallet } from '../use-vega-wallet';
import { useInjectedConnector } from '../use-injected-connector';
import { InjectedConnectorForm } from './injected-connector-form';
export const CLOSE_DELAY = 1700; export const CLOSE_DELAY = 1700;
type Connectors = { [key: string]: VegaConnector }; type Connectors = { [key: string]: VegaConnector };
type WalletType = 'jsonRpc' | 'hosted' | 'view'; export type WalletType = 'injected' | 'jsonRpc' | 'rest' | 'view';
export interface VegaConnectDialogProps { export interface VegaConnectDialogProps {
connectors: Connectors; connectors: Connectors;
onChangeOpen?: (open: boolean) => void;
riskMessage?: React.ReactNode; riskMessage?: React.ReactNode;
} }
export interface VegaWalletDialogStore {
vegaWalletDialogOpen: boolean;
updateVegaWalletDialog: (open: boolean) => void;
openVegaWalletDialog: () => void;
closeVegaWalletDialog: () => void;
}
export const useVegaWalletDialogStore = create<VegaWalletDialogStore>()( export const useVegaWalletDialogStore = create<VegaWalletDialogStore>()(
(set) => ({ (set) => ({
vegaWalletDialogOpen: false, vegaWalletDialogOpen: false,
@@ -52,32 +57,20 @@ export const useVegaWalletDialogStore = create<VegaWalletDialogStore>()(
}) })
); );
export interface VegaWalletDialogStore {
vegaWalletDialogOpen: boolean;
updateVegaWalletDialog: (open: boolean) => void;
openVegaWalletDialog: () => void;
closeVegaWalletDialog: () => void;
}
export const VegaConnectDialog = ({ export const VegaConnectDialog = ({
connectors, connectors,
onChangeOpen,
riskMessage, riskMessage,
}: VegaConnectDialogProps) => { }: VegaConnectDialogProps) => {
const { disconnect, acknowledgeNeeded } = useVegaWallet();
const vegaWalletDialogOpen = useVegaWalletDialogStore( const vegaWalletDialogOpen = useVegaWalletDialogStore(
(store) => store.vegaWalletDialogOpen (store) => store.vegaWalletDialogOpen
); );
const updateVegaWalletDialog = useVegaWalletDialogStore( const updateVegaWalletDialog = useVegaWalletDialogStore(
(store) => (open: boolean) => { (store) => (open: boolean) => {
store.updateVegaWalletDialog(open); store.updateVegaWalletDialog(open);
onChangeOpen?.(open);
} }
); );
const closeVegaWalletDialog = useVegaWalletDialogStore((store) => () => {
store.closeVegaWalletDialog();
onChangeOpen?.(false);
});
const { disconnect, acknowledgeNeeded } = useVegaWallet();
const onVegaWalletDialogChange = useCallback( const onVegaWalletDialogChange = useCallback(
(open: boolean) => { (open: boolean) => {
updateVegaWalletDialog(open); updateVegaWalletDialog(open);
@@ -88,41 +81,9 @@ export const VegaConnectDialog = ({
[updateVegaWalletDialog, acknowledgeNeeded, disconnect] [updateVegaWalletDialog, acknowledgeNeeded, disconnect]
); );
const { data, error, loading } = useChainIdQuery(); // Ensure we have a chain Id so we can compare with wallet chain id.
// This value will already be in the cache, if it failed the app wont render
const renderContent = () => { const { data } = useChainIdQuery();
if (error) {
return (
<ConnectDialogContent>
<ConnectDialogTitle>
{t('Could not retrieve chain id')}
</ConnectDialogTitle>
<ConnectDialogFooter />
</ConnectDialogContent>
);
}
if (loading || !data) {
return (
<ConnectDialogContent>
<ConnectDialogTitle>{t('Fetching chain ID')}</ConnectDialogTitle>
<div className="flex justify-center items-center my-6">
<Loader />
</div>
<ConnectDialogFooter />
</ConnectDialogContent>
);
}
return (
<ConnectDialogContainer
connectors={connectors}
closeDialog={closeVegaWalletDialog}
appChainId={data.statistics.chainId}
riskMessage={riskMessage}
/>
);
};
return ( return (
<Dialog <Dialog
@@ -130,29 +91,35 @@ export const VegaConnectDialog = ({
size="small" size="small"
onChange={onVegaWalletDialogChange} onChange={onVegaWalletDialogChange}
> >
{renderContent()} {data && (
<ConnectDialogContainer
connectors={connectors}
appChainId={data.statistics.chainId}
riskMessage={riskMessage}
/>
)}
</Dialog> </Dialog>
); );
}; };
const ConnectDialogContainer = ({ const ConnectDialogContainer = ({
connectors, connectors,
closeDialog,
appChainId, appChainId,
riskMessage, riskMessage,
}: { }: {
connectors: Connectors; connectors: Connectors;
closeDialog: () => void;
appChainId: string; appChainId: string;
riskMessage?: React.ReactNode; riskMessage?: React.ReactNode;
}) => { }) => {
const { VEGA_WALLET_URL, VEGA_ENV, HOSTED_WALLET_URL } = useEnvironment(); const { VEGA_WALLET_URL, VEGA_ENV, HOSTED_WALLET_URL } = useEnvironment();
const closeDialog = useVegaWalletDialogStore(
(store) => store.closeVegaWalletDialog
);
const [selectedConnector, setSelectedConnector] = useState<VegaConnector>(); const [selectedConnector, setSelectedConnector] = useState<VegaConnector>();
const [walletUrl, setWalletUrl] = useState(VEGA_WALLET_URL || ''); const [walletUrl, setWalletUrl] = useState(VEGA_WALLET_URL || '');
const [walletType, setWalletType] = useState<WalletType>();
const reset = useCallback(() => { const reset = useCallback(() => {
setSelectedConnector(undefined); setSelectedConnector(undefined);
setWalletType(undefined);
}, []); }, []);
const delayedOnConnect = useCallback(() => { const delayedOnConnect = useCallback(() => {
@@ -161,52 +128,59 @@ const ConnectDialogContainer = ({
}, CLOSE_DELAY); }, CLOSE_DELAY);
}, [closeDialog]); }, [closeDialog]);
const { connect, ...jsonRpcState } = useJsonRpcConnect(delayedOnConnect); const { connect: jsonRpcConnect, ...jsonRpcState } =
useJsonRpcConnect(delayedOnConnect);
const { connect: injectedConnect, ...injectedState } =
useInjectedConnector(delayedOnConnect);
const handleSelect = (type: WalletType, isHosted = false) => { const handleSelect = (type: WalletType) => {
let connector; const connector = connectors[type];
if (isHosted) { // If type is rest user has selected the hosted wallet option. So here
// If the user has selected hosted wallet ensure that we are connecting to https://vega-hosted-wallet.on.fleek.co/ // we ensure that we are connecting to https://vega-hosted-wallet.on.fleek.co/
// otherwise use the default walletUrl or what has been put in the input // otherwise use walletUrl which defaults to the localhost:1789
connector = connectors['rest']; connector.url = type === 'rest' ? HOSTED_WALLET_URL : walletUrl;
connector.url = HOSTED_WALLET_URL || walletUrl;
} else {
connector = connectors[type];
connector.url = walletUrl;
}
if (!connector) { if (!connector) {
// we should never get here unless connectors are not configured correctly
throw new Error(`Connector type: ${type} not configured`); throw new Error(`Connector type: ${type} not configured`);
} }
setSelectedConnector(connector); setSelectedConnector(connector);
setWalletType(type);
// Immediately connect on selection if jsonRpc is selected, we can't do this // Immediately connect on selection if jsonRpc is selected, we can't do this
// for rest because we need to show an authentication form // for rest because we need to show an authentication form
if (connector instanceof JsonRpcConnector) { if (connector instanceof JsonRpcConnector) {
connect(connector, appChainId); jsonRpcConnect(connector, appChainId);
} else if (connector instanceof InjectedConnector) {
injectedConnect(connector, appChainId);
} }
}; };
return selectedConnector !== undefined && walletType !== undefined ? ( return (
<SelectedForm <>
type={walletType} <ConnectDialogContent>
connector={selectedConnector} {selectedConnector !== undefined ? (
jsonRpcState={jsonRpcState} <SelectedForm
onConnect={closeDialog} connector={selectedConnector}
appChainId={appChainId} jsonRpcState={jsonRpcState}
reset={reset} injectedState={injectedState}
riskMessage={riskMessage} onConnect={closeDialog}
/> appChainId={appChainId}
) : ( reset={reset}
<ConnectorList riskMessage={riskMessage}
walletUrl={walletUrl} />
setWalletUrl={setWalletUrl} ) : (
onSelect={handleSelect} <ConnectorList
isMainnet={VEGA_ENV === Networks.MAINNET} walletUrl={walletUrl}
/> setWalletUrl={setWalletUrl}
onSelect={handleSelect}
isMainnet={VEGA_ENV === Networks.MAINNET}
/>
)}
</ConnectDialogContent>
<ConnectDialogFooter connector={selectedConnector} />
</>
); );
}; };
@@ -216,136 +190,129 @@ const ConnectorList = ({
setWalletUrl, setWalletUrl,
isMainnet, isMainnet,
}: { }: {
onSelect: (type: WalletType, isHosted?: boolean) => void; onSelect: (type: WalletType) => void;
walletUrl: string; walletUrl: string;
setWalletUrl: (value: string) => void; setWalletUrl: (value: string) => void;
isMainnet: boolean; isMainnet: boolean;
}) => { }) => {
return ( return (
<> <>
<ConnectDialogContent> <ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle> <CustomUrlInput walletUrl={walletUrl} setWalletUrl={setWalletUrl} />
<CustomUrlInput walletUrl={walletUrl} setWalletUrl={setWalletUrl} /> <ul data-testid="connectors-list" className="mb-6">
<ul data-testid="connectors-list" className="mb-6"> <li className="mb-4 last:mb-0">
<ConnectionOption
type="jsonRpc"
text={t('Connect Vega wallet')}
onClick={() => onSelect('jsonRpc')}
/>
</li>
{'vega' in window && (
<li className="mb-4 last:mb-0"> <li className="mb-4 last:mb-0">
<ConnectionOption <ConnectionOption
type="jsonRpc" type="injected"
text={t('Connect Vega wallet')} text={t('Connect Web wallet')}
onClick={() => onSelect('jsonRpc')} onClick={() => onSelect('injected')}
/> />
</li> </li>
{!isMainnet && ( )}
<li className="mb-4 last:mb-0"> {!isMainnet && (
<ConnectionOption
type="hosted"
text={t('Hosted Fairground wallet')}
onClick={() => onSelect('hosted', true)}
/>
</li>
)}
<li className="mb-4 last:mb-0"> <li className="mb-4 last:mb-0">
<div className="my-4 text-center text-vega-dark-400">{t('OR')}</div>
<ConnectionOption <ConnectionOption
type="view" type="rest"
text={t('View as vega user')} text={t('Hosted Fairground wallet')}
onClick={() => onSelect('view')} onClick={() => onSelect('rest')}
/> />
</li> </li>
</ul> )}
</ConnectDialogContent> <li className="mb-4 last:mb-0">
<ConnectDialogFooter /> <div className="my-4 text-center">{t('OR')}</div>
<ConnectionOption
type="view"
text={t('View as vega user')}
onClick={() => onSelect('view')}
/>
</li>
</ul>
</> </>
); );
}; };
const SelectedForm = ({ const SelectedForm = ({
type,
connector, connector,
appChainId, appChainId,
jsonRpcState, jsonRpcState,
injectedState,
reset, reset,
onConnect, onConnect,
riskMessage, riskMessage,
}: { }: {
type: WalletType;
connector: VegaConnector; connector: VegaConnector;
appChainId: string; appChainId: string;
jsonRpcState: { jsonRpcState: {
status: Status; status: JsonRpcStatus;
error: WalletClientError | null; error: WalletClientError | null;
}; };
injectedState: {
status: InjectedStatus;
error: Error | null;
};
reset: () => void; reset: () => void;
onConnect: () => void; onConnect: () => void;
riskMessage?: React.ReactNode; riskMessage?: React.ReactNode;
}) => { }) => {
if (connector instanceof InjectedConnector) {
return (
<InjectedConnectorForm
status={injectedState.status}
error={injectedState.error}
onConnect={onConnect}
appChainId={appChainId}
reset={reset}
riskMessage={riskMessage}
/>
);
}
if (connector instanceof RestConnector) { if (connector instanceof RestConnector) {
return ( return (
<> <>
<ConnectDialogContent> <button
<button onClick={reset}
onClick={reset} className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
className="absolute p-2 top-0 left-0 md:top-2 md:left-2" data-testid="back-button"
data-testid="back-button" >
> <VegaIcon name={VegaIconNames.CHEVRON_LEFT} />
<Icon name={'chevron-left'} ariaLabel="back" size={4} /> </button>
</button> <ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle> <div className="mb-2">
<div className="mb-2"> <RestConnectorForm connector={connector} onConnect={onConnect} />
<RestConnectorForm connector={connector} onConnect={onConnect} /> </div>
</div>
</ConnectDialogContent>
{type === 'hosted' ? (
<ConnectDialogFooter>
<p className="text-center">
{t('For demo purposes get a ')}
<Link
href={ExternalLinks.VEGA_WALLET_HOSTED_URL}
target="_blank"
rel="noreferrer"
>
{t('hosted wallet')}
</Link>
{t(', or for the real experience create a wallet in the ')}
<Link href={ExternalLinks.VEGA_WALLET_URL}>
{t('Vega wallet app')}
</Link>
</p>
</ConnectDialogFooter>
) : (
<ConnectDialogFooter />
)}
</> </>
); );
} }
if (connector instanceof JsonRpcConnector) { if (connector instanceof JsonRpcConnector) {
return ( return (
<ConnectDialogContent> <JsonRpcConnectorForm
<JsonRpcConnectorForm connector={connector}
connector={connector} status={jsonRpcState.status}
status={jsonRpcState.status} error={jsonRpcState.error}
error={jsonRpcState.error} onConnect={onConnect}
onConnect={onConnect} appChainId={appChainId}
appChainId={appChainId} reset={reset}
reset={reset} riskMessage={riskMessage}
riskMessage={riskMessage} />
/>
</ConnectDialogContent>
); );
} }
if (connector instanceof ViewConnector) { if (connector instanceof ViewConnector) {
return ( return (
<> <ViewConnectorForm
<ConnectDialogContent> connector={connector}
<ViewConnectorForm onConnect={onConnect}
connector={connector} reset={reset}
onConnect={onConnect} />
reset={reset}
/>
</ConnectDialogContent>
<ConnectDialogFooter />
</>
); );
} }
@@ -366,12 +333,12 @@ const ConnectionOption = ({
onClick={onClick} onClick={onClick}
size="lg" size="lg"
fill={true} fill={true}
variant={['hosted', 'view'].includes(type) ? 'default' : 'primary'} variant={['rest', 'view'].includes(type) ? 'default' : 'primary'}
data-testid={`connector-${type}`} data-testid={`connector-${type}`}
> >
<span className="-mx-6 flex text-left justify-between items-center"> <span className="-mx-10 flex text-left justify-between items-center">
{text} {text}
<Icon name="chevron-right" /> <VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</span> </span>
</Button> </Button>
); );
@@ -387,9 +354,7 @@ const CustomUrlInput = ({
const [urlInputExpanded, setUrlInputExpanded] = useState(false); const [urlInputExpanded, setUrlInputExpanded] = useState(false);
return urlInputExpanded ? ( return urlInputExpanded ? (
<> <>
<p className="mb-2 text-neutral-600 dark:text-neutral-400"> <p className="mb-2">{t('Custom wallet location')}</p>
{t('Custom wallet location')}
</p>
<FormGroup <FormGroup
labelFor="wallet-url" labelFor="wallet-url"
label={t('Custom wallet location')} label={t('Custom wallet location')}
@@ -401,12 +366,10 @@ const CustomUrlInput = ({
name="wallet-url" name="wallet-url"
/> />
</FormGroup> </FormGroup>
<p className="mb-2 text-neutral-600 dark:text-neutral-400"> <p className="mb-2">{t('Choose wallet app to connect')}</p>
{t('Choose wallet app to connect')}
</p>
</> </>
) : ( ) : (
<p className="mb-6 text-neutral-600 dark:text-neutral-400"> <p className="mb-6">
{t( {t(
'Choose wallet app to connect, or to change port or server URL enter a ' 'Choose wallet app to connect, or to change port or server URL enter a '
)} )}
@@ -0,0 +1,153 @@
import { t } from '@vegaprotocol/i18n';
import { Status } from '../use-injected-connector';
import { ConnectDialogTitle } from './connect-dialog-elements';
import type { ReactNode } from 'react';
import {
Button,
ButtonLink,
Diamond,
Loader,
Tick,
} from '@vegaprotocol/ui-toolkit';
import { setAcknowledged } from '../storage';
import { useVegaWallet } from '../use-vega-wallet';
export const InjectedConnectorForm = ({
status,
onConnect,
riskMessage,
appChainId,
reset,
error,
}: {
// connector: JsonRpcConnector;
appChainId: string;
status: Status;
error: Error | null;
onConnect: () => void;
reset: () => void;
riskMessage?: React.ReactNode;
}) => {
const { disconnect } = useVegaWallet();
if (status === Status.Idle) {
return null;
}
if (status === Status.Error) {
return <Error error={error} appChainId={appChainId} onTryAgain={reset} />;
}
if (status === Status.GettingChainId) {
return (
<>
<ConnectDialogTitle>{t('Verifying chain')}</ConnectDialogTitle>
<Center>
<Loader />
</Center>
</>
);
}
if (status === Status.Connected) {
return (
<>
<ConnectDialogTitle>{t('Successfully connected')}</ConnectDialogTitle>
<Center>
<Tick />
</Center>
</>
);
}
if (status === Status.Connecting) {
return (
<>
<ConnectDialogTitle>{t('Connecting...')}</ConnectDialogTitle>
<Center>
<Diamond />
</Center>
<p className="text-center">
{t(
"Approve the connection from your Vega wallet app. If you have multiple wallets you'll need to choose which to connect with."
)}
</p>
</>
);
}
if (status === Status.AcknowledgeNeeded) {
const setConnection = () => {
setAcknowledged();
onConnect();
};
const handleDisagree = () => {
disconnect();
onConnect(); // this is dialog closing
};
return (
<>
<ConnectDialogTitle>{t('Understand the risk')}</ConnectDialogTitle>
{riskMessage}
<div className="grid grid-cols-2 gap-5">
<div>
<Button onClick={handleDisagree} fill>
{t('Cancel')}
</Button>
</div>
<div>
<Button onClick={setConnection} variant="primary" fill>
{t('I agree')}
</Button>
</div>
</div>
</>
);
}
return null;
};
const Center = ({ children }: { children: ReactNode }) => {
return (
<div className="flex justify-center items-center my-6">{children}</div>
);
};
const Error = ({
error,
appChainId,
onTryAgain,
}: {
error: Error | null;
appChainId: string;
onTryAgain: () => void;
}) => {
let title = t('Something went wrong');
let text: ReactNode | undefined = t('An unknown error occurred');
const tryAgain: ReactNode | null = (
<p className="text-center">
<ButtonLink onClick={onTryAgain}>{t('Try again')}</ButtonLink>
</p>
);
if (error) {
if (error.message === 'Invalid chain') {
title = t('Wrong network');
text = t(
'To complete your wallet connection, set your wallet network in your app to "%s".',
appChainId
);
} else if (error.message === 'window.vega not found') {
title = t('No wallet detected');
text = t('Vega browser extension not installed');
}
}
return (
<>
<ConnectDialogTitle>{title}</ConnectDialogTitle>
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
{tryAgain}
</>
);
};
@@ -1,21 +1,83 @@
import type { VegaConnector } from './vega-connector'; import { clearConfig, setConfig } from '../storage';
import type { Transaction, VegaConnector } from './vega-connector';
declare global {
interface Vega {
getChainId: () => Promise<{ chainID: string }>;
connectWallet: () => Promise<null>;
disconnectWallet: () => Promise<void>;
listKeys: () => Promise<{
keys: Array<{ name: string; publicKey: string }>;
}>;
sendTransaction: (params: {
publicKey: string;
transaction: Transaction;
sendingMode: 'TYPE_SYNC';
}) => Promise<{
receivedAt: string;
sentAt: string;
transaction: {
from: {
pubKey: string;
};
inputData: string;
pow: {
tid: string;
nonce: string;
};
signature: {
algo: string;
value: string;
version: number;
};
version: number;
};
transactionHash: string;
}>;
}
interface Window {
vega: Vega;
}
}
/**
* Dummy injected connector that we may use when browser wallet is implemented
*/
export class InjectedConnector implements VegaConnector { export class InjectedConnector implements VegaConnector {
description = 'Connects using the Vega wallet browser extension'; description = 'Connects using the Vega wallet browser extension';
async getChainId() {
return window.vega.getChainId();
}
connectWallet() {
return window.vega.connectWallet();
}
async connect() { async connect() {
return [{ publicKey: '0x123', name: 'text key' }]; const res = await window.vega.listKeys();
setConfig({
connector: 'injected',
token: null, // no token required for injected
url: null, // no url for injected
});
return res.keys;
} }
async disconnect() { disconnect() {
return; clearConfig();
return window.vega.disconnectWallet();
} }
// @ts-ignore injected connector is not implemented async sendTx(pubKey: string, transaction: Transaction) {
sendTx() { const result = await window.vega.sendTransaction({
throw new Error('Not implemented'); publicKey: pubKey,
transaction,
sendingMode: 'TYPE_SYNC' as const,
});
return {
transactionHash: result.transactionHash,
receivedAt: result.receivedAt,
sentAt: result.sentAt,
signature: result.transaction.signature.value,
};
} }
} }
+5 -1
View File
@@ -47,6 +47,10 @@ export interface OrderSubmission {
expiresAt?: string; expiresAt?: string;
postOnly?: boolean; postOnly?: boolean;
reduceOnly?: boolean; reduceOnly?: boolean;
icebergOpts?: {
peakSize: string;
minimumVisibleSize: string;
};
} }
export interface OrderCancellation { export interface OrderCancellation {
@@ -411,7 +415,7 @@ export interface PubKey {
} }
export interface VegaConnector { export interface VegaConnector {
url: string | null; url?: string | null;
/** Connect to wallet and return keys */ /** Connect to wallet and return keys */
connect(): Promise<PubKey[] | null>; connect(): Promise<PubKey[] | null>;
-1
View File
@@ -106,7 +106,6 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
if (!connector.current) { if (!connector.current) {
throw new Error('No connector'); throw new Error('No connector');
} }
return connector.current.sendTx(pubkey, transaction); return connector.current.sendTx(pubkey, transaction);
}, []); }, []);
+1 -1
View File
@@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/utils';
interface ConnectorConfig { interface ConnectorConfig {
token: string | null; token: string | null;
connector: 'rest' | 'jsonRpc' | 'view'; connector: 'injected' | 'rest' | 'jsonRpc' | 'view';
url: string | null; url: string | null;
} }
+39
View File
@@ -0,0 +1,39 @@
export function mockBrowserWallet(overrides?: Partial<Vega>) {
const vega: Vega = {
getChainId: jest.fn().mockReturnValue(Promise.resolve({ chainID: '1' })),
connectWallet: jest.fn().mockReturnValue(Promise.resolve(null)),
disconnectWallet: jest.fn().mockReturnValue(Promise.resolve()),
listKeys: jest
.fn()
.mockReturnValue({ keys: [{ name: 'test key', publicKey: '0x123' }] }),
sendTransaction: jest.fn().mockReturnValue({
code: 1,
data: '',
height: '1',
log: '',
success: true,
txHash: '0x123',
}),
...overrides,
};
// @ts-ignore globalThis has no index signature
globalThis.vega = vega;
return vega;
}
export function clearBrowserWallet() {
// @ts-ignore no index signature on globalThis
delete globalThis['vega'];
}
export function delayedResolve<T>(result: T, delay = 0): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => resolve(result), delay);
});
}
export function delayedReject<T>(result: T, delay = 0): Promise<T> {
return new Promise((_, reject) => {
setTimeout(() => reject(result), delay);
});
}
+8 -1
View File
@@ -31,7 +31,14 @@ export function useEagerConnect(Connectors: {
return; return;
} }
try { try {
await connect(Connectors[cfg.connector]); if (cfg.connector === 'injected') {
const injectedInstance = Connectors[cfg.connector];
// @ts-ignore only injected wallet has connectWallet method
await injectedInstance.connectWallet();
await connect(injectedInstance);
} else {
await connect(Connectors[cfg.connector]);
}
} catch { } catch {
console.warn(`Failed to connect with connector: ${cfg.connector}`); console.warn(`Failed to connect with connector: ${cfg.connector}`);
} finally { } finally {
@@ -0,0 +1,104 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { Status, useInjectedConnector } from './use-injected-connector';
import type { ReactNode } from 'react';
import { VegaWalletProvider } from './provider';
import { InjectedConnector } from './connectors';
import { mockBrowserWallet } from './test-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import { Networks } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/environment');
const setup = (callback = jest.fn()) => {
const wrapper = ({ children }: { children: ReactNode }) => (
<VegaWalletProvider>{children}</VegaWalletProvider>
);
return renderHook(() => useInjectedConnector(callback), { wrapper });
};
const injected = new InjectedConnector();
describe('useInjectedConnector', () => {
beforeEach(() => {
// @ts-ignore useEnvironment has been mocked
useEnvironment.mockImplementation(() => ({ VEGA_ENV: Networks.TESTNET }));
});
it('attempts connection', async () => {
const { result } = setup();
expect(typeof result.current.connect).toBe('function');
expect(result.current.status).toBe(Status.Idle);
expect(result.current.error).toBe(null);
});
it('errors if vega not injected', async () => {
const { result } = setup();
await act(async () => {
result.current.connect(injected, '1');
});
expect(result.current.error?.message).toBe('window.vega not found');
expect(result.current.status).toBe(Status.Error);
});
it('errors if chain ids dont match', async () => {
mockBrowserWallet();
const { result } = setup();
await act(async () => {
result.current.connect(injected, '2'); // default mock chainId is '1'
});
expect(result.current.error?.message).toBe('Invalid chain');
expect(result.current.status).toBe(Status.Error);
});
it('errors if connection throws', async () => {
const callback = jest.fn();
mockBrowserWallet({
getChainId: () => Promise.reject('failed'),
});
const { result } = setup(callback);
await act(async () => {
result.current.connect(injected, '1'); // default mock chainId is '1'
});
expect(result.current.status).toBe(Status.Error);
expect(result.current.error?.message).toBe('injected connection failed');
});
it('connects', async () => {
const callback = jest.fn();
const vega = mockBrowserWallet();
const { result } = setup(callback);
act(() => {
result.current.connect(injected, '1'); // default mock chainId is '1'
});
expect(result.current.status).toBe(Status.GettingChainId);
await waitFor(() => {
expect(vega.connectWallet).toHaveBeenCalled();
expect(vega.listKeys).toHaveBeenCalled();
});
expect(result.current.status).toBe(Status.Connected);
expect(callback).toHaveBeenCalled();
});
it('connects when aknowledgement required', async () => {
const callback = jest.fn();
// @ts-ignore useEnvironment has been mocked
useEnvironment.mockImplementation(() => ({ VEGA_ENV: Networks.MAINNET }));
const vega = mockBrowserWallet();
const { result } = setup(callback);
act(() => {
result.current.connect(injected, '1'); // default mock chainId is '1'
});
await waitFor(() => {
expect(vega.listKeys).toHaveBeenCalled();
});
expect(result.current.status).toBe(Status.AcknowledgeNeeded);
expect(callback).not.toHaveBeenCalled();
});
});
+61
View File
@@ -0,0 +1,61 @@
import { useCallback, useState } from 'react';
import type { InjectedConnector } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
export enum Status {
Idle = 'Idle',
GettingChainId = 'GettingChainId',
Connecting = 'Connecting',
Connected = 'Connected',
Error = 'Error',
AcknowledgeNeeded = 'AcknowledgeNeeded',
}
export const useInjectedConnector = (onConnect: () => void) => {
const { connect, acknowledgeNeeded } = useVegaWallet();
const [status, setStatus] = useState(Status.Idle);
const [error, setError] = useState<Error | null>(null);
const attemptConnect = useCallback(
async (connector: InjectedConnector, appChainId: string) => {
try {
if (!('vega' in window)) {
throw new Error('window.vega not found');
}
setStatus(Status.GettingChainId);
const { chainID } = await connector.getChainId();
if (chainID !== appChainId) {
throw new Error('Invalid chain');
}
setStatus(Status.Connecting);
await connector.connectWallet(); // authorize wallet
await connect(connector); // connect with keys
if (acknowledgeNeeded) {
setStatus(Status.AcknowledgeNeeded);
} else {
setStatus(Status.Connected);
onConnect();
}
} catch (err) {
if (err instanceof Error) {
setError(err);
} else {
setError(new Error('injected connection failed'));
}
setStatus(Status.Error);
}
},
[acknowledgeNeeded, connect, onConnect]
);
return {
status,
error,
connect: attemptConnect,
};
};
-1
View File
@@ -11,7 +11,6 @@ export enum Status {
GettingChainId = 'GettingChainId', GettingChainId = 'GettingChainId',
Connecting = 'Connecting', Connecting = 'Connecting',
GettingPerms = 'GettingPerms', GettingPerms = 'GettingPerms',
ListingKeys = 'ListingKeys',
Connected = 'Connected', Connected = 'Connected',
Error = 'Error', Error = 'Error',
AcknowledgeNeeded = 'AcknowledgeNeeded', AcknowledgeNeeded = 'AcknowledgeNeeded',
+110
View File
@@ -0,0 +1,110 @@
from os import environ
from subprocess import check_output
from argparse import ArgumentParser
import json
projects = []
projects_e2e = []
previews = {
'governance': 'not deployed',
'explorer': 'not deployed',
'trading': 'not deployed',
'tools': 'not deployed',
}
main_apps = ['governance', 'explorer', 'trading']
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
# take input from the pipeline
parser = ArgumentParser()
# let's generate slug from bash spell for now
parser.add_argument('--branch-slug', help='slug of branch')
parser.add_argument('--github-ref', help='current github ref')
parser.add_argument('--event-name', help='name of event in CI')
args = parser.parse_args()
# run yarn affected command
affected=check_output(f'yarn nx print-affected --base={environ["NX_BASE"]} --head={environ["NX_HEAD"]} --select=projects'.split()).decode('utf-8')
# print useful information
print(">>>> debug")
print(f"NX_BASE: { environ['NX_BASE'] }")
print(f"NX_HEAD: { environ['NX_HEAD'] }")
print(f"Branch slug: {args.branch_slug}")
print(f"Current ref: {args.github_ref}")
print(">> Affected output")
print(affected)
print(">>>> eof debug")
# define affection actions -> add to projects arrays and generate preview link
def affect_app(app, preview_name=None):
print(f"{app} is affected")
projects.append(app)
if not preview_name:
preview_name=app
previews[app] = f'https://{preview_name}.{args.branch_slug}.vega.rocks'
# check appearance in the affected string for main apps
for app in main_apps:
if app in affected:
affect_app(app)
# if non of main apps is affected - test all of them
if not projects:
for app in main_apps:
affect_app(app)
# generate e2e targets
projects_e2e = [f'{app}-e2e' for app in projects]
# check affection for multisig-signer which is deployed only from develop and pull requests
if args.event_name == 'pull_request' or 'develop' in args.github_ref:
if 'multisig-signer' in affected:
affect_app('multisig-signer', 'tools')
# now parse apps that are deployed from develop but don't have previews
if 'develop' in args.github_ref:
for app in ['static', 'ui-toolkit']:
if app in affected:
projects.append(app)
# if ref is in format release/{env}-{app} then only {app} is deployed
if 'release' in args.github_ref:
for app in main_apps:
if f'{args.github_ref}'.endswith(app):
projects = [app]
projects_e2e = [f'{app}-e2e']
projects = json.dumps(projects)
projects_e2e = json.dumps(projects_e2e)
print(f'Projects: {projects}')
print(f'Projects E2E: {projects_e2e}')
print('>> Previews')
for preview, preview_value in previews.items():
print(f'{preview}: {preview_value}')
print('>> EOF Previews')
lines_to_write = [
f'PREVIEW_GOVERNANCE={previews["governance"]}',
f'PREVIEW_EXPLORER={previews["explorer"]}',
f'PREVIEW_TRADING={previews["trading"]}',
f'PREVIEW_TOOLS={previews["tools"]}',
f'PROJECTS={projects}',
f'PROJECTS_E2E={projects_e2e}',
]
env_file = environ['GITHUB_ENV']
print(f'Line to add to GITHUB_ENV file: {env_file}')
print(lines_to_write)
with open(env_file, 'a') as _f:
_f.write('\n'.join(lines_to_write))
+66
View File
@@ -0,0 +1,66 @@
from argparse import ArgumentParser
from os import environ
# take input from the pipeline
parser = ArgumentParser()
# let's generate slug from bash spell for now
parser.add_argument('--github-ref', help='current github ref')
parser.add_argument('--app', help='current app')
args = parser.parse_args()
env_name = ''
domain = 'vega.rocks'
bucket_name = ''
if 'release/' in args.github_ref:
if 'mainnet-mirror' in args.github_ref:
env_name = 'mainnet-mirror'
if 'validators-testnet' in args.github_ref:
env_name = 'validators-testnet'
else:
# remove prefixing release/ and take the first string limited by - which is supposed to be name of the environment for releasing (format: release/testnet-trading)
env_name = args.github_ref.replace('refs/heads/release/', '').split('-')[0]
elif 'develop' in args.github_ref:
env_name = 'stagnet1'
apps_deployed_from_develop_to_mainnet = {
'multisig-signer' :'tools.vega.xyz',
'static': 'static.vega.xyz',
'ui-toolkit' : 'ui.vega.rocks',
}
if args.app in apps_deployed_from_develop_to_mainnet:
env_name = 'mainnet'
bucket_name = apps_deployed_from_develop_to_mainnet[args.app]
# endswith to avoid confusion with mirror env
elif args.github_ref.endswith('mainnet'):
env_name = 'mainnet'
other_domains_to_deploy = {
'mainnet': 'vega.xyz',
'testnet': 'fairground.wtf',
}
if env_name in other_domains_to_deploy:
domain = other_domains_to_deploy[env_name]
if not bucket_name:
bucket_name = f'{args.app}.{domain}'
# testing envs on vega.rocks contain env_name in the url not like testnet / mainnet
if not bucket_name:
bucket_name = f'{args.app}.{env_name}.{domain}'
print(f'env name: {env_name}')
print(f'domain: {domain}')
print(f'bucket name: {bucket_name}')
lines_to_write = [
f'ENV_NAME={env_name}',
f'BUCKET_NAME={bucket_name}',
]
env_file = environ['GITHUB_ENV']
print(f'Line to add to GITHUB_ENV file: {env_file}')
print(lines_to_write)
with open(env_file, 'a') as _f:
_f.write('\n'.join(lines_to_write))