Compare commits
387
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cde6577356 | ||
|
|
b41de05cfc | ||
|
|
00679c75f2 | ||
|
|
7fe722dad0 | ||
|
|
0d8a74dfd0 | ||
|
|
928b899c46 | ||
|
|
72d93bb568 | ||
|
|
db75261cd5 | ||
|
|
66fbe28cf0 | ||
|
|
9f3b8ecade | ||
|
|
cd13c61e90 | ||
|
|
246577f401 | ||
|
|
503ca26f68 | ||
|
|
13e374a6bb | ||
|
|
bed411040c | ||
|
|
d0173f15b2 | ||
|
|
106b040977 | ||
|
|
bf84b04a30 | ||
|
|
9f442698a4 | ||
|
|
d1265a6af7 | ||
|
|
313eff1c95 | ||
|
|
7f949a276c | ||
|
|
76a53dc7e3 | ||
|
|
72821dd183 | ||
|
|
40d2e033e0 | ||
|
|
e6d0b6ac1f | ||
|
|
99161497fe | ||
|
|
bf6ab32230 | ||
|
|
28bcb4ada1 | ||
|
|
5812c8858d | ||
|
|
abf146895d | ||
|
|
4476e6c6d1 | ||
|
|
d967000e8f | ||
|
|
91d3d97399 | ||
|
|
ca700aa103 | ||
|
|
265bf7fb35 | ||
|
|
7a5c6f3a25 | ||
|
|
91e3460e9d | ||
|
|
cf0c45aaa5 | ||
|
|
bf63f9d46d | ||
|
|
023a83e0f7 | ||
|
|
4f6ed1eadf | ||
|
|
85e293f3ec | ||
|
|
7b15d2db10 | ||
|
|
b1b9695998 | ||
|
|
6b8b33a78b | ||
|
|
4fd0f6819a | ||
|
|
5032d87f98 | ||
|
|
b15d9cb7dc | ||
|
|
6aa7cc5bab | ||
|
|
c37f9ebe66 | ||
|
|
768b3b29f0 | ||
|
|
378946f22b | ||
|
|
523598645a | ||
|
|
a9267de653 | ||
|
|
ce22da1c9a | ||
|
|
2460a62a3e | ||
|
|
f37a8720a2 | ||
|
|
f5fe3cc18d | ||
|
|
288f075cac | ||
|
|
ca46861fd8 | ||
|
|
2cb6ede492 | ||
|
|
eefcc96149 | ||
|
|
7a99ded8e9 | ||
|
|
4a4cdaa2b8 | ||
|
|
cd969549ab | ||
|
|
5a73b7e769 | ||
|
|
a4f41eabb6 | ||
|
|
351a20abad | ||
|
|
8d42481130 | ||
|
|
8a59722f09 | ||
|
|
de48ffa563 | ||
|
|
00c5252aea | ||
|
|
9acc083c11 | ||
|
|
77264e80f1 | ||
|
|
50378bc8d2 | ||
|
|
b88da51c68 | ||
|
|
2ae56bb113 | ||
|
|
752af9a983 | ||
|
|
a01df081eb | ||
|
|
c2d14d1abe | ||
|
|
a76ff44ffd | ||
|
|
ac2e262bf3 | ||
|
|
ab180875b1 | ||
|
|
66d3e89b9e | ||
|
|
c9cce87606 | ||
|
|
50232ab089 | ||
|
|
e86fb6d0b4 | ||
|
|
473ef3ae83 | ||
|
|
77ab719b37 | ||
|
|
a10f67d7ab | ||
|
|
66d2b83513 | ||
|
|
e4fd040933 | ||
|
|
30c1dbd019 | ||
|
|
5e558c4cd3 | ||
|
|
85060a0421 | ||
|
|
b336291b49 | ||
|
|
0f7bffd38b | ||
|
|
3665375b75 | ||
|
|
8572c0a570 | ||
|
|
6a29dc82ef | ||
|
|
c19bcc5e0c | ||
|
|
6593bbf37b | ||
|
|
1e5e57b978 | ||
|
|
5394b00349 | ||
|
|
32f5eaade5 | ||
|
|
b7a440132d | ||
|
|
d6ecdf80fa | ||
|
|
21e545d941 | ||
|
|
ef5fa7cba6 | ||
|
|
d1f465e2af | ||
|
|
2740df48a3 | ||
|
|
cf1f10e324 | ||
|
|
f09a1abcfa | ||
|
|
3de83c6eb8 | ||
|
|
e6545c13af | ||
|
|
613582ee00 | ||
|
|
77582a62a8 | ||
|
|
384c96376a | ||
|
|
0a92f62a66 | ||
|
|
f589f4a9fa | ||
|
|
b9869da492 | ||
|
|
653fec7025 | ||
|
|
8bf948578c | ||
|
|
11b08d16e4 | ||
|
|
5081b11c93 | ||
|
|
a1366e59ec | ||
|
|
0b035d5064 | ||
|
|
e1a38f2197 | ||
|
|
8201e87050 | ||
|
|
3aaf4e724d | ||
|
|
6fe06e9f62 | ||
|
|
cd8b0e7caf | ||
|
|
4282faa9d9 | ||
|
|
a24846fd06 | ||
|
|
72de53cf10 | ||
|
|
f3c0313a59 | ||
|
|
4c0de8f22b | ||
|
|
254fb57af0 | ||
|
|
6371199537 | ||
|
|
75cb48a4b9 | ||
|
|
933878acf5 | ||
|
|
5995df8e68 | ||
|
|
4d5b7cf5e7 | ||
|
|
e1f60446f0 | ||
|
|
7f271069ab | ||
|
|
1ae7af5d59 | ||
|
|
b9c5d9ee96 | ||
|
|
ae1768a669 | ||
|
|
a1bfe7f1a7 | ||
|
|
b0f1aeedae | ||
|
|
da59e83b19 | ||
|
|
360624282d | ||
|
|
9e054c7c85 | ||
|
|
911e927ab0 | ||
|
|
e4a51061a3 | ||
|
|
c15051d457 | ||
|
|
460ccdb3a2 | ||
|
|
959fb9cf62 | ||
|
|
2c7170d0e9 | ||
|
|
8a28a4afd4 | ||
|
|
48072eb4c3 | ||
|
|
ae92ff2580 | ||
|
|
c99b7fbb02 | ||
|
|
a9b1767a42 | ||
|
|
3cd60ad3e6 | ||
|
|
2f4bc9484b | ||
|
|
d4f5643799 | ||
|
|
72c49e62a8 | ||
|
|
0988edc129 | ||
|
|
4d266963ae | ||
|
|
ec12c7ecfd | ||
|
|
045dace274 | ||
|
|
2346b2f1a0 | ||
|
|
31859cb779 | ||
|
|
20507ecdd1 | ||
|
|
13ba661ea0 | ||
|
|
03f5b9fc7b | ||
|
|
b9fed65615 | ||
|
|
d671e51edf | ||
|
|
4f3295d3fc | ||
|
|
7be7bab1ca | ||
|
|
61380f06b9 | ||
|
|
c113856d63 | ||
|
|
90884beee8 | ||
|
|
d9d2e428f4 | ||
|
|
ba8d54b761 | ||
|
|
dce4edb943 | ||
|
|
b468a0fd84 | ||
|
|
c98fbe7453 | ||
|
|
d20928ec06 | ||
|
|
289559fe4f | ||
|
|
071fd6128c | ||
|
|
554d07a31b | ||
|
|
48b62380b0 | ||
|
|
a9f4604bad | ||
|
|
54f2519a62 | ||
|
|
260e7bf9cf | ||
|
|
83db796cdb | ||
|
|
2f6800212e | ||
|
|
77e16ad1b7 | ||
|
|
dcce983842 | ||
|
|
53ce9bfaa2 | ||
|
|
6ae34d296f | ||
|
|
ee2aafb99c | ||
|
|
e8fdb63323 | ||
|
|
3df9270b57 | ||
|
|
ab26667265 | ||
|
|
9fc6b255c8 | ||
|
|
91f61d8bb5 | ||
|
|
bbfda65bcc | ||
|
|
45f2e926c3 | ||
|
|
ccadbc2cb6 | ||
|
|
7128acb7b8 | ||
|
|
357bdaa4f3 | ||
|
|
f3fe43724e | ||
|
|
03f5ca3096 | ||
|
|
03d19349af | ||
|
|
1f53b2cbb5 | ||
|
|
b010c98346 | ||
|
|
29d57af2ba | ||
|
|
fd7940c4cf | ||
|
|
8ab7ac8414 | ||
|
|
ce7cd03c3e | ||
|
|
dfa3355a68 | ||
|
|
8553904d81 | ||
|
|
10afa3c1d4 | ||
|
|
ecd362615e | ||
|
|
0d9bd67465 | ||
|
|
b5aa92f007 | ||
|
|
2955790be1 | ||
|
|
89ff9309e9 | ||
|
|
e1df3489f6 | ||
|
|
f0fa9ca5e1 | ||
|
|
9d92477c2a | ||
|
|
b1a198be85 | ||
|
|
5fb385d43c | ||
|
|
8014b279fe | ||
|
|
f4bde57294 | ||
|
|
2e5ff07824 | ||
|
|
7accb42793 | ||
|
|
9229e7f686 | ||
|
|
b570b9c4c1 | ||
|
|
ed78261aad | ||
|
|
363ca9c6e1 | ||
|
|
bafd3c384c | ||
|
|
3b830abe07 | ||
|
|
1787e22b69 | ||
|
|
f105133959 | ||
|
|
aff0e46d23 | ||
|
|
7e4aafcb77 | ||
|
|
643d5408cd | ||
|
|
2a2af3ba9c | ||
|
|
8d4e4a1228 | ||
|
|
84068c8081 | ||
|
|
e1185b9a96 | ||
|
|
f390448c24 | ||
|
|
70943c523c | ||
|
|
dcb79e70d3 | ||
|
|
cc72cdbe16 | ||
|
|
3fd1817e1e | ||
|
|
b483c13f81 | ||
|
|
bde7a9fbf9 | ||
|
|
5fd93ee9c8 | ||
|
|
9ac07fd98a | ||
|
|
9c975ed822 | ||
|
|
138bf7da00 | ||
|
|
3e7fe517f3 | ||
|
|
4f960d09b3 | ||
|
|
351a5aaf96 | ||
|
|
381d9011a0 | ||
|
|
861760b4f9 | ||
|
|
18f1c0014c | ||
|
|
474543d91b | ||
|
|
662753c74b | ||
|
|
71aa8882bc | ||
|
|
ecfbccf8ed | ||
|
|
2aad6b1a14 | ||
|
|
8463d371ad | ||
|
|
0914e7ce4b | ||
|
|
7c82144f37 | ||
|
|
17ad0cc975 | ||
|
|
eb58dcd350 | ||
|
|
c8c0bf2cc0 | ||
|
|
00579fcb1f | ||
|
|
5e13173efb | ||
|
|
3a6c0554cc | ||
|
|
d4b237a6ba | ||
|
|
5e765f3172 | ||
|
|
5ef8c11e6e | ||
|
|
84bae12def | ||
|
|
c5b22f5bfb | ||
|
|
071dcc61a8 | ||
|
|
ce0ccdfebc | ||
|
|
f1524d3fcd | ||
|
|
82e5128ba1 | ||
|
|
2bfc3abd15 | ||
|
|
bd3b557f3d | ||
|
|
c899da52c2 | ||
|
|
b9c4057ce5 | ||
|
|
d02feee5c6 | ||
|
|
6d1ab36a72 | ||
|
|
5690fe91a8 | ||
|
|
1daae6a233 | ||
|
|
cd35a34246 | ||
|
|
77ada964b5 | ||
|
|
d7404bcd76 | ||
|
|
91862701a7 | ||
|
|
0136efd78f | ||
|
|
c3a4954d0d | ||
|
|
528fd96721 | ||
|
|
155923ff8e | ||
|
|
5f7e60d22f | ||
|
|
351583ce8f | ||
|
|
88bf58b974 | ||
|
|
fcda4d7fcd | ||
|
|
b1b0039b33 | ||
|
|
6854980b32 | ||
|
|
d8aac83f2d | ||
|
|
5c17045752 | ||
|
|
8863209342 | ||
|
|
462bf6b8b5 | ||
|
|
e0b701e05a | ||
|
|
eca212eee0 | ||
|
|
c25858037a | ||
|
|
081f5b48e2 | ||
|
|
e306d1c9fa | ||
|
|
737dec4f5f | ||
|
|
6780b2f618 | ||
|
|
08dc60086d | ||
|
|
4595db07b0 | ||
|
|
c3da31146f | ||
|
|
b6a9faaa7c | ||
|
|
477d523582 | ||
|
|
a2b112d898 | ||
|
|
621ba497e8 | ||
|
|
a6964bc3cf | ||
|
|
48a9998441 | ||
|
|
bd1bd94ee7 | ||
|
|
2fdebbd694 | ||
|
|
96af8035d0 | ||
|
|
051e3da366 | ||
|
|
af5e666d5e | ||
|
|
333a7d6206 | ||
|
|
cb6b988466 | ||
|
|
806ea74f7e | ||
|
|
8693f3bfd8 | ||
|
|
58c6652e29 | ||
|
|
e8adff25c4 | ||
|
|
3e594a783a | ||
|
|
1f7117075b | ||
|
|
4c850ecead | ||
|
|
19f8fa909e | ||
|
|
8214858685 | ||
|
|
868f8e21dc | ||
|
|
6ead84b57c | ||
|
|
3975477985 | ||
|
|
8a0c15ac11 | ||
|
|
a4f512f946 | ||
|
|
88f3c278f8 | ||
|
|
34d5f3fd13 | ||
|
|
a060f94146 | ||
|
|
f649d78565 | ||
|
|
b611d30b31 | ||
|
|
545324ccd6 | ||
|
|
8c16759621 | ||
|
|
e724aac2f1 | ||
|
|
e3a6b75130 | ||
|
|
8d8f6b4928 | ||
|
|
6c1c5bf2a9 | ||
|
|
74a3aa8566 | ||
|
|
4cbd7a4928 | ||
|
|
201cf07a01 | ||
|
|
90ceeebf15 | ||
|
|
965f7f6e2d | ||
|
|
18633efec3 | ||
|
|
a0b758334a | ||
|
|
9f3026aa4b | ||
|
|
faa359e2fb | ||
|
|
1d178b940f | ||
|
|
b2dd053621 | ||
|
|
77f5193b7a | ||
|
|
055a0070db | ||
|
|
29b221c86a | ||
|
|
3e619acd4b | ||
|
|
c720719ac5 | ||
|
|
c676a7c1e2 |
@@ -3,3 +3,7 @@ apps/**/node_modules/*
|
||||
tmp/*
|
||||
.dockerignore
|
||||
dockerfiles
|
||||
node_modules
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
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
|
||||
@@ -13,7 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
add_issue:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: 'Add issue to project board'
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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,211 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
- develop
|
||||
pull_request:
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- 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: |
|
||||
echo ">>>> debug"
|
||||
echo "NX Version: $nx_version"
|
||||
echo "NX_BASE: ${{ env.NX_BASE }}"
|
||||
echo "NX_HEAD: ${{ env.NX_HEAD }}"
|
||||
echo ">>>> eof debug"
|
||||
|
||||
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
|
||||
echo -n "Affected projects: $affected"
|
||||
|
||||
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 )"
|
||||
projects_e2e=""
|
||||
preview_governance="not deployed"
|
||||
preview_trading="not deployed"
|
||||
preview_explorer="not deployed"
|
||||
if [[ -z "$projects_e2e" ]]; then
|
||||
projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '
|
||||
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")
|
||||
else
|
||||
if [[ $affected == *"governance"* ]]; then
|
||||
projects_e2e+='"governance-e2e" '
|
||||
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
||||
fi
|
||||
if [[ $affected == *"trading"* ]]; then
|
||||
projects_e2e+='"trading-e2e" '
|
||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||
fi
|
||||
if [[ $affected == *"explorer"* ]]; then
|
||||
projects_e2e+='"explorer-e2e" '
|
||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||
fi
|
||||
fi
|
||||
projects_e2e=${projects_e2e%?}
|
||||
projects_e2e=[${projects_e2e// /,}]
|
||||
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
|
||||
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
|
||||
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
|
||||
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
|
||||
echo PREVIEW_EXPLORER=$preview_explorer >> $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 }}
|
||||
|
||||
cypress:
|
||||
needs: lint-test-build
|
||||
name: '(CI) cypress'
|
||||
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
||||
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' }}
|
||||
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: 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 }}
|
||||
|
||||
cypress-check:
|
||||
name: '(CI) cypress - check'
|
||||
runs-on: ubuntu-latest
|
||||
needs: cypress
|
||||
steps:
|
||||
- run: echo Done!
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
cypress-result:
|
||||
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
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
cypress-run:
|
||||
name: Run Cypress Trading tests -- live environment
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
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:
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
- name: Remove package.json & yarn.lock to avoid installing everything
|
||||
run: rm package.json yarn.lock
|
||||
|
||||
- name: Install nx
|
||||
run: yarn add nx
|
||||
|
||||
# Check SHAs
|
||||
- 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 || github.ref_name }}
|
||||
set-environment-variables-for-job: true
|
||||
|
||||
# See affected apps
|
||||
- name: See affected apps
|
||||
run: |
|
||||
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
||||
echo -n "Affected projects: $affected"
|
||||
projects=""
|
||||
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
|
||||
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
||||
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
|
||||
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||
projects=${projects%?}
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
|
||||
run:
|
||||
needs: pr
|
||||
if: ${{ needs.pr.outputs.projects != '[]' }}
|
||||
uses: ./.github/workflows/cypress-run.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
projects: ${{ needs.pr.outputs.projects }}
|
||||
tags: '@smoke @regression'
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
result:
|
||||
if: ${{ always() }}
|
||||
needs: run
|
||||
runs-on: ubuntu-latest
|
||||
name: Cypress result
|
||||
steps:
|
||||
- run: |
|
||||
result="${{ needs.run.result }}"
|
||||
if [[ $result == "success" || $result == "skipped" ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,3 +1,4 @@
|
||||
name: (CI) Cypress Run
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -17,8 +18,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
project: ${{ fromJSON(inputs.projects) }}
|
||||
name: ${{ matrix.project }}
|
||||
runs-on: self-hosted-runner
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
# Checks if skip cache was requested
|
||||
- name: Set skip-nx-cache flag
|
||||
@@ -91,3 +93,9 @@ jobs:
|
||||
with:
|
||||
name: logs-${{ matrix.project }}
|
||||
path: /home/runner/.vegacapsule/testnet/logs
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: test-report-${{ matrix.project }}
|
||||
path: frontend-monorepo/apps/trading-e2e/cypress/reports
|
||||
|
||||
@@ -8,22 +8,25 @@ on:
|
||||
jobs:
|
||||
master:
|
||||
name: Generate Queries
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
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
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Generate queries
|
||||
run: node ./scripts/get-queries.js
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: queries
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
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
|
||||
|
||||
- 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
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Verify PR title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, reopened, edited, synchronize]
|
||||
jobs:
|
||||
lint_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
- name: Check PR title
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
master:
|
||||
name: Generate Queries
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_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 variables
|
||||
run: |
|
||||
envName=''
|
||||
dockerfile="dist.Dockerfile"
|
||||
if [[ "${{ github.event_name }}" = "push" ]]; then
|
||||
domain="vega.rocks"
|
||||
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
|
||||
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
||||
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
|
||||
domain="vega.community"
|
||||
if [[ "${{ matrix.app }}" = "trading" ]]; then
|
||||
dockerfile="ipfs.Dockerfile"
|
||||
fi
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
envName="stagnet1"
|
||||
fi
|
||||
bucketName="${{ matrix.app }}.${envName}.${domain}"
|
||||
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
|
||||
fi
|
||||
nodeVersion=$(cat .nvmrc | head -n 1)
|
||||
echo ENV_NAME=${envName} >> $GITHUB_ENV
|
||||
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
|
||||
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
|
||||
|
||||
- name: Build local dist
|
||||
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
|
||||
run: |
|
||||
flags=""
|
||||
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
|
||||
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
|
||||
flags="--env=${{ env.ENV_NAME }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ matrix.app }}" = "trading" ]; then
|
||||
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
|
||||
DIST_LOCATION=dist/apps/trading/exported
|
||||
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
|
||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
load: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
|
||||
- name: Image digest
|
||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
- name: Sanity check docker image
|
||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
run: |
|
||||
echo "Check ipfs-hash"
|
||||
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
|
||||
fi
|
||||
|
||||
echo "List html directory"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
|
||||
|
||||
- name: Copy dist to local filesystem
|
||||
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
|
||||
run: |
|
||||
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
docker cp dist:/usr/share/nginx/html dist
|
||||
|
||||
echo "check local dist files"
|
||||
tree dist/html
|
||||
mv dist/html dist-result
|
||||
|
||||
- name: Publish dist as docker image
|
||||
uses: docker/build-push-action@v3
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
# bucket creation in github.com/vegaprotocol/terraform//frontend
|
||||
- name: Publish dist to s3
|
||||
uses: jakejarvis/s3-sync-action@master
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
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 }}
|
||||
@@ -1,126 +0,0 @@
|
||||
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 }}
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- announcements
|
||||
- ui-toolkit
|
||||
- react-helpers
|
||||
- tailwindcss-config
|
||||
@@ -18,29 +19,27 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
name: Build & Publish - Tag
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: User Node.js 16
|
||||
id: Node
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
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:
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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.15.1
|
||||
- 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
|
||||
@@ -46,3 +46,6 @@ cypress.env.json
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
||||
#cypress
|
||||
/apps/trading-e2e/cypress/reports/
|
||||
|
||||
@@ -7,5 +7,4 @@ __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
+1
-19
@@ -1,20 +1,2 @@
|
||||
@Library('vega-shared-library') _
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runApprobation ignoreFailure: false, frontendBranch: env.BRANCH_NAME, type: 'frontend'
|
||||
|
||||
@@ -72,7 +72,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.n11.testnet.vega.xyz/graphql
|
||||
export GRAPHQL_SCHEMA_PATH=https://api.n07.testnet.vega.xyz/graphql
|
||||
yarn nx run types:generate
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||
NX_TENDERMINT_URL=http://localhost:26617
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
NX_VEGA_URL=http://localhost:3028/query
|
||||
NX_VEGA_URL=http://localhost:3008/graphql
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_VEGA_CONFIG_URL=
|
||||
|
||||
@@ -18,4 +18,4 @@ NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN=
|
||||
CYPRESS_VEGA_URL=http://localhost:3028/query
|
||||
CYPRESS_VEGA_URL=http://localhost:3008/graphql
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||
NX_TENDERMINT_URL=https://mainnet-observer-proxy01.ops.vega.xyz/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket
|
||||
NX_VEGA_URL=https://api.vega.xyz/query
|
||||
NX_VEGA_URL=https://api.vega.community/graphql
|
||||
NX_VEGA_ENV=MAINNET
|
||||
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
NX_EXPLORER_GENESIS=1
|
||||
NX_EXPLORER_GOVERNANCE=1
|
||||
NX_EXPLORER_MARKETS=1
|
||||
NX_EXPLORER_ORACLES=1
|
||||
NX_EXPLORER_TXS_LIST=0
|
||||
NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
@@ -2,7 +2,7 @@
|
||||
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
|
||||
NX_TENDERMINT_URL=https://tm.n07.testnet.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://lb.testnet.vega.xyz/tm/websocket
|
||||
NX_VEGA_URL=https://api.n11.testnet.vega.xyz/graphql
|
||||
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@ module.exports = defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
testIsolation: false,
|
||||
},
|
||||
env: {
|
||||
environment: 'CUSTOM',
|
||||
networkQueryUrl: 'http://localhost:3028/query',
|
||||
networkQueryUrl: 'http://localhost:3008/graphql',
|
||||
ethUrl: 'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
|
||||
commitHash: 'dev',
|
||||
tsConfig: 'tsconfig.json',
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"governance.proposal.updateMarket.minVoterBalance",
|
||||
"governance.proposal.updateNetParam.minProposerBalance",
|
||||
"governance.proposal.updateNetParam.minVoterBalance",
|
||||
"governance.proposal.updateAsset.minProposerBalance",
|
||||
"governance.proposal.updateAsset.minVoterBalance",
|
||||
"reward.staking.delegation.maxPayoutPerEpoch",
|
||||
"reward.staking.delegation.maxPayoutPerParticipant",
|
||||
"reward.staking.delegation.minimumValidatorStake",
|
||||
@@ -19,9 +21,6 @@
|
||||
"validators.delegation.minAmount"
|
||||
],
|
||||
"fiveDecimal": [
|
||||
"governance.proposal.updateAsset.minProposerBalance",
|
||||
"governance.proposal.updateAsset.minVoterBalance",
|
||||
"governance.proposal.updateAsset.requiredParticipation",
|
||||
"market.fee.factors.infrastructureFee",
|
||||
"market.fee.factors.makerFee",
|
||||
"market.liquidity.bondPenaltyParameter",
|
||||
@@ -77,6 +76,7 @@
|
||||
"governance.proposal.updateNetParam.requiredMajority",
|
||||
"governance.proposal.updateNetParam.requiredParticipation",
|
||||
"governance.proposal.updateMarket.minProposerEquityLikeShare",
|
||||
"governance.proposal.updateAsset.requiredParticipation",
|
||||
"validators.vote.required"
|
||||
],
|
||||
"duration": [
|
||||
|
||||
@@ -6,75 +6,39 @@ context('Home Page', function () {
|
||||
describe('Stats page', { tags: '@smoke' }, function () {
|
||||
const statsValue = '[data-testid="stats-value"]';
|
||||
|
||||
it('Should show connected environment', function () {
|
||||
const deployedEnv = Cypress.env('environment').toUpperCase();
|
||||
cy.get('[data-testid="stats-environment"]').should(
|
||||
'have.text',
|
||||
`/ ${deployedEnv}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should show connected environment stats', function () {
|
||||
const statTitles = {
|
||||
0: 'Status',
|
||||
1: 'Height',
|
||||
2: 'Uptime',
|
||||
3: 'Total nodes',
|
||||
4: 'Total staked',
|
||||
5: 'Backlog',
|
||||
6: 'Trades / second',
|
||||
7: 'Orders / block',
|
||||
8: 'Orders / second',
|
||||
9: 'Transactions / block',
|
||||
10: 'Block time',
|
||||
11: 'Time',
|
||||
12: 'App',
|
||||
13: 'Tendermint',
|
||||
14: 'Up since',
|
||||
15: 'Chain ID',
|
||||
1: 'Epoch',
|
||||
2: 'Block height',
|
||||
3: 'Uptime',
|
||||
4: 'Total nodes',
|
||||
5: 'Total staked',
|
||||
6: 'Backlog',
|
||||
7: 'Trades / second',
|
||||
8: 'Orders / block',
|
||||
9: 'Orders / second',
|
||||
10: 'Transactions / block',
|
||||
11: 'Block time',
|
||||
12: 'Time',
|
||||
13: 'App',
|
||||
14: 'Tendermint',
|
||||
15: 'Up since',
|
||||
16: 'Chain ID',
|
||||
};
|
||||
|
||||
cy.get('[data-testid="stats-title"]')
|
||||
.each(($list, index) => {
|
||||
cy.wrap($list).should('have.text', statTitles[index]);
|
||||
cy.wrap($list).should('contain.text', statTitles[index]);
|
||||
})
|
||||
.then(($list) => {
|
||||
cy.wrap($list).should('have.length', 16);
|
||||
cy.wrap($list).should('have.length', 17);
|
||||
});
|
||||
|
||||
cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
|
||||
cy.get(statsValue).eq(1).should('not.be.empty');
|
||||
cy.get(statsValue)
|
||||
.eq(2)
|
||||
.invoke('text')
|
||||
.should('match', /\d+d \d+h \d+m \d+s/i);
|
||||
cy.get(statsValue).eq(3).should('have.text', '2');
|
||||
cy.get(statsValue)
|
||||
.eq(4)
|
||||
.invoke('text')
|
||||
.should('match', /\d+\.\d\d(?!\d)/i);
|
||||
cy.get(statsValue).eq(5).should('have.text', '0');
|
||||
cy.get(statsValue).eq(6).should('have.text', '0');
|
||||
cy.get(statsValue).eq(7).should('have.text', '0');
|
||||
cy.get(statsValue).eq(8).should('have.text', '0');
|
||||
cy.get(statsValue).eq(9).should('not.be.empty');
|
||||
cy.get(statsValue).eq(10).should('not.be.empty');
|
||||
cy.get(statsValue).eq(11).should('not.be.empty');
|
||||
cy.get(statsValue)
|
||||
.eq(12)
|
||||
.invoke('text')
|
||||
.should('match', /v\d+\.\d+\.\d+/i);
|
||||
cy.get(statsValue)
|
||||
.eq(13)
|
||||
.invoke('text')
|
||||
.should('match', /\d+\.\d+\.\d+/i);
|
||||
cy.get(statsValue).eq(14).should('not.be.empty');
|
||||
cy.get(statsValue).eq(15).should('not.be.empty');
|
||||
});
|
||||
|
||||
it('Block height should be updating', function () {
|
||||
cy.get(statsValue)
|
||||
.eq(1)
|
||||
.eq(2)
|
||||
.invoke('text')
|
||||
.then((blockHeightTxt) => {
|
||||
cy.get(statsValue)
|
||||
@@ -86,75 +50,4 @@ context('Home Page', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Git info', function () {
|
||||
it('git info is rendered on the footer of the page', function () {
|
||||
cy.getByTestId('git-info').within(() => {
|
||||
cy.getByTestId('git-network-data').within(() => {
|
||||
cy.contains('Reading network data from').should('be.visible');
|
||||
cy.get('span').should('have.text', Cypress.env('networkQueryUrl'));
|
||||
cy.getByTestId('link').should('be.visible');
|
||||
});
|
||||
|
||||
cy.getByTestId('git-eth-data').within(() => {
|
||||
cy.contains('Reading Ethereum data from').should('be.visible');
|
||||
cy.get('span').should('have.text', Cypress.env('ethUrl'));
|
||||
});
|
||||
|
||||
cy.getByTestId('git-commit-hash').within(() => {
|
||||
cy.contains('Version/commit hash:').should('be.visible');
|
||||
cy.getByTestId('link').should('have.text', Cypress.env('commitHash'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search bar', function () {
|
||||
it('Successful search for specific id by block id', function () {
|
||||
const blockId = '973624';
|
||||
search(blockId);
|
||||
cy.url().should('include', blockId);
|
||||
});
|
||||
|
||||
it('Successful search for specific id by tx hash', function () {
|
||||
const txHash =
|
||||
'9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8';
|
||||
search(txHash);
|
||||
cy.url().should('include', txHash);
|
||||
});
|
||||
|
||||
it('Successful search for specific id by tx id', function () {
|
||||
const txId =
|
||||
'0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898';
|
||||
search(txId);
|
||||
cy.url().should('include', txId);
|
||||
});
|
||||
|
||||
it('Error message displayed when invalid search by wrong string length', function () {
|
||||
search('9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8');
|
||||
validateSearchError("Something doesn't look right");
|
||||
});
|
||||
|
||||
it('Error message displayed when invalid search by invalid hash', function () {
|
||||
search(
|
||||
'9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8'
|
||||
);
|
||||
validateSearchError('Transaction is not hexadecimal');
|
||||
});
|
||||
|
||||
it('Error message displayed when searching empty field', function () {
|
||||
cy.get('[data-testid="search"]').clear();
|
||||
cy.get('[data-testid="search-button"]').click();
|
||||
validateSearchError('Search required');
|
||||
});
|
||||
|
||||
function search(searchTxt) {
|
||||
cy.get('[data-testid="search"]').clear().type(searchTxt);
|
||||
cy.get('[data-testid="search-button"]').click();
|
||||
}
|
||||
|
||||
function validateSearchError(expectedError) {
|
||||
cy.get('[data-testid="search-error"]').should('have.text', expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,7 +138,7 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should list each network parameter displayed as a currency value with four decimals - in the correct format', function () {
|
||||
it.skip('should list each network parameter displayed as a currency value with four decimals - in the correct format', function () {
|
||||
cy.get_network_parameters().then((network_parameters) => {
|
||||
network_parameters = Object.entries(network_parameters);
|
||||
network_parameters.forEach((network_parameter) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ const customNodeBtn = 'custom-node';
|
||||
context.skip('Node switcher', { tags: '@regression' }, function () {
|
||||
beforeEach('visit home page', function () {
|
||||
cy.intercept('GET', 'https://static.vega.xyz/assets/capsule-network.json', {
|
||||
hosts: ['http://localhost:3028/query'],
|
||||
hosts: ['http://localhost:3008/graphql'],
|
||||
}).as('nodeData');
|
||||
cy.visit('/');
|
||||
cy.wait('@nodeData');
|
||||
|
||||
@@ -219,7 +219,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
|
||||
balance type asset {id symbol decimals}}}}}}}}';
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `http://localhost:3028/query`,
|
||||
url: `http://localhost:3008/graphql`,
|
||||
body: {
|
||||
query: mutation,
|
||||
},
|
||||
|
||||
+13
-8
@@ -1,13 +1,18 @@
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
|
||||
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
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://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
|
||||
@@ -3,14 +3,8 @@ NX_TENDERMINT_URL=http://localhost:26617
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
|
||||
NX_VEGA_EXPLORER_URL=/
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
NX_EXPLORER_GENESIS=1
|
||||
NX_EXPLORER_GOVERNANCE=1
|
||||
NX_EXPLORER_MARKETS=1
|
||||
NX_EXPLORER_ORACLES=1
|
||||
NX_EXPLORER_TXS_LIST=0
|
||||
NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
|
||||
@@ -8,3 +8,5 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_EXPLORER_URL=/
|
||||
@@ -1,8 +1,10 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
||||
NX_VEGA_ENV=MAINNET
|
||||
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
|
||||
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
|
||||
@@ -1,19 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||
NX_VEGA_ENV=MIRROR
|
||||
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
NX_EXPLORER_GENESIS=1
|
||||
NX_EXPLORER_GOVERNANCE=1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
NX_EXPLORER_MARKETS=0
|
||||
NX_EXPLORER_ORACLES=0
|
||||
NX_EXPLORER_TXS_LIST=1
|
||||
@@ -1,11 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=SANDBOX
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
|
||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
||||
@@ -1,13 +1 @@
|
||||
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://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
|
||||
# .env is stagnet1, so there are no overrides required
|
||||
@@ -1,7 +0,0 @@
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
|
||||
@@ -8,3 +8,5 @@ NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
@@ -5,8 +5,10 @@ NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
|
||||
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
|
||||
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
|
||||
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||
NX_VEGA_EXPLORER_URL=https://validator-testnet.explorer.vega.xyz/
|
||||
@@ -4,3 +4,5 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_BLOCK_EXPLORER=
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
|
||||
NX_VEGA_EXPLORER_URL=/
|
||||
@@ -35,12 +35,11 @@ Example configurations are provided here:
|
||||
- [Devnet](./.env.devnet)
|
||||
- [Capsule](./.env.capsule)
|
||||
- [Testnet](./.env.testnet)
|
||||
- [Stagnet3](./.env.stagnet3)
|
||||
|
||||
For convenience, you can boot the app injecting one of the configurations above by running:
|
||||
|
||||
```bash
|
||||
yarn nx run explorer:serve --env={env} # e.g. stagnet3
|
||||
yarn nx run explorer:serve --env={env} # e.g. stagnet1
|
||||
```
|
||||
|
||||
There are a few different configuration options offered for this app:
|
||||
|
||||
@@ -1,87 +1,21 @@
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
import { Footer } from './components/footer/footer';
|
||||
import {
|
||||
AnnouncementBanner,
|
||||
ExternalLink,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes/router-config';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
asJson={asJson}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MainnetSimAd = () => {
|
||||
const [shouldDisplayBanner, setShouldDisplayBanner] = useState<boolean>(true);
|
||||
|
||||
// Return an empty div so that the grid layout in _app.page.ts
|
||||
// renders correctly
|
||||
if (!shouldDisplayBanner) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBanner>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
|
||||
<button
|
||||
className="flex items-center"
|
||||
onClick={() => setShouldDisplayBanner(false)}
|
||||
>
|
||||
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
|
||||
</button>
|
||||
<div>
|
||||
<span className="pr-4">Mainnet sim 3 is live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
);
|
||||
};
|
||||
const splashLoading = (
|
||||
<Splash>
|
||||
<Loader />
|
||||
</Splash>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<TendermintWebsocketProvider>
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[1500px] min-h-[100vh]',
|
||||
'mx-auto my-0',
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Header />
|
||||
<MainnetSimAd />
|
||||
</div>
|
||||
<div>
|
||||
<Main />
|
||||
</div>
|
||||
<div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||
</NetworkLoader>
|
||||
</TendermintWebsocketProvider>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ export type AssetBalanceProps = {
|
||||
assetId: string;
|
||||
price: string;
|
||||
showAssetLink?: boolean;
|
||||
showAssetSymbol?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -16,18 +17,21 @@ const AssetBalance = ({
|
||||
assetId,
|
||||
price,
|
||||
showAssetLink = true,
|
||||
showAssetSymbol = false,
|
||||
}: AssetBalanceProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
const { data: asset, loading } = useAssetDataProvider(assetId);
|
||||
|
||||
const label =
|
||||
asset && asset.decimals
|
||||
!loading && asset && asset.decimals
|
||||
? addDecimalsFormatNumber(price, asset.decimals)
|
||||
: price;
|
||||
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<span>{label}</span>{' '}
|
||||
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
|
||||
{showAssetLink && asset?.id ? (
|
||||
<AssetLink showAssetSymbol={showAssetSymbol} assetId={assetId} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,11 +13,17 @@ const DEFAULT_DECIMALS = 18;
|
||||
* the governance asset first, which is set by a network parameter
|
||||
*/
|
||||
const GovernanceAssetBalance = ({ price }: GovernanceAssetBalanceProps) => {
|
||||
const { data } = useExplorerGovernanceAssetQuery();
|
||||
const { data, loading } = useExplorerGovernanceAssetQuery();
|
||||
|
||||
if (data && data.networkParameter?.value) {
|
||||
if (!loading && data && data.networkParameter?.value) {
|
||||
const governanceAssetId = data.networkParameter.value;
|
||||
return <AssetBalance price={price} assetId={governanceAssetId} />;
|
||||
return (
|
||||
<AssetBalance
|
||||
price={price}
|
||||
showAssetSymbol={true}
|
||||
assetId={governanceAssetId}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="inline-block">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
||||
import { useRef, useLayoutEffect } from 'react';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const Footer = () => {
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const showFullFeedbackLabel = useMemo(
|
||||
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jest.mock('../search', () => ({
|
||||
}));
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<MemoryRouter initialEntries={['/txs']}>
|
||||
<Header />
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -24,6 +24,7 @@ describe('Header', () => {
|
||||
|
||||
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
|
||||
});
|
||||
|
||||
it('should render search', () => {
|
||||
render(renderComponent());
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { matchPath, useLocation, useMatch } from 'react-router-dom';
|
||||
import {
|
||||
ThemeSwitcher,
|
||||
Navigation,
|
||||
@@ -13,23 +13,26 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||
import type { Navigable } from '../../routes/router-config';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import { isNavigable } from '../../routes/router-config';
|
||||
import { routerConfig } from '../../routes/router-config';
|
||||
import { useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import { Search } from '../search';
|
||||
|
||||
const routeToNavigationItem = (r: Navigable) => (
|
||||
<NavigationItem key={r.name}>
|
||||
<NavigationLink to={r.path}>{r.text}</NavigationLink>
|
||||
<NavigationItem key={r.handle.name}>
|
||||
<NavigationLink to={r.path}>{r.handle.text}</NavigationLink>
|
||||
</NavigationItem>
|
||||
);
|
||||
|
||||
export const Header = () => {
|
||||
const isHome = Boolean(useMatch(Routes.HOME));
|
||||
const pages = routerConfig[0].children || [];
|
||||
const mainItems = compact(
|
||||
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
|
||||
routerConfig.find((r) => r.path === n)
|
||||
pages.find((r) => r.path === n)
|
||||
)
|
||||
);
|
||||
).filter(isNavigable);
|
||||
|
||||
const groupedItems = compact(
|
||||
[
|
||||
@@ -39,8 +42,8 @@ export const Header = () => {
|
||||
Routes.GOVERNANCE,
|
||||
Routes.NETWORK_PARAMETERS,
|
||||
Routes.GENESIS,
|
||||
].map((n) => routerConfig.find((r) => r.path === n))
|
||||
);
|
||||
].map((n) => pages.find((r) => r.path === n))
|
||||
).filter(isNavigable);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -67,7 +70,7 @@ export const Header = () => {
|
||||
actions={
|
||||
<>
|
||||
<ThemeSwitcher />
|
||||
<Search />
|
||||
{!isHome && <Search />}
|
||||
</>
|
||||
}
|
||||
onResize={(width, el) => {
|
||||
@@ -90,7 +93,7 @@ export const Header = () => {
|
||||
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
|
||||
>
|
||||
{mainItems.map(routeToNavigationItem)}
|
||||
{groupedItems && (
|
||||
{groupedItems && groupedItems.length > 0 && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
||||
{t('Other')}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const AssetLink = ({
|
||||
if (asDialog) {
|
||||
open(assetId, e.target as HTMLElement);
|
||||
} else {
|
||||
navigate(`${Routes.ASSETS}/${asset?.id}`);
|
||||
navigate(`/${Routes.ASSETS}/${asset?.id}`);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { AppRouter } from '../../routes';
|
||||
|
||||
export const Main = () => {
|
||||
return (
|
||||
<main className="p-4">
|
||||
<AppRouter />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -14,11 +14,72 @@ import {
|
||||
SettlementAssetInfoPanel,
|
||||
} from '@vegaprotocol/market-info';
|
||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { DataSourceDefinition } from '@vegaprotocol/types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
if (!market) return null;
|
||||
|
||||
const settlementData =
|
||||
market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
|
||||
.data;
|
||||
const terminationData =
|
||||
market.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForTradingTermination.data;
|
||||
|
||||
const getSigners = (data: DataSourceDefinition) => {
|
||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||
const signers = data.sourceType.sourceType.signers || [];
|
||||
|
||||
return signers.map(({ signer }, i) => {
|
||||
return (
|
||||
(signer.__typename === 'ETHAddress' && signer.address) ||
|
||||
(signer.__typename === 'PubKey' && signer.key)
|
||||
);
|
||||
});
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const oraclePanels = isEqual(
|
||||
getSigners(settlementData),
|
||||
getSigners(terminationData)
|
||||
)
|
||||
? [
|
||||
{
|
||||
title: t('Settlement Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel
|
||||
noBorder={false}
|
||||
market={market}
|
||||
type="settlementData"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Termination Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel
|
||||
noBorder={false}
|
||||
market={market}
|
||||
type="termination"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel
|
||||
noBorder={false}
|
||||
market={market}
|
||||
type="settlementData"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const panels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
@@ -60,9 +121,11 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
<>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={trigger}
|
||||
data={{
|
||||
maxValidPrice: trigger.maxValidPrice,
|
||||
minValidPrice: trigger.minValidPrice,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
omits={['referencePrice', '__typename']}
|
||||
/>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
@@ -94,25 +157,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel noBorder={false} market={market}>
|
||||
<Link
|
||||
className="text-xs hover:underline"
|
||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
>
|
||||
{t('View settlement data oracle specification')}
|
||||
</Link>
|
||||
<Link
|
||||
className="text-xs hover:underline"
|
||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForTradingTermination.id}`}
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</Link>
|
||||
</OracleInfoPanel>
|
||||
),
|
||||
},
|
||||
...oraclePanels,
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueGetterParams,
|
||||
|
||||
@@ -9,7 +9,7 @@ import SizeInMarket from '../size-in-market/size-in-market';
|
||||
export interface DeterministicOrderDetailsProps {
|
||||
id: string;
|
||||
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
|
||||
version?: number;
|
||||
version?: number | null;
|
||||
}
|
||||
|
||||
export const wrapperClasses =
|
||||
@@ -28,7 +28,7 @@ export const wrapperClasses =
|
||||
*/
|
||||
const DeterministicOrderDetails = ({
|
||||
id,
|
||||
version = 0,
|
||||
version = null,
|
||||
}: DeterministicOrderDetailsProps) => {
|
||||
const { data, error } = useExplorerDeterministicOrderQuery({
|
||||
variables: { orderId: id, version },
|
||||
|
||||
@@ -20,7 +20,7 @@ export const PageHeader = ({
|
||||
copy = false,
|
||||
className,
|
||||
}: PageHeaderProps) => {
|
||||
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt';
|
||||
const titleClasses = 'text-xl uppercase font-alpha calt';
|
||||
return (
|
||||
<header className={className}>
|
||||
<span className={`${titleClasses} block`}>{prefix}</span>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { VoteProgress } from '@vegaprotocol/proposals';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
@@ -12,7 +12,10 @@ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface RouteErrorBoundaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export class RouteErrorBoundary extends React.Component<
|
||||
RouteErrorBoundaryProps,
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
constructor(props: RouteErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error) {
|
||||
console.log(`Error caught in App error boundary ${error.message}`, error);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.hasError) {
|
||||
return <h1>{t('Something went wrong')}</h1>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,189 +1,70 @@
|
||||
import {
|
||||
detectTypeByFetching,
|
||||
detectTypeFromQuery,
|
||||
getSearchType,
|
||||
determineType,
|
||||
isBlock,
|
||||
isHexadecimal,
|
||||
isNetworkParty,
|
||||
isNonHex,
|
||||
SearchTypes,
|
||||
toHex,
|
||||
toNonHex,
|
||||
isHash,
|
||||
} from './detect-search';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe('Detect Search', () => {
|
||||
it("should detect that it's a hexadecimal", () => {
|
||||
const expected = true;
|
||||
const testString =
|
||||
'0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
|
||||
const actual = isHexadecimal(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect that it's not hexadecimal", () => {
|
||||
const expected = true;
|
||||
const testString =
|
||||
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
|
||||
const actual = isNonHex(testString);
|
||||
expect(actual).toBe(expected);
|
||||
it.each([
|
||||
['0000000000000000000000000000000000000000000000000000000000000000', true],
|
||||
['0000000000000000000000000000000000000000000000000000000000000001', true],
|
||||
[
|
||||
'LOOONG0000000000000000000000000000000000000000000000000000000000000000',
|
||||
false,
|
||||
],
|
||||
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', false],
|
||||
['something else', false],
|
||||
])("should detect that it's a hash", (input, expected) => {
|
||||
expect(isHash(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect that it's a network party", () => {
|
||||
const expected = true;
|
||||
const testString = 'network';
|
||||
const actual = isNetworkParty(testString);
|
||||
expect(actual).toBe(expected);
|
||||
expect(isNetworkParty('network')).toBe(true);
|
||||
expect(isNetworkParty('web')).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect that it's a block", () => {
|
||||
const expected = true;
|
||||
const testString = '3188';
|
||||
const actual = isBlock(testString);
|
||||
expect(actual).toBe(expected);
|
||||
expect(isBlock('123')).toBe(true);
|
||||
expect(isBlock('x123')).toBe(false);
|
||||
});
|
||||
|
||||
it('should convert from non-hex to hex', () => {
|
||||
const expected = '0x123';
|
||||
const testString = '123';
|
||||
const actual = toHex(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should convert from hex to non-hex', () => {
|
||||
const expected = '123';
|
||||
const testString = '0x123';
|
||||
const actual = toNonHex(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a hexadecimal", () => {
|
||||
const expected = [SearchTypes.Party, SearchTypes.Transaction];
|
||||
const testString =
|
||||
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a non hex", () => {
|
||||
const expected = [SearchTypes.Party, SearchTypes.Transaction];
|
||||
const testString =
|
||||
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a network party", () => {
|
||||
const expected = [SearchTypes.Party];
|
||||
const testString = 'network';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a block (number)", () => {
|
||||
const expected = [SearchTypes.Block];
|
||||
const testString = '23432';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
|
||||
const query = '0xabc';
|
||||
const type = SearchTypes.Transaction;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: query,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await detectTypeByFetching(query);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
|
||||
);
|
||||
expect(result).toBe(type);
|
||||
});
|
||||
|
||||
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
|
||||
const query = 'abc';
|
||||
const type = SearchTypes.Party;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await detectTypeByFetching(query);
|
||||
expect(result).toBe(type);
|
||||
});
|
||||
|
||||
it('getSearchType should return party from fetch response', async () => {
|
||||
const query =
|
||||
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const expected = SearchTypes.Party;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return transaction from fetch response', async () => {
|
||||
const query =
|
||||
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const expected = SearchTypes.Transaction;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: query,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return undefined from transaction response', async () => {
|
||||
const query = 'u';
|
||||
const expected = undefined;
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return block if query is number', async () => {
|
||||
const query = '123';
|
||||
const expected = SearchTypes.Block;
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return party if query is network', async () => {
|
||||
const query = 'network';
|
||||
const expected = SearchTypes.Party;
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
SearchTypes.Transaction,
|
||||
],
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
SearchTypes.Party,
|
||||
],
|
||||
['123', SearchTypes.Block],
|
||||
['network', SearchTypes.Party],
|
||||
['something else', SearchTypes.Unknown],
|
||||
])(
|
||||
"detectTypeByFetching should call fetch with non-hex query it's a transaction",
|
||||
async (input, type) => {
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok:
|
||||
input ===
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: input,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await determineType(input);
|
||||
expect(result).toBe(type);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
|
||||
|
||||
@@ -6,15 +7,20 @@ export enum SearchTypes {
|
||||
Party = 'party',
|
||||
Block = 'block',
|
||||
Order = 'order',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export const TX_LENGTH = 64;
|
||||
export const HASH_LENGTH = 64;
|
||||
|
||||
export const isHash = (value: string) =>
|
||||
/[0-9a-fA-F]+/.test(remove0x(value)) &&
|
||||
remove0x(value).length === HASH_LENGTH;
|
||||
|
||||
export const isHexadecimal = (search: string) =>
|
||||
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
|
||||
search.startsWith('0x') && search.length === 2 + HASH_LENGTH;
|
||||
|
||||
export const isNonHex = (search: string) =>
|
||||
!search.startsWith('0x') && search.length === TX_LENGTH;
|
||||
!search.startsWith('0x') && search.length === HASH_LENGTH;
|
||||
|
||||
export const isBlock = (search: string) => !Number.isNaN(Number(search));
|
||||
|
||||
@@ -23,121 +29,44 @@ export const isNetworkParty = (search: string) => search === 'network';
|
||||
export const toHex = (query: string) =>
|
||||
isHexadecimal(query) ? query : `0x${query}`;
|
||||
|
||||
export const toNonHex = (query: string) =>
|
||||
isNonHex(query) ? query : `${query.replace('0x', '')}`;
|
||||
export const toNonHex = remove0x;
|
||||
|
||||
export const detectTypeFromQuery = (
|
||||
query: string
|
||||
): SearchTypes[] | undefined => {
|
||||
const i = query.toLowerCase();
|
||||
|
||||
if (isHexadecimal(i) || isNonHex(i)) {
|
||||
return [SearchTypes.Party, SearchTypes.Transaction];
|
||||
} else if (isNetworkParty(i)) {
|
||||
return [SearchTypes.Party];
|
||||
} else if (isBlock(i)) {
|
||||
return [SearchTypes.Block];
|
||||
/**
|
||||
* Determine the type of the given query
|
||||
*/
|
||||
export const determineType = async (query: string): Promise<SearchTypes> => {
|
||||
const value = query.toLowerCase();
|
||||
if (isHash(value)) {
|
||||
// it can be either `SearchTypes.Party` or `SearchTypes.Transaction`
|
||||
if (await isTransactionHash(value)) {
|
||||
return SearchTypes.Transaction;
|
||||
} else {
|
||||
return SearchTypes.Party;
|
||||
}
|
||||
} else if (isNetworkParty(value)) {
|
||||
return SearchTypes.Party;
|
||||
} else if (isBlock(value)) {
|
||||
return SearchTypes.Block;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return SearchTypes.Unknown;
|
||||
};
|
||||
|
||||
export const detectTypeByFetching = async (
|
||||
query: string
|
||||
): Promise<SearchTypes | undefined> => {
|
||||
const hash = toNonHex(query);
|
||||
/**
|
||||
* Checks if given input is a transaction hash by querying the transactions
|
||||
* endpoint
|
||||
*/
|
||||
export const isTransactionHash = async (input: string): Promise<boolean> => {
|
||||
const hash = remove0x(input);
|
||||
const request = await fetch(
|
||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
||||
);
|
||||
|
||||
if (request?.ok) {
|
||||
const body: BlockExplorerTransaction = await request.json();
|
||||
|
||||
if (body?.transaction) {
|
||||
return SearchTypes.Transaction;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return SearchTypes.Party;
|
||||
};
|
||||
|
||||
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
|
||||
// export const detectTypeByFetching = async (
|
||||
// query: string,
|
||||
// type: SearchTypes
|
||||
// ): Promise<SearchTypes | undefined> => {
|
||||
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
|
||||
//
|
||||
// if (!TYPES.includes(type)) {
|
||||
// throw new Error('Search type provided not recognised');
|
||||
// }
|
||||
//
|
||||
// if (type === SearchTypes.Transaction) {
|
||||
// const hash = toNonHex(query);
|
||||
// const request = await fetch(
|
||||
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
||||
// );
|
||||
//
|
||||
// if (request?.ok) {
|
||||
// const body: BlockExplorerTransaction = await request.json();
|
||||
//
|
||||
// if (body?.transaction) {
|
||||
// return SearchTypes.Transaction;
|
||||
// }
|
||||
// }
|
||||
// } else if (type === SearchTypes.Party) {
|
||||
// const party = toNonHex(query);
|
||||
//
|
||||
// const request = await fetch(
|
||||
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
|
||||
// );
|
||||
//
|
||||
// if (request.ok) {
|
||||
// const body: BlockExplorerTransactions = await request.json();
|
||||
//
|
||||
// if (body?.transactions?.length) {
|
||||
// return SearchTypes.Party;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return undefined;
|
||||
// };
|
||||
|
||||
// export const getSearchType = async (
|
||||
// query: string
|
||||
// ): Promise<SearchTypes | undefined> => {
|
||||
// const searchTypes = detectTypeFromQuery(query);
|
||||
// const hasResults = searchTypes?.length;
|
||||
//
|
||||
// if (hasResults) {
|
||||
// if (hasResults > 1) {
|
||||
// const promises = searchTypes.map((type) =>
|
||||
// detectTypeByFetching(query, type)
|
||||
// );
|
||||
// const results = await Promise.all(promises);
|
||||
// return results.find((result) => result !== undefined);
|
||||
// }
|
||||
//
|
||||
// return searchTypes[0];
|
||||
// }
|
||||
//
|
||||
// return undefined;
|
||||
// };
|
||||
|
||||
export const getSearchType = async (
|
||||
query: string
|
||||
): Promise<SearchTypes | undefined> => {
|
||||
const searchTypes = detectTypeFromQuery(query);
|
||||
const hasResults = searchTypes?.length;
|
||||
|
||||
if (hasResults) {
|
||||
if (hasResults > 1) {
|
||||
return await detectTypeByFetching(query);
|
||||
}
|
||||
|
||||
return searchTypes[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,157 +1,82 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { Search } from './search';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { SearchForm } from './search';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { SearchTypes, getSearchType } from './detect-search';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
const mockedNavigate = jest.fn();
|
||||
const mockGetSearchType = getSearchType as jest.MockedFunction<
|
||||
typeof getSearchType
|
||||
>;
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('./detect-search', () => ({
|
||||
...jest.requireActual('./detect-search'),
|
||||
getSearchType: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
mockedNavigate.mockClear();
|
||||
});
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<Search />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SearchForm />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const getInputs = () => ({
|
||||
input: screen.getByTestId('search'),
|
||||
button: screen.getByTestId('search-button'),
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
describe('SearchForm', () => {
|
||||
it('should render search input and button', () => {
|
||||
render(renderComponent());
|
||||
renderComponent();
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('search-button')).toHaveTextContent('Search');
|
||||
});
|
||||
|
||||
it('should render error if input is not known', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, { target: { value: 'asd' } });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Transaction type is not recognised'
|
||||
it.each([
|
||||
[
|
||||
Routes.TX,
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
],
|
||||
[
|
||||
Routes.PARTIES,
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
],
|
||||
[Routes.BLOCKS, '123'],
|
||||
[undefined, 'something else'],
|
||||
])('should redirect to %s', async (route, input) => {
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok:
|
||||
input ===
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: input,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should render error if no input is given', async () => {
|
||||
render(renderComponent());
|
||||
const { button } = getInputs();
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Search query required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to transactions page', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'0x1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
renderComponent();
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('search'), {
|
||||
target: {
|
||||
value: input,
|
||||
},
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('search-button'));
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to transactions page without proceeding 0x', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to parties page', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'0x1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to parties page without proceeding 0x', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to blocks page if passed a number', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Block);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '123',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(`${Routes.BLOCKS}/123`);
|
||||
expect(mockedNavigate).toBeCalledTimes(route ? 1 : 0);
|
||||
if (route) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(mockedNavigate).toBeCalledWith(`${route}/${input}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Button, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getSearchType, SearchTypes, toHex } from './detect-search';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { determineType, isBlock, isHash, SearchTypes } from './detect-search';
|
||||
|
||||
interface FormFields {
|
||||
search: string;
|
||||
@@ -30,101 +31,26 @@ const MagnifyingGlass = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Clear = () => (
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.3748 1.37478L1.37478 11.3748L0.625244 10.6252L10.6252 0.625244L11.3748 1.37478Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.37478 0.625244L11.3748 10.6252L10.6252 11.3748L0.625244 1.37478L1.37478 0.625244Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Search = () => {
|
||||
const { register, handleSubmit } = useForm<FormFields>();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (fields: FormFields) => {
|
||||
setError(null);
|
||||
|
||||
const query = fields.search;
|
||||
|
||||
if (!query) {
|
||||
return setError(new Error(t('Search query required')));
|
||||
}
|
||||
|
||||
const result = await getSearchType(query);
|
||||
const urlAsHex = toHex(query);
|
||||
const unrecognisedError = new Error(
|
||||
t('Transaction type is not recognised')
|
||||
);
|
||||
|
||||
if (result) {
|
||||
switch (result) {
|
||||
case SearchTypes.Party:
|
||||
return navigate(`${Routes.PARTIES}/${urlAsHex}`);
|
||||
case SearchTypes.Transaction:
|
||||
return navigate(`${Routes.TX}/${urlAsHex}`);
|
||||
case SearchTypes.Block:
|
||||
return navigate(`${Routes.BLOCKS}/${Number(query)}`);
|
||||
default:
|
||||
return setError(unrecognisedError);
|
||||
}
|
||||
}
|
||||
|
||||
return setError(unrecognisedError);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const searchForm = (
|
||||
<form className="block min-w-[290px]" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex relative items-stretch gap-2 text-xs">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
{t('Search by block number or transaction hash')}
|
||||
</label>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute top-[50%] translate-y-[-50%] left-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
<MagnifyingGlass />
|
||||
</button>
|
||||
<Input
|
||||
{...register('search')}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
className={classNames(
|
||||
'peer',
|
||||
'pl-8 py-2 text-xs',
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200'
|
||||
)}
|
||||
hasError={Boolean(error?.message)}
|
||||
type="text"
|
||||
placeholder={t('Enter block number, public key or transaction hash')}
|
||||
/>
|
||||
{error?.message && (
|
||||
<div
|
||||
className={classNames(
|
||||
'hidden peer-focus:block',
|
||||
'bg-white dark:bg-black',
|
||||
'border rounded-b border-t-0 border-vega-light-200 dark:border-vega-dark-200',
|
||||
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
|
||||
)}
|
||||
>
|
||||
<InputError
|
||||
data-testid="search-error"
|
||||
intent="danger"
|
||||
className="text-xs"
|
||||
>
|
||||
{error.message}
|
||||
</InputError>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="hidden [.search-dropdown_&]:block"
|
||||
type="submit"
|
||||
size="xs"
|
||||
data-testid="search-button"
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
const searchForm = <SearchForm />;
|
||||
|
||||
const searchTrigger = (
|
||||
<DropdownMenu.Root>
|
||||
@@ -154,10 +80,135 @@ export const Search = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden [.nav-search-full_&]:block">{searchForm}</div>
|
||||
<div className="hidden [.nav-search-full_&]:block min-w-[290px]">
|
||||
{searchForm}
|
||||
</div>
|
||||
<div className="hidden [.nav-search-compact_&]:block">
|
||||
{searchTrigger}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchForm = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState,
|
||||
watch,
|
||||
} = useForm<FormFields>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (fields: FormFields) => {
|
||||
clearErrors();
|
||||
const type = await determineType(fields.search);
|
||||
if (type) {
|
||||
switch (type) {
|
||||
case SearchTypes.Party:
|
||||
return navigate(`${Routes.PARTIES}/${remove0x(fields.search)}`);
|
||||
case SearchTypes.Transaction:
|
||||
return navigate(`${Routes.TX}/${remove0x(fields.search)}`);
|
||||
case SearchTypes.Block:
|
||||
return navigate(`${Routes.BLOCKS}/${Number(fields.search)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setError('search', new Error(t('The search term is not a valid query')));
|
||||
},
|
||||
[clearErrors, navigate, setError]
|
||||
);
|
||||
|
||||
const searchQuery = watch('search', '');
|
||||
|
||||
return (
|
||||
<form className="block min-w-[200px]" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex relative items-stretch gap-2 text-xs">
|
||||
<div className="relative w-full">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
{t('Search by block number or transaction hash')}
|
||||
</label>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute top-[50%] translate-y-[-50%] left-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
<MagnifyingGlass />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setValue('search', '');
|
||||
clearErrors();
|
||||
}}
|
||||
className={classNames(
|
||||
{ hidden: searchQuery.length === 0 },
|
||||
'absolute top-[50%] translate-y-[-50%] right-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
<Clear />
|
||||
</button>
|
||||
<Input
|
||||
{...register('search', {
|
||||
required: t('Search query is required'),
|
||||
validate: (value) =>
|
||||
isHash(value) ||
|
||||
isBlock(value) ||
|
||||
t('Search query has to be a number or a 64 character hash'),
|
||||
onBlur: () => clearErrors('search'),
|
||||
})}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
className={classNames(
|
||||
'pl-8 py-2 text-xs',
|
||||
{ 'pr-8': searchQuery.length > 1 },
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200',
|
||||
{
|
||||
'border-vega-pink dark:border-vega-pink': Boolean(
|
||||
formState.errors.search
|
||||
),
|
||||
}
|
||||
)}
|
||||
hasError={Boolean(formState.errors.search)}
|
||||
type="text"
|
||||
placeholder={t(
|
||||
'Enter block number, public key or transaction hash'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{formState.errors.search && (
|
||||
<div
|
||||
className={classNames(
|
||||
'[nav_&]:border [nav_&]:rounded [nav_&]:border-vega-light-300 [nav_&]:dark:border-vega-light-300',
|
||||
'[.search-dropdown_&]:border [.search-dropdown_&]:rounded [.search-dropdown_&]:border-vega-light-300 [.search-dropdown_&]:dark:border-vega-light-300',
|
||||
'bg-white dark:bg-black',
|
||||
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
|
||||
)}
|
||||
>
|
||||
<InputError
|
||||
data-testid="search-error"
|
||||
intent="danger"
|
||||
className="text-xs"
|
||||
>
|
||||
{formState.errors.search.message}
|
||||
</InputError>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="xs"
|
||||
data-testid="search-button"
|
||||
className="[nav_&]:hidden"
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export const BundleExists = ({
|
||||
// Note if this is wrong, the wrong decoder will be used which will give incorrect data
|
||||
|
||||
return (
|
||||
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
|
||||
<div className="w-auto h-10 max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
|
||||
<IconForBundleStatus status={status} />
|
||||
<h1 className="text-xl pb-1">
|
||||
{status === 'STATUS_ENABLED'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { TxList } from './tx-list';
|
||||
export { TxOrderType } from './tx-order-type';
|
||||
export { TxsInfiniteList } from './txs-infinite-list';
|
||||
export { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { TendermintUnconfirmedTransactionsResponse } from '../../routes/txs/tendermint-unconfirmed-transactions-response.d';
|
||||
|
||||
interface TxsProps {
|
||||
data: TendermintUnconfirmedTransactionsResponse | undefined;
|
||||
}
|
||||
|
||||
export const TxList = ({ data }: TxsProps) => {
|
||||
if (!data) {
|
||||
return <div>{t('Awaiting transactions')}</div>;
|
||||
}
|
||||
|
||||
return <div>{JSON.stringify(data, null, ' ')}</div>;
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { toHex } from '../search/detect-search';
|
||||
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
|
||||
const TRUNCATE_LENGTH = 5;
|
||||
const TRUNCATE_LENGTH = 10;
|
||||
|
||||
export const TxsInfiniteListItem = ({
|
||||
hash,
|
||||
@@ -34,10 +34,10 @@ export const TxsInfiniteListItem = ({
|
||||
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
|
||||
>
|
||||
<div
|
||||
className="text-sm col-span-10 xl:col-span-3 leading-none"
|
||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
||||
data-testid="tx-hash"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
<span className="md:hidden uppercase text-vega-dark-300">
|
||||
ID:
|
||||
</span>
|
||||
<TruncatedLink
|
||||
@@ -48,10 +48,10 @@ export const TxsInfiniteListItem = ({
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-sm col-span-10 xl:col-span-3 leading-none"
|
||||
className="text-sm col-span-10 md:col-span-3 leading-none"
|
||||
data-testid="pub-key"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
<span className="md:hidden uppercase text-vega-dark-300">
|
||||
By:
|
||||
</span>
|
||||
<TruncatedLink
|
||||
@@ -61,14 +61,14 @@ export const TxsInfiniteListItem = ({
|
||||
endChars={TRUNCATE_LENGTH}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm col-span-5 xl:col-span-2 leading-none flex items-center">
|
||||
<div className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
|
||||
<TxOrderType orderType={type} command={command} />
|
||||
</div>
|
||||
<div
|
||||
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
|
||||
className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
|
||||
data-testid="tx-block"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
<span className="md:hidden uppercase text-vega-dark-300">
|
||||
Block:
|
||||
</span>
|
||||
<TruncatedLink
|
||||
@@ -79,10 +79,10 @@ export const TxsInfiniteListItem = ({
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
|
||||
className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
|
||||
data-testid="tx-success"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
<span className="md:hidden uppercase text-vega-dark-300">
|
||||
Success:
|
||||
</span>
|
||||
{isNumber(code) ? (
|
||||
|
||||
@@ -64,9 +64,7 @@ describe('Txs infinite list', () => {
|
||||
error={Error('test error!')}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Cannot fetch transaction: Error: test error!')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Cannot fetch transaction')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('item renders data of n length into list of n length', () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ const NOOP = () => {};
|
||||
const Item = ({ index, style, isLoading, error }: ItemProps) => {
|
||||
let content;
|
||||
if (error) {
|
||||
content = t(`Cannot fetch transaction: ${error}`);
|
||||
content = t(`Cannot fetch transaction`);
|
||||
} else if (isLoading) {
|
||||
content = <Loader />;
|
||||
} else {
|
||||
@@ -68,7 +68,7 @@ export const TxsInfiniteList = ({
|
||||
className,
|
||||
}: TxsInfiniteListProps) => {
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const isStacked = ['xs', 'sm', 'md', 'lg'].includes(screenSize);
|
||||
const isStacked = ['xs', 'sm'].includes(screenSize);
|
||||
|
||||
if (!txs) {
|
||||
if (!areTxsLoading) {
|
||||
@@ -95,15 +95,15 @@ export const TxsInfiniteList = ({
|
||||
|
||||
return (
|
||||
<div className={className} data-testid="transactions-list">
|
||||
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
|
||||
<div className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
|
||||
<div className="col-span-3">
|
||||
<span className="hidden xl:inline">Transaction </span>
|
||||
<span className="hidden xl:inline">{t('Transaction')} </span>
|
||||
<span>ID</span>
|
||||
</div>
|
||||
<div className="col-span-3">Submitted By</div>
|
||||
<div className="col-span-2">Type</div>
|
||||
<div className="col-span-1">Block</div>
|
||||
<div className="col-span-1">Success</div>
|
||||
<div className="col-span-3">{t('Submitted By')}</div>
|
||||
<div className="col-span-2">{t('Type')}</div>
|
||||
<div className="col-span-1">{t('Block')}</div>
|
||||
<div className="col-span-1">{t('Success')}</div>
|
||||
</div>
|
||||
<div data-testid="infinite-scroll-wrapper">
|
||||
<InfiniteLoader
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { RenderFetched } from '../render-fetched';
|
||||
import { TruncatedLink } from '../truncate/truncated-link';
|
||||
import { TxOrderType } from './tx-order-type';
|
||||
import { Table, TableRow, TableCell } from '../table';
|
||||
@@ -9,7 +8,7 @@ import type { BlockExplorerTransactions } from '../../routes/types/block-explore
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
|
||||
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import EmptyList from '../empty-list/empty-list';
|
||||
|
||||
interface TxsPerBlockProps {
|
||||
@@ -27,7 +26,7 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||
} = useFetch<BlockExplorerTransactions>(url);
|
||||
|
||||
return (
|
||||
<RenderFetched error={error} loading={loading} className="text-body-large">
|
||||
<AsyncRenderer data={data} error={error} loading={!!loading}>
|
||||
{data && data.transactions.length > 0 ? (
|
||||
<div className="overflow-x-auto whitespace-nowrap mb-28">
|
||||
<Table>
|
||||
@@ -95,6 +94,6 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||
label={t('0 transactions')}
|
||||
/>
|
||||
)}
|
||||
</RenderFetched>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,12 +144,12 @@ afterEach(() => {
|
||||
describe('Block', () => {
|
||||
it('renders error state if error is present', async () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: null, loading: false, error: 'asd' },
|
||||
state: { data: null, loading: false, error: new Error('asd') },
|
||||
});
|
||||
render(renderComponent());
|
||||
|
||||
expect(screen.getByText(`BLOCK ${blockId}`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Error retrieving data')).toBeInTheDocument();
|
||||
expect(screen.getByText('Something went wrong: asd')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders loading state if present', async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
@@ -12,9 +11,8 @@ import {
|
||||
TableCell,
|
||||
} from '../../../components/table';
|
||||
import { TxsPerBlock } from '../../../components/txs/txs-per-block';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { Routes } from '../../route-names';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { NodeLink } from '../../../components/links';
|
||||
@@ -34,7 +32,7 @@ const Block = () => {
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="block-header">{t(`BLOCK ${block}`)}</RouteTitle>
|
||||
<RenderFetched error={error} loading={loading}>
|
||||
<AsyncRenderer data={blockData} error={error} loading={!!loading}>
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2 mb-8">
|
||||
<Link
|
||||
@@ -123,7 +121,7 @@ const Block = () => {
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</RenderFetched>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import type { TendermintGenesisResponse } from './tendermint-genesis-response';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
@@ -10,21 +10,26 @@ const Genesis = () => {
|
||||
useDocumentTitle(['Genesis']);
|
||||
|
||||
const {
|
||||
state: { data: genesis, loading },
|
||||
state: { data, loading, error },
|
||||
} = useFetch<TendermintGenesisResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/genesis`
|
||||
);
|
||||
if (!genesis?.result.genesis) {
|
||||
if (loading) {
|
||||
return <Loader />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<>
|
||||
<RouteTitle data-testid="genesis-header">{t('Genesis')}</RouteTitle>
|
||||
<SyntaxHighlighter data={genesis?.result.genesis} />
|
||||
</section>
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
error={error}
|
||||
loading={!!loading}
|
||||
loadingMessage={t('Loading genesis information...')}
|
||||
errorMessage={t('Could not fetch genesis data')}
|
||||
>
|
||||
<section>
|
||||
<SyntaxHighlighter data={data?.result.genesis} />
|
||||
</section>
|
||||
</AsyncRenderer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { proposalsDataProvider } from '@vegaprotocol/proposals';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalsTable } from '../../components/proposals/proposals-table';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { StatsManager } from '@vegaprotocol/network-stats';
|
||||
import { SearchForm } from '../../components/search';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
const Home = () => {
|
||||
const classnames = 'mt-4 grid grid-cols-1 lg:grid-cols-2 lg:gap-4';
|
||||
|
||||
const classnames = 'mt-4 mb-4';
|
||||
useDocumentTitle();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<StatsManager className={classnames} />
|
||||
<div className="p-20 max-sm:py-10 max-sm:px-0">
|
||||
<SearchForm />
|
||||
</div>
|
||||
<div className="px-20 max-sm:px-0">
|
||||
<StatsManager className={classnames} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import { RouteErrorBoundary } from '../components/router-error-boundary';
|
||||
|
||||
import routerConfig from './router-config';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export interface RouteChildProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const AppRouter = () => {
|
||||
const routes = useRoutes(routerConfig);
|
||||
|
||||
const splashLoading = (
|
||||
<Splash>
|
||||
<Loader />
|
||||
</Splash>
|
||||
);
|
||||
|
||||
return (
|
||||
<RouteErrorBoundary>
|
||||
<React.Suspense fallback={splashLoading}>{routes}</React.Suspense>
|
||||
</RouteErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { AnnouncementBanner } from '@vegaprotocol/announcements';
|
||||
import {
|
||||
BackgroundVideo,
|
||||
BreadcrumbsContainer,
|
||||
ButtonLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Link,
|
||||
Outlet,
|
||||
useMatch,
|
||||
useRouteError,
|
||||
} from 'react-router-dom';
|
||||
import { Footer } from '../components/footer/footer';
|
||||
import { Header } from '../components/header';
|
||||
import { Routes } from './route-names';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
asJson={asJson}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Layout = () => {
|
||||
const isHome = Boolean(useMatch(Routes.HOME));
|
||||
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
|
||||
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'min-h-screen',
|
||||
'mx-auto my-0',
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||
'border-vega-light-200 dark:border-vega-dark-200',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{ANNOUNCEMENTS_CONFIG_URL && (
|
||||
<AnnouncementBanner
|
||||
app="explorer"
|
||||
configUrl={ANNOUNCEMENTS_CONFIG_URL}
|
||||
/>
|
||||
)}
|
||||
<Header />
|
||||
</div>
|
||||
<div className={fixedWidthClasses}>
|
||||
<main className="p-4">
|
||||
{!isHome && <BreadcrumbsContainer className="mb-4" />}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<div className={fixedWidthClasses}>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorBoundary = () => {
|
||||
const error = useRouteError();
|
||||
|
||||
const errorTitle = isRouteErrorResponse(error)
|
||||
? `${error.status} ${error.statusText}`
|
||||
: t('Something went wrong');
|
||||
|
||||
const errorMessage = isRouteErrorResponse(error)
|
||||
? error.error?.message
|
||||
: (error as Error).message || JSON.stringify(error);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackgroundVideo className="brightness-50" />
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[620px] p-2 mt-[10vh]',
|
||||
'mx-auto my-0',
|
||||
'antialiased text-white',
|
||||
'overflow-hidden relative',
|
||||
'flex flex-col gap-2'
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div>{GHOST}</div>
|
||||
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
|
||||
{errorTitle}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-sm mt-10 overflow-auto break-all font-mono">
|
||||
{errorMessage}
|
||||
</div>
|
||||
<div>
|
||||
<ButtonLink onClick={() => window.location.reload()}>
|
||||
{t('Try refreshing')}
|
||||
</ButtonLink>{' '}
|
||||
{t('or go back to')}{' '}
|
||||
<Link className="underline" to="/">
|
||||
{t('Home')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
viewBox="0 0 56 85"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M41 0.5H3V60.5H41V0.5Z" fill="white" />
|
||||
<path d="M15 18.5H13V20.5H15V18.5Z" fill="black" />
|
||||
<path d="M17 20.5H15V22.5H17V20.5Z" fill="black" />
|
||||
<path d="M19 18.5H17V20.5H19V18.5Z" fill="black" />
|
||||
<path d="M15 22.5H13V24.5H15V22.5Z" fill="black" />
|
||||
<path d="M19 22.5H17V24.5H19V22.5Z" fill="black" />
|
||||
<path d="M29 28.5H15V30.5H29V28.5Z" fill="black" />
|
||||
<path d="M27 18.5H25V20.5H27V18.5Z" fill="black" />
|
||||
<path d="M29 20.5H27V22.5H29V20.5Z" fill="black" />
|
||||
<path d="M31 18.5H29V20.5H31V18.5Z" fill="black" />
|
||||
<path d="M27 22.5H25V24.5H27V22.5Z" fill="black" />
|
||||
<path d="M31 22.5H29V24.5H31V22.5Z" fill="black" />
|
||||
<path d="M31 26.5H29V28.5H31V26.5Z" fill="black" />
|
||||
<path d="M19 60.5H17V84.5H19V60.5Z" fill="black" />
|
||||
<path d="M27 60.5H25V84.5H27V60.5Z" fill="black" />
|
||||
<path
|
||||
d="M3 42.5V58.64V60.5V64.5H21V60.5H23V64.5H41V60.5V58.64V42.5H3Z"
|
||||
fill="#FF077F"
|
||||
/>
|
||||
<path d="M35 46.5H41V42.5H3V46.5H31H35Z" fill="#CB0666" />
|
||||
<path d="M3 32.32V29.5L0 32.5V60.5H2V33.33L3 32.32Z" fill="black" />
|
||||
<path d="M41 31.8V29.49L54.79 21.53L55.79 23.26L41 31.8Z" fill="black" />
|
||||
<path d="M36 54.5H35V55.5H36V54.5Z" fill="black" />
|
||||
<path d="M35 53.5H34V54.5H35V53.5Z" fill="black" />
|
||||
<path d="M34 48.5H33V53.5H34V48.5Z" fill="black" />
|
||||
<path d="M38 48.5H37V52.5H38V48.5Z" fill="black" />
|
||||
<path d="M37 53.5H36V54.5H37V53.5Z" fill="black" />
|
||||
<path d="M39 52.5H38V53.5H39V52.5Z" fill="black" />
|
||||
<path
|
||||
d="M55.7901 23.27L53.0601 22.54L45.1001 8.75L46.8301 7.75L55.7901 23.27Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -49,6 +49,7 @@ export const MarketPage = () => {
|
||||
/>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no markets')}
|
||||
errorMessage={t('Could not fetch market') + ' ' + marketId}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { MarketsTable } from '../../components/markets/markets-table';
|
||||
|
||||
export const MarketsPage = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||
import { NetworkParametersTable } from './network-parameters';
|
||||
|
||||
describe('NetworkParametersTable', () => {
|
||||
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { useNetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/react-helpers';
|
||||
import { useNetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
const PERCENTAGE_PARAMS = [
|
||||
'governance.proposal.asset.requiredMajority',
|
||||
'governance.proposal.asset.requiredParticipation',
|
||||
'governance.proposal.updateAsset.requiredParticipation',
|
||||
'governance.proposal.freeform.requiredMajority',
|
||||
'governance.proposal.freeform.requiredParticipation',
|
||||
'governance.proposal.market.requiredMajority',
|
||||
@@ -53,6 +54,8 @@ const BIG_NUMBER_PARAMS = [
|
||||
'governance.proposal.asset.minProposerBalance',
|
||||
'governance.proposal.market.minProposerBalance',
|
||||
'governance.proposal.market.minVoterBalance',
|
||||
'governance.proposal.updateAsset.minProposerBalance',
|
||||
'governance.proposal.updateAsset.minVoterBalance',
|
||||
];
|
||||
|
||||
export const NetworkParameterRow = ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useExplorerOracleSpecsQuery } from '../__generated__/Oracles';
|
||||
@@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../../hooks/scroll-to-location';
|
||||
import filter from 'recursive-key-filter';
|
||||
|
||||
const Oracles = () => {
|
||||
const { data, loading } = useExplorerOracleSpecsQuery();
|
||||
const { data, loading, error } = useExplorerOracleSpecsQuery();
|
||||
|
||||
useDocumentTitle(['Oracles']);
|
||||
useScrollToLocation();
|
||||
@@ -16,28 +16,40 @@ const Oracles = () => {
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
|
||||
{loading ? <Loader /> : null}
|
||||
{data?.oracleSpecsConnection?.edges
|
||||
? data.oracleSpecsConnection.edges.map((o) => {
|
||||
const id = o?.node.dataSourceSpec.spec.id;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div id={id} key={id} className="mb-10">
|
||||
<OracleDetails
|
||||
id={id}
|
||||
dataSource={o?.node}
|
||||
showBroadcasts={false}
|
||||
/>
|
||||
<details>
|
||||
<summary className="pointer">JSON</summary>
|
||||
<SyntaxHighlighter data={filter(o, ['__typename'])} />
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
loadingMessage={t('Loading oracle data...')}
|
||||
errorMessage={t('Oracle data could not be loaded')}
|
||||
noDataMessage={t('No oracles found')}
|
||||
noDataCondition={(data) =>
|
||||
!data?.oracleSpecsConnection?.edges ||
|
||||
data.oracleSpecsConnection.edges?.length === 0
|
||||
}
|
||||
>
|
||||
{data?.oracleSpecsConnection?.edges
|
||||
? data.oracleSpecsConnection.edges.map((o) => {
|
||||
const id = o?.node.dataSourceSpec.spec.id;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div id={id} key={id} className="mb-10">
|
||||
<OracleDetails
|
||||
id={id}
|
||||
dataSource={o?.node}
|
||||
showBroadcasts={false}
|
||||
/>
|
||||
<details>
|
||||
<summary className="pointer">JSON</summary>
|
||||
<SyntaxHighlighter data={filter(o, ['__typename'])} />
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useExplorerOracleSpecByIdQuery } from '../__generated__/Oracles';
|
||||
import { OracleDetails } from '../components/oracle';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import filter from 'recursive-key-filter';
|
||||
import { TruncateInline } from '../../../components/truncate/truncate';
|
||||
|
||||
@@ -27,7 +26,14 @@ export const Oracle = () => {
|
||||
{t(`Oracle `)}
|
||||
<TruncateInline startChars={5} endChars={5} text={id || '1'} />
|
||||
</RouteTitle>
|
||||
<RenderFetched error={error} loading={loading}>
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
error={error}
|
||||
loading={loading}
|
||||
noDataCondition={(data) => !data?.oracleSpec}
|
||||
errorMessage={t('Could not load oracle data')}
|
||||
loadingMessage={t('Loading oracle data...')}
|
||||
>
|
||||
{data?.oracleSpec ? (
|
||||
<div id={id} key={id} className="mb-10">
|
||||
<OracleDetails
|
||||
@@ -44,7 +50,7 @@ export const Oracle = () => {
|
||||
) : (
|
||||
<span></span>
|
||||
)}
|
||||
</RenderFetched>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,13 @@ query ExplorerPartyAssets($partyId: ID!) {
|
||||
}
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
linkings(pagination: { first: 100 }) {
|
||||
edges {
|
||||
node {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
accountsConnection {
|
||||
edges {
|
||||
|
||||
@@ -10,7 +10,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
|
||||
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
|
||||
|
||||
export const ExplorerPartyAssetsAccountsFragmentDoc = gql`
|
||||
fragment ExplorerPartyAssetsAccounts on AccountBalance {
|
||||
@@ -64,6 +64,13 @@ export const ExplorerPartyAssetsDocument = gql`
|
||||
}
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
linkings(pagination: {first: 100}) {
|
||||
edges {
|
||||
node {
|
||||
amount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
accountsConnection {
|
||||
edges {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { toNonHex } from '../../../../components/search/detect-search';
|
||||
import { PageHeader } from '../../../../components/page-header';
|
||||
import { useDocumentTitle } from '../../../../hooks/use-document-title';
|
||||
|
||||
import { PartyAccounts } from '../components/party-accounts';
|
||||
|
||||
const PartyAccountsByAsset = () => {
|
||||
const { party } = useParams<{ party: string }>();
|
||||
|
||||
useDocumentTitle(['Public keys', party || '-']);
|
||||
const partyId = toNonHex(party ? party : '');
|
||||
|
||||
return (
|
||||
<section>
|
||||
<PageHeader title={t('Balances by asset')} />
|
||||
|
||||
<PartyAccounts partyId={partyId} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { PartyAccountsByAsset };
|
||||
@@ -1,32 +1,9 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import get from 'lodash/get';
|
||||
import AssetBalance from '../../../../components/asset-balance/asset-balance';
|
||||
import { AssetLink, MarketLink } from '../../../../components/links';
|
||||
import { Table, TableRow } from '../../../../components/table';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { ExplorerPartyAssetsAccountsFragment } from '../__generated__/Party-assets';
|
||||
|
||||
const accountTypeString: Record<Schema.AccountType, string> = {
|
||||
ACCOUNT_TYPE_BOND: t('Bond'),
|
||||
ACCOUNT_TYPE_EXTERNAL: t('External'),
|
||||
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: t('Fees (Infrastructure)'),
|
||||
ACCOUNT_TYPE_FEES_LIQUIDITY: t('Fees (Liquidity)'),
|
||||
ACCOUNT_TYPE_FEES_MAKER: t('Fees (Maker)'),
|
||||
ACCOUNT_TYPE_GENERAL: t('General'),
|
||||
ACCOUNT_TYPE_GLOBAL_INSURANCE: t('Global Insurance Pool'),
|
||||
ACCOUNT_TYPE_GLOBAL_REWARD: t('Global Reward Pool'),
|
||||
ACCOUNT_TYPE_INSURANCE: t('Insurance'),
|
||||
ACCOUNT_TYPE_MARGIN: t('Margin'),
|
||||
ACCOUNT_TYPE_PENDING_TRANSFERS: t('Pending Transfers'),
|
||||
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: t('Reward - LP Fees received'),
|
||||
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: t('Reward - Maker fees paid'),
|
||||
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: t('Reward - Maker fees received'),
|
||||
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: t('Reward - Market proposers'),
|
||||
ACCOUNT_TYPE_SETTLEMENT: t('Settlement'),
|
||||
};
|
||||
import { AccountManager } from '@vegaprotocol/accounts';
|
||||
import { useCallback } from 'react';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
|
||||
interface PartyAccountsProps {
|
||||
accounts: ExplorerPartyAssetsAccountsFragment[];
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,49 +11,22 @@ interface PartyAccountsProps {
|
||||
* probably do with sorting by asset, and then within asset, by type with general
|
||||
* appearing first and... tbd
|
||||
*/
|
||||
export const PartyAccounts = ({ accounts }: PartyAccountsProps) => {
|
||||
return (
|
||||
<Table className="max-w-5xl min-w-fit">
|
||||
<thead>
|
||||
<TableRow modifier="bordered" className="font-mono">
|
||||
<td>{t('Type')}</td>
|
||||
<td>{t('Market')}</td>
|
||||
<td className="text-right pr-2">{t('Balance')}</td>
|
||||
<td>{t('Asset')}</td>
|
||||
</TableRow>
|
||||
</thead>
|
||||
<tbody>
|
||||
{accounts.map((account) => {
|
||||
const m = get(account, 'market.tradableInstrument.instrument.name');
|
||||
export const PartyAccounts = ({ partyId }: PartyAccountsProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const onClickAsset = useCallback(
|
||||
(assetId?: string) => {
|
||||
assetId && openAssetDetailsDialog(assetId);
|
||||
},
|
||||
[openAssetDetailsDialog]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={`pa-${account.asset.id}-${account.type}`}
|
||||
title={account.asset.name}
|
||||
id={`${accountTypeString[account.type]} ${m ? ` - ${m}` : ''}`}
|
||||
>
|
||||
<td className="text-md">{accountTypeString[account.type]}</td>
|
||||
<td className="text-md">
|
||||
{account.market?.id ? (
|
||||
<MarketLink id={account.market?.id} />
|
||||
) : (
|
||||
<p>-</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-md text-right pr-2">
|
||||
<AssetBalance
|
||||
assetId={account.asset.id}
|
||||
price={account.balance}
|
||||
showAssetLink={false}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-md">
|
||||
<AssetLink assetId={account.asset.id} />
|
||||
</td>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
return (
|
||||
<div className="block min-h-44 h-60 4 w-full border-red-800 relative">
|
||||
<AccountManager
|
||||
partyId={partyId}
|
||||
onClickAsset={onClickAsset}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../../../routes/route-names';
|
||||
import { Button, Icon, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { PartyBlock } from './party-block';
|
||||
import type { AccountFields } from '@vegaprotocol/accounts';
|
||||
|
||||
export interface PartyBlockAccountProps {
|
||||
partyId: string;
|
||||
accountData: AccountFields[] | null;
|
||||
accountLoading: boolean;
|
||||
accountError?: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an overview of a party's assets. This uses existing data
|
||||
* providers to structure the details by asset, rather than looking at
|
||||
* it by account. The assumption is that this is a more natural way to
|
||||
* get an idea of the assets and activity of a party.
|
||||
*/
|
||||
export const PartyBlockAccounts = ({
|
||||
partyId,
|
||||
accountData,
|
||||
accountLoading,
|
||||
accountError,
|
||||
}: PartyBlockAccountProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const shouldShowActionButton =
|
||||
accountData && accountData.length > 0 && !accountLoading && !accountError;
|
||||
|
||||
const action = shouldShowActionButton ? (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => navigate(`/${Routes.PARTIES}/${partyId}/assets`)}
|
||||
>
|
||||
{t('Show all')}
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<PartyBlock title={t('Assets')} action={action}>
|
||||
{accountData && accountData.length > 0 ? (
|
||||
<p>
|
||||
{accountData.length} {t('assets, including')}{' '}
|
||||
{accountData
|
||||
.map((a) => a.asset.symbol)
|
||||
.slice(0, 3)
|
||||
.join(', ')}
|
||||
</p>
|
||||
) : accountLoading && !accountError ? (
|
||||
<Loader size="small" />
|
||||
) : accountData && accountData.length === 0 ? (
|
||||
<p>{t('No accounts found')}</p>
|
||||
) : (
|
||||
<p>
|
||||
<Icon className="mr-1" name="error" />
|
||||
<span className="text-sm">{t('Could not load assets')}</span>
|
||||
</p>
|
||||
)}
|
||||
</PartyBlock>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useExplorerPartyAssetsQuery } from '../__generated__/Party-assets';
|
||||
import GovernanceAssetBalance from '../../../../components/asset-balance/governance-asset-balance';
|
||||
import {
|
||||
Icon,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Loader,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { PartyBlock } from './party-block';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export interface PartyBlockStakeProps {
|
||||
partyId: string;
|
||||
accountLoading: boolean;
|
||||
accountError?: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an overview of a single party's staking balance, importantly maintaining'
|
||||
* the same height before and after the details are loaded in.
|
||||
*
|
||||
* Unlike PartyBlockAccounts there is not action button in the title of this block as
|
||||
* there is no page for it to link to currently. That's a future task.
|
||||
*/
|
||||
export const PartyBlockStake = ({
|
||||
partyId,
|
||||
accountLoading,
|
||||
accountError,
|
||||
}: PartyBlockStakeProps) => {
|
||||
const partyRes = useExplorerPartyAssetsQuery({
|
||||
// Don't cache data for this query, party information can move quite quickly
|
||||
fetchPolicy: 'network-only',
|
||||
variables: { partyId: partyId },
|
||||
skip: !partyId,
|
||||
});
|
||||
|
||||
const p = partyRes.data?.partiesConnection?.edges[0].node;
|
||||
|
||||
const linkedLength = p?.stakingSummary?.linkings?.edges?.length;
|
||||
const linkedStake =
|
||||
linkedLength && linkedLength > 0
|
||||
? p?.stakingSummary?.linkings?.edges
|
||||
?.reduce((total, e) => {
|
||||
return new BigNumber(total).plus(
|
||||
new BigNumber(e?.node.amount || 0)
|
||||
);
|
||||
}, new BigNumber(0))
|
||||
.toString()
|
||||
: '0';
|
||||
|
||||
return (
|
||||
<PartyBlock title={t('Staking')}>
|
||||
{p?.stakingSummary.currentStakeAvailable ? (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<div>{t('Available stake')}</div>
|
||||
<div>
|
||||
<GovernanceAssetBalance
|
||||
price={p.stakingSummary.currentStakeAvailable}
|
||||
/>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<div>{t('Active stake')}</div>
|
||||
<div>
|
||||
<GovernanceAssetBalance price={linkedStake || '0'} />
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
) : accountLoading && !accountError ? (
|
||||
<Loader size="small" />
|
||||
) : !accountError ? (
|
||||
<p>{t('No staking balance')}</p>
|
||||
) : (
|
||||
<p>
|
||||
<Icon className="mr-1" name="error" />
|
||||
<span className="text-sm">{t('Could not load stake details')}</span>
|
||||
</p>
|
||||
)}
|
||||
</PartyBlock>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface PartyBlockProps {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function PartyBlock({ children, title, action }: PartyBlockProps) {
|
||||
return (
|
||||
<div className="border-2 min-h-[138px] border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
|
||||
<div
|
||||
className="flex flex-col md:flex-row gap-1 justify-between content-start mb-2"
|
||||
data-testid="page-title"
|
||||
>
|
||||
<h3 className="font-semibold text-lg">{title}</h3>
|
||||
|
||||
{action ? action : null}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,26 @@
|
||||
import { getNodes } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { SubHeading } from '../../../components/sub-heading';
|
||||
import { Panel } from '../../../components/panel';
|
||||
import { toNonHex } from '../../../components/search/detect-search';
|
||||
import { useTxsData } from '../../../hooks/use-txs-data';
|
||||
import { TxsInfiniteList } from '../../../components/txs';
|
||||
import { PageHeader } from '../../../components/page-header';
|
||||
import { useExplorerPartyAssetsQuery } from './__generated__/Party-assets';
|
||||
import type { ExplorerPartyAssetsAccountsFragment } from './__generated__/Party-assets';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
import GovernanceAssetBalance from '../../../components/asset-balance/governance-asset-balance';
|
||||
import { PartyAccounts } from './components/party-accounts';
|
||||
import { Icon, Intent, Notification, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
|
||||
import { PartyBlockStake } from './components/party-block-stake';
|
||||
import { PartyBlockAccounts } from './components/party-block-accounts';
|
||||
import { isValidPartyId } from './components/party-id-error';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
const Party = () => {
|
||||
const { party } = useParams<{ party: string }>();
|
||||
|
||||
useDocumentTitle(['Public keys', party || '-']);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const partyId = toNonHex(party ? party : '');
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||
@@ -28,71 +30,72 @@ const Party = () => {
|
||||
filters,
|
||||
});
|
||||
|
||||
const partyRes = useExplorerPartyAssetsQuery({
|
||||
// Don't cache data for this query, party information can move quite quickly
|
||||
fetchPolicy: 'network-only',
|
||||
variables: { partyId: partyId },
|
||||
skip: !party,
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const {
|
||||
data: AccountData,
|
||||
loading: AccountLoading,
|
||||
error: AccountError,
|
||||
} = useDataProvider({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
variables,
|
||||
});
|
||||
|
||||
const p = partyRes.data?.partiesConnection?.edges[0].node;
|
||||
|
||||
const header = p?.id ? (
|
||||
<PageHeader
|
||||
title={p.id}
|
||||
copy
|
||||
truncateStart={visibleChars}
|
||||
truncateEnd={visibleChars}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<p>No data found for public key {party}</p>
|
||||
</Panel>
|
||||
);
|
||||
|
||||
const staking = (
|
||||
<section>
|
||||
{p?.stakingSummary?.currentStakeAvailable ? (
|
||||
<div className="mt-4 leading-3">
|
||||
<strong className="font-semibold">{t('Staking Balance: ')}</strong>
|
||||
<GovernanceAssetBalance
|
||||
price={p.stakingSummary.currentStakeAvailable}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
|
||||
const accounts = getNodes<ExplorerPartyAssetsAccountsFragment>(
|
||||
p?.accountsConnection
|
||||
);
|
||||
if (!isValidPartyId(partyId)) {
|
||||
return (
|
||||
<div className="max-w-sm mx-auto">
|
||||
<Notification
|
||||
message={t('Invalid party ID')}
|
||||
intent={Intent.Danger}
|
||||
buttonProps={{
|
||||
text: t('Go back'),
|
||||
action: () => navigate(-1),
|
||||
className: 'py-1',
|
||||
size: 'sm',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h1
|
||||
className="font-alpha calt uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
|
||||
data-testid="parties-header"
|
||||
>
|
||||
{t('Public key')}
|
||||
</h1>
|
||||
{partyRes.data ? (
|
||||
<>
|
||||
{header}
|
||||
<SubHeading>{t('Asset data')}</SubHeading>
|
||||
{accounts ? <PartyAccounts accounts={accounts} /> : null}
|
||||
{staking}
|
||||
<PageHeader
|
||||
title={partyId}
|
||||
copy
|
||||
truncateStart={visibleChars}
|
||||
truncateEnd={visibleChars}
|
||||
/>
|
||||
|
||||
<SubHeading>{t('Transactions')}</SubHeading>
|
||||
<TxsInfiniteList
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
areTxsLoading={loading}
|
||||
txs={txsData}
|
||||
loadMoreTxs={loadTxs}
|
||||
error={error}
|
||||
className="mb-28"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<div className="grid md:grid-flow-col grid-flow-row md:space-x-4 grid-cols-1 md:grid-cols-2 w-full">
|
||||
<PartyBlockAccounts
|
||||
accountError={AccountError}
|
||||
accountLoading={AccountLoading}
|
||||
accountData={AccountData}
|
||||
partyId={partyId}
|
||||
/>
|
||||
<PartyBlockStake
|
||||
accountError={AccountError}
|
||||
accountLoading={AccountLoading}
|
||||
partyId={partyId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SubHeading>{t('Transactions')}</SubHeading>
|
||||
{!error && txsData ? (
|
||||
<TxsInfiniteList
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
areTxsLoading={loading}
|
||||
txs={txsData}
|
||||
loadMoreTxs={loadTxs}
|
||||
error={error}
|
||||
className="mb-28"
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
<Icon name="error" className="mr-1" />
|
||||
{t('Could not load transaction list for party')}
|
||||
</Splash>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const PartiesSubPage = () => {
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default PartiesSubPage;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import type { TendermintUnconfirmedTransactionsResponse } from '../txs/tendermint-unconfirmed-transactions-response.d';
|
||||
import { TxList } from '../../components/txs';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
const PendingTxs = () => {
|
||||
const {
|
||||
state: { data: unconfirmedTransactions },
|
||||
} = useFetch<TendermintUnconfirmedTransactionsResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/unconfirmed_txs`
|
||||
);
|
||||
|
||||
useDocumentTitle(['Pending transactions']);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="unconfirmed-transactions-header">
|
||||
{t('Unconfirmed transactions')}
|
||||
</RouteTitle>
|
||||
<br />
|
||||
<div>{t(`Number: ${unconfirmedTransactions?.result?.n_txs || 0}`)}</div>
|
||||
<br />
|
||||
<div>
|
||||
<br />
|
||||
<TxList data={unconfirmedTransactions} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { PendingTxs };
|
||||
@@ -8,35 +8,66 @@ import { Oracle } from './oracles/id';
|
||||
import Party from './parties';
|
||||
import { Parties } from './parties/home';
|
||||
import { Party as PartySingle } from './parties/id';
|
||||
import Txs from './txs';
|
||||
import { ValidatorsPage } from './validators';
|
||||
import Genesis from './genesis';
|
||||
import { Block } from './blocks/id';
|
||||
import { Blocks } from './blocks/home';
|
||||
import { Tx } from './txs/id';
|
||||
import { TxsList } from './txs/home';
|
||||
import { PendingTxs } from './pending';
|
||||
import flags from '../config/flags';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Routes } from './route-names';
|
||||
import { NetworkParameters } from './network-parameters';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import type { Params, RouteObject } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MarketPage, MarketsPage } from './markets';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ErrorBoundary, Layout } from './layout';
|
||||
import compact from 'lodash/compact';
|
||||
import { AssetLink, MarketLink } from '../components/links';
|
||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { PartyAccountsByAsset } from './parties/id/accounts';
|
||||
|
||||
export type Navigable = {
|
||||
path: string;
|
||||
name: string;
|
||||
text: string;
|
||||
handle: {
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
export const isNavigable = (item: RouteObject): item is Navigable =>
|
||||
(item as Navigable).path !== undefined &&
|
||||
(item as Navigable).handle !== undefined &&
|
||||
(item as Navigable).handle.name !== undefined &&
|
||||
(item as Navigable).handle.text !== undefined;
|
||||
|
||||
export type Breadcrumbable = {
|
||||
handle: { breadcrumb: (data?: Params<string>) => ReactNode | string };
|
||||
};
|
||||
export const isBreadcrumbable = (item: RouteObject): item is Breadcrumbable =>
|
||||
(item as Breadcrumbable).handle !== undefined &&
|
||||
(item as Breadcrumbable).handle.breadcrumb !== undefined;
|
||||
|
||||
type RouteItem =
|
||||
| RouteObject
|
||||
| (RouteObject & Navigable)
|
||||
| (RouteObject & Breadcrumbable);
|
||||
type Route = RouteItem & {
|
||||
children?: RouteItem[];
|
||||
};
|
||||
type Route = RouteObject & Navigable;
|
||||
|
||||
const partiesRoutes: Route[] = flags.parties
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
element: <Party />,
|
||||
handle: {
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -44,7 +75,43 @@ const partiesRoutes: Route[] = flags.parties
|
||||
},
|
||||
{
|
||||
path: ':party',
|
||||
element: <PartySingle />,
|
||||
element: <Party />,
|
||||
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PartySingle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
element: <Party />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PartyAccountsByAsset />,
|
||||
handle: {
|
||||
breadcrumb: () => {
|
||||
return t('Assets');
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -55,8 +122,11 @@ const assetsRoutes: Route[] = flags.assets
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
text: t('Assets'),
|
||||
name: t('Assets'),
|
||||
handle: {
|
||||
name: t('Assets'),
|
||||
text: t('Assets'),
|
||||
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -65,6 +135,11 @@ const assetsRoutes: Route[] = flags.assets
|
||||
{
|
||||
path: ':assetId',
|
||||
element: <AssetPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<AssetLink assetId={params.assetId as string} />
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -75,8 +150,13 @@ const genesisRoutes: Route[] = flags.genesis
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
handle: {
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
|
||||
),
|
||||
},
|
||||
element: <Genesis />,
|
||||
},
|
||||
]
|
||||
@@ -86,8 +166,13 @@ const governanceRoutes: Route[] = flags.governance
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
handle: {
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
|
||||
),
|
||||
},
|
||||
element: <Proposals />,
|
||||
},
|
||||
]
|
||||
@@ -97,8 +182,11 @@ const marketsRoutes: Route[] = flags.markets
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
handle: {
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -107,6 +195,11 @@ const marketsRoutes: Route[] = flags.markets
|
||||
{
|
||||
path: ':marketId',
|
||||
element: <MarketPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<MarketLink id={params.marketId as string} />
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -117,8 +210,15 @@ const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
handle: {
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.NETWORK_PARAMETERS}>
|
||||
{t('Network Parameters')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
element: <NetworkParameters />,
|
||||
},
|
||||
]
|
||||
@@ -128,80 +228,122 @@ const validators: Route[] = flags.validators
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
handle: {
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
|
||||
),
|
||||
},
|
||||
element: <ValidatorsPage />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const routerConfig: Route[] = [
|
||||
const linkTo = (...segments: (string | undefined)[]) =>
|
||||
compact(segments).join('/');
|
||||
|
||||
export const routerConfig: Route[] = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
element: <Home />,
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
path: Routes.TX,
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
element: <Txs />,
|
||||
children: [
|
||||
{
|
||||
path: 'pending',
|
||||
element: <PendingTxs />,
|
||||
},
|
||||
{
|
||||
path: ':txHash',
|
||||
element: <Tx />,
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <TxsList />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
element: <BlockPage />,
|
||||
element: <Layout />,
|
||||
handle: {
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
|
||||
},
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Blocks />,
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: ':block',
|
||||
element: <Block />,
|
||||
path: Routes.TX,
|
||||
handle: {
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: ':txHash',
|
||||
element: <Tx />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.TX, params.txHash)}>
|
||||
{truncateMiddle(remove0x(params.txHash as string))}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <TxsList />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
handle: {
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
|
||||
},
|
||||
element: <BlockPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Blocks />,
|
||||
},
|
||||
{
|
||||
path: ':block',
|
||||
element: <Block />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.BLOCKS, params.block)}>
|
||||
{params.block}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
handle: {
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
|
||||
},
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Oracles />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <Oracle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.ORACLES, params.id)}>
|
||||
{truncateMiddle(params.id as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
...partiesRoutes,
|
||||
...assetsRoutes,
|
||||
...genesisRoutes,
|
||||
...governanceRoutes,
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Oracles />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <Oracle />,
|
||||
},
|
||||
],
|
||||
},
|
||||
...partiesRoutes,
|
||||
...assetsRoutes,
|
||||
...genesisRoutes,
|
||||
...governanceRoutes,
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
];
|
||||
|
||||
export default routerConfig;
|
||||
export const router = createBrowserRouter(routerConfig);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
@@ -8,9 +7,6 @@ import { TxDetails } from './tx-details';
|
||||
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
|
||||
import { toNonHex } from '../../../components/search/detect-search';
|
||||
import { PageHeader } from '../../../components/page-header';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
|
||||
const Tx = () => {
|
||||
@@ -35,17 +31,6 @@ const Tx = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Link
|
||||
className="font-normal underline underline-offset-4 block mb-5"
|
||||
to={`/${Routes.TX}`}
|
||||
>
|
||||
<Icon
|
||||
className="text-vega-light-150 dark:text-vega-light-150"
|
||||
name={IconNames.CHEVRON_LEFT}
|
||||
/>
|
||||
All Transactions
|
||||
</Link>
|
||||
|
||||
<PageHeader
|
||||
title="transaction"
|
||||
truncateStart={5}
|
||||
|
||||
@@ -10,7 +10,7 @@ interface TxDetailsProps {
|
||||
|
||||
export const txDetailsTruncateLength = 30;
|
||||
|
||||
export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
|
||||
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from '@vegaprotocol/environment';
|
||||
import classNames from 'classnames';
|
||||
import { NodeStatus, NodeStatusMapping } from '@vegaprotocol/types';
|
||||
import { PartyLink } from '../../components/links';
|
||||
|
||||
type RateProps = {
|
||||
value: BigNumber | number | undefined;
|
||||
@@ -191,7 +192,9 @@ export const ValidatorsPage = () => {
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('ID')}</div>
|
||||
<div className="break-all text-xs">{v.id}</div>
|
||||
<div className="break-all text-xs font-mono">
|
||||
{v.id}
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Status')}</div>
|
||||
@@ -218,12 +221,14 @@ export const ValidatorsPage = () => {
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Public key')}</div>
|
||||
<div className="break-all text-xs">{v.pubkey}</div>
|
||||
<div>{t('Key')}</div>
|
||||
<div className="break-all text-xs">
|
||||
<PartyLink id={v.pubkey} />
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Ethereum address')}</div>
|
||||
<div className="break-all text-xs">
|
||||
<div className="break-all text-xs font-mono">
|
||||
<EtherscanLink address={v.ethereumAddress} />{' '}
|
||||
<CopyWithTooltip text={v.ethereumAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
@@ -234,7 +239,9 @@ export const ValidatorsPage = () => {
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Tendermint public key')}</div>
|
||||
<div className="break-all text-xs">{v.tmPubkey}</div>
|
||||
<div className="break-all text-xs font-mono">
|
||||
{v.tmPubkey}
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
|
||||
<KeyValueTableRow>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './styles.css';
|
||||
|
||||
import App from './app/app';
|
||||
@@ -10,8 +9,6 @@ const root = rootElement && createRoot(rootElement);
|
||||
|
||||
root?.render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@import 'ag-grid-community/dist/styles/ag-grid.css';
|
||||
@import 'ag-grid-community/dist/styles/ag-theme-balham.css';
|
||||
@import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css';
|
||||
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -11,3 +15,48 @@
|
||||
.react-markdown-container a:before {
|
||||
content: '🔗 ';
|
||||
}
|
||||
|
||||
/* AG GRID - Do not edit without updating other global stylesheets for each app */
|
||||
|
||||
.vega-ag-grid .ag-root-wrapper {
|
||||
border: solid 0px;
|
||||
}
|
||||
|
||||
.vega-ag-grid .ag-react-container {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vega-ag-grid .ag-cell,
|
||||
.vega-ag-grid .ag-full-width-row .ag-cell-wrapper.ag-row-group {
|
||||
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
|
||||
}
|
||||
|
||||
.vega-ag-grid .ag-row {
|
||||
border-width: 1px 0;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* Light variables */
|
||||
.ag-theme-balham {
|
||||
--ag-background-color: theme(colors.white);
|
||||
--ag-border-color: theme(colors.neutral[300]);
|
||||
--ag-header-background-color: theme(colors.white);
|
||||
--ag-odd-row-background-color: theme(colors.white);
|
||||
--ag-header-column-separator-color: theme(colors.neutral[300]);
|
||||
--ag-row-border-color: theme(colors.white);
|
||||
--ag-row-hover-color: theme(colors.neutral[100]);
|
||||
--ag-font-size: 12px;
|
||||
}
|
||||
|
||||
/* Dark variables */
|
||||
.ag-theme-balham-dark {
|
||||
--ag-background-color: theme(colors.black);
|
||||
--ag-border-color: theme(colors.neutral[700]);
|
||||
--ag-header-background-color: theme(colors.black);
|
||||
--ag-odd-row-background-color: theme(colors.black);
|
||||
--ag-header-column-separator-color: theme(colors.neutral[600]);
|
||||
--ag-row-border-color: theme(colors.black);
|
||||
--ag-row-hover-color: theme(colors.neutral[800]);
|
||||
--ag-font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,22 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_FAIRGROUND=false
|
||||
NX_VEGA_NETWORKS={}
|
||||
|
||||
NX_VEGA_URL=http://localhost:3028/query
|
||||
NX_VEGA_URL=http://localhost:3008/graphql
|
||||
NX_ETHEREUM_CHAIN_ID=1440
|
||||
NX_ETH_URL_CONNECT=1
|
||||
NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
|
||||
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
CYPRESS_VEGA_URL=http://localhost:3028/query
|
||||
CYPRESS_VEGA_URL=http://localhost:3008/graphql
|
||||
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
|
||||
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
|
||||
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
@@ -27,6 +31,5 @@ CYPRESS_VEGA_ENV=CUSTOM
|
||||
CYPRESS_VEGA_PUBLIC_KEY=02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65
|
||||
CYPRESS_VEGA_PUBLIC_KEY2=7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535
|
||||
CYPRESS_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
CYPRESS_VEGA_URL=http://localhost:3028/query
|
||||
CYPRESS_VEGA_WALLET_URL=http://localhost:1789
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=MAINNET
|
||||
NX_VEGA_URL=https://api.vega.xyz/query
|
||||
NX_VEGA_URL=https://api.vega.community/graphql
|
||||
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user