Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo
This commit is contained in:
commit
e9c1668da1
@ -12,7 +12,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GO111MODULE: 'on'
|
GO111MODULE: 'on'
|
||||||
GOPROXY: ${{ secrets.GO_PROXY }}
|
|
||||||
steps:
|
steps:
|
||||||
#######
|
#######
|
||||||
## Setup langs
|
## Setup langs
|
||||||
|
1
.github/workflows/capsule-cypress.yml
vendored
1
.github/workflows/capsule-cypress.yml
vendored
@ -14,7 +14,6 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
env:
|
env:
|
||||||
GO111MODULE: 'on'
|
GO111MODULE: 'on'
|
||||||
GOPROXY: ${{ secrets.GO_PROXY }}
|
|
||||||
steps:
|
steps:
|
||||||
#######
|
#######
|
||||||
## Setup langs
|
## Setup langs
|
||||||
|
@ -15,6 +15,8 @@ module.exports = defineConfig({
|
|||||||
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
||||||
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
environment: 'CUSTOM',
|
environment: 'CUSTOM',
|
||||||
|
@ -15,5 +15,7 @@ module.exports = defineConfig({
|
|||||||
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
||||||
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -42,8 +42,8 @@ describe('market selector', () => {
|
|||||||
.find('[role="button"]')
|
.find('[role="button"]')
|
||||||
.should('have.length', 1);
|
.should('have.length', 1);
|
||||||
cy.get('input[placeholder="Search"]').clear();
|
cy.get('input[placeholder="Search"]').clear();
|
||||||
cy.get('input[placeholder="Search"]').type('a');
|
cy.get('input[placeholder="Search"]').type('app');
|
||||||
const filtered = markets.filter((market) => market.name.match(/a/i));
|
const filtered = markets.filter((market) => market.name.match(/app/i));
|
||||||
cy.getByTestId('market-pane')
|
cy.getByTestId('market-pane')
|
||||||
.children()
|
.children()
|
||||||
.find('[role="button"]')
|
.find('[role="button"]')
|
||||||
@ -76,7 +76,6 @@ describe('market selector', () => {
|
|||||||
.children()
|
.children()
|
||||||
.find('[role="button"]')
|
.find('[role="button"]')
|
||||||
.should('have.length', markets.length);
|
.should('have.length', markets.length);
|
||||||
cy.pause();
|
|
||||||
cy.getByTestId('dialog-close').click();
|
cy.getByTestId('dialog-close').click();
|
||||||
cy.get('input[placeholder="Search"]').should(
|
cy.get('input[placeholder="Search"]').should(
|
||||||
'have.value',
|
'have.value',
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||||
"total_added": "129999.45",
|
"total_added": "129999.45",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "120687.60294909203186805",
|
"locked_amount": "119916.524711292323769405",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "129999.45",
|
"amount": "129999.45",
|
||||||
@ -521,7 +521,7 @@
|
|||||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||||
"total_added": "97499.58",
|
"total_added": "97499.58",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "61605.4975572212290407",
|
"locked_amount": "60849.141635126632187184",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "97499.58",
|
"amount": "97499.58",
|
||||||
@ -554,7 +554,7 @@
|
|||||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||||
"total_added": "135173.4239508",
|
"total_added": "135173.4239508",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "84204.0794152667057160273426",
|
"locked_amount": "83170.27144916586454715062596",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "135173.4239508",
|
"amount": "135173.4239508",
|
||||||
@ -587,7 +587,7 @@
|
|||||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||||
"total_added": "32499.86",
|
"total_added": "32499.86",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "25916.368952284834442322",
|
"locked_amount": "25598.183077427765580772",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "32499.86",
|
"amount": "32499.86",
|
||||||
@ -620,7 +620,7 @@
|
|||||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||||
"total_added": "10833.29",
|
"total_added": "10833.29",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "8435.52660747719195897",
|
"locked_amount": "8331.960192813861431555",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "10833.29",
|
"amount": "10833.29",
|
||||||
@ -708,7 +708,7 @@
|
|||||||
"tranche_end": "2022-11-01T00:00:00.000Z",
|
"tranche_end": "2022-11-01T00:00:00.000Z",
|
||||||
"total_added": "22500",
|
"total_added": "22500",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "13420.127094655797",
|
"locked_amount": "13022.659080615942",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "15000",
|
"amount": "15000",
|
||||||
@ -794,7 +794,7 @@
|
|||||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||||
"total_added": "1939928.38",
|
"total_added": "1939928.38",
|
||||||
"total_removed": "179856.049568108351",
|
"total_removed": "179856.049568108351",
|
||||||
"locked_amount": "1715358.109875174368620276",
|
"locked_amount": "1698082.621969374507314448",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "1852091.69",
|
"amount": "1852091.69",
|
||||||
@ -1846,7 +1846,7 @@
|
|||||||
"tranche_end": "2022-09-30T00:00:00.000Z",
|
"tranche_end": "2022-09-30T00:00:00.000Z",
|
||||||
"total_added": "60916.66666633337",
|
"total_added": "60916.66666633337",
|
||||||
"total_removed": "19238.601152747179372649",
|
"total_removed": "19238.601152747179372649",
|
||||||
"locked_amount": "12143.787034497502163478850106205",
|
"locked_amount": "11636.0848715333587885626384956186",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "2833.333333",
|
"amount": "2833.333333",
|
||||||
@ -3330,8 +3330,8 @@
|
|||||||
"tranche_id": 10,
|
"tranche_id": 10,
|
||||||
"tranche_start": "2021-07-15T23:37:11.000Z",
|
"tranche_start": "2021-07-15T23:37:11.000Z",
|
||||||
"tranche_end": "2021-07-15T23:37:11.000Z",
|
"tranche_end": "2021-07-15T23:37:11.000Z",
|
||||||
"total_added": "3904968.150000000000000001",
|
"total_added": "4170968.150000000000000001",
|
||||||
"total_removed": "3896778.640000000000000001",
|
"total_removed": "4162778.640000000000000001",
|
||||||
"locked_amount": "0",
|
"locked_amount": "0",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
@ -3339,6 +3339,11 @@
|
|||||||
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
"tx": "0x167fe6c654897398dcfdab9305133b4410b15bac8f5a30bb1260a94a14a65aad"
|
"tx": "0x167fe6c654897398dcfdab9305133b4410b15bac8f5a30bb1260a94a14a65aad"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "266000",
|
||||||
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
|
"tx": "0x38ac30c041395b629839409fb10385426997cf40be0e3026dd726e7c77e019cc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "30000",
|
"amount": "30000",
|
||||||
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
@ -3786,6 +3791,11 @@
|
|||||||
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
"tx": "0x873061bbe5702251e087833b87033296fef9c6fb4f6c2be7586e1e74e20a4f52"
|
"tx": "0x873061bbe5702251e087833b87033296fef9c6fb4f6c2be7586e1e74e20a4f52"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "266000",
|
||||||
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
|
"tx": "0x5b5515073f03593ae5cddd66030154af5847aedbd680835bbc1998a168e74699"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "30000",
|
"amount": "30000",
|
||||||
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
@ -4177,6 +4187,12 @@
|
|||||||
"tranche_id": 10,
|
"tranche_id": 10,
|
||||||
"tx": "0x167fe6c654897398dcfdab9305133b4410b15bac8f5a30bb1260a94a14a65aad"
|
"tx": "0x167fe6c654897398dcfdab9305133b4410b15bac8f5a30bb1260a94a14a65aad"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "266000",
|
||||||
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
|
"tranche_id": 10,
|
||||||
|
"tx": "0x38ac30c041395b629839409fb10385426997cf40be0e3026dd726e7c77e019cc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "30000",
|
"amount": "30000",
|
||||||
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
@ -4281,6 +4297,12 @@
|
|||||||
"tranche_id": 10,
|
"tranche_id": 10,
|
||||||
"tx": "0x873061bbe5702251e087833b87033296fef9c6fb4f6c2be7586e1e74e20a4f52"
|
"tx": "0x873061bbe5702251e087833b87033296fef9c6fb4f6c2be7586e1e74e20a4f52"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "266000",
|
||||||
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
|
"tranche_id": 10,
|
||||||
|
"tx": "0x5b5515073f03593ae5cddd66030154af5847aedbd680835bbc1998a168e74699"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "30000",
|
"amount": "30000",
|
||||||
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
|
||||||
@ -4372,8 +4394,8 @@
|
|||||||
"tx": "0xac16a4ce688d40a482a59914d68c3a676592f8804ee8f0781b66a4ba5ccfbdfc"
|
"tx": "0xac16a4ce688d40a482a59914d68c3a676592f8804ee8f0781b66a4ba5ccfbdfc"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "745651",
|
"total_tokens": "1011651",
|
||||||
"withdrawn_tokens": "745651",
|
"withdrawn_tokens": "1011651",
|
||||||
"remaining_tokens": "0"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -5393,9 +5415,9 @@
|
|||||||
"tranche_id": 11,
|
"tranche_id": 11,
|
||||||
"tranche_start": "2021-09-03T00:00:00.000Z",
|
"tranche_start": "2021-09-03T00:00:00.000Z",
|
||||||
"tranche_end": "2022-09-03T00:00:00.000Z",
|
"tranche_end": "2022-09-03T00:00:00.000Z",
|
||||||
"total_added": "24284.000000000000000003",
|
"total_added": "24801.000000000000000003",
|
||||||
"total_removed": "6875.88980798578",
|
"total_removed": "7424.31284562443",
|
||||||
"locked_amount": "3376.2614414003058740004170970319634705",
|
"locked_amount": "3227.28360254946787092039038146879756476",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "25",
|
"amount": "25",
|
||||||
@ -5862,6 +5884,41 @@
|
|||||||
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||||
"tx": "0x8e664532cfa2031eb5f6ce7c99cd673fa767599654a35b73d76407ed46763ad4"
|
"tx": "0x8e664532cfa2031eb5f6ce7c99cd673fa767599654a35b73d76407ed46763ad4"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "60",
|
||||||
|
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||||
|
"tx": "0x23fdff0f268bba36817cb496454cbc4be34306eaaaf330d5d26134849c480153"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "200",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tx": "0xf88b8ed5ce899f00f466830936599112ea678a4e5911801fb3e445c22eb0295f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "100",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tx": "0x0c0ea0139cc752d22c0f8a6b3ffa5c4bcdca1c5604089ebd0f34898f1049db61"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "90",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tx": "0x29371816157718f7d5136d0ed8a0f1e09045ee3cfcd67199d45e3f4157f7e011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "35",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tx": "0x6a081b2c5dfa2d5c916a598569519605322721c5511d26cf4aed8e08e49cf700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "30",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tx": "0xc37be614ef14bbbedc808ffc7651fbb7a3c04e4d1612f00f19299da7916003e6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "2",
|
||||||
|
"user": "0x667b28dAAe318F230aA319cf06fa74c050151903",
|
||||||
|
"tx": "0x46c9ef536fda8271cf157260c8ad6c67c058c5dddef03fdc1514a5d68212fcda"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "75",
|
"amount": "75",
|
||||||
"user": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
"user": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
||||||
@ -9084,6 +9141,11 @@
|
|||||||
"user": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
"user": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
||||||
"tx": "0x4c4e0fd7a8adbad24d6cc6e8fdf4f832ac9a39b2a04b954ec0d77e36a79c0e90"
|
"tx": "0x4c4e0fd7a8adbad24d6cc6e8fdf4f832ac9a39b2a04b954ec0d77e36a79c0e90"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "548.42303763865",
|
||||||
|
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||||
|
"tx": "0x48c16186c1e0021ee4b7194df991c93ec3a26971d009a9a3aaaec67c0abc89e9"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "4.429693048",
|
"amount": "4.429693048",
|
||||||
"user": "0xc5467213593778E528f0eB8117cc7AFBC5b7491b",
|
"user": "0xc5467213593778E528f0eB8117cc7AFBC5b7491b",
|
||||||
@ -10086,12 +10148,79 @@
|
|||||||
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||||
"tranche_id": 11,
|
"tranche_id": 11,
|
||||||
"tx": "0x8e664532cfa2031eb5f6ce7c99cd673fa767599654a35b73d76407ed46763ad4"
|
"tx": "0x8e664532cfa2031eb5f6ce7c99cd673fa767599654a35b73d76407ed46763ad4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "60",
|
||||||
|
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0x23fdff0f268bba36817cb496454cbc4be34306eaaaf330d5d26134849c480153"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "548.42303763865",
|
||||||
|
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0x48c16186c1e0021ee4b7194df991c93ec3a26971d009a9a3aaaec67c0abc89e9"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_tokens": "635",
|
||||||
|
"withdrawn_tokens": "548.42303763865",
|
||||||
|
"remaining_tokens": "86.57696236135"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"deposits": [
|
||||||
|
{
|
||||||
|
"amount": "200",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0xf88b8ed5ce899f00f466830936599112ea678a4e5911801fb3e445c22eb0295f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "100",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0x0c0ea0139cc752d22c0f8a6b3ffa5c4bcdca1c5604089ebd0f34898f1049db61"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "90",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0x29371816157718f7d5136d0ed8a0f1e09045ee3cfcd67199d45e3f4157f7e011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "35",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0x6a081b2c5dfa2d5c916a598569519605322721c5511d26cf4aed8e08e49cf700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "30",
|
||||||
|
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0xc37be614ef14bbbedc808ffc7651fbb7a3c04e4d1612f00f19299da7916003e6"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [],
|
"withdrawals": [],
|
||||||
"total_tokens": "575",
|
"total_tokens": "455",
|
||||||
"withdrawn_tokens": "0",
|
"withdrawn_tokens": "0",
|
||||||
"remaining_tokens": "575"
|
"remaining_tokens": "455"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "0x667b28dAAe318F230aA319cf06fa74c050151903",
|
||||||
|
"deposits": [
|
||||||
|
{
|
||||||
|
"amount": "2",
|
||||||
|
"user": "0x667b28dAAe318F230aA319cf06fa74c050151903",
|
||||||
|
"tranche_id": 11,
|
||||||
|
"tx": "0x46c9ef536fda8271cf157260c8ad6c67c058c5dddef03fdc1514a5d68212fcda"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"withdrawals": [],
|
||||||
|
"total_tokens": "2",
|
||||||
|
"withdrawn_tokens": "0",
|
||||||
|
"remaining_tokens": "2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
"address": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
||||||
@ -15925,7 +16054,7 @@
|
|||||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||||
"total_added": "3732368.4671",
|
"total_added": "3732368.4671",
|
||||||
"total_removed": "106452.6400159857469",
|
"total_removed": "106452.6400159857469",
|
||||||
"locked_amount": "2660409.42081862296035011689",
|
"locked_amount": "2633863.10512018801324199073",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "1998.95815",
|
"amount": "1998.95815",
|
||||||
@ -16659,8 +16788,8 @@
|
|||||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||||
"total_added": "15788853.065470999700000001",
|
"total_added": "15788853.065470999700000001",
|
||||||
"total_removed": "62889.84066484668888",
|
"total_removed": "64322.360020714867605",
|
||||||
"locked_amount": "14657898.8329277585456883041909973431305758",
|
"locked_amount": "14564249.2240429993620865617393038380967829",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "16249.93",
|
"amount": "16249.93",
|
||||||
@ -17209,6 +17338,21 @@
|
|||||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
"tx": "0x148f389a5771abdc96c5a51c1fc0e306d135763c60f6839db6763690022a1a36"
|
"tx": "0x148f389a5771abdc96c5a51c1fc0e306d135763c60f6839db6763690022a1a36"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "398.806145147265375",
|
||||||
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
|
"tx": "0x4961efbe6ce942c38f74c5cbf8a5c7f8f6586c1bda1df0a17c7a1fd80aae6905"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "693.68209419841605",
|
||||||
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
|
"tx": "0x1b34e389cc5a8530434dd873000f7b9f6289f08b21e9285601897ca428a72e5b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "340.0311165224973",
|
||||||
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
|
"tx": "0x85229baef1e2ddcb37b92221d742c81c70e47d34002d6eaa8fb7c7048e9119d9"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "579.636872035866225",
|
"amount": "579.636872035866225",
|
||||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
@ -17411,6 +17555,24 @@
|
|||||||
"tranche_id": 2,
|
"tranche_id": 2,
|
||||||
"tx": "0x148f389a5771abdc96c5a51c1fc0e306d135763c60f6839db6763690022a1a36"
|
"tx": "0x148f389a5771abdc96c5a51c1fc0e306d135763c60f6839db6763690022a1a36"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "398.806145147265375",
|
||||||
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
|
"tranche_id": 2,
|
||||||
|
"tx": "0x4961efbe6ce942c38f74c5cbf8a5c7f8f6586c1bda1df0a17c7a1fd80aae6905"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "693.68209419841605",
|
||||||
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
|
"tranche_id": 2,
|
||||||
|
"tx": "0x1b34e389cc5a8530434dd873000f7b9f6289f08b21e9285601897ca428a72e5b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "340.0311165224973",
|
||||||
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
|
"tranche_id": 2,
|
||||||
|
"tx": "0x85229baef1e2ddcb37b92221d742c81c70e47d34002d6eaa8fb7c7048e9119d9"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "579.636872035866225",
|
"amount": "579.636872035866225",
|
||||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||||
@ -17551,8 +17713,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "194999.1675",
|
"total_tokens": "194999.1675",
|
||||||
"withdrawn_tokens": "13652.2521999360072",
|
"withdrawn_tokens": "15084.771555804185925",
|
||||||
"remaining_tokens": "181346.9153000639928"
|
"remaining_tokens": "179914.395944195814075"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||||
@ -18991,8 +19153,8 @@
|
|||||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||||
"total_added": "14597706.0446472999",
|
"total_added": "14597706.0446472999",
|
||||||
"total_removed": "2413723.131389663831937297",
|
"total_removed": "2416064.425425840477256547",
|
||||||
"locked_amount": "7880269.64303812390931654596122858",
|
"locked_amount": "7793368.01462647593665331865886541",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "129284.449",
|
"amount": "129284.449",
|
||||||
@ -19276,6 +19438,26 @@
|
|||||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
"tx": "0x5b2ba568e3f60b624456972f011948b6870274b94ddba2f11eb876b4e875be1a"
|
"tx": "0x5b2ba568e3f60b624456972f011948b6870274b94ddba2f11eb876b4e875be1a"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "430.6491357418236805",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tx": "0x63919ba0a519b04dfe5a5b222e5952117f1cdbb1e6194ae85b22a4ed8cef6fa5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "586.7623058017099795",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tx": "0xd2d49b3e73cd00446605bc1838132e4a985c89452600447ec1886cd64f54c200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "694.13150009555900275",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tx": "0xc541f5d785515b5ebd43f50aeb4bb972ae35998638f1c803f46aaab7810ace7e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "629.7510945375526565",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tx": "0xd3b5acda2db6f999875f4a8c747cb0f711cd4fc4b183c2d05618822d7b988237"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "509.31853983395369725",
|
"amount": "509.31853983395369725",
|
||||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
@ -21004,6 +21186,30 @@
|
|||||||
"tranche_id": 3,
|
"tranche_id": 3,
|
||||||
"tx": "0x5b2ba568e3f60b624456972f011948b6870274b94ddba2f11eb876b4e875be1a"
|
"tx": "0x5b2ba568e3f60b624456972f011948b6870274b94ddba2f11eb876b4e875be1a"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "430.6491357418236805",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tranche_id": 3,
|
||||||
|
"tx": "0x63919ba0a519b04dfe5a5b222e5952117f1cdbb1e6194ae85b22a4ed8cef6fa5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "586.7623058017099795",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tranche_id": 3,
|
||||||
|
"tx": "0xd2d49b3e73cd00446605bc1838132e4a985c89452600447ec1886cd64f54c200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "694.13150009555900275",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tranche_id": 3,
|
||||||
|
"tx": "0xc541f5d785515b5ebd43f50aeb4bb972ae35998638f1c803f46aaab7810ace7e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "629.7510945375526565",
|
||||||
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
|
"tranche_id": 3,
|
||||||
|
"tx": "0xd3b5acda2db6f999875f4a8c747cb0f711cd4fc4b183c2d05618822d7b988237"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "509.31853983395369725",
|
"amount": "509.31853983395369725",
|
||||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||||
@ -22350,8 +22556,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "359123.469575",
|
"total_tokens": "359123.469575",
|
||||||
"withdrawn_tokens": "164980.154441085289241",
|
"withdrawn_tokens": "167321.44847726193456025",
|
||||||
"remaining_tokens": "194143.315133914710759"
|
"remaining_tokens": "191802.02109773806543975"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||||
@ -23414,8 +23620,8 @@
|
|||||||
"tranche_start": "2021-10-05T00:00:00.000Z",
|
"tranche_start": "2021-10-05T00:00:00.000Z",
|
||||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||||
"total_added": "5778205.3912159303",
|
"total_added": "5778205.3912159303",
|
||||||
"total_removed": "1543188.041113165491270727",
|
"total_removed": "1572374.955637071973431516",
|
||||||
"locked_amount": "2796638.79189818404440572191528324",
|
"locked_amount": "2762303.432936069128726079669817045",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "552496.6455",
|
"amount": "552496.6455",
|
||||||
@ -23569,6 +23775,16 @@
|
|||||||
"user": "0xafa64cCa337eFEE0AD827F6C2684e69275226e90",
|
"user": "0xafa64cCa337eFEE0AD827F6C2684e69275226e90",
|
||||||
"tx": "0x9c4cfa505550e13a41c2eb84a767412539ccae45bb280b5235423aadce942f2e"
|
"tx": "0x9c4cfa505550e13a41c2eb84a767412539ccae45bb280b5235423aadce942f2e"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "4864.10259512983202665",
|
||||||
|
"user": "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D",
|
||||||
|
"tx": "0x36fed3740bfbe979fd84ff55a1f65310eaf83047b27a9dc5516e083acfcc3b3f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "24322.811928776650134139",
|
||||||
|
"user": "0xC24da173A250e9Ca5c54870639EbE5f88be5102d",
|
||||||
|
"tx": "0xc391f34bee2ee3186396191a8828e498e817ec73ebde827b525322561d6c10ac"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "4089.266768653121589",
|
"amount": "4089.266768653121589",
|
||||||
"user": "0xafa64cCa337eFEE0AD827F6C2684e69275226e90",
|
"user": "0xafa64cCa337eFEE0AD827F6C2684e69275226e90",
|
||||||
@ -24191,6 +24407,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "4864.10259512983202665",
|
||||||
|
"user": "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D",
|
||||||
|
"tranche_id": 4,
|
||||||
|
"tx": "0x36fed3740bfbe979fd84ff55a1f65310eaf83047b27a9dc5516e083acfcc3b3f"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "3536.43115201266299695",
|
"amount": "3536.43115201266299695",
|
||||||
"user": "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D",
|
"user": "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D",
|
||||||
@ -24223,8 +24445,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "92082.572555",
|
"total_tokens": "92082.572555",
|
||||||
"withdrawn_tokens": "42713.498699646151249",
|
"withdrawn_tokens": "47577.60129477598327565",
|
||||||
"remaining_tokens": "49369.073855353848751"
|
"remaining_tokens": "44504.97126022401672435"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x1dC9B91DE003fd503F25cB5d114cf0fc68F7aFe6",
|
"address": "0x1dC9B91DE003fd503F25cB5d114cf0fc68F7aFe6",
|
||||||
@ -24252,6 +24474,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "24322.811928776650134139",
|
||||||
|
"user": "0xC24da173A250e9Ca5c54870639EbE5f88be5102d",
|
||||||
|
"tranche_id": 4,
|
||||||
|
"tx": "0xc391f34bee2ee3186396191a8828e498e817ec73ebde827b525322561d6c10ac"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "17683.07845319664597116",
|
"amount": "17683.07845319664597116",
|
||||||
"user": "0xC24da173A250e9Ca5c54870639EbE5f88be5102d",
|
"user": "0xC24da173A250e9Ca5c54870639EbE5f88be5102d",
|
||||||
@ -24290,8 +24518,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "460415.072915097",
|
"total_tokens": "460415.072915097",
|
||||||
"withdrawn_tokens": "213567.593206108177102902",
|
"withdrawn_tokens": "237890.405134884827237041",
|
||||||
"remaining_tokens": "246847.479708988822897098"
|
"remaining_tokens": "222524.667780212172762959"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0xd66e4853c0880df150e7329974715BFC8d2da47D",
|
"address": "0xd66e4853c0880df150e7329974715BFC8d2da47D",
|
||||||
@ -24615,7 +24843,7 @@
|
|||||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||||
"total_added": "472355.6199999996",
|
"total_added": "472355.6199999996",
|
||||||
"total_removed": "279.2296426169",
|
"total_removed": "279.2296426169",
|
||||||
"locked_amount": "421557.080277297961550599391172",
|
"locked_amount": "417350.6648096711466747044799594",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "3000",
|
"amount": "3000",
|
||||||
@ -50413,7 +50641,7 @@
|
|||||||
"tranche_start": "2021-12-05T00:00:00.000Z",
|
"tranche_start": "2021-12-05T00:00:00.000Z",
|
||||||
"tranche_end": "2022-06-05T00:00:00.000Z",
|
"tranche_end": "2022-06-05T00:00:00.000Z",
|
||||||
"total_added": "171288.42",
|
"total_added": "171288.42",
|
||||||
"total_removed": "36329.8823647506377",
|
"total_removed": "38047.7065343307989",
|
||||||
"locked_amount": "0",
|
"locked_amount": "0",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
@ -54678,6 +54906,61 @@
|
|||||||
"user": "0x5cce57661D758497446B57592D31667aafd60465",
|
"user": "0x5cce57661D758497446B57592D31667aafd60465",
|
||||||
"tx": "0x8d9b31eaaecaa4f74f05f93d33e61d0691eba3a6f23a9033dbfcaeca1c9a00c4"
|
"tx": "0x8d9b31eaaecaa4f74f05f93d33e61d0691eba3a6f23a9033dbfcaeca1c9a00c4"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0x57a88037954e15AFFDf9bc29097e0B5f5A1BfF35",
|
||||||
|
"tx": "0x3da3cd5b323439c576ad7a3410d833da0da994ee652b29dd751a4c0edbf0ca99"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0x5f3Bce4B242d00ED748d48172C1f2D47A0bcB19B",
|
||||||
|
"tx": "0xc06577f7fd53a100da581edbc8231a870d7b148329c8aa250eb942473004e5e6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "22.830993081",
|
||||||
|
"user": "0xe627552C6338855983a147fa5235E5D92a3C2891",
|
||||||
|
"tx": "0x3c75d0f2db07e4b2c54411e3d1a6124e63bc0f99ca4d171bd8679158aee3e698"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "117.3375178075",
|
||||||
|
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
||||||
|
"tx": "0x6d40af7a502c2fb843a6bdd7313a5c66811e62cadc5a75521aa49d886635634d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "116.827485434478",
|
||||||
|
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||||
|
"tx": "0x04b50ec6c4ff0f0cc3d9f2b500e6e91e920c901c98241d4ea595c03dfef12dfb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "117.344131565",
|
||||||
|
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||||
|
"tx": "0xe297545c0eb18382b44e8bc9fdd1fb78db661757843ef2f5513d980a470ed873"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "116.1440755221832",
|
||||||
|
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||||
|
"tx": "0xa789793e6cad59188752a73b219c335d19aaf5a2c7199edf45db321a92a6c0c9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "117.33996617",
|
||||||
|
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||||
|
"tx": "0x69b4d26b8ecf677bc94a8b715fd4c7f56cfb535f50cb8cbf01f06520917cf97f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "110",
|
||||||
|
"user": "0x09a4E26d69821EDEffa5Eb29f134D0d8cad8EfC4",
|
||||||
|
"tx": "0xaf4d41f0ec93d00ba98766ffea1806375add6f99baf5a3f71033a78afcc8ffb2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0xcF3c43cFF5C96FE9B65FE35dD2e7BF49DB66F1fE",
|
||||||
|
"tx": "0xf368a5e2e5cd7bcbfbbec26d2620397acf344da7363d34c845d35b8e0037e7bd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0xC41d8caeD946723bFc670FCc837C8473437982F7",
|
||||||
|
"tx": "0x7169697982aca05b9a8e121d65c96458d745788f7e127523160a8803f3063626"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "60.4448387275",
|
"amount": "60.4448387275",
|
||||||
"user": "0xEe3183EcE9ee7d73Fb7bA7F4eB262A2dE68C42B0",
|
"user": "0xEe3183EcE9ee7d73Fb7bA7F4eB262A2dE68C42B0",
|
||||||
@ -56736,6 +57019,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "22.830993081",
|
||||||
|
"user": "0xe627552C6338855983a147fa5235E5D92a3C2891",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0x3c75d0f2db07e4b2c54411e3d1a6124e63bc0f99ca4d171bd8679158aee3e698"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "24.17784646",
|
"amount": "24.17784646",
|
||||||
"user": "0xe627552C6338855983a147fa5235E5D92a3C2891",
|
"user": "0xe627552C6338855983a147fa5235E5D92a3C2891",
|
||||||
@ -56756,8 +57045,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "100",
|
"total_tokens": "100",
|
||||||
"withdrawn_tokens": "77.169006919",
|
"withdrawn_tokens": "100",
|
||||||
"remaining_tokens": "22.830993081"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x2394462d77c51A174D912E229d426917051af04F",
|
"address": "0x2394462d77c51A174D912E229d426917051af04F",
|
||||||
@ -62204,10 +62493,17 @@
|
|||||||
"tx": "0x057f65938b1360b6d2c4ebf5e789c67897cf60cb6a13f947004def03891afbd8"
|
"tx": "0x057f65938b1360b6d2c4ebf5e789c67897cf60cb6a13f947004def03891afbd8"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [],
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "110",
|
||||||
|
"user": "0x09a4E26d69821EDEffa5Eb29f134D0d8cad8EfC4",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0xaf4d41f0ec93d00ba98766ffea1806375add6f99baf5a3f71033a78afcc8ffb2"
|
||||||
|
}
|
||||||
|
],
|
||||||
"total_tokens": "110",
|
"total_tokens": "110",
|
||||||
"withdrawn_tokens": "0",
|
"withdrawn_tokens": "110",
|
||||||
"remaining_tokens": "110"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0xc25d35024Dd497D3825115828994Bb08D12a3aa7",
|
"address": "0xc25d35024Dd497D3825115828994Bb08D12a3aa7",
|
||||||
@ -65914,10 +66210,17 @@
|
|||||||
"tx": "0xb1425d9b0d5f10c5b06236d6f9d61ebddd21c4027e0f41deb5ec227a1753c715"
|
"tx": "0xb1425d9b0d5f10c5b06236d6f9d61ebddd21c4027e0f41deb5ec227a1753c715"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [],
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0xcF3c43cFF5C96FE9B65FE35dD2e7BF49DB66F1fE",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0xf368a5e2e5cd7bcbfbbec26d2620397acf344da7363d34c845d35b8e0037e7bd"
|
||||||
|
}
|
||||||
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "0",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "250"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x3db215C9a674496882ed88FfEdAAa477Fdd92644",
|
"address": "0x3db215C9a674496882ed88FfEdAAa477Fdd92644",
|
||||||
@ -66197,6 +66500,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "117.3375178075",
|
||||||
|
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0x6d40af7a502c2fb843a6bdd7313a5c66811e62cadc5a75521aa49d886635634d"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "132.6624821925",
|
"amount": "132.6624821925",
|
||||||
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
||||||
@ -66205,8 +66514,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "132.6624821925",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "117.3375178075"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
"address": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||||
@ -66219,6 +66528,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "117.33996617",
|
||||||
|
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0x69b4d26b8ecf677bc94a8b715fd4c7f56cfb535f50cb8cbf01f06520917cf97f"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "132.66003383",
|
"amount": "132.66003383",
|
||||||
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||||
@ -66227,8 +66542,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "132.66003383",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "117.33996617"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
"address": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||||
@ -66241,6 +66556,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "116.1440755221832",
|
||||||
|
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0xa789793e6cad59188752a73b219c335d19aaf5a2c7199edf45db321a92a6c0c9"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "131.2759244778168",
|
"amount": "131.2759244778168",
|
||||||
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||||
@ -66249,8 +66570,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "247.42",
|
"total_tokens": "247.42",
|
||||||
"withdrawn_tokens": "131.2759244778168",
|
"withdrawn_tokens": "247.42",
|
||||||
"remaining_tokens": "116.1440755221832"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
"address": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||||
@ -66263,6 +66584,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "116.827485434478",
|
||||||
|
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0x04b50ec6c4ff0f0cc3d9f2b500e6e91e920c901c98241d4ea595c03dfef12dfb"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "132.092514565522",
|
"amount": "132.092514565522",
|
||||||
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||||
@ -66271,8 +66598,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "248.92",
|
"total_tokens": "248.92",
|
||||||
"withdrawn_tokens": "132.092514565522",
|
"withdrawn_tokens": "248.92",
|
||||||
"remaining_tokens": "116.827485434478"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
"address": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||||
@ -66285,6 +66612,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "117.344131565",
|
||||||
|
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0xe297545c0eb18382b44e8bc9fdd1fb78db661757843ef2f5513d980a470ed873"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"amount": "132.655868435",
|
"amount": "132.655868435",
|
||||||
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||||
@ -66293,8 +66626,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "132.655868435",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "117.344131565"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0xB93EB9807aC74B3004B38E5fB66605578C86A145",
|
"address": "0xB93EB9807aC74B3004B38E5fB66605578C86A145",
|
||||||
@ -69208,10 +69541,17 @@
|
|||||||
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
|
"tx": "0xb59405747c8088945a412703637a7b422f3639439ec2ee15e180c0a2a0d71ee4"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [],
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0xC41d8caeD946723bFc670FCc837C8473437982F7",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0x7169697982aca05b9a8e121d65c96458d745788f7e127523160a8803f3063626"
|
||||||
|
}
|
||||||
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "0",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "250"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0xED380970F4f0746C56C033f00507634a985ec330",
|
"address": "0xED380970F4f0746C56C033f00507634a985ec330",
|
||||||
@ -69757,10 +70097,17 @@
|
|||||||
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
|
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [],
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0x5f3Bce4B242d00ED748d48172C1f2D47A0bcB19B",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0xc06577f7fd53a100da581edbc8231a870d7b148329c8aa250eb942473004e5e6"
|
||||||
|
}
|
||||||
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "0",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "250"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x57a88037954e15AFFDf9bc29097e0B5f5A1BfF35",
|
"address": "0x57a88037954e15AFFDf9bc29097e0B5f5A1BfF35",
|
||||||
@ -69772,10 +70119,17 @@
|
|||||||
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
|
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withdrawals": [],
|
"withdrawals": [
|
||||||
|
{
|
||||||
|
"amount": "250",
|
||||||
|
"user": "0x57a88037954e15AFFDf9bc29097e0B5f5A1BfF35",
|
||||||
|
"tranche_id": 6,
|
||||||
|
"tx": "0x3da3cd5b323439c576ad7a3410d833da0da994ee652b29dd751a4c0edbf0ca99"
|
||||||
|
}
|
||||||
|
],
|
||||||
"total_tokens": "250",
|
"total_tokens": "250",
|
||||||
"withdrawn_tokens": "0",
|
"withdrawn_tokens": "250",
|
||||||
"remaining_tokens": "250"
|
"remaining_tokens": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"address": "0x949ECa8174BBa367901e9a195B77B2eA7d862c43",
|
"address": "0x949ECa8174BBa367901e9a195B77B2eA7d862c43",
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"tranche_end": "2022-11-26T13:48:10.000Z",
|
"tranche_end": "2022-11-26T13:48:10.000Z",
|
||||||
"total_added": "100",
|
"total_added": "100",
|
||||||
"total_removed": "0",
|
"total_removed": "0",
|
||||||
"locked_amount": "37.074508498224257",
|
"locked_amount": "36.18399923896499",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "100",
|
"amount": "100",
|
||||||
@ -242,7 +242,7 @@
|
|||||||
"tranche_end": "2022-10-12T00:53:20.000Z",
|
"tranche_end": "2022-10-12T00:53:20.000Z",
|
||||||
"total_added": "1100",
|
"total_added": "1100",
|
||||||
"total_removed": "673.04388635",
|
"total_removed": "673.04388635",
|
||||||
"locked_amount": "270.58154807204466",
|
"locked_amount": "260.785946220192745",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "1000",
|
"amount": "1000",
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
"tranche_end": "2022-10-12T00:53:20.000Z",
|
"tranche_end": "2022-10-12T00:53:20.000Z",
|
||||||
"total_added": "1010.000000000000000001",
|
"total_added": "1010.000000000000000001",
|
||||||
"total_removed": "668.4622323651",
|
"total_removed": "668.4622323651",
|
||||||
"locked_amount": "248.4430577752410060002459832255200406",
|
"locked_amount": "239.44891425672242950023707813292744795",
|
||||||
"deposits": [
|
"deposits": [
|
||||||
{
|
{
|
||||||
"amount": "1000",
|
"amount": "1000",
|
||||||
|
@ -15,5 +15,7 @@ module.exports = defineConfig({
|
|||||||
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
||||||
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,8 @@ module.exports = defineConfig({
|
|||||||
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
videosFolder: '../../dist/cypress/apps/explorer-e2e/videos',
|
||||||
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
screenshotsFolder: '../../dist/cypress/apps/explorer-e2e/screenshots',
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
ethProviderUrl: 'http://localhost:8545/',
|
ethProviderUrl: 'http://localhost:8545/',
|
||||||
|
@ -1,28 +1,44 @@
|
|||||||
const pageSpinner = 'splash-loader';
|
/// <reference types="cypress" />
|
||||||
const menuBar = 'nav';
|
const stakeValidatorList = '[data-testid="node-list-item-name"]';
|
||||||
const validatorList = '[data-testid="node-list-item-name"]';
|
const stakeValidatorWithinList = '[data-testid="node-list-item"]';
|
||||||
const removeStakeRadioButton = '[data-testid="remove-stake-radio"]';
|
const stakeRemoveStakeRadioButton = '[data-testid="remove-stake-radio"]';
|
||||||
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
|
const stakeTokenAmountInputBox = '[data-testid="token-amount-input"]';
|
||||||
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
|
const stakeTokenSubmitButton = '[data-testid="token-input-submit-button"]';
|
||||||
const stakeNextEpochValue = '[data-testid="stake-next-epoch"]';
|
const stakeNextEpochValue = '[data-testid="stake-next-epoch"]';
|
||||||
const vegaWalletContainer = '[data-testid="vega-wallet"]';
|
const stakeThisEpochValue = '[data-testid="stake-this-epoch"]';
|
||||||
|
const stakeAddStakeRadioButton = '[data-testid="add-stake-radio"]';
|
||||||
|
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
|
||||||
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
|
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
|
||||||
|
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
|
||||||
|
const vegaWalletUnstakedBalance =
|
||||||
|
'[data-testid="vega-wallet-balance-unstaked"]';
|
||||||
|
const vegaWalletStakedBalances =
|
||||||
|
'[data-testid="vega-wallet-balance-staked-validators"]';
|
||||||
|
const vegaWalletThisEpochBalances =
|
||||||
|
'[data-testid="vega-wallet-balance-this-epoch"]';
|
||||||
|
const vegaWalletNextEpochBalances =
|
||||||
|
'[data-testid="vega-wallet-balance-next-epoch"]';
|
||||||
|
const ethWalletAssociatedBalances =
|
||||||
|
'[data-testid="eth-wallet-associated-balances"]';
|
||||||
|
const ethWalletTotalAssociatedBalance = '[data-testid="currency-locked"]';
|
||||||
|
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
|
||||||
|
const txTimeout = { timeout: 40000 };
|
||||||
|
const epochTimeout = { timeout: 10000 };
|
||||||
|
|
||||||
context('Staking Flow - with eth and vega wallets connected', function () {
|
context('Staking Flow - with eth and vega wallets connected', function () {
|
||||||
before('visit staking tab and connect vega wallet', function () {
|
before('visit staking tab and connect vega wallet', function () {
|
||||||
cy.vega_wallet_import();
|
cy.vega_wallet_import();
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get(menuBar, { timeout: 20000 }).should('be.visible');
|
cy.verify_page_header('The $VEGA token');
|
||||||
cy.vega_wallet_connect();
|
cy.vega_wallet_connect();
|
||||||
cy.vega_wallet_set_specified_approval_amount('1000');
|
cy.vega_wallet_set_specified_approval_amount('1000');
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.get(menuBar, { timeout: 20000 }).should('be.visible');
|
cy.verify_page_header('The $VEGA token');
|
||||||
cy.ethereum_wallet_connect();
|
cy.ethereum_wallet_connect();
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(pageSpinner, { timeout: 20000 }).should('not.exist');
|
cy.wait_for_spinner();
|
||||||
cy.get(validatorList).first().invoke('text').as('validatorName');
|
cy.get(stakeValidatorList).first().invoke('text').as('validatorName');
|
||||||
cy.get(validatorList).last().invoke('text').as('otherValidatorName');
|
cy.get(stakeValidatorList).last().invoke('text').as('otherValidatorName');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Eth wallet - contains VEGA tokens', function () {
|
describe('Eth wallet - contains VEGA tokens', function () {
|
||||||
@ -36,219 +52,505 @@ context('Staking Flow - with eth and vega wallets connected', function () {
|
|||||||
|
|
||||||
it('Able to stake against a validator', function () {
|
it('Able to stake against a validator', function () {
|
||||||
cy.staking_page_associate_tokens('3');
|
cy.staking_page_associate_tokens('3');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
cy.ethereum_wallet_check_associated_value_is('3.0');
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
cy.ethereum_wallet_check_associated_vega_key_value_is(
|
'contain',
|
||||||
vegaWalletPublicKeyShort,
|
3.0,
|
||||||
'3.000000000000000000'
|
txTimeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
|
.contains('3.0', txTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
|
.contains(vegaWalletPublicKeyShort)
|
||||||
|
.parent()
|
||||||
|
.should('contain', 3.0, txTimeout);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('2');
|
cy.staking_validator_page_add_stake('2');
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
|
||||||
this.validatorName,
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
'2.000000000000000000'
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
1.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.vega_wallet_check_unstaked_value_is('1.000000000000000000');
|
|
||||||
vega_wallet_check_validator_staked_value_is(
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
this.validatorName,
|
.should('contain', 2.0, txTimeout)
|
||||||
'2.000000000000000000'
|
.and('contain', this.validatorName);
|
||||||
);
|
|
||||||
staking_validator_page_check_stake_next_epoch_value('2.0');
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
staking_validator_page_check_stake_this_epoch_value('2.0');
|
.contains(2.0, epochTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(stakeThisEpochValue, epochTimeout)
|
||||||
|
.contains(2.0, epochTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.validatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '2.0')
|
||||||
|
.and('contain', '100%');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to stake against mulitple validators', function () {
|
it('Able to stake against mulitple validators', function () {
|
||||||
cy.staking_page_associate_tokens('5');
|
cy.staking_page_associate_tokens('5');
|
||||||
cy.vega_wallet_check_unstaked_value_is('5.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
5.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('2');
|
cy.staking_validator_page_add_stake('2');
|
||||||
vega_wallet_check_validator_staked_value_is(
|
|
||||||
this.validatorName,
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
'2.000000000000000000'
|
.contains(this.validatorName, txTimeout)
|
||||||
);
|
.parent()
|
||||||
|
.should('contain', 2.0, txTimeout);
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(validatorList).contains(this.otherValidatorName).click();
|
cy.get(stakeValidatorList).contains(this.otherValidatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('1');
|
cy.staking_validator_page_add_stake('1');
|
||||||
vega_wallet_check_validator_staked_value_is(
|
|
||||||
this.otherValidatorName,
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
'1.000000000000000000'
|
.should('have.length', 2, txTimeout)
|
||||||
|
.contains(this.otherValidatorName, txTimeout)
|
||||||
|
.parent()
|
||||||
|
.should('contain', 1.0, txTimeout);
|
||||||
|
|
||||||
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
|
.contains(this.validatorName, txTimeout)
|
||||||
|
.parent()
|
||||||
|
.should('contain', 2.0, txTimeout);
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
2.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.vega_wallet_check_unstaked_value_is('2.000000000000000000');
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.validatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '2.0')
|
||||||
|
.and('contain', '66.67%');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.otherValidatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '1.0')
|
||||||
|
.and('contain', '33.33%');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Able to remove part of a stake against a validator', function () {
|
it('Able to remove part of a stake against a validator', function () {
|
||||||
cy.staking_page_associate_tokens('4');
|
cy.staking_page_associate_tokens('4');
|
||||||
cy.vega_wallet_check_unstaked_value_is('4.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
4.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('3');
|
cy.staking_validator_page_add_stake('3');
|
||||||
staking_validator_page_check_stake_next_epoch_value('3.0');
|
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
this.validatorName,
|
.contains(3.0, epochTimeout)
|
||||||
'3.000000000000000000'
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
|
.should('contain', 3.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
1.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.vega_wallet_check_unstaked_value_is('1.000000000000000000');
|
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_removeStake('1');
|
cy.staking_validator_page_remove_stake('1');
|
||||||
staking_validator_page_check_stake_next_epoch_value('2.0');
|
|
||||||
staking_validator_page_check_stake_this_epoch_value('3.0');
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
.contains(2.0, epochTimeout)
|
||||||
this.validatorName,
|
.should('be.visible');
|
||||||
'2.000000000000000000'
|
|
||||||
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletThisEpochBalances, txTimeout)
|
||||||
|
.should('contain', 3.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'This Epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
2.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
vega_wallet_check_validator_stake_this_epoch_value_is(
|
|
||||||
this.validatorName,
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
'3.000000000000000000'
|
.should('contain', 2.0, txTimeout)
|
||||||
);
|
.and('contain', this.validatorName);
|
||||||
cy.vega_wallet_check_unstaked_value_is('2.000000000000000000');
|
|
||||||
vega_wallet_check_validator_staked_value_is(
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
this.validatorName,
|
.contains(2.0, epochTimeout)
|
||||||
'2.000000000000000000'
|
.should('be.visible');
|
||||||
);
|
|
||||||
staking_validator_page_check_stake_next_epoch_value('2.0');
|
cy.get(stakeThisEpochValue, epochTimeout)
|
||||||
staking_validator_page_check_stake_this_epoch_value('2.0');
|
.contains(2.0, epochTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.validatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '2.0')
|
||||||
|
.and('contain', '100%');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to remove a full stake against a validator', function () {
|
it('Able to remove a full stake against a validator', function () {
|
||||||
cy.staking_page_associate_tokens('3');
|
cy.staking_page_associate_tokens('3');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('1');
|
cy.staking_validator_page_add_stake('1');
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
|
||||||
this.validatorName,
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
'1.000000000000000000'
|
.should('contain', 1.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
2.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.vega_wallet_check_unstaked_value_is('2.000000000000000000');
|
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
|
|
||||||
cy.staking_validator_page_removeStake('1');
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
staking_validator_page_check_stake_next_epoch_value('0.0');
|
|
||||||
|
|
||||||
vega_wallet_check_validator_stake_this_epoch_value_is(
|
cy.staking_validator_page_remove_stake('1');
|
||||||
this.validatorName,
|
|
||||||
'1.000000000000000000'
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
|
.contains(0.0, epochTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletThisEpochBalances, txTimeout)
|
||||||
|
.should('contain', 1.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'This Epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
|
.should('contain', 0.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
|
||||||
this.validatorName,
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
'0.000000000000000000'
|
.contains(0.0, epochTimeout)
|
||||||
);
|
.should('be.visible');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
staking_validator_page_check_stake_next_epoch_value('0.0');
|
cy.get(stakeThisEpochValue, epochTimeout)
|
||||||
staking_validator_page_check_stake_this_epoch_value('0.0');
|
.contains(0.0, epochTimeout)
|
||||||
vega_wallet_check_validator_no_longer_showing(this.validatorName);
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
|
.contains(this.validatorName, txTimeout)
|
||||||
|
.should('not.exist', txTimeout);
|
||||||
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.validatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '0.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Unable to remove a stake with a negative value for a validator', function () {
|
it('Unable to remove a stake with a negative value for a validator', function () {
|
||||||
cy.staking_page_associate_tokens('3');
|
cy.staking_page_associate_tokens('3');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('2');
|
cy.staking_validator_page_add_stake('2');
|
||||||
staking_validator_page_check_stake_next_epoch_value('2.0');
|
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
this.validatorName,
|
.contains(2.0, epochTimeout)
|
||||||
'2.000000000000000000'
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.vega_wallet_check_unstaked_value_is('1.000000000000000000');
|
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
cy.get(removeStakeRadioButton).click({ force: true });
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
cy.get(tokenAmountInputBox).type('-0.1');
|
|
||||||
cy.contains('Waiting for next epoch to start', { timeout: 10000 });
|
cy.get(stakeRemoveStakeRadioButton).click({ force: true });
|
||||||
cy.get(tokenSubmitButton)
|
|
||||||
.should('be.disabled', { timeout: 8000 })
|
cy.get(stakeTokenAmountInputBox).type('-0.1');
|
||||||
|
|
||||||
|
cy.contains('Waiting for next epoch to start', epochTimeout);
|
||||||
|
|
||||||
|
cy.get(stakeTokenSubmitButton)
|
||||||
|
.should('be.disabled', epochTimeout)
|
||||||
.and('contain', `Remove -0.1 $VEGA tokens at the end of epoch`)
|
.and('contain', `Remove -0.1 $VEGA tokens at the end of epoch`)
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
|
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
|
||||||
cy.staking_page_associate_tokens('3');
|
cy.staking_page_associate_tokens('3');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('2');
|
cy.staking_validator_page_add_stake('2');
|
||||||
staking_validator_page_check_stake_next_epoch_value('2.0');
|
|
||||||
vega_wallet_check_validator_stake_next_epoch_value_is(
|
cy.get(stakeNextEpochValue, epochTimeout)
|
||||||
this.validatorName,
|
.contains(2.0, epochTimeout)
|
||||||
'2.000000000000000000'
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletNextEpochBalances, txTimeout)
|
||||||
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName)
|
||||||
|
.and('contain', 'Next epoch');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
1.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.vega_wallet_check_unstaked_value_is('1.000000000000000000');
|
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
cy.get(removeStakeRadioButton).click({ force: true });
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
cy.get(tokenAmountInputBox).type(4);
|
|
||||||
cy.contains('Waiting for next epoch to start', { timeout: 10000 });
|
cy.get(stakeRemoveStakeRadioButton).click({ force: true });
|
||||||
cy.get(tokenSubmitButton)
|
|
||||||
.should('be.disabled', { timeout: 8000 })
|
cy.get(stakeTokenAmountInputBox).type(4);
|
||||||
|
|
||||||
|
cy.contains('Waiting for next epoch to start', epochTimeout);
|
||||||
|
|
||||||
|
cy.get(stakeTokenSubmitButton)
|
||||||
|
.should('be.disabled', epochTimeout)
|
||||||
.and('contain', `Remove 4 $VEGA tokens at the end of epoch`)
|
.and('contain', `Remove 4 $VEGA tokens at the end of epoch`)
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('Disassociating all tokens - removes all staked tokens', function () {
|
it('Disassociating all tokens - removes all staked tokens', function () {
|
||||||
cy.staking_page_associate_tokens('3');
|
cy.staking_page_associate_tokens('3');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
|
||||||
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('2');
|
cy.staking_validator_page_add_stake('2');
|
||||||
cy.vega_wallet_check_unstaked_value_is('1.000000000000000000');
|
|
||||||
vega_wallet_check_validator_staked_value_is(
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
this.validatorName,
|
'contain',
|
||||||
'2.000000000000000000'
|
1.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName);
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
cy.staking_page_disassociate_all_tokens();
|
cy.staking_page_disassociate_all_tokens();
|
||||||
cy.ethereum_wallet_check_associated_vega_key_is_no_longer_showing(
|
|
||||||
vegaWalletPublicKeyShort
|
cy.get(ethWalletContainer).within(() => {
|
||||||
|
cy.contains(vegaWalletPublicKeyShort, { timeout: 20000 }).should(
|
||||||
|
'not.exist'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
|
.contains('0.0', txTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
'0.000000000000000000',
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.ethereum_wallet_check_associated_value_is('0.0');
|
|
||||||
cy.vega_wallet_check_associated_value_is('0.000000000000000000');
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
vega_wallet_check_validator_no_longer_showing(this.validatorName);
|
.contains(this.validatorName, txTimeout)
|
||||||
|
.should('not.exist', txTimeout);
|
||||||
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.validatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '0.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
|
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
|
||||||
cy.staking_page_associate_tokens('3');
|
cy.staking_page_associate_tokens('3');
|
||||||
cy.vega_wallet_check_unstaked_value_is('3.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
cy.get(validatorList).contains(this.validatorName).click();
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
cy.staking_validator_page_add_stake('2');
|
cy.staking_validator_page_add_stake('2');
|
||||||
cy.vega_wallet_check_unstaked_value_is('1.000000000000000000');
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
vega_wallet_check_validator_staked_value_is(
|
'contain',
|
||||||
this.validatorName,
|
1.0,
|
||||||
'2.000000000000000000'
|
txTimeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName);
|
||||||
|
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
cy.staking_page_disassociate_tokens('1');
|
cy.staking_page_disassociate_tokens('1');
|
||||||
cy.ethereum_wallet_check_associated_value_is('2.0');
|
|
||||||
cy.vega_wallet_check_associated_value_is('2.000000000000000000');
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
vega_wallet_check_validator_staked_value_is(
|
.contains('2.0', txTimeout)
|
||||||
this.validatorName,
|
.should('be.visible');
|
||||||
'2.000000000000000000'
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
'2.000000000000000000',
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cy.get(vegaWalletStakedBalances, txTimeout)
|
||||||
|
.should('contain', 2.0, txTimeout)
|
||||||
|
.and('contain', this.validatorName);
|
||||||
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorWithinList, epochTimeout)
|
||||||
|
.contains(this.validatorName)
|
||||||
|
.parent()
|
||||||
|
.contains('Total stake')
|
||||||
|
.parent()
|
||||||
|
.should('contain', '2.0')
|
||||||
|
.and('contain', '100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Selecting use maximum where tokens are allready staked - suggests the unstaked token amount', function () {
|
||||||
|
cy.staking_page_associate_tokens('3');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
3.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
|
cy.get(stakeValidatorList).contains(this.validatorName).click();
|
||||||
|
|
||||||
|
cy.staking_validator_page_add_stake('2');
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
1.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.navigate_to('staking');
|
||||||
|
|
||||||
|
cy.get(stakeValidatorList).contains(this.otherValidatorName).click();
|
||||||
|
|
||||||
|
cy.get(stakeAddStakeRadioButton).click({ force: true });
|
||||||
|
|
||||||
|
cy.get(stakeMaximumTokens, { timeout: 60000 }).click();
|
||||||
|
|
||||||
|
cy.get(stakeTokenSubmitButton).should('contain', 'Add 1 $VEGA tokens');
|
||||||
});
|
});
|
||||||
|
|
||||||
after(
|
after(
|
||||||
@ -259,77 +561,3 @@ context('Staking Flow - with eth and vega wallets connected', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function staking_validator_page_check_stake_next_epoch_value(expectedVal) {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking Staking Page - Validator Stake Next Epoch Value is ${expectedVal}`
|
|
||||||
);
|
|
||||||
cy.get(stakeNextEpochValue, { timeout: 10000 })
|
|
||||||
.contains(expectedVal, { timeout: 10000 })
|
|
||||||
.should('be.visible');
|
|
||||||
}
|
|
||||||
|
|
||||||
function staking_validator_page_check_stake_this_epoch_value(expectedVal) {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking Staking Page - Validator Stake This Epoch Value is ${expectedVal}`
|
|
||||||
);
|
|
||||||
cy.get(stakeNextEpochValue, { timeout: 10000 })
|
|
||||||
.contains(expectedVal, { timeout: 10000 })
|
|
||||||
.should('be.visible');
|
|
||||||
}
|
|
||||||
|
|
||||||
function vega_wallet_check_validator_stake_next_epoch_value_is(
|
|
||||||
validatorName,
|
|
||||||
expectedVal
|
|
||||||
) {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking vega wallet - Stake Next Epoch Value for ${validatorName} is ${expectedVal}`
|
|
||||||
);
|
|
||||||
cy.get(vegaWalletContainer).within(() => {
|
|
||||||
cy.contains(`${validatorName} (Next epoch)`, { timeout: 40000 })
|
|
||||||
.siblings()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function vega_wallet_check_validator_stake_this_epoch_value_is(
|
|
||||||
validatorName,
|
|
||||||
expectedVal
|
|
||||||
) {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking vega wallet - Stake This Epoch Value for ${validatorName} is ${expectedVal}`
|
|
||||||
);
|
|
||||||
cy.get(vegaWalletContainer).within(() => {
|
|
||||||
cy.contains(`${validatorName} (This Epoch)`, { timeout: 40000 })
|
|
||||||
.siblings()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function vega_wallet_check_validator_no_longer_showing(validatorName) {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking Validator and therefore stake removed for ${validatorName}`
|
|
||||||
);
|
|
||||||
cy.get(vegaWalletContainer).within(() => {
|
|
||||||
cy.contains(`${validatorName}`, { timeout: 40000 }).should('not.exist', {
|
|
||||||
timeout: 40000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function vega_wallet_check_validator_staked_value_is(
|
|
||||||
validatorName,
|
|
||||||
expectedVal
|
|
||||||
) {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking Validator Stake Value for ${validatorName} is ${expectedVal}`
|
|
||||||
);
|
|
||||||
cy.get(vegaWalletContainer).within(() => {
|
|
||||||
cy.contains(`${validatorName}`, { timeout: 40000 })
|
|
||||||
.siblings()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
const pageSpinner = 'splash-loader';
|
|
||||||
const menuBar = 'nav';
|
|
||||||
const validatorList = '[data-testid="node-list-item-name"]';
|
const validatorList = '[data-testid="node-list-item-name"]';
|
||||||
|
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
|
||||||
|
const ethWalletAssociatedBalances =
|
||||||
|
'[data-testid="eth-wallet-associated-balances"]';
|
||||||
|
const ethWalletTotalAssociatedBalance = '[data-testid="currency-locked"]';
|
||||||
|
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
|
||||||
|
const vegaWalletUnstakedBalance =
|
||||||
|
'[data-testid="vega-wallet-balance-unstaked"]';
|
||||||
|
const txTimeout = { timeout: 40000 };
|
||||||
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
|
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
|
||||||
|
|
||||||
context(
|
context(
|
||||||
@ -10,14 +15,14 @@ context(
|
|||||||
before('visit staking tab and connect vega wallet', function () {
|
before('visit staking tab and connect vega wallet', function () {
|
||||||
cy.vega_wallet_import();
|
cy.vega_wallet_import();
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get(menuBar, { timeout: 20000 }).should('be.visible');
|
cy.verify_page_header('The $VEGA token');
|
||||||
cy.vega_wallet_connect();
|
cy.vega_wallet_connect();
|
||||||
cy.vega_wallet_set_specified_approval_amount('1000');
|
cy.vega_wallet_set_specified_approval_amount('1000');
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.get(menuBar, { timeout: 20000 }).should('be.visible');
|
cy.verify_page_header('The $VEGA token');
|
||||||
cy.ethereum_wallet_connect();
|
cy.ethereum_wallet_connect();
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(pageSpinner, { timeout: 20000 }).should('not.exist');
|
cy.wait_for_spinner();
|
||||||
cy.get(validatorList).first().invoke('text').as('validatorName');
|
cy.get(validatorList).first().invoke('text').as('validatorName');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -27,71 +32,140 @@ context(
|
|||||||
function () {
|
function () {
|
||||||
cy.vega_wallet_teardown();
|
cy.vega_wallet_teardown();
|
||||||
cy.navigate_to('staking');
|
cy.navigate_to('staking');
|
||||||
cy.get(pageSpinner, { timeout: 20000 }).should('not.exist');
|
cy.wait_for_spinner();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
it('Able to associate tokens', function () {
|
it('Able to associate tokens', function () {
|
||||||
cy.staking_page_associate_tokens('2');
|
cy.staking_page_associate_tokens('2');
|
||||||
cy.ethereum_wallet_check_associated_vega_key_value_is(
|
|
||||||
vegaWalletPublicKeyShort,
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
'2.000000000000000000'
|
.contains(vegaWalletPublicKeyShort)
|
||||||
|
.parent()
|
||||||
|
.should('contain', 2.0, txTimeout);
|
||||||
|
|
||||||
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
|
.contains('2.0', txTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
2.0,
|
||||||
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
2.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.ethereum_wallet_check_associated_value_is('2.0');
|
|
||||||
cy.vega_wallet_check_associated_value_is('2.000000000000000000');
|
|
||||||
cy.vega_wallet_check_unstaked_value_is('2.000000000000000000');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to disassociate tokens', function () {
|
it('Able to disassociate tokens', function () {
|
||||||
cy.staking_page_associate_tokens('2');
|
cy.staking_page_associate_tokens('2');
|
||||||
cy.ethereum_wallet_check_associated_vega_key_value_is(
|
|
||||||
vegaWalletPublicKeyShort,
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
'2.000000000000000000'
|
.contains(vegaWalletPublicKeyShort)
|
||||||
);
|
.parent()
|
||||||
cy.vega_wallet_check_associated_value_is('2.000000000000000000');
|
.should('contain', 2.0, txTimeout);
|
||||||
|
|
||||||
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
|
.contains('2.0', txTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
|
|
||||||
cy.staking_page_disassociate_tokens('1');
|
cy.staking_page_disassociate_tokens('1');
|
||||||
cy.ethereum_wallet_check_associated_vega_key_value_is(
|
|
||||||
vegaWalletPublicKeyShort,
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
'1.000000000000000000'
|
.contains(vegaWalletPublicKeyShort)
|
||||||
);
|
.parent()
|
||||||
cy.ethereum_wallet_check_associated_value_is('1.0');
|
.should('contain', 1.0, txTimeout);
|
||||||
cy.vega_wallet_check_associated_value_is('1.000000000000000000');
|
|
||||||
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
|
.contains('1.0', txTimeout)
|
||||||
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to associate more tokens than the approved amount of 1000 - requires re-approval', function () {
|
it('Able to associate more tokens than the approved amount of 1000 - requires re-approval', function () {
|
||||||
cy.staking_page_associate_tokens('1001', true);
|
cy.staking_page_associate_tokens('1001', true);
|
||||||
cy.ethereum_wallet_check_associated_vega_key_value_is(
|
|
||||||
vegaWalletPublicKeyShort,
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
'1,001.000000000000000000'
|
.contains(vegaWalletPublicKeyShort)
|
||||||
|
.parent()
|
||||||
|
.should('contain', '1,001.000000000000000000', txTimeout);
|
||||||
|
|
||||||
|
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
|
||||||
|
.contains('1,001.00', txTimeout)
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
'1,001.000000000000000000',
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.ethereum_wallet_check_associated_value_is('1,001.00');
|
|
||||||
cy.vega_wallet_check_associated_value_is('1,001.000000000000000000');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to disassociate a partial amount of tokens currently associated', function () {
|
it('Able to disassociate a partial amount of tokens currently associated', function () {
|
||||||
cy.staking_page_associate_tokens('2');
|
cy.staking_page_associate_tokens('2');
|
||||||
cy.vega_wallet_check_associated_value_is('2.000000000000000000');
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
cy.staking_page_disassociate_tokens('1');
|
'contain',
|
||||||
cy.ethereum_wallet_check_associated_vega_key_value_is(
|
2.0,
|
||||||
vegaWalletPublicKeyShort,
|
txTimeout
|
||||||
'1.000000000000000000'
|
);
|
||||||
|
|
||||||
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
|
|
||||||
|
cy.staking_page_disassociate_tokens('1');
|
||||||
|
|
||||||
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
|
.contains(vegaWalletPublicKeyShort)
|
||||||
|
.parent()
|
||||||
|
.should('contain', 1.0, txTimeout);
|
||||||
|
|
||||||
|
cy.get(ethWalletAssociatedBalances, txTimeout)
|
||||||
|
.contains(vegaWalletPublicKeyShort)
|
||||||
|
.parent()
|
||||||
|
.should('contain', 1.0, txTimeout);
|
||||||
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
1.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.ethereum_wallet_check_associated_value_is('1.0');
|
|
||||||
cy.vega_wallet_check_associated_value_is('1.000000000000000000');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to disassociate all tokens', function () {
|
it('Able to disassociate all tokens', function () {
|
||||||
cy.staking_page_associate_tokens('2');
|
cy.staking_page_associate_tokens('2');
|
||||||
cy.vega_wallet_check_associated_value_is('2.000000000000000000');
|
|
||||||
cy.get('button').contains('Select a validator to nominate').click();
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
cy.staking_page_disassociate_all_tokens();
|
'contain',
|
||||||
cy.ethereum_wallet_check_associated_vega_key_is_no_longer_showing(
|
2.0,
|
||||||
vegaWalletPublicKeyShort
|
txTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('button').contains('Select a validator to nominate').click();
|
||||||
|
|
||||||
|
cy.staking_page_disassociate_all_tokens();
|
||||||
|
|
||||||
|
cy.get(ethWalletContainer).within(() => {
|
||||||
|
cy.contains(vegaWalletPublicKeyShort, { timeout: 20000 }).should(
|
||||||
|
'not.exist'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(ethWalletContainer).within(() => {
|
||||||
|
cy.contains(vegaWalletPublicKeyShort, { timeout: 20000 }).should(
|
||||||
|
'not.exist'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||||
|
'contain',
|
||||||
|
0.0,
|
||||||
|
txTimeout
|
||||||
);
|
);
|
||||||
cy.ethereum_wallet_check_associated_value_is('0.0');
|
|
||||||
cy.vega_wallet_check_associated_value_is('0.000000000000000000');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ const navigation = {
|
|||||||
rewards: '[href="/rewards"]',
|
rewards: '[href="/rewards"]',
|
||||||
withdraw: '[href="/withdraw"]',
|
withdraw: '[href="/withdraw"]',
|
||||||
governance: '[href="/governance"]',
|
governance: '[href="/governance"]',
|
||||||
|
pageSpinner: 'splash-loader',
|
||||||
};
|
};
|
||||||
|
|
||||||
Cypress.Commands.add('navigate_to', (page) => {
|
Cypress.Commands.add('navigate_to', (page) => {
|
||||||
@ -31,3 +32,7 @@ Cypress.Commands.add('verify_tab_highlighted', (page) => {
|
|||||||
Cypress.Commands.add('verify_page_header', (text) => {
|
Cypress.Commands.add('verify_page_header', (text) => {
|
||||||
return cy.get('header h1').should('be.visible').and('have.text', text);
|
return cy.get('header h1').should('be.visible').and('have.text', text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('wait_for_spinner', () => {
|
||||||
|
cy.get(navigation.pageSpinner, { timeout: 20000 }).should('not.exist');
|
||||||
|
});
|
||||||
|
@ -9,7 +9,6 @@ const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
|
|||||||
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
|
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
|
||||||
|
|
||||||
Cypress.Commands.add('wait_for_begining_of_epoch', () => {
|
Cypress.Commands.add('wait_for_begining_of_epoch', () => {
|
||||||
cy.highlight(`Waiting for next epoch to start`);
|
|
||||||
cy.contains('Waiting for next epoch to start', { timeout: 10000 }).should(
|
cy.contains('Waiting for next epoch to start', { timeout: 10000 }).should(
|
||||||
'not.exist'
|
'not.exist'
|
||||||
);
|
);
|
||||||
@ -26,13 +25,9 @@ Cypress.Commands.add('staking_validator_page_add_stake', (stake) => {
|
|||||||
.and('contain', `Add ${stake} $VEGA tokens`)
|
.and('contain', `Add ${stake} $VEGA tokens`)
|
||||||
.and('be.visible')
|
.and('be.visible')
|
||||||
.click();
|
.click();
|
||||||
cy.contains(
|
|
||||||
'At the beginning of the next epoch your $VEGA will be nominated to the validator',
|
|
||||||
{ timeout: 20000 }
|
|
||||||
).should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('staking_validator_page_removeStake', (stake) => {
|
Cypress.Commands.add('staking_validator_page_remove_stake', (stake) => {
|
||||||
cy.highlight(`Removing a stake of ${stake}`);
|
cy.highlight(`Removing a stake of ${stake}`);
|
||||||
cy.get(removeStakeRadioButton).click({ force: true });
|
cy.get(removeStakeRadioButton).click({ force: true });
|
||||||
cy.get(tokenAmountInputBox).type(stake);
|
cy.get(tokenAmountInputBox).type(stake);
|
||||||
@ -42,9 +37,6 @@ Cypress.Commands.add('staking_validator_page_removeStake', (stake) => {
|
|||||||
.and('contain', `Remove ${stake} $VEGA tokens at the end of epoch`)
|
.and('contain', `Remove ${stake} $VEGA tokens at the end of epoch`)
|
||||||
.and('be.visible')
|
.and('be.visible')
|
||||||
.click();
|
.click();
|
||||||
cy.contains(`${stake} $VEGA has been removed from validator`).should(
|
|
||||||
'be.visible'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
|
@ -16,42 +16,3 @@ Cypress.Commands.add('ethereum_wallet_connect', () => {
|
|||||||
cy.contains('Locked', { timeout: 15000 }).should('be.visible');
|
cy.contains('Locked', { timeout: 15000 }).should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
|
||||||
'ethereum_wallet_check_associated_value_is',
|
|
||||||
(expectedVal) => {
|
|
||||||
cy.highlight(`Checking Eth Wallet - Associated Value is ${expectedVal}`);
|
|
||||||
cy.get(ethWalletContainer).within(() => {
|
|
||||||
cy.contains('Associated', { timeout: 20000 })
|
|
||||||
.parent()
|
|
||||||
.siblings()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Cypress.Commands.add(
|
|
||||||
'ethereum_wallet_check_associated_vega_key_value_is',
|
|
||||||
(vegaShortPublicKey, expectedVal) => {
|
|
||||||
cy.highlight(
|
|
||||||
`Checking Eth Wallet - Vega Key Associated Value is ${expectedVal} for key ${vegaShortPublicKey}`
|
|
||||||
);
|
|
||||||
cy.get(ethWalletContainer).within(() => {
|
|
||||||
cy.contains(vegaShortPublicKey, { timeout: 20000 })
|
|
||||||
.parent()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Cypress.Commands.add(
|
|
||||||
'ethereum_wallet_check_associated_vega_key_is_no_longer_showing',
|
|
||||||
(vegaShortPublicKey) => {
|
|
||||||
cy.highlight('Checking Eth Wallet - Vega Key Associated is not showing');
|
|
||||||
cy.get(ethWalletContainer).within(() => {
|
|
||||||
cy.contains(vegaShortPublicKey, { timeout: 20000 }).should('not.exist');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
} from '@vegaprotocol/smart-contracts';
|
} from '@vegaprotocol/smart-contracts';
|
||||||
import { ethers, Wallet } from 'ethers';
|
import { ethers, Wallet } from 'ethers';
|
||||||
|
|
||||||
|
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
|
||||||
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
|
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
|
||||||
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
|
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
|
||||||
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
|
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
|
||||||
@ -41,7 +42,12 @@ before('Vega wallet teardown prep', function () {
|
|||||||
Cypress.Commands.add('vega_wallet_teardown', function () {
|
Cypress.Commands.add('vega_wallet_teardown', function () {
|
||||||
cy.vega_wallet_teardown_staking(this.stakingBridgeContract);
|
cy.vega_wallet_teardown_staking(this.stakingBridgeContract);
|
||||||
cy.vega_wallet_teardown_vesting(this.vestingContract);
|
cy.vega_wallet_teardown_vesting(this.vestingContract);
|
||||||
cy.vega_wallet_check_associated_value_is('0.000000000000000000');
|
|
||||||
|
cy.get(vegaWalletAssociatedBalance, { timeout: transactionTimeout }).should(
|
||||||
|
'contain',
|
||||||
|
'0.000000000000000000',
|
||||||
|
{ timeout: transactionTimeout }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
|
@ -33,24 +33,3 @@ Cypress.Commands.add('vega_wallet_connect', () => {
|
|||||||
});
|
});
|
||||||
cy.contains(`${vegaWalletName} key`, { timeout: 20000 }).should('be.visible');
|
cy.contains(`${vegaWalletName} key`, { timeout: 20000 }).should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('vega_wallet_check_unstaked_value_is', (expectedVal) => {
|
|
||||||
cy.highlight(`Checking vega wallet - Unstaked Value is ${expectedVal}`);
|
|
||||||
cy.get(vegaWalletContainer).within(() => {
|
|
||||||
cy.contains('Unstaked', { timeout: 40000 })
|
|
||||||
.siblings()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('vega_wallet_check_associated_value_is', (expectedVal) => {
|
|
||||||
cy.highlight(`Checking vega wallet - Associated Value is ${expectedVal}`);
|
|
||||||
cy.get(vegaWalletContainer).within(() => {
|
|
||||||
cy.contains('Associated', { timeout: 40000 })
|
|
||||||
.parent()
|
|
||||||
.siblings()
|
|
||||||
.contains(expectedVal, { timeout: 40000 })
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -83,12 +83,14 @@ const AssociatedAmounts = ({
|
|||||||
/>
|
/>
|
||||||
{vestingAssociationByVegaKey.map(([key, amount]) => {
|
{vestingAssociationByVegaKey.map(([key, amount]) => {
|
||||||
return (
|
return (
|
||||||
<WalletCardRow
|
<div data-testid="eth-wallet-associated-balances">
|
||||||
key={key}
|
<WalletCardRow
|
||||||
label={removeLeadingAddressSymbol(key)}
|
key={key}
|
||||||
value={amount}
|
label={removeLeadingAddressSymbol(key)}
|
||||||
dark={true}
|
value={amount}
|
||||||
/>
|
dark={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,31 +154,37 @@ const VegaWalletConnected = ({ vegaKeys }: VegaWalletConnectedProps) => {
|
|||||||
balance={currentStakeAvailable}
|
balance={currentStakeAvailable}
|
||||||
dark={true}
|
dark={true}
|
||||||
/>
|
/>
|
||||||
<WalletCardRow label={t('unstaked')} value={unstaked} dark={true} />
|
<div data-testid="vega-wallet-balance-unstaked">
|
||||||
|
<WalletCardRow label={t('unstaked')} value={unstaked} dark={true} />
|
||||||
|
</div>
|
||||||
{delegatedNodes.length ? (
|
{delegatedNodes.length ? (
|
||||||
<WalletCardRow label={t('stakedValidators')} dark={true} bold={true} />
|
<WalletCardRow label={t('stakedValidators')} dark={true} bold={true} />
|
||||||
) : null}
|
) : null}
|
||||||
{delegatedNodes.map((d) => (
|
{delegatedNodes.map((d) => (
|
||||||
<div key={d.nodeId}>
|
<div key={d.nodeId} data-testid="vega-wallet-balance-staked-validators">
|
||||||
{d.currentEpochStake && d.currentEpochStake.isGreaterThan(0) && (
|
{d.currentEpochStake && d.currentEpochStake.isGreaterThan(0) && (
|
||||||
<WalletCardRow
|
<div data-testid="vega-wallet-balance-this-epoch">
|
||||||
label={`${d.name || truncateMiddle(d.nodeId)} ${
|
<WalletCardRow
|
||||||
d.hasStakePending ? `(${t('thisEpoch')})` : ''
|
label={`${d.name || truncateMiddle(d.nodeId)} ${
|
||||||
}`}
|
d.hasStakePending ? `(${t('thisEpoch')})` : ''
|
||||||
link={`${Routes.STAKING}/${d.nodeId}`}
|
}`}
|
||||||
value={d.currentEpochStake}
|
link={`${Routes.STAKING}/${d.nodeId}`}
|
||||||
dark={true}
|
value={d.currentEpochStake}
|
||||||
/>
|
dark={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{d.hasStakePending && (
|
{d.hasStakePending && (
|
||||||
<WalletCardRow
|
<div data-testid="vega-wallet-balance-next-epoch">
|
||||||
label={`${d.name || truncateMiddle(d.nodeId)} (${t(
|
<WalletCardRow
|
||||||
'nextEpoch'
|
label={`${d.name || truncateMiddle(d.nodeId)} (${t(
|
||||||
)})`}
|
'nextEpoch'
|
||||||
link={`${Routes.STAKING}/${d.nodeId}`}
|
)})`}
|
||||||
value={d.nextEpochStake}
|
link={`${Routes.STAKING}/${d.nodeId}`}
|
||||||
dark={true}
|
value={d.nextEpochStake}
|
||||||
/>
|
dark={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -27,6 +27,8 @@ module.exports = defineConfig({
|
|||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
projectId: 'et4snf',
|
projectId: 'et4snf',
|
||||||
defaultCommandTimeout: 10000,
|
defaultCommandTimeout: 10000,
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
TRADING_TEST_VEGA_WALLET_NAME: 'UI_Trading_Test',
|
TRADING_TEST_VEGA_WALLET_NAME: 'UI_Trading_Test',
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||||
import { generateDepositPage } from '../support/mocks/generate-deposit-page';
|
import { generateDepositPage } from '../support/mocks/generate-deposit-page';
|
||||||
|
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
||||||
|
|
||||||
describe('deposit form validation', () => {
|
describe('deposit form validation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.mockWeb3Provider();
|
cy.mockWeb3Provider();
|
||||||
cy.mockGQL((req) => {
|
cy.mockGQL((req) => {
|
||||||
|
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||||
aliasQuery(req, 'DepositPage', generateDepositPage());
|
aliasQuery(req, 'DepositPage', generateDepositPage());
|
||||||
});
|
});
|
||||||
cy.visit('/portfolio/deposit');
|
cy.visit('/portfolio/deposit');
|
||||||
|
@ -32,11 +32,11 @@ describe('home', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.wait('@GQL');
|
|
||||||
|
|
||||||
cy.contains('Loading...').should('be.visible');
|
cy.contains('Loading...').should('be.visible');
|
||||||
cy.contains('Loading...').should('not.exist');
|
cy.contains('Loading...').should('not.exist');
|
||||||
cy.get('main[data-testid="market"]').should('exist'); // Wait for page to be rendered to before checking url
|
cy.wait('@GQL');
|
||||||
|
|
||||||
|
cy.get('main[data-testid="market"]', { timeout: 20000 }).should('exist'); // Wait for page to be rendered to before checking url
|
||||||
|
|
||||||
cy.url().should('include', `/markets/${oldestMarket.id}`); // Should redirect to oldest market
|
cy.url().should('include', `/markets/${oldestMarket.id}`); // Should redirect to oldest market
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,7 @@ describe('markets table', () => {
|
|||||||
const expectedMarketHeaders = [
|
const expectedMarketHeaders = [
|
||||||
'Market',
|
'Market',
|
||||||
'Settlement asset',
|
'Settlement asset',
|
||||||
'State',
|
'Trading mode',
|
||||||
'Best bid',
|
'Best bid',
|
||||||
'Best offer',
|
'Best offer',
|
||||||
'Mark price',
|
'Mark price',
|
||||||
@ -64,44 +64,22 @@ describe('markets table', () => {
|
|||||||
mockTradingPage(req, MarketState.Active);
|
mockTradingPage(req, MarketState.Active);
|
||||||
});
|
});
|
||||||
|
|
||||||
// click on active market
|
// click on market
|
||||||
cy.get('[role="gridcell"][col-id=data]').should('be.visible');
|
cy.get('[role="gridcell"][col-id=data]').should('be.visible');
|
||||||
cy.get('[role="gridcell"][col-id=data]').contains('Active').click();
|
cy.get('[role="gridcell"][col-id=name]').contains('ACTIVE MARKET').click();
|
||||||
|
|
||||||
cy.wait('@Market');
|
cy.wait('@Market');
|
||||||
cy.get('.ag-root-wrapper').should('be.visible');
|
|
||||||
cy.contains('ACTIVE MARKET');
|
cy.contains('ACTIVE MARKET');
|
||||||
cy.url().should('include', '/markets/market-0');
|
cy.url().should('include', '/markets/market-0');
|
||||||
|
verifyMarketSummaryDisplayed();
|
||||||
verifyMarketSummaryDisplayed('Active');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can select a suspended market', () => {
|
function verifyMarketSummaryDisplayed() {
|
||||||
cy.wait('@Markets');
|
|
||||||
cy.get('.ag-root-wrapper').should('be.visible');
|
|
||||||
|
|
||||||
cy.mockGQL((req) => {
|
|
||||||
mockTradingPage(req, MarketState.Suspended);
|
|
||||||
});
|
|
||||||
|
|
||||||
// click on active market
|
|
||||||
cy.get('[role="gridcell"][col-id=data]').should('be.visible');
|
|
||||||
cy.get('[role="gridcell"][col-id=data]').contains('Suspended').click();
|
|
||||||
|
|
||||||
cy.wait('@Market');
|
|
||||||
cy.contains('SUSPENDED MARKET');
|
|
||||||
cy.url().should('include', '/markets/market-1');
|
|
||||||
|
|
||||||
verifyMarketSummaryDisplayed('Suspended');
|
|
||||||
});
|
|
||||||
|
|
||||||
function verifyMarketSummaryDisplayed(expectedMarketState: string) {
|
|
||||||
const marketSummaryBlock = 'market-summary';
|
const marketSummaryBlock = 'market-summary';
|
||||||
const percentageValue = 'price-change-percentage';
|
const percentageValue = 'price-change-percentage';
|
||||||
const priceChangeValue = 'price-change';
|
const priceChangeValue = 'price-change';
|
||||||
const tradingVolume = 'trading-volume';
|
const tradingVolume = 'trading-volume';
|
||||||
const tradingMode = 'trading-mode';
|
const tradingMode = 'trading-mode';
|
||||||
const marketState = 'market-state';
|
|
||||||
|
|
||||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||||
cy.contains('Change (24h)');
|
cy.contains('Change (24h)');
|
||||||
@ -111,8 +89,6 @@ describe('markets table', () => {
|
|||||||
cy.getByTestId(tradingVolume).should('not.be.empty');
|
cy.getByTestId(tradingVolume).should('not.be.empty');
|
||||||
cy.contains('Trading mode');
|
cy.contains('Trading mode');
|
||||||
cy.getByTestId(tradingMode).should('not.be.empty');
|
cy.getByTestId(tradingMode).should('not.be.empty');
|
||||||
cy.contains('State');
|
|
||||||
cy.getByTestId(marketState).should('have.text', expectedMarketState);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { aliasQuery } from '@vegaprotocol/cypress';
|
|||||||
import { generateFill, generateFills } from '../support/mocks/generate-fills';
|
import { generateFill, generateFills } from '../support/mocks/generate-fills';
|
||||||
import { Side } from '@vegaprotocol/types';
|
import { Side } from '@vegaprotocol/types';
|
||||||
import { connectVegaWallet } from '../support/vega-wallet';
|
import { connectVegaWallet } from '../support/vega-wallet';
|
||||||
|
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
||||||
|
|
||||||
describe('fills', () => {
|
describe('fills', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
@ -45,7 +46,7 @@ describe('fills', () => {
|
|||||||
];
|
];
|
||||||
const result = generateFills({
|
const result = generateFills({
|
||||||
party: {
|
party: {
|
||||||
tradesPaged: {
|
tradesConnection: {
|
||||||
edges: fills.map((f, i) => {
|
edges: fills.map((f, i) => {
|
||||||
return {
|
return {
|
||||||
__typename: 'TradeEdge',
|
__typename: 'TradeEdge',
|
||||||
@ -58,6 +59,7 @@ describe('fills', () => {
|
|||||||
});
|
});
|
||||||
cy.mockGQL((req) => {
|
cy.mockGQL((req) => {
|
||||||
aliasQuery(req, 'Fills', result);
|
aliasQuery(req, 'Fills', result);
|
||||||
|
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||||
});
|
});
|
||||||
cy.visit('/portfolio');
|
cy.visit('/portfolio');
|
||||||
cy.get('main[data-testid="portfolio"]').should('exist');
|
cy.get('main[data-testid="portfolio"]').should('exist');
|
||||||
@ -65,7 +67,7 @@ describe('fills', () => {
|
|||||||
|
|
||||||
it('renders fills', () => {
|
it('renders fills', () => {
|
||||||
cy.getByTestId('Fills').click();
|
cy.getByTestId('Fills').click();
|
||||||
cy.getByTestId('tab-fills').contains('Please connect Vega wallet');
|
cy.getByTestId('tab-fills').contains('Connect your Vega wallet');
|
||||||
|
|
||||||
connectVegaWallet();
|
connectVegaWallet();
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('accounts', () => {
|
describe('accounts', () => {
|
||||||
it('renders accounts', () => {
|
it('renders accounts', () => {
|
||||||
cy.getByTestId('Accounts').click();
|
cy.getByTestId('Collateral').click();
|
||||||
cy.getByTestId('tab-accounts').contains('Please connect Vega wallet');
|
cy.getByTestId('tab-accounts').contains('Please connect Vega wallet');
|
||||||
|
|
||||||
connectVegaWallet();
|
connectVegaWallet();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||||
|
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
||||||
import { generateWithdrawPageQuery } from '../support/mocks/generate-withdraw-page-query';
|
import { generateWithdrawPageQuery } from '../support/mocks/generate-withdraw-page-query';
|
||||||
import { connectVegaWallet } from '../support/vega-wallet';
|
import { connectVegaWallet } from '../support/vega-wallet';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ describe('withdraw', () => {
|
|||||||
cy.mockWeb3Provider();
|
cy.mockWeb3Provider();
|
||||||
cy.mockGQL((req) => {
|
cy.mockGQL((req) => {
|
||||||
aliasQuery(req, 'WithdrawPageQuery', generateWithdrawPageQuery());
|
aliasQuery(req, 'WithdrawPageQuery', generateWithdrawPageQuery());
|
||||||
|
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||||
});
|
});
|
||||||
cy.visit('/portfolio/withdraw');
|
cy.visit('/portfolio/withdraw');
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||||
|
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
||||||
import { generateWithdrawals } from '../support/mocks/generate-withdrawals';
|
import { generateWithdrawals } from '../support/mocks/generate-withdrawals';
|
||||||
import { connectVegaWallet } from '../support/vega-wallet';
|
import { connectVegaWallet } from '../support/vega-wallet';
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ describe('withdrawals', () => {
|
|||||||
cy.mockWeb3Provider();
|
cy.mockWeb3Provider();
|
||||||
cy.mockGQL((req) => {
|
cy.mockGQL((req) => {
|
||||||
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
||||||
|
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||||
});
|
});
|
||||||
cy.visit('/portfolio/withdrawals');
|
cy.visit('/portfolio/withdrawals');
|
||||||
|
|
||||||
@ -16,8 +18,6 @@ describe('withdrawals', () => {
|
|||||||
|
|
||||||
// It also requires connection Ethereum wallet
|
// It also requires connection Ethereum wallet
|
||||||
connectEthereumWallet();
|
connectEthereumWallet();
|
||||||
|
|
||||||
cy.contains('Withdrawals');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders history of withdrawals', () => {
|
it('renders history of withdrawals', () => {
|
||||||
@ -27,7 +27,6 @@ describe('withdrawals', () => {
|
|||||||
const etherScanLink = `${Cypress.env(
|
const etherScanLink = `${Cypress.env(
|
||||||
'ETHERSCAN_URL'
|
'ETHERSCAN_URL'
|
||||||
)}/tx/0x5d7b1a35ba6bd23be17bb7a159c13cdbb3121fceb94e9c6c510f5503dce48d03`;
|
)}/tx/0x5d7b1a35ba6bd23be17bb7a159c13cdbb3121fceb94e9c6c510f5503dce48d03`;
|
||||||
cy.contains('Withdrawals');
|
|
||||||
|
|
||||||
const row = '.ag-center-cols-container[role="rowgroup"] > [role="row"]';
|
const row = '.ag-center-cols-container[role="rowgroup"] > [role="row"]';
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import type {
|
import type {
|
||||||
Fills,
|
Fills,
|
||||||
Fills_party_tradesPaged_edges_node,
|
Fills_party_tradesConnection_edges_node,
|
||||||
} from '@vegaprotocol/fills';
|
} from '@vegaprotocol/fills';
|
||||||
import { Side } from '@vegaprotocol/types';
|
import { Side } from '@vegaprotocol/types';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||||
const fills: Fills_party_tradesPaged_edges_node[] = [
|
const fills: Fills_party_tradesConnection_edges_node[] = [
|
||||||
generateFill({
|
generateFill({
|
||||||
buyer: {
|
buyer: {
|
||||||
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
@ -49,7 +49,7 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
const defaultResult: Fills = {
|
const defaultResult: Fills = {
|
||||||
party: {
|
party: {
|
||||||
id: 'buyer-id',
|
id: 'buyer-id',
|
||||||
tradesPaged: {
|
tradesConnection: {
|
||||||
__typename: 'TradeConnection',
|
__typename: 'TradeConnection',
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
edges: fills.map((f) => {
|
edges: fills.map((f) => {
|
||||||
@ -73,9 +73,9 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateFill = (
|
export const generateFill = (
|
||||||
override?: PartialDeep<Fills_party_tradesPaged_edges_node>
|
override?: PartialDeep<Fills_party_tradesConnection_edges_node>
|
||||||
) => {
|
) => {
|
||||||
const defaultFill: Fills_party_tradesPaged_edges_node = {
|
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
||||||
__typename: 'Trade',
|
__typename: 'Trade',
|
||||||
id: '0',
|
id: '0',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { NetworkParamsQuery } from '@vegaprotocol/web3';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
|
export const generateNetworkParameters = (
|
||||||
|
override?: PartialDeep<NetworkParamsQuery>
|
||||||
|
): NetworkParamsQuery => {
|
||||||
|
const defaultResult: NetworkParamsQuery = {
|
||||||
|
networkParameters: [
|
||||||
|
{
|
||||||
|
__typename: 'NetworkParameter',
|
||||||
|
key: 'blockchains.ethereumConfig',
|
||||||
|
value: JSON.stringify({
|
||||||
|
network_id: '3',
|
||||||
|
chain_id: '3',
|
||||||
|
collateral_bridge_contract: {
|
||||||
|
address: '0x947893AaA0A7b55f66990b3B4781514b691Fdd4a',
|
||||||
|
},
|
||||||
|
multisig_control_contract: {
|
||||||
|
address: '0xaE15126d2d1fAbF7cfA7cAD3cbD4921DfC87F620',
|
||||||
|
deployment_block_height: 12341882,
|
||||||
|
},
|
||||||
|
staking_bridge_contract: {
|
||||||
|
address: '0x7896C9491962D5839783CB6e0492ECebd34Bb35F',
|
||||||
|
deployment_block_height: 11177313,
|
||||||
|
},
|
||||||
|
token_vesting_contract: {
|
||||||
|
address: '0x9F10cBeEf03A564Fb914c2010c0Cd55E9BB11406',
|
||||||
|
deployment_block_height: 11177353,
|
||||||
|
},
|
||||||
|
confirmations: 3,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return merge(defaultResult, override);
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=TESTNET
|
NX_VEGA_ENV=TESTNET
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
|
||||||
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=TESTNET
|
NX_VEGA_ENV=TESTNET
|
||||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
|
||||||
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||||
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
|
@ -10,7 +10,7 @@ export const Navbar = () => {
|
|||||||
<Link href="/" passHref={true}>
|
<Link href="/" passHref={true}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
<a className="px-[26px]">
|
<a className="px-[26px]">
|
||||||
<Vega className="fill-black dark:fill-white" />
|
<Vega className="fill-white" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{[
|
{[
|
||||||
@ -39,7 +39,7 @@ const NavLink = ({ name, path, exact, testId = name }: NavLinkProps) => {
|
|||||||
'uppercase xs:text-ui sm:text-body-large md:text-h5 lg:text-h4',
|
'uppercase xs:text-ui sm:text-body-large md:text-h5 lg:text-h4',
|
||||||
{
|
{
|
||||||
'bg-vega-pink dark:bg-vega-yellow text-white dark:text-black': isActive,
|
'bg-vega-pink dark:bg-vega-yellow text-white dark:text-black': isActive,
|
||||||
'text-black dark:text-white': !isActive,
|
'text-white': !isActive,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@ -24,12 +24,14 @@ export const VegaWalletConnectButton = ({
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{isConnected && (
|
{isConnected && (
|
||||||
<span className="text-ui-small font-mono mr-2">Vega key:</span>
|
<span className="text-ui-small font-mono mr-2 text-white-90">
|
||||||
|
Vega key:
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
data-testid={isConnected ? 'manage-vega-wallet' : 'connect-vega-wallet'}
|
data-testid={isConnected ? 'manage-vega-wallet' : 'connect-vega-wallet'}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className="ml-auto inline-block text-ui-small font-mono hover:underline"
|
className="ml-auto inline-block text-ui-small font-mono hover:underline text-white-90"
|
||||||
>
|
>
|
||||||
{isConnected ? truncateByChars(keypair.pub) : 'Connect Vega wallet'}
|
{isConnected ? truncateByChars(keypair.pub) : 'Connect Vega wallet'}
|
||||||
</button>
|
</button>
|
||||||
|
@ -43,6 +43,7 @@ const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
|||||||
|
|
||||||
const mockEnvironment = {
|
const mockEnvironment = {
|
||||||
VEGA_ENV: 'TESTNET',
|
VEGA_ENV: 'TESTNET',
|
||||||
|
VEGA_URL: 'https://vega-node.url',
|
||||||
VEGA_NETWORKS: JSON.stringify({}),
|
VEGA_NETWORKS: JSON.stringify({}),
|
||||||
GIT_BRANCH: 'test',
|
GIT_BRANCH: 'test',
|
||||||
GIT_COMMIT_HASH: 'abcdef',
|
GIT_COMMIT_HASH: 'abcdef',
|
||||||
|
@ -28,9 +28,9 @@ function AppBody({ Component, pageProps }: AppProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={theme}>
|
<ThemeContext.Provider value={theme}>
|
||||||
<div className="h-full dark:bg-black dark:text-white-60 bg-white relative z-0 text-black-60 grid grid-rows-[min-content,1fr]">
|
<div className="h-full text-white relative text-black-60 dark:text-white-60 z-0 grid grid-rows-[min-content,1fr]">
|
||||||
<AppLoader>
|
<AppLoader>
|
||||||
<div className="flex items-stretch border-b-[7px] border-vega-pink dark:border-vega-yellow">
|
<div className="flex items-stretch border-b-[7px] bg-black border-vega-pink dark:border-vega-yellow">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="flex items-center gap-4 ml-auto mr-8">
|
<div className="flex items-center gap-4 ml-auto mr-8">
|
||||||
<VegaWalletConnectButton
|
<VegaWalletConnectButton
|
||||||
|
@ -20,6 +20,7 @@ const MARKET_QUERY = gql`
|
|||||||
tradingMode
|
tradingMode
|
||||||
state
|
state
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
data {
|
data {
|
||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
@ -31,6 +32,7 @@ const MARKET_QUERY = gql`
|
|||||||
bestStaticBidVolume
|
bestStaticBidVolume
|
||||||
bestStaticOfferVolume
|
bestStaticOfferVolume
|
||||||
indicativeVolume
|
indicativeVolume
|
||||||
|
trigger
|
||||||
}
|
}
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
|
12
apps/trading/pages/markets/__generated__/Market.ts
generated
12
apps/trading/pages/markets/__generated__/Market.ts
generated
@ -3,7 +3,7 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { Interval, MarketTradingMode, MarketState } from "@vegaprotocol/types";
|
import { Interval, MarketTradingMode, MarketState, AuctionTrigger } from "@vegaprotocol/types";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: Market
|
// GraphQL query operation: Market
|
||||||
@ -47,6 +47,10 @@ export interface Market_market_data {
|
|||||||
* the aggregated volume being offered at the best static offer price, excluding pegged orders.
|
* the aggregated volume being offered at the best static offer price, excluding pegged orders.
|
||||||
*/
|
*/
|
||||||
bestStaticOfferVolume: string;
|
bestStaticOfferVolume: string;
|
||||||
|
/**
|
||||||
|
* what triggered an auction (if an auction was started)
|
||||||
|
*/
|
||||||
|
trigger: AuctionTrigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Market_market_tradableInstrument_instrument_metadata {
|
export interface Market_market_tradableInstrument_instrument_metadata {
|
||||||
@ -144,6 +148,12 @@ export interface Market_market {
|
|||||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||||
*/
|
*/
|
||||||
decimalPlaces: number;
|
decimalPlaces: number;
|
||||||
|
/**
|
||||||
|
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
|
||||||
|
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
|
||||||
|
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
|
||||||
|
*/
|
||||||
|
positionDecimalPlaces: number;
|
||||||
/**
|
/**
|
||||||
* marketData for the given market
|
* marketData for the given market
|
||||||
*/
|
*/
|
||||||
|
@ -11,7 +11,11 @@ import { TradesContainer } from '@vegaprotocol/trades';
|
|||||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||||
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
||||||
import type { Market_market } from './__generated__/Market';
|
import type { Market_market } from './__generated__/Market';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatLabel,
|
||||||
|
t,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||||
@ -23,17 +27,19 @@ import {
|
|||||||
PriceCellChange,
|
PriceCellChange,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { CandleClose } from '@vegaprotocol/types';
|
import type { CandleClose } from '@vegaprotocol/types';
|
||||||
|
import { AuctionTrigger } from '@vegaprotocol/types';
|
||||||
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
|
||||||
const TradingViews = {
|
const TradingViews = {
|
||||||
Candles: CandlesChartContainer,
|
Candles: CandlesChartContainer,
|
||||||
Depth: DepthChartContainer,
|
Depth: DepthChartContainer,
|
||||||
Ticket: DealTicketContainer,
|
Ticket: DealTicketContainer,
|
||||||
Orderbook: OrderbookContainer,
|
|
||||||
Orders: OrderListContainer,
|
|
||||||
Positions: PositionsContainer,
|
|
||||||
Accounts: AccountsContainer,
|
|
||||||
Trades: TradesContainer,
|
|
||||||
Info: MarketInfoContainer,
|
Info: MarketInfoContainer,
|
||||||
|
Orderbook: OrderbookContainer,
|
||||||
|
Trades: TradesContainer,
|
||||||
|
Positions: PositionsContainer,
|
||||||
|
Orders: OrderListContainer,
|
||||||
|
Collateral: AccountsContainer,
|
||||||
};
|
};
|
||||||
|
|
||||||
type TradingView = keyof typeof TradingViews;
|
type TradingView = keyof typeof TradingViews;
|
||||||
@ -53,9 +59,9 @@ export const TradeMarketHeader = ({
|
|||||||
.filter((c): c is CandleClose => c !== null);
|
.filter((c): c is CandleClose => c !== null);
|
||||||
const headerItemClassName = 'whitespace-nowrap flex flex-col';
|
const headerItemClassName = 'whitespace-nowrap flex flex-col';
|
||||||
const itemClassName =
|
const itemClassName =
|
||||||
'font-sans font-normal mb-0 text-dark/80 dark:text-white/80 text-ui-small';
|
'font-sans font-normal mb-0 text-black-60 dark:text-white-80 text-ui-small';
|
||||||
const itemValueClassName =
|
const itemValueClassName =
|
||||||
'capitalize font-sans tracking-tighter text-black dark:text-white text-ui';
|
'font-sans tracking-tighter text-black dark:text-white text-ui';
|
||||||
const headerClassName = classNames(
|
const headerClassName = classNames(
|
||||||
'w-full p-8 bg-white dark:bg-black',
|
'w-full p-8 bg-white dark:bg-black',
|
||||||
className
|
className
|
||||||
@ -66,7 +72,7 @@ export const TradeMarketHeader = ({
|
|||||||
<div className="flex flex-col md:flex-row gap-20 md:gap-64 ml-auto mr-8">
|
<div className="flex flex-col md:flex-row gap-20 md:gap-64 ml-auto mr-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
className="shrink-0 dark:text-vega-yellow text-black text-h5 flex items-center gap-8 px-4 py-0 h-37 hover:bg-black/20 dark:hover:bg-white/20"
|
className="shrink-0 text-vega-pink dark:text-vega-yellow font-medium text-h5 flex items-center gap-8 px-4 py-0 h-37 hover:bg-black/10 dark:hover:bg-white/20"
|
||||||
>
|
>
|
||||||
<span className="break-words text-left">{market.name}</span>
|
<span className="break-words text-left">{market.name}</span>
|
||||||
<ArrowDown color="yellow" borderX={8} borderTop={12} />
|
<ArrowDown color="yellow" borderX={8} borderTop={12} />
|
||||||
@ -74,7 +80,7 @@ export const TradeMarketHeader = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
data-testid="market-summary"
|
data-testid="market-summary"
|
||||||
className="flex flex-auto items-start gap-64 overflow-x-auto whitespace-nowrap w-[400px]"
|
className="flex flex-auto items-start gap-64 overflow-x-auto whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<div className={headerItemClassName}>
|
<div className={headerItemClassName}>
|
||||||
<span className={itemClassName}>Change (24h)</span>
|
<span className={itemClassName}>Change (24h)</span>
|
||||||
@ -87,20 +93,23 @@ export const TradeMarketHeader = ({
|
|||||||
<span className={itemClassName}>Volume</span>
|
<span className={itemClassName}>Volume</span>
|
||||||
<span data-testid="trading-volume" className={itemValueClassName}>
|
<span data-testid="trading-volume" className={itemValueClassName}>
|
||||||
{market.data && market.data.indicativeVolume !== '0'
|
{market.data && market.data.indicativeVolume !== '0'
|
||||||
? market.data.indicativeVolume
|
? addDecimalsFormatNumber(
|
||||||
|
market.data.indicativeVolume,
|
||||||
|
market.positionDecimalPlaces
|
||||||
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={headerItemClassName}>
|
<div className={headerItemClassName}>
|
||||||
<span className={itemClassName}>Trading mode</span>
|
<span className={itemClassName}>Trading mode</span>
|
||||||
<span data-testid="trading-mode" className={itemValueClassName}>
|
<span data-testid="trading-mode" className={itemValueClassName}>
|
||||||
{market.tradingMode}
|
{market.tradingMode === MarketTradingMode.MonitoringAuction &&
|
||||||
</span>
|
market.data?.trigger &&
|
||||||
</div>
|
market.data.trigger !== AuctionTrigger.Unspecified
|
||||||
<div className={headerItemClassName}>
|
? `${formatLabel(
|
||||||
<span className={itemClassName}>State</span>
|
market.tradingMode
|
||||||
<span data-testid="market-state" className={itemValueClassName}>
|
)} - ${market.data?.trigger.toLowerCase()}`
|
||||||
{market.state}
|
: formatLabel(market.tradingMode)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,8 +125,8 @@ interface TradeGridProps {
|
|||||||
export const TradeGrid = ({ market }: TradeGridProps) => {
|
export const TradeGrid = ({ market }: TradeGridProps) => {
|
||||||
const wrapperClasses = classNames(
|
const wrapperClasses = classNames(
|
||||||
'h-full max-h-full',
|
'h-full max-h-full',
|
||||||
'grid gap-4 grid-cols-[1fr_375px_460px] grid-rows-[min-content_1fr_300px]',
|
'grid gap-x-8 gap-y-4 grid-cols-[1fr_325px_425px] grid-rows-[min-content_1fr_200px]',
|
||||||
'bg-black-10 dark:bg-white-10',
|
'bg-black-10 dark:bg-black-70',
|
||||||
'text-ui'
|
'text-ui'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -150,24 +159,24 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
|
|||||||
</TradeGridChild>
|
</TradeGridChild>
|
||||||
<TradeGridChild className="row-start-2 row-end-3 col-start-3 col-end-4">
|
<TradeGridChild className="row-start-2 row-end-3 col-start-3 col-end-4">
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab id="trades" name={t('Trades')}>
|
|
||||||
<TradingViews.Trades marketId={market.id} />
|
|
||||||
</Tab>
|
|
||||||
<Tab id="orderbook" name={t('Orderbook')}>
|
<Tab id="orderbook" name={t('Orderbook')}>
|
||||||
<TradingViews.Orderbook marketId={market.id} />
|
<TradingViews.Orderbook marketId={market.id} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab id="trades" name={t('Trades')}>
|
||||||
|
<TradingViews.Trades marketId={market.id} />
|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</TradeGridChild>
|
</TradeGridChild>
|
||||||
<TradeGridChild className="col-span-3">
|
<TradeGridChild className="col-span-3">
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab id="orders" name={t('Orders')}>
|
|
||||||
<TradingViews.Orders />
|
|
||||||
</Tab>
|
|
||||||
<Tab id="positions" name={t('Positions')}>
|
<Tab id="positions" name={t('Positions')}>
|
||||||
<TradingViews.Positions />
|
<TradingViews.Positions />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="accounts" name={t('Accounts')}>
|
<Tab id="orders" name={t('Orders')}>
|
||||||
<TradingViews.Accounts />
|
<TradingViews.Orders />
|
||||||
|
</Tab>
|
||||||
|
<Tab id="accounts" name={t('Collateral')}>
|
||||||
|
<TradingViews.Collateral />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</TradeGridChild>
|
</TradeGridChild>
|
||||||
|
15
apps/trading/pages/portfolio/deposits-container.tsx
Normal file
15
apps/trading/pages/portfolio/deposits-container.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export const DepositsContainer = () => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[1fr_min-content] gap-12 h-full">
|
||||||
|
<div />
|
||||||
|
<div className="p-12">
|
||||||
|
<AnchorButton data-testid="deposit" href="/portfolio/deposit">
|
||||||
|
{t('Deposit')}
|
||||||
|
</AnchorButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -3,11 +3,13 @@ import { t } from '@vegaprotocol/react-helpers';
|
|||||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
import { OrderListContainer } from '@vegaprotocol/orders';
|
||||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||||
import { AnchorButton, Tab, Tabs } from '@vegaprotocol/ui-toolkit';
|
import { Tab, Tabs } from '@vegaprotocol/ui-toolkit';
|
||||||
import { WithdrawalsContainer } from './withdrawals/withdrawals-container';
|
import { WithdrawalsContainer } from './withdrawals-container';
|
||||||
import { FillsContainer } from '@vegaprotocol/fills';
|
import { FillsContainer } from '@vegaprotocol/fills';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||||
|
import { DepositsContainer } from './deposits-container';
|
||||||
|
|
||||||
const Portfolio = () => {
|
const Portfolio = () => {
|
||||||
const wrapperClasses = classNames(
|
const wrapperClasses = classNames(
|
||||||
@ -22,54 +24,58 @@ const Portfolio = () => {
|
|||||||
<PortfolioGridChild>
|
<PortfolioGridChild>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab id="positions" name={t('Positions')}>
|
<Tab id="positions" name={t('Positions')}>
|
||||||
<div className={tabContentClassName}>
|
<VegaWalletContainer>
|
||||||
<h4 className="text-h4 text-black dark:text-white p-8">
|
<div className={tabContentClassName}>
|
||||||
{t('Positions')}
|
<h4 className="text-h4 text-black dark:text-white p-8">
|
||||||
</h4>
|
{t('Positions')}
|
||||||
<div>
|
</h4>
|
||||||
<PositionsContainer />
|
<div>
|
||||||
|
<PositionsContainer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="orders" name={t('Orders')}>
|
<Tab id="orders" name={t('Orders')}>
|
||||||
<div className={tabContentClassName}>
|
<VegaWalletContainer>
|
||||||
<h4 className="text-h4 text-black dark:text-white p-8">
|
<div className={tabContentClassName}>
|
||||||
{t('Orders')}
|
<h4 className="text-h4 text-black dark:text-white p-8">
|
||||||
</h4>
|
{t('Orders')}
|
||||||
<div>
|
</h4>
|
||||||
<OrderListContainer />
|
<div>
|
||||||
|
<OrderListContainer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="fills" name={t('Fills')}>
|
<Tab id="fills" name={t('Fills')}>
|
||||||
<div className={tabContentClassName}>
|
<VegaWalletContainer>
|
||||||
<h4 className="text-h4 text-black dark:text-white p-8">
|
<div className={tabContentClassName}>
|
||||||
{t('Fills')}
|
<h4 className="text-h4 text-black dark:text-white p-8">
|
||||||
</h4>
|
{t('Fills')}
|
||||||
<div>
|
</h4>
|
||||||
<FillsContainer />
|
<div>
|
||||||
|
<FillsContainer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</PortfolioGridChild>
|
</PortfolioGridChild>
|
||||||
<PortfolioGridChild>
|
<PortfolioGridChild>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab id="collateral" name={t('Collateral')}>
|
<Tab id="collateral" name={t('Collateral')}>
|
||||||
<AccountsContainer />
|
<VegaWalletContainer>
|
||||||
|
<AccountsContainer />
|
||||||
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="deposits" name={t('Deposits')}>
|
<Tab id="deposits" name={t('Deposits')}>
|
||||||
<div className={tabContentClassName}>
|
<DepositsContainer />
|
||||||
<div className="p-8">
|
|
||||||
<AnchorButton data-testid="deposit" href="/portfolio/deposit">
|
|
||||||
{t('Deposit')}
|
|
||||||
</AnchorButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="withdrawals" name={t('Withdrawals')}>
|
<Tab id="withdrawals" name={t('Withdrawals')}>
|
||||||
<Web3Container>
|
<Web3Container>
|
||||||
<WithdrawalsContainer />
|
<VegaWalletContainer>
|
||||||
|
<WithdrawalsContainer />
|
||||||
|
</VegaWalletContainer>
|
||||||
</Web3Container>
|
</Web3Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useWithdrawals, WithdrawalsTable } from '@vegaprotocol/withdraws';
|
import { useWithdrawals, WithdrawalsTable } from '@vegaprotocol/withdraws';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const WithdrawalsContainer = () => {
|
export const WithdrawalsContainer = () => {
|
||||||
const { data, loading, error } = useWithdrawals();
|
const { data, loading, error } = useWithdrawals();
|
||||||
@ -16,7 +17,16 @@ export const WithdrawalsContainer = () => {
|
|||||||
(w) => new Date(w.createdTimestamp).getTime(),
|
(w) => new Date(w.createdTimestamp).getTime(),
|
||||||
'desc'
|
'desc'
|
||||||
);
|
);
|
||||||
return <WithdrawalsTable withdrawals={withdrawals} />;
|
return (
|
||||||
|
<div className="grid grid-cols-[1fr_min-content] gap-12 h-full">
|
||||||
|
<WithdrawalsTable withdrawals={withdrawals} />
|
||||||
|
<div className="p-12">
|
||||||
|
<Link href="/portfolio/withdraw" passHref={true}>
|
||||||
|
<Button data-testid="start-withdrawal">Withdraw</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
@ -1,25 +1,12 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
|
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
|
||||||
import { Web3Container } from '../../../components/web3-container';
|
import { Web3Container } from '../../../components/web3-container';
|
||||||
import { WithdrawalsContainer } from './withdrawals-container';
|
import { WithdrawalsContainer } from '../withdrawals-container';
|
||||||
|
|
||||||
const Withdrawals = () => {
|
const Withdrawals = () => {
|
||||||
return (
|
return (
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<Web3Container>
|
<Web3Container>
|
||||||
<div className="h-full grid grid grid-rows-[min-content,1fr]">
|
<WithdrawalsContainer />
|
||||||
<header className="flex justify-between p-24">
|
|
||||||
<h1 className="text-h3">{t('Withdrawals')}</h1>
|
|
||||||
<AnchorButton
|
|
||||||
href="/portfolio/withdraw"
|
|
||||||
data-testid="start-withdrawal"
|
|
||||||
>
|
|
||||||
{t('Start withdrawal')}
|
|
||||||
</AnchorButton>
|
|
||||||
</header>
|
|
||||||
<WithdrawalsContainer />
|
|
||||||
</div>
|
|
||||||
</Web3Container>
|
</Web3Container>
|
||||||
</VegaWalletContainer>
|
</VegaWalletContainer>
|
||||||
);
|
);
|
||||||
|
@ -32,7 +32,7 @@ export const DealTicketLimitAmount = ({
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
<div>@</div>
|
<div className="pt-4 text-black dark:text-white">@</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormGroup
|
<FormGroup
|
||||||
labelFor="input-price-quote"
|
labelFor="input-price-quote"
|
||||||
|
@ -33,8 +33,11 @@ export const DealTicketMarketAmount = ({
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4">@</div>
|
<div className="pt-4 text-black dark:text-white">@</div>
|
||||||
<div className="flex-1 pt-4" data-testid="last-price">
|
<div
|
||||||
|
className="flex-1 pt-4 text-black dark:text-white"
|
||||||
|
data-testid="last-price"
|
||||||
|
>
|
||||||
{price && quoteName ? (
|
{price && quoteName ? (
|
||||||
<>
|
<>
|
||||||
~{price} {quoteName}
|
~{price} {quoteName}
|
||||||
|
@ -4,7 +4,11 @@ import {
|
|||||||
VegaWalletOrderType,
|
VegaWalletOrderType,
|
||||||
VegaWalletOrderTimeInForce,
|
VegaWalletOrderTimeInForce,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
t,
|
||||||
|
toDecimal,
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
|
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
|
||||||
import { TypeSelector } from './type-selector';
|
import { TypeSelector } from './type-selector';
|
||||||
import { SideSelector } from './side-selector';
|
import { SideSelector } from './side-selector';
|
||||||
@ -43,26 +47,26 @@ export const DealTicket = ({
|
|||||||
const step = toDecimal(market.positionDecimalPlaces);
|
const step = toDecimal(market.positionDecimalPlaces);
|
||||||
const orderType = watch('type');
|
const orderType = watch('type');
|
||||||
const orderTimeInForce = watch('timeInForce');
|
const orderTimeInForce = watch('timeInForce');
|
||||||
const invalidText = useOrderValidation({
|
const { message, isDisabled: disabled } = useOrderValidation({
|
||||||
step,
|
step,
|
||||||
market,
|
market,
|
||||||
orderType,
|
orderType,
|
||||||
orderTimeInForce,
|
orderTimeInForce,
|
||||||
fieldErrors: errors,
|
fieldErrors: errors,
|
||||||
});
|
});
|
||||||
const isDisabled = transactionStatus === 'pending' || Boolean(invalidText);
|
const isDisabled = transactionStatus === 'pending' || disabled;
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(order: Order) => {
|
(order: Order) => {
|
||||||
if (!isDisabled && !invalidText) {
|
if (!isDisabled) {
|
||||||
submit(order);
|
submit(order);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isDisabled, invalidText, submit]
|
[isDisabled, submit]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="px-4 py-8" noValidate>
|
<form onSubmit={handleSubmit(onSubmit)} className="px-12 py-8" noValidate>
|
||||||
<Controller
|
<Controller
|
||||||
name="type"
|
name="type"
|
||||||
control={control}
|
control={control}
|
||||||
@ -83,7 +87,10 @@ export const DealTicket = ({
|
|||||||
register={register}
|
register={register}
|
||||||
price={
|
price={
|
||||||
market.depth.lastTrade
|
market.depth.lastTrade
|
||||||
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
|
? addDecimalsFormatNumber(
|
||||||
|
market.depth.lastTrade.price,
|
||||||
|
market.decimalPlaces
|
||||||
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
quoteName={market.tradableInstrument.instrument.product.quoteName}
|
quoteName={market.tradableInstrument.instrument.product.quoteName}
|
||||||
@ -118,9 +125,12 @@ export const DealTicket = ({
|
|||||||
>
|
>
|
||||||
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
|
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
|
||||||
</Button>
|
</Button>
|
||||||
{invalidText && (
|
{message && (
|
||||||
<InputError className="mb-8" data-testid="dealticket-error-message">
|
<InputError
|
||||||
{invalidText}
|
className="mt-12 mb-12"
|
||||||
|
data-testid="dealticket-error-message"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
@ -134,7 +134,7 @@ export const MarketInfoContainer = ({ marketId }: MarketInfoContainerProps) => {
|
|||||||
|
|
||||||
export const Info = ({ market }: InfoProps) => {
|
export const Info = ({ market }: InfoProps) => {
|
||||||
const headerClassName =
|
const headerClassName =
|
||||||
'text-h5 font-bold uppercase text-black dark:text-white';
|
'text-h5 font-medium uppercase text-black dark:text-white';
|
||||||
const marketDataPanels = [
|
const marketDataPanels = [
|
||||||
{
|
{
|
||||||
title: t('Current fees'),
|
title: t('Current fees'),
|
||||||
|
@ -8,12 +8,19 @@ interface DepositLimitsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DepositLimits = ({ limits }: DepositLimitsProps) => {
|
export const DepositLimits = ({ limits }: DepositLimitsProps) => {
|
||||||
const maxLimit = limits.max.isEqualTo(Infinity)
|
let maxLimit = '';
|
||||||
? t('No limit')
|
|
||||||
: limits.max.toString();
|
if (limits.max.isEqualTo(Infinity)) {
|
||||||
|
maxLimit = t('No limit');
|
||||||
|
} else if (limits.max.isGreaterThan(1_000_000)) {
|
||||||
|
maxLimit = t('1m+');
|
||||||
|
} else {
|
||||||
|
maxLimit = limits.max.toString();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="text-ui font-bold">{t('Temporary deposit limits')}</p>
|
<p className="text-ui font-bold">{t('Deposit limits')}</p>
|
||||||
<table className="w-full text-ui">
|
<table className="w-full text-ui">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -77,7 +77,7 @@ export const DepositManager = ({
|
|||||||
const allowance = useGetAllowance(tokenContract, decimals);
|
const allowance = useGetAllowance(tokenContract, decimals);
|
||||||
|
|
||||||
// Set up approve transaction
|
// Set up approve transaction
|
||||||
const approve = useSubmitApproval(tokenContract);
|
const approve = useSubmitApproval(tokenContract, decimals);
|
||||||
|
|
||||||
// Set up deposit transaction
|
// Set up deposit transaction
|
||||||
const { confirmationEvent, ...deposit } = useSubmitDeposit();
|
const { confirmationEvent, ...deposit } = useSubmitDeposit();
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
|
import { removeDecimal } from '@vegaprotocol/react-helpers';
|
||||||
import type { Token } from '@vegaprotocol/smart-contracts';
|
import type { Token } from '@vegaprotocol/smart-contracts';
|
||||||
import { useEthereumConfig, useEthereumTransaction } from '@vegaprotocol/web3';
|
import { useEthereumConfig, useEthereumTransaction } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
export const useSubmitApproval = (contract: Token | null) => {
|
export const useSubmitApproval = (
|
||||||
|
contract: Token | null,
|
||||||
|
decimals: number | undefined
|
||||||
|
) => {
|
||||||
const { config } = useEthereumConfig();
|
const { config } = useEthereumConfig();
|
||||||
|
|
||||||
const transaction = useEthereumTransaction(() => {
|
const transaction = useEthereumTransaction(() => {
|
||||||
if (!contract || !config) {
|
if (!contract || !config || decimals === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return contract.approve(
|
|
||||||
config.collateral_bridge_contract.address,
|
const amount = removeDecimal('1000000', decimals);
|
||||||
Number.MAX_SAFE_INTEGER.toString()
|
|
||||||
);
|
return contract.approve(config.collateral_bridge_contract.address, amount);
|
||||||
});
|
});
|
||||||
|
|
||||||
return transaction;
|
return transaction;
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from './network-loader';
|
|
1
libs/environment/src/components/network-loader/index.tsx
Normal file
1
libs/environment/src/components/network-loader/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { NetworkLoader } from './network-loader';
|
@ -1,128 +1,8 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ApolloClient } from '@apollo/client';
|
import type { ApolloClient } from '@apollo/client';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
import { ApolloProvider } from '@apollo/client';
|
||||||
import {
|
|
||||||
Callout,
|
|
||||||
Intent,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
Loader,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import { useEnvironment } from '../../hooks';
|
import { useEnvironment } from '../../hooks';
|
||||||
import type { ConfigStatus } from '../../types';
|
|
||||||
|
|
||||||
type MessageComponentProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusMessage = ({ children }: MessageComponentProps) => (
|
|
||||||
<div className="flex items-center fixed bottom-0 right-0 px-16 bg-intent-highlight text-black">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type ErrorComponentProps = MessageComponentProps & {
|
|
||||||
children?: ReactNode;
|
|
||||||
showTryAgain?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Error = ({ children, showTryAgain }: ErrorComponentProps) => (
|
|
||||||
<div>
|
|
||||||
<div className="mb-16">{children}</div>
|
|
||||||
{showTryAgain && (
|
|
||||||
<Button
|
|
||||||
className="mt-8"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
{t('Try again')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type StatusComponentProps = {
|
|
||||||
status: ConfigStatus;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusComponent = ({ status, children }: StatusComponentProps) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'error-loading-config':
|
|
||||||
return (
|
|
||||||
<Callout
|
|
||||||
title={t('Error')}
|
|
||||||
intent={Intent.Danger}
|
|
||||||
iconName="error"
|
|
||||||
iconDescription={t('Error')}
|
|
||||||
children={
|
|
||||||
<Error>
|
|
||||||
{t('There was an error fetching the network configuration.')}
|
|
||||||
</Error>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'error-validating-config':
|
|
||||||
return (
|
|
||||||
<Callout
|
|
||||||
title={t('Error')}
|
|
||||||
intent={Intent.Danger}
|
|
||||||
iconName="error"
|
|
||||||
iconDescription={t('Error')}
|
|
||||||
children={
|
|
||||||
<Error>
|
|
||||||
{t('The network configuration for the app is invalid.')}
|
|
||||||
</Error>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'error-loading-node':
|
|
||||||
return (
|
|
||||||
<Callout
|
|
||||||
title={t('Error')}
|
|
||||||
intent={Intent.Danger}
|
|
||||||
iconName="error"
|
|
||||||
iconDescription={t('Error')}
|
|
||||||
children={
|
|
||||||
<Error showTryAgain>{t('Failed to connect to a data node.')}</Error>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'idle':
|
|
||||||
case 'loading-config':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<StatusMessage>
|
|
||||||
<Loader size="small" forceTheme="light" />
|
|
||||||
<span className="ml-8">{t('Loading configuration...')}</span>
|
|
||||||
</StatusMessage>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'loading-node':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<StatusMessage>
|
|
||||||
<Loader size="small" forceTheme="light" />
|
|
||||||
<span className="ml-8">{t('Finding a node...')}</span>
|
|
||||||
</StatusMessage>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case 'success':
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<StatusMessage>
|
|
||||||
<Icon name="antenna" />
|
|
||||||
<span className="ml-8">{t("You're connected!")}</span>
|
|
||||||
</StatusMessage>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type NetworkLoaderProps<T> = {
|
type NetworkLoaderProps<T> = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@ -135,9 +15,7 @@ export function NetworkLoader<T>({
|
|||||||
children,
|
children,
|
||||||
createClient,
|
createClient,
|
||||||
}: NetworkLoaderProps<T>) {
|
}: NetworkLoaderProps<T>) {
|
||||||
// this is to prevent an error rendering callouts on the server side
|
const { VEGA_URL } = useEnvironment();
|
||||||
const [canShowCallout, setShowCallout] = useState(false);
|
|
||||||
const { configStatus, VEGA_URL } = useEnvironment();
|
|
||||||
|
|
||||||
const client = useMemo(() => {
|
const client = useMemo(() => {
|
||||||
if (VEGA_URL) {
|
if (VEGA_URL) {
|
||||||
@ -146,16 +24,12 @@ export function NetworkLoader<T>({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [VEGA_URL, createClient]);
|
}, [VEGA_URL, createClient]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setShowCallout(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return canShowCallout ? (
|
return (
|
||||||
<div className="h-full min-h-screen flex items-center justify-center">
|
<div className="h-full min-h-screen flex items-center justify-center">
|
||||||
<StatusComponent status={configStatus}>{skeleton}</StatusComponent>
|
{skeleton}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
||||||
|
@ -30,8 +30,10 @@ export const NetworkSwitcher = ({ onConnect }: NetworkSwitcherProps) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Select value={field.value} onChange={field.onChange}>
|
<Select value={field.value} onChange={field.onChange}>
|
||||||
{Object.keys(VEGA_NETWORKS).map((network) => (
|
{Object.keys(VEGA_NETWORKS).map((network, index) => (
|
||||||
<option value={network}>{network}</option>
|
<option key={index} value={network}>
|
||||||
|
{network}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
@ -1,27 +1,59 @@
|
|||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
import { Dialog, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { NodeSwitcher } from '../node-switcher';
|
import { NodeSwitcher } from '../node-switcher';
|
||||||
|
import { useEnvironment } from '../../hooks/use-environment';
|
||||||
|
import type { Configuration } from '../../types';
|
||||||
|
|
||||||
type NodeSwitcherDialogProps = ComponentProps<typeof NodeSwitcher> & {
|
type NodeSwitcherDialogProps = Pick<
|
||||||
|
ComponentProps<typeof NodeSwitcher>,
|
||||||
|
'initialErrorType' | 'onConnect'
|
||||||
|
> & {
|
||||||
|
loading: boolean;
|
||||||
|
config?: Configuration;
|
||||||
dialogOpen: boolean;
|
dialogOpen: boolean;
|
||||||
toggleDialogOpen: (dialogOpen: boolean) => void;
|
setDialogOpen: (dialogOpen: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NodeSwitcherDialog = ({
|
export const NodeSwitcherDialog = ({
|
||||||
config,
|
config,
|
||||||
|
loading,
|
||||||
|
initialErrorType,
|
||||||
dialogOpen,
|
dialogOpen,
|
||||||
toggleDialogOpen,
|
setDialogOpen,
|
||||||
onConnect,
|
onConnect,
|
||||||
}: NodeSwitcherDialogProps) => {
|
}: NodeSwitcherDialogProps) => {
|
||||||
|
const { VEGA_ENV } = useEnvironment();
|
||||||
return (
|
return (
|
||||||
<Dialog open={dialogOpen} onChange={toggleDialogOpen}>
|
<Dialog open={dialogOpen} onChange={setDialogOpen}>
|
||||||
<NodeSwitcher
|
<div className="uppercase text-h3 text-center mb-8">
|
||||||
config={config}
|
{t('Connected node')}
|
||||||
onConnect={(url) => {
|
</div>
|
||||||
onConnect(url);
|
{!config && loading && (
|
||||||
toggleDialogOpen(false);
|
<div className="py-16">
|
||||||
}}
|
<p className="mb-32 text-center">{t('Loading configuration...')}</p>
|
||||||
/>
|
<Loader size="large" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{config && dialogOpen && (
|
||||||
|
<>
|
||||||
|
<p className="mb-32 text-center">
|
||||||
|
{t(`This app will only work on a `)}
|
||||||
|
<span className="font-mono capitalize">
|
||||||
|
{VEGA_ENV.toLowerCase()}
|
||||||
|
</span>
|
||||||
|
{t(' chain ID')}
|
||||||
|
</p>
|
||||||
|
<NodeSwitcher
|
||||||
|
config={config}
|
||||||
|
initialErrorType={initialErrorType}
|
||||||
|
onConnect={(url) => {
|
||||||
|
onConnect(url);
|
||||||
|
setDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL query operation: BlockHeightStats
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface BlockHeightStats_statistics {
|
|
||||||
__typename: "Statistics";
|
|
||||||
/**
|
|
||||||
* Current block number
|
|
||||||
*/
|
|
||||||
blockHeight: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlockHeightStats {
|
|
||||||
/**
|
|
||||||
* get statistics about the vega node
|
|
||||||
*/
|
|
||||||
statistics: BlockHeightStats_statistics;
|
|
||||||
}
|
|
@ -3,6 +3,7 @@ import classnames from 'classnames';
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
type LayoutCellProps = {
|
type LayoutCellProps = {
|
||||||
|
label?: string;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@ -10,17 +11,27 @@ type LayoutCellProps = {
|
|||||||
|
|
||||||
export const LayoutCell = ({
|
export const LayoutCell = ({
|
||||||
hasError,
|
hasError,
|
||||||
|
label,
|
||||||
isLoading,
|
isLoading,
|
||||||
children,
|
children,
|
||||||
}: LayoutCellProps) => {
|
}: LayoutCellProps) => {
|
||||||
|
const classes = [
|
||||||
|
'px-8 lg:text-right flex justify-between lg:block',
|
||||||
|
'bg-white-60 dark:bg-black-60 lg:bg-transparent lg:dark:bg-transparent',
|
||||||
|
'm-2 lg:m-0',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classnames(classes)}>
|
||||||
className={classnames('px-8 text-right', {
|
{label && <span className="lg:hidden">{label}</span>}
|
||||||
'text-danger': !isLoading && hasError,
|
<span
|
||||||
'text-white-60 dark:text-black-60': isLoading,
|
className={classnames('font-mono', {
|
||||||
})}
|
'text-danger': !isLoading && hasError,
|
||||||
>
|
'text-white-60 dark:text-black-60': isLoading,
|
||||||
{isLoading ? t('Checking') : children || '-'}
|
})}
|
||||||
|
>
|
||||||
|
{isLoading ? t('Checking') : children || '-'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ type LayoutRowProps = {
|
|||||||
|
|
||||||
export const LayoutRow = ({ children }: LayoutRowProps) => {
|
export const LayoutRow = ({ children }: LayoutRowProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 py-8 w-full grid-cols-[minmax(200px,_1fr),_150px_125px_100px]">
|
<div className="lg:grid lg:gap-4 py-8 w-full lg:h-[42px] lg:grid-cols-[minmax(200px,_1fr),_150px_125px_100px]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { gql, useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import type { BlockHeightStats } from './__generated__/BlockHeightStats';
|
import type { Statistics } from '../../utils/__generated__/Statistics';
|
||||||
|
import { STATS_QUERY } from '../../utils/request-node';
|
||||||
|
|
||||||
type NodeBlockHeightProps = {
|
type NodeBlockHeightProps = {
|
||||||
value?: number;
|
value?: number;
|
||||||
@ -9,17 +10,9 @@ type NodeBlockHeightProps = {
|
|||||||
|
|
||||||
const POLL_INTERVAL = 3000;
|
const POLL_INTERVAL = 3000;
|
||||||
|
|
||||||
const BLOCK_HEIGHT_QUERY = gql`
|
|
||||||
query BlockHeightStats {
|
|
||||||
statistics {
|
|
||||||
blockHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const NodeBlockHeight = ({ value, setValue }: NodeBlockHeightProps) => {
|
export const NodeBlockHeight = ({ value, setValue }: NodeBlockHeightProps) => {
|
||||||
const { data, startPolling, stopPolling } = useQuery<BlockHeightStats>(
|
const { data, startPolling, stopPolling } = useQuery<Statistics>(
|
||||||
BLOCK_HEIGHT_QUERY,
|
STATS_QUERY,
|
||||||
{
|
{
|
||||||
pollInterval: POLL_INTERVAL,
|
pollInterval: POLL_INTERVAL,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,16 @@
|
|||||||
export const NodeError = () => {
|
type NodeErrorProps = {
|
||||||
return <div />;
|
headline?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeError = ({ headline, message }: NodeErrorProps) => {
|
||||||
|
if (!headline && !message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="p-16 my-16 border border-danger">
|
||||||
|
<p className="font-bold">{headline}</p>
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,55 +1,57 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
|
||||||
import { ApolloProvider } from '@apollo/client';
|
import { ApolloProvider } from '@apollo/client';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import type { NodeData } from '../../types';
|
import type { NodeData } from '../../types';
|
||||||
import { createClient } from '../../utils/apollo-client';
|
import type createClient from '../../utils/apollo-client';
|
||||||
import { useNode } from '../../hooks/use-node';
|
|
||||||
import { LayoutRow } from './layout-row';
|
import { LayoutRow } from './layout-row';
|
||||||
import { LayoutCell } from './layout-cell';
|
import { LayoutCell } from './layout-cell';
|
||||||
import { NodeBlockHeight } from './node-block-height';
|
import { NodeBlockHeight } from './node-block-height';
|
||||||
|
|
||||||
type NodeStatsContentProps = {
|
type NodeStatsContentProps = {
|
||||||
data: NodeData;
|
data?: NodeData;
|
||||||
highestBlock: number;
|
highestBlock: number;
|
||||||
setBlock: (value: number) => void;
|
setBlock: (value: number) => void;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResponseTimeDisplayValue = (
|
const getResponseTimeDisplayValue = (
|
||||||
responseTime: NodeData['responseTime']
|
responseTime?: NodeData['responseTime']
|
||||||
) => {
|
) => {
|
||||||
if (typeof responseTime.value === 'number') {
|
if (typeof responseTime?.value === 'number') {
|
||||||
return `${Number(responseTime.value).toFixed(2)}ms`;
|
return `${Number(responseTime.value).toFixed(2)}ms`;
|
||||||
}
|
}
|
||||||
if (responseTime.hasError) {
|
if (responseTime?.hasError) {
|
||||||
return t('n/a');
|
return t('n/a');
|
||||||
}
|
}
|
||||||
return '-';
|
return '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBlockDisplayValue = (block: NodeData['block']) => {
|
const getBlockDisplayValue = (
|
||||||
if (block.value) {
|
block: NodeData['block'] | undefined,
|
||||||
return '';
|
setBlock: (block: number) => void
|
||||||
|
) => {
|
||||||
|
if (block?.value) {
|
||||||
|
return <NodeBlockHeight value={block?.value} setValue={setBlock} />;
|
||||||
}
|
}
|
||||||
if (block.hasError) {
|
if (block?.hasError) {
|
||||||
return t('n/a');
|
return t('n/a');
|
||||||
}
|
}
|
||||||
return '-';
|
return '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSslDisplayValue = (ssl: NodeData['ssl']) => {
|
const getSslDisplayValue = (ssl?: NodeData['ssl']) => {
|
||||||
if (ssl.value) {
|
if (ssl?.value) {
|
||||||
return t('Yes');
|
return t('Yes');
|
||||||
}
|
}
|
||||||
if (ssl.hasError) {
|
if (ssl?.hasError) {
|
||||||
return t('No');
|
return t('No');
|
||||||
}
|
}
|
||||||
return '-';
|
return '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeStatsContent = ({
|
const NodeStatsContent = ({
|
||||||
data: { url, responseTime, block, ssl },
|
// @ts-ignore Allow defaulting to an empty object
|
||||||
|
data = {},
|
||||||
highestBlock,
|
highestBlock,
|
||||||
setBlock,
|
setBlock,
|
||||||
children,
|
children,
|
||||||
@ -58,24 +60,28 @@ const NodeStatsContent = ({
|
|||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
{children}
|
{children}
|
||||||
<LayoutCell
|
<LayoutCell
|
||||||
isLoading={responseTime.isLoading}
|
label={t('Response time')}
|
||||||
hasError={responseTime.hasError}
|
isLoading={data.responseTime?.isLoading}
|
||||||
|
hasError={data.responseTime?.hasError}
|
||||||
>
|
>
|
||||||
{getResponseTimeDisplayValue(responseTime)}
|
{getResponseTimeDisplayValue(data.responseTime)}
|
||||||
</LayoutCell>
|
</LayoutCell>
|
||||||
<LayoutCell
|
<LayoutCell
|
||||||
isLoading={block.isLoading}
|
label={t('Block')}
|
||||||
|
isLoading={data.block?.isLoading}
|
||||||
hasError={
|
hasError={
|
||||||
block.hasError || (!!block.value && highestBlock > block.value)
|
data.block?.hasError ||
|
||||||
|
(!!data.block?.value && highestBlock > data.block.value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{url && block.value && (
|
{getBlockDisplayValue(data.block, setBlock)}
|
||||||
<NodeBlockHeight value={block.value} setValue={setBlock} />
|
|
||||||
)}
|
|
||||||
{getBlockDisplayValue(block)}
|
|
||||||
</LayoutCell>
|
</LayoutCell>
|
||||||
<LayoutCell isLoading={ssl.isLoading} hasError={ssl.hasError}>
|
<LayoutCell
|
||||||
{getSslDisplayValue(ssl)}
|
label={t('SSL')}
|
||||||
|
isLoading={data.ssl?.isLoading}
|
||||||
|
hasError={data.ssl?.hasError}
|
||||||
|
>
|
||||||
|
{getSslDisplayValue(data.ssl)}
|
||||||
</LayoutCell>
|
</LayoutCell>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
);
|
);
|
||||||
@ -95,47 +101,28 @@ const Wrapper = ({ client, children }: WrapperProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type NodeStatsProps = {
|
export type NodeStatsProps = {
|
||||||
url?: string;
|
data?: NodeData;
|
||||||
|
client?: ReturnType<typeof createClient>;
|
||||||
highestBlock: number;
|
highestBlock: number;
|
||||||
setBlock: (value: number) => void;
|
setBlock: (value: number) => void;
|
||||||
render: (data: NodeData) => ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NodeStats = ({
|
export const NodeStats = ({
|
||||||
url,
|
data,
|
||||||
|
client,
|
||||||
highestBlock,
|
highestBlock,
|
||||||
render,
|
children,
|
||||||
setBlock,
|
setBlock,
|
||||||
}: NodeStatsProps) => {
|
}: NodeStatsProps) => {
|
||||||
const [client, setClient] = useState<
|
|
||||||
undefined | ReturnType<typeof createClient>
|
|
||||||
>();
|
|
||||||
const { state, reset, updateBlockState } = useNode(url, client);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client?.stop();
|
|
||||||
reset();
|
|
||||||
setClient(url ? createClient(url) : undefined);
|
|
||||||
return () => client?.stop();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
const onHandleBlockChange = useCallback(
|
|
||||||
(value: number) => {
|
|
||||||
updateBlockState(value);
|
|
||||||
setBlock(value);
|
|
||||||
},
|
|
||||||
[updateBlockState, setBlock]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper client={client}>
|
<Wrapper client={client}>
|
||||||
<NodeStatsContent
|
<NodeStatsContent
|
||||||
data={state}
|
data={data}
|
||||||
highestBlock={highestBlock}
|
highestBlock={highestBlock}
|
||||||
setBlock={onHandleBlockChange}
|
setBlock={setBlock}
|
||||||
>
|
>
|
||||||
{render(state)}
|
{children}
|
||||||
</NodeStatsContent>
|
</NodeStatsContent>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,32 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { RadioGroup, Button, Radio } from '@vegaprotocol/ui-toolkit';
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Link,
|
||||||
|
Radio,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useEnvironment } from '../../hooks/use-environment';
|
import { useEnvironment } from '../../hooks/use-environment';
|
||||||
import type { Configuration, NodeData, Networks } from '../../types';
|
import { useNodes } from '../../hooks/use-nodes';
|
||||||
|
import {
|
||||||
|
getIsNodeLoading,
|
||||||
|
getIsNodeDisabled,
|
||||||
|
getIsFormDisabled,
|
||||||
|
getErrorType,
|
||||||
|
getErrorByType,
|
||||||
|
getHasInvalidChain,
|
||||||
|
} from '../../utils/validate-node';
|
||||||
|
import { CUSTOM_NODE_KEY } from '../../types';
|
||||||
|
import type { Configuration, NodeData, ErrorType, Networks } from '../../types';
|
||||||
import { LayoutRow } from './layout-row';
|
import { LayoutRow } from './layout-row';
|
||||||
import { LayoutCell } from './layout-cell';
|
|
||||||
import { NodeError } from './node-error';
|
import { NodeError } from './node-error';
|
||||||
import { NodeStats } from './node-stats';
|
import { NodeStats } from './node-stats';
|
||||||
|
|
||||||
type NodeSwitcherProps = {
|
type NodeSwitcherProps = {
|
||||||
error?: string;
|
error?: string;
|
||||||
config: Configuration;
|
config: Configuration;
|
||||||
|
initialErrorType?: ErrorType;
|
||||||
onConnect: (url: string) => void;
|
onConnect: (url: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,85 +34,153 @@ const getDefaultNode = (urls: string[], currentUrl?: string) => {
|
|||||||
return currentUrl && urls.includes(currentUrl) ? currentUrl : undefined;
|
return currentUrl && urls.includes(currentUrl) ? currentUrl : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIsLoading = ({ chain, responseTime, block, ssl }: NodeData) => {
|
const getHighestBlock = (env: Networks, state: Record<string, NodeData>) => {
|
||||||
return (
|
return Object.keys(state).reduce((acc, node) => {
|
||||||
chain.isLoading ||
|
if (getHasInvalidChain(env, state[node].chain.value)) return acc;
|
||||||
responseTime.isLoading ||
|
const value = Number(state[node].block.value);
|
||||||
block.isLoading ||
|
return value ? Math.max(acc, value) : acc;
|
||||||
ssl.isLoading
|
}, 0);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHasMatchingChain = (env: Networks, chain?: string) => {
|
export const NodeSwitcher = ({
|
||||||
return chain?.includes(env.toLowerCase()) ?? false;
|
config,
|
||||||
};
|
initialErrorType,
|
||||||
|
onConnect,
|
||||||
const getIsDisabled = (env: Networks, data: NodeData) => {
|
}: NodeSwitcherProps) => {
|
||||||
const { chain, responseTime, block, ssl } = data;
|
|
||||||
return (
|
|
||||||
!getHasMatchingChain(env, data.chain.value) ||
|
|
||||||
getIsLoading(data) ||
|
|
||||||
chain.hasError ||
|
|
||||||
responseTime.hasError ||
|
|
||||||
block.hasError ||
|
|
||||||
ssl.hasError
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => {
|
|
||||||
const { VEGA_ENV, VEGA_URL } = useEnvironment();
|
const { VEGA_ENV, VEGA_URL } = useEnvironment();
|
||||||
const [node, setNode] = useState(getDefaultNode(config.hosts, VEGA_URL));
|
const [networkError, setNetworkError] = useState(
|
||||||
const [highestBlock, setHighestBlock] = useState(0);
|
getErrorByType(initialErrorType, VEGA_ENV, VEGA_URL)
|
||||||
|
);
|
||||||
|
const [customNodeText, setCustomNodeText] = useState('');
|
||||||
|
const [nodeRadio, setNodeRadio] = useState(
|
||||||
|
getDefaultNode(config.hosts, VEGA_URL)
|
||||||
|
);
|
||||||
|
const { state, clients, updateNodeUrl, updateNodeBlock } = useNodes(
|
||||||
|
VEGA_ENV,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
const highestBlock = useMemo(
|
||||||
|
() => getHighestBlock(VEGA_ENV, state),
|
||||||
|
[VEGA_ENV, state]
|
||||||
|
);
|
||||||
|
const customUrl = state[CUSTOM_NODE_KEY]?.url;
|
||||||
|
|
||||||
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
|
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
|
||||||
if (node) {
|
if (node && state[node]) {
|
||||||
onConnect(node);
|
onConnect(state[node].url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSubmitDisabled = !node;
|
const isSubmitDisabled = getIsFormDisabled(
|
||||||
|
nodeRadio,
|
||||||
|
customNodeText,
|
||||||
|
VEGA_ENV,
|
||||||
|
state
|
||||||
|
);
|
||||||
|
|
||||||
|
const customNodeData =
|
||||||
|
nodeRadio &&
|
||||||
|
state[CUSTOM_NODE_KEY] &&
|
||||||
|
state[CUSTOM_NODE_KEY].url === customNodeText
|
||||||
|
? state[nodeRadio]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const customNodeError = getErrorByType(
|
||||||
|
getErrorType(VEGA_ENV, customNodeData),
|
||||||
|
VEGA_ENV,
|
||||||
|
customUrl
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-black dark:text-white w-full">
|
<div className="text-black dark:text-white w-full lg:min-w-[800px]">
|
||||||
<NodeError />
|
<NodeError {...(customNodeError || networkError)} />
|
||||||
<form onSubmit={() => onSubmit(node)}>
|
<form onSubmit={() => onSubmit(nodeRadio)}>
|
||||||
<p className="text-body-large font-bold mb-32">
|
<p className="text-body-large font-bold mt-16 mb-32">
|
||||||
{t('Select a GraphQL node to connect to:')}
|
{t('Select a GraphQL node to connect to:')}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<LayoutRow>
|
<div className="hidden lg:block">
|
||||||
<div />
|
<LayoutRow>
|
||||||
<LayoutCell>{t('Response time')}</LayoutCell>
|
<div />
|
||||||
<LayoutCell>{t('Block')}</LayoutCell>
|
<span className="px-8 text-right">{t('Response time')}</span>
|
||||||
<LayoutCell>{t('SSL')}</LayoutCell>
|
<span className="px-8 text-right">{t('Block')}</span>
|
||||||
</LayoutRow>
|
<span className="px-8 text-right">{t('SSL')}</span>
|
||||||
|
</LayoutRow>
|
||||||
|
</div>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="block"
|
className="block"
|
||||||
value={node}
|
value={nodeRadio}
|
||||||
onChange={(value) => setNode(value)}
|
onChange={(value) => {
|
||||||
|
setNodeRadio(value);
|
||||||
|
setNetworkError(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="w-full">
|
||||||
{config.hosts.map((url, index) => (
|
{config.hosts.map((node, index) => (
|
||||||
<NodeStats
|
<NodeStats
|
||||||
key={index}
|
key={index}
|
||||||
url={url}
|
data={state[node]}
|
||||||
|
client={clients[node]}
|
||||||
highestBlock={highestBlock}
|
highestBlock={highestBlock}
|
||||||
setBlock={(block) =>
|
setBlock={(block) => updateNodeBlock(node, block)}
|
||||||
setHighestBlock(Math.max(block, highestBlock))
|
>
|
||||||
}
|
<div className="mb-8 break-all">
|
||||||
render={(data) => (
|
<Radio
|
||||||
<div>
|
id={`node-url-${index}`}
|
||||||
<Radio
|
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
||||||
id={`node-url-${index}`}
|
value={node}
|
||||||
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
label={node}
|
||||||
value={url}
|
disabled={getIsNodeDisabled(VEGA_ENV, state[node])}
|
||||||
label={url}
|
/>
|
||||||
disabled={getIsDisabled(VEGA_ENV, data)}
|
</div>
|
||||||
|
</NodeStats>
|
||||||
|
))}
|
||||||
|
<NodeStats
|
||||||
|
data={state[CUSTOM_NODE_KEY]}
|
||||||
|
client={customUrl ? clients[customUrl] : undefined}
|
||||||
|
highestBlock={highestBlock}
|
||||||
|
setBlock={(block) => updateNodeBlock(CUSTOM_NODE_KEY, block)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full mb-8">
|
||||||
|
<Radio
|
||||||
|
id={`node-url-custom`}
|
||||||
|
value={CUSTOM_NODE_KEY}
|
||||||
|
label={
|
||||||
|
nodeRadio === CUSTOM_NODE_KEY || !!state[CUSTOM_NODE_KEY]
|
||||||
|
? ''
|
||||||
|
: t('Other')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{(customNodeText || nodeRadio === CUSTOM_NODE_KEY) && (
|
||||||
|
<div className="flex w-full gap-8">
|
||||||
|
<Input
|
||||||
|
placeholder="https://"
|
||||||
|
value={customNodeText}
|
||||||
|
hasError={
|
||||||
|
!!customNodeText &&
|
||||||
|
!!(
|
||||||
|
customNodeError?.headline ||
|
||||||
|
customNodeError?.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={(e) => setCustomNodeText(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
aria-disabled={!customNodeText}
|
||||||
|
onClick={() => {
|
||||||
|
setNetworkError(null);
|
||||||
|
updateNodeUrl(CUSTOM_NODE_KEY, customNodeText);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{state[CUSTOM_NODE_KEY] &&
|
||||||
|
getIsNodeLoading(state[CUSTOM_NODE_KEY])
|
||||||
|
? t('Checking')
|
||||||
|
: t('Check')}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
))}
|
</NodeStats>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
82
libs/environment/src/hooks/mocks/apollo-client.tsx
Normal file
82
libs/environment/src/hooks/mocks/apollo-client.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
STATS_QUERY,
|
||||||
|
TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
} from '../../utils/request-node';
|
||||||
|
import type { Statistics } from '../../utils/__generated__/Statistics';
|
||||||
|
import type { BlockTime } from '../../utils/__generated__/BlockTime';
|
||||||
|
import { Networks } from '../../types';
|
||||||
|
import type { RequestHandlerResponse } from 'mock-apollo-client';
|
||||||
|
import { createMockClient } from 'mock-apollo-client';
|
||||||
|
|
||||||
|
export type MockRequestConfig = {
|
||||||
|
hasError?: boolean;
|
||||||
|
delay?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MockClientProps = {
|
||||||
|
network?: Networks;
|
||||||
|
statistics?: MockRequestConfig;
|
||||||
|
busEvents?: MockRequestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockBusEventsResult = (): BlockTime => ({
|
||||||
|
busEvents: [
|
||||||
|
{
|
||||||
|
__typename: 'BusEvent',
|
||||||
|
eventId: '0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMockStatisticsResult = (
|
||||||
|
env: Networks = Networks.TESTNET
|
||||||
|
): Statistics => ({
|
||||||
|
statistics: {
|
||||||
|
__typename: 'Statistics',
|
||||||
|
chainId: `${env.toLowerCase()}-0123`,
|
||||||
|
blockHeight: '11',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMockQueryResult = (env: Networks): Statistics => ({
|
||||||
|
statistics: {
|
||||||
|
__typename: 'Statistics',
|
||||||
|
chainId: `${env.toLowerCase()}-0123`,
|
||||||
|
blockHeight: '11',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getHandler<T>(
|
||||||
|
{ hasError, delay = 0 }: MockRequestConfig = {},
|
||||||
|
result: T
|
||||||
|
) {
|
||||||
|
return () =>
|
||||||
|
new Promise<RequestHandlerResponse<T>>((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (hasError) {
|
||||||
|
reject(new Error('Failed to execute query.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ data: result });
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({
|
||||||
|
network,
|
||||||
|
statistics,
|
||||||
|
busEvents,
|
||||||
|
}: MockClientProps = {}) {
|
||||||
|
const mockClient = createMockClient();
|
||||||
|
|
||||||
|
mockClient.setRequestHandler(
|
||||||
|
STATS_QUERY,
|
||||||
|
getHandler(statistics, getMockStatisticsResult(network))
|
||||||
|
);
|
||||||
|
mockClient.setRequestHandler(
|
||||||
|
TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
getHandler(busEvents, getMockBusEventsResult())
|
||||||
|
);
|
||||||
|
|
||||||
|
return mockClient;
|
||||||
|
}
|
@ -1,19 +1,17 @@
|
|||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
import type { EnvironmentWithOptionalUrl } from './use-config';
|
import type { EnvironmentWithOptionalUrl } from './use-config';
|
||||||
import { useConfig, LOCAL_STORAGE_NETWORK_KEY } from './use-config';
|
import { useConfig } from './use-config';
|
||||||
import { Networks } from '../types';
|
import { Networks, ErrorType } from '../types';
|
||||||
|
|
||||||
type HostMapping = Record<string, number | Error>;
|
const mockConfig = {
|
||||||
|
hosts: [
|
||||||
const mockHostsMap: HostMapping = {
|
'https://vega-host-1.com',
|
||||||
'https://host1.com': 300,
|
'https://vega-host-2.com',
|
||||||
'https://host2.com': 500,
|
'https://vega-host-3.com',
|
||||||
'https://host3.com': 100,
|
'https://vega-host-4.com',
|
||||||
'https://host4.com': 650,
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const hostList = Object.keys(mockHostsMap);
|
|
||||||
|
|
||||||
const mockEnvironment: EnvironmentWithOptionalUrl = {
|
const mockEnvironment: EnvironmentWithOptionalUrl = {
|
||||||
VEGA_ENV: Networks.TESTNET,
|
VEGA_ENV: Networks.TESTNET,
|
||||||
VEGA_CONFIG_URL: 'https://vega.url/config.json',
|
VEGA_CONFIG_URL: 'https://vega.url/config.json',
|
||||||
@ -26,281 +24,110 @@ const mockEnvironment: EnvironmentWithOptionalUrl = {
|
|||||||
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupFetch(configUrl: string, hostMap: HostMapping) {
|
function setupFetch(configUrl: string) {
|
||||||
const hostUrls = Object.keys(hostMap);
|
|
||||||
return (url: RequestInfo) => {
|
return (url: RequestInfo) => {
|
||||||
if (url === configUrl) {
|
if (url === configUrl) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: () => Promise.resolve({ hosts: hostUrls }),
|
json: () => Promise.resolve(mockConfig),
|
||||||
} as Response);
|
} as Response);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostUrls.includes(url as string)) {
|
|
||||||
const value = hostMap[url as string];
|
|
||||||
return new Promise<Response>((resolve, reject) => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({ ok: true } as Response);
|
|
||||||
}, value);
|
|
||||||
} else {
|
|
||||||
reject(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: true,
|
ok: true,
|
||||||
} as Response);
|
} as Response);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
global.fetch = jest.fn();
|
global.fetch = jest.fn();
|
||||||
|
|
||||||
const mockUpdate = jest.fn();
|
const onError = jest.fn();
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
onError.mockClear();
|
||||||
mockUpdate.mockClear();
|
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
global.fetch.mockReset();
|
global.fetch.mockReset();
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
global.fetch.mockImplementation(
|
global.fetch.mockImplementation(
|
||||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMap)
|
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// @ts-ignore: typescript doesn't recognise the mocked fetch instance
|
jest.clearAllMocks();
|
||||||
fetch.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useConfig hook', () => {
|
describe('useConfig hook', () => {
|
||||||
it('updates the environment with a host url from the network configuration', async () => {
|
it("doesn't update when there is no VEGA_CONFIG_URL in the environment", async () => {
|
||||||
const allowedStatuses = [
|
const mockEnvWithoutUrl = {
|
||||||
'idle',
|
...mockEnvironment,
|
||||||
'loading-config',
|
VEGA_CONFIG_URL: undefined,
|
||||||
'loading-node',
|
|
||||||
'success',
|
|
||||||
];
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('success');
|
|
||||||
result.all.forEach((state) => {
|
|
||||||
expect(allowedStatuses).toContain('status' in state && state.status);
|
|
||||||
});
|
|
||||||
|
|
||||||
// fetches config
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
// calls each node
|
|
||||||
hostList.forEach((url) => {
|
|
||||||
expect(fetch).toHaveBeenCalledWith(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
// updates the environment
|
|
||||||
expect(hostList).toContain(mockUpdate.mock.calls[0][0]({}).VEGA_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the host from the configuration which responds first', async () => {
|
|
||||||
const shortestResponseTime = Object.values(mockHostsMap).sort()[0];
|
|
||||||
const expectedHost = hostList.find((url: keyof typeof mockHostsMap) => {
|
|
||||||
return mockHostsMap[url] === shortestResponseTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('success');
|
|
||||||
expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe(expectedHost);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores failing hosts and uses one which returns a success response', async () => {
|
|
||||||
const mockHostsMapScoped = {
|
|
||||||
'https://host1.com': 350,
|
|
||||||
'https://host2.com': new Error('Server error'),
|
|
||||||
'https://host3.com': 230,
|
|
||||||
'https://host4.com': new Error('Server error'),
|
|
||||||
};
|
};
|
||||||
|
const { result } = renderHook(() => useConfig(mockEnvWithoutUrl, onError));
|
||||||
|
|
||||||
|
expect(result.current.config).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches configuration from the provided url', async () => {
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useConfig(mockEnvironment, onError)
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||||
|
expect(result.current.config).toEqual(mockConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches the configuration', async () => {
|
||||||
|
const { result: firstResult, waitForNextUpdate: waitForFirstUpdate } =
|
||||||
|
renderHook(() => useConfig(mockEnvironment, onError));
|
||||||
|
|
||||||
|
await waitForFirstUpdate();
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(firstResult.current.config).toEqual(mockConfig);
|
||||||
|
|
||||||
|
const { result: secondResult } = renderHook(() =>
|
||||||
|
useConfig(mockEnvironment, onError)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(secondResult.current.config).toEqual(mockConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes the error callback when the config endpoint fails', async () => {
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
global.fetch.mockImplementation(
|
global.fetch.mockImplementation(() => Promise.reject());
|
||||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
|
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useConfig(mockEnvironment, onError)
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
expect(result.current.config).toEqual({ hosts: [] });
|
||||||
|
expect(onError).toHaveBeenCalledWith(ErrorType.CONFIG_LOAD_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes the error callback when the config validation fails', async () => {
|
||||||
|
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||||
|
global.fetch.mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ data: 'not-valid-config' }),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
useConfig(mockEnvironment, onError)
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForNextUpdate();
|
await waitForNextUpdate();
|
||||||
jest.runAllTimers();
|
expect(result.current.config).toBe(undefined);
|
||||||
await waitForNextUpdate();
|
expect(onError).toHaveBeenCalledWith(ErrorType.CONFIG_VALIDATION_ERROR);
|
||||||
|
|
||||||
expect(result.current.status).toBe('success');
|
|
||||||
expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe('https://host3.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct error status for when the config cannot be accessed', async () => {
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation((url: RequestInfo) => {
|
|
||||||
if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
|
||||||
return Promise.reject(new Error('Server error'));
|
|
||||||
}
|
|
||||||
return Promise.resolve({ ok: true } as Response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('error-loading-config');
|
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct error status for when the config is not valid', async () => {
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation((url: RequestInfo) => {
|
|
||||||
if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
|
||||||
return Promise.resolve({
|
|
||||||
ok: true,
|
|
||||||
json: () => Promise.resolve({ some: 'data' }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve({ ok: true } as Response);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('error-validating-config');
|
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct error status for when no hosts can be accessed', async () => {
|
|
||||||
const mockHostsMapScoped = {
|
|
||||||
'https://host1.com': new Error('Server error'),
|
|
||||||
'https://host2.com': new Error('Server error'),
|
|
||||||
'https://host3.com': new Error('Server error'),
|
|
||||||
'https://host4.com': new Error('Server error'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
global.fetch.mockImplementation(
|
|
||||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useConfig(mockEnvironment, mockUpdate)
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.status).toBe('error-loading-node');
|
|
||||||
expect(mockUpdate).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('caches the list of networks', async () => {
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
fetch.mockClear();
|
|
||||||
|
|
||||||
const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run2.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run2.result.current.status).toBe('success');
|
|
||||||
expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('caches the list of networks between runs', async () => {
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
|
|
||||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
|
||||||
fetch.mockClear();
|
|
||||||
|
|
||||||
const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run2.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run2.result.current.status).toBe('success');
|
|
||||||
expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
`${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
|
|
||||||
'{not:{valid:{json'
|
|
||||||
);
|
|
||||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
|
||||||
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
`${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
|
|
||||||
JSON.stringify({ invalid: 'data' })
|
|
||||||
);
|
|
||||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
|
||||||
|
|
||||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
|
||||||
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
jest.runAllTimers();
|
|
||||||
await run1.waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(run1.result.current.status).toBe('success');
|
|
||||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
|
||||||
expect(consoleSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,33 +1,26 @@
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
||||||
import type {
|
import { ErrorType } from '../types';
|
||||||
Environment,
|
import type { Environment, Configuration, Networks } from '../types';
|
||||||
Configuration,
|
|
||||||
ConfigStatus,
|
|
||||||
Networks,
|
|
||||||
} from '../types';
|
|
||||||
import { validateConfiguration } from '../utils/validate-configuration';
|
import { validateConfiguration } from '../utils/validate-configuration';
|
||||||
import { promiseRaceToSuccess } from '../utils/promise-race-success';
|
|
||||||
|
|
||||||
export const LOCAL_STORAGE_NETWORK_KEY = 'vegaNetworkConfig';
|
export const LOCAL_STORAGE_NETWORK_KEY = 'vegaNetworkConfig';
|
||||||
|
|
||||||
export type EnvironmentWithOptionalUrl = Partial<Environment> &
|
export type EnvironmentWithOptionalUrl = Partial<Environment> &
|
||||||
Omit<Environment, 'VEGA_URL'>;
|
Omit<Environment, 'VEGA_URL'>;
|
||||||
|
|
||||||
const requestToNode = async (url: string, index: number): Promise<number> => {
|
const compileHosts = (hosts: string[], envUrl?: string) => {
|
||||||
const response = await fetch(url);
|
if (envUrl && !hosts.includes(envUrl)) {
|
||||||
if (!response.ok) {
|
return [...hosts, envUrl];
|
||||||
throw new Error(`Failed connecting to node: ${url}.`);
|
|
||||||
}
|
}
|
||||||
return index;
|
return hosts;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCacheKey = (env: Networks) => `${LOCAL_STORAGE_NETWORK_KEY}-${env}`;
|
const getCacheKey = (env: Networks) => `${LOCAL_STORAGE_NETWORK_KEY}-${env}`;
|
||||||
|
|
||||||
const getCachedConfig = (env: Networks) => {
|
const getCachedConfig = (env: Networks) => {
|
||||||
const key = getCacheKey(env);
|
const cacheKey = getCacheKey(env);
|
||||||
const value = LocalStorage.getItem(key);
|
const value = LocalStorage.getItem(cacheKey);
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
try {
|
try {
|
||||||
@ -40,9 +33,9 @@ const getCachedConfig = (env: Networks) => {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LocalStorage.removeItem(key);
|
LocalStorage.removeItem(cacheKey);
|
||||||
console.warn(
|
console.warn(
|
||||||
'Malformed data found for network configuration. Removed and continuing...'
|
'Malformed data found for network configuration. Removed cached configuration, continuing...'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,80 +45,54 @@ const getCachedConfig = (env: Networks) => {
|
|||||||
|
|
||||||
export const useConfig = (
|
export const useConfig = (
|
||||||
environment: EnvironmentWithOptionalUrl,
|
environment: EnvironmentWithOptionalUrl,
|
||||||
updateEnvironment: Dispatch<SetStateAction<Environment>>
|
onError: (errorType: ErrorType) => void
|
||||||
) => {
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [config, setConfig] = useState<Configuration | undefined>(
|
const [config, setConfig] = useState<Configuration | undefined>(
|
||||||
getCachedConfig(environment.VEGA_ENV)
|
getCachedConfig(environment.VEGA_ENV)
|
||||||
);
|
);
|
||||||
const [status, setStatus] = useState<ConfigStatus>(
|
|
||||||
environment.VEGA_CONFIG_URL ? 'idle' : 'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config && status === 'idle') {
|
let isMounted = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
setStatus('loading-config');
|
if (!config && environment.VEGA_CONFIG_URL) {
|
||||||
|
isMounted && setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(environment.VEGA_CONFIG_URL ?? '');
|
const response = await fetch(environment.VEGA_CONFIG_URL);
|
||||||
const configData: Configuration = await response.json();
|
const configData: Configuration = await response.json();
|
||||||
|
|
||||||
if (validateConfiguration(configData)) {
|
if (validateConfiguration(configData)) {
|
||||||
setStatus('error-validating-config');
|
onError(ErrorType.CONFIG_VALIDATION_ERROR);
|
||||||
|
isMounted && setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig({ hosts: configData.hosts });
|
const hosts = compileHosts(configData.hosts, environment.VEGA_URL);
|
||||||
|
|
||||||
|
isMounted && setConfig({ hosts });
|
||||||
LocalStorage.setItem(
|
LocalStorage.setItem(
|
||||||
getCacheKey(environment.VEGA_ENV),
|
getCacheKey(environment.VEGA_ENV),
|
||||||
JSON.stringify({ hosts: configData.hosts })
|
JSON.stringify({ hosts })
|
||||||
);
|
);
|
||||||
|
isMounted && setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus('error-loading-config');
|
if (isMounted) {
|
||||||
|
setLoading(false);
|
||||||
|
setConfig({ hosts: [] });
|
||||||
|
}
|
||||||
|
onError(ErrorType.CONFIG_LOAD_ERROR);
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
}
|
})();
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
// load config only once per runtime
|
// load config only once per runtime
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [environment.VEGA_CONFIG_URL, !!config, status, setStatus, setConfig]);
|
}, [environment.VEGA_CONFIG_URL, !!config, onError, setLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
config &&
|
|
||||||
!['loading-node', 'success', 'error-loading-node'].includes(status)
|
|
||||||
) {
|
|
||||||
(async () => {
|
|
||||||
setStatus('loading-node');
|
|
||||||
|
|
||||||
// if there's only one configured node to choose from, set is as the env url
|
|
||||||
if (config.hosts.length === 1) {
|
|
||||||
setStatus('success');
|
|
||||||
updateEnvironment((prevEnvironment) => ({
|
|
||||||
...prevEnvironment,
|
|
||||||
VEGA_URL: prevEnvironment.VEGA_URL || config.hosts[0],
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// when there are multiple possible hosts, set the env url to the node which responds first
|
|
||||||
try {
|
|
||||||
const requests = config.hosts.map(requestToNode);
|
|
||||||
const index = await promiseRaceToSuccess(requests);
|
|
||||||
setStatus('success');
|
|
||||||
updateEnvironment((prevEnvironment) => ({
|
|
||||||
...prevEnvironment,
|
|
||||||
VEGA_URL: prevEnvironment.VEGA_URL || config.hosts[index],
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
setStatus('error-loading-node');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
// load config only once per runtime
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [status, !!config, setStatus, updateEnvironment]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
loading,
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
import type { ComponentProps } from 'react';
|
// having the node switcher dialog in the environment provider breaks the test renderer
|
||||||
|
// workaround based on: https://github.com/facebook/react/issues/11565
|
||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
import type { EnvironmentState } from './use-environment';
|
import createClient from '../utils/apollo-client';
|
||||||
import { useEnvironment, EnvironmentProvider } from './use-environment';
|
import { useEnvironment, EnvironmentProvider } from './use-environment';
|
||||||
import { Networks } from '../types';
|
import { Networks, ErrorType } from '../types';
|
||||||
|
import type { MockRequestConfig } from './mocks/apollo-client';
|
||||||
|
import createMockClient from './mocks/apollo-client';
|
||||||
|
import { getErrorByType } from '../utils/validate-node';
|
||||||
|
|
||||||
|
jest.mock('../utils/apollo-client');
|
||||||
|
|
||||||
|
jest.mock('react-dom', () => ({
|
||||||
|
...jest.requireActual('react-dom'),
|
||||||
|
createPortal: (node: ReactNode) => node,
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.fetch = jest.fn();
|
||||||
|
|
||||||
const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
||||||
return <EnvironmentProvider {...props} />;
|
return <EnvironmentProvider {...props} />;
|
||||||
@ -10,26 +24,10 @@ const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
|||||||
|
|
||||||
const MOCK_HOST = 'https://vega.host/query';
|
const MOCK_HOST = 'https://vega.host/query';
|
||||||
|
|
||||||
global.fetch = jest.fn();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
const mockFetch = (url: RequestInfo) => {
|
const mockEnvironmentState = {
|
||||||
if (url === mockEnvironmentState.VEGA_CONFIG_URL) {
|
|
||||||
return Promise.resolve({
|
|
||||||
ok: true,
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
hosts: [MOCK_HOST],
|
|
||||||
}),
|
|
||||||
} as Response);
|
|
||||||
}
|
|
||||||
return Promise.resolve({ ok: true } as Response);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockEnvironmentState: EnvironmentState = {
|
|
||||||
configStatus: 'success',
|
|
||||||
VEGA_URL: 'https://vega.xyz',
|
VEGA_URL: 'https://vega.xyz',
|
||||||
VEGA_ENV: Networks.TESTNET,
|
VEGA_ENV: Networks.TESTNET,
|
||||||
VEGA_CONFIG_URL: 'https://vega.xyz/testnet-config.json',
|
VEGA_CONFIG_URL: 'https://vega.xyz/testnet-config.json',
|
||||||
@ -45,25 +43,76 @@ const mockEnvironmentState: EnvironmentState = {
|
|||||||
GIT_COMMIT_HASH: 'abcde01234',
|
GIT_COMMIT_HASH: 'abcde01234',
|
||||||
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||||
setNodeSwitcherOpen: noop,
|
setNodeSwitcherOpen: noop,
|
||||||
|
networkError: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MOCK_DURATION = 76;
|
||||||
|
|
||||||
|
window.performance.getEntriesByName = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((url: string) => [
|
||||||
|
{
|
||||||
|
entryType: 'resource',
|
||||||
|
name: url,
|
||||||
|
startTime: 0,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
duration: MOCK_DURATION,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function setupFetch(
|
||||||
|
configUrl: string = mockEnvironmentState.VEGA_CONFIG_URL,
|
||||||
|
hosts?: string[]
|
||||||
|
) {
|
||||||
|
return (url: RequestInfo) => {
|
||||||
|
if (url === configUrl) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ hosts: hosts || [MOCK_HOST] }),
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
} as Response);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQuickestNode = (mockNodes: Record<string, MockRequestConfig>) => {
|
||||||
|
const { nodeUrl } = Object.keys(mockNodes).reduce<{
|
||||||
|
nodeUrl?: string;
|
||||||
|
delay: number;
|
||||||
|
}>(
|
||||||
|
(acc, url) => {
|
||||||
|
const { delay = 0, hasError = false } = mockNodes[url];
|
||||||
|
if (!hasError && delay < acc.delay) {
|
||||||
|
return { nodeUrl: url, delay };
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ nodeUrl: undefined, delay: Infinity }
|
||||||
|
);
|
||||||
|
return nodeUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// @ts-ignore typscript doesn't recognise the mock implementation
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
global.fetch.mockReset();
|
global.fetch.mockImplementation(setupFetch());
|
||||||
// @ts-ignore typscript doesn't recognise the mock implementation
|
|
||||||
global.fetch.mockImplementation(mockFetch);
|
|
||||||
|
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() => createMockClient());
|
||||||
|
|
||||||
process.env['NX_VEGA_URL'] = mockEnvironmentState.VEGA_URL;
|
process.env['NX_VEGA_URL'] = mockEnvironmentState.VEGA_URL;
|
||||||
process.env['NX_VEGA_ENV'] = mockEnvironmentState.VEGA_ENV;
|
process.env['NX_VEGA_ENV'] = mockEnvironmentState.VEGA_ENV;
|
||||||
process.env['NX_VEGA_CONFIG_URL'] = mockEnvironmentState.VEGA_CONFIG_URL;
|
process.env['NX_VEGA_CONFIG_URL'] = mockEnvironmentState.VEGA_CONFIG_URL;
|
||||||
process.env['NX_ETHEREUM_PROVIDER_URL'] =
|
|
||||||
mockEnvironmentState.ETHEREUM_PROVIDER_URL;
|
|
||||||
process.env['NX_ETHERSCAN_URL'] = mockEnvironmentState.ETHERSCAN_URL;
|
|
||||||
process.env['NX_VEGA_NETWORKS'] = JSON.stringify(
|
process.env['NX_VEGA_NETWORKS'] = JSON.stringify(
|
||||||
mockEnvironmentState.VEGA_NETWORKS
|
mockEnvironmentState.VEGA_NETWORKS
|
||||||
);
|
);
|
||||||
|
process.env['NX_ETHEREUM_PROVIDER_URL'] =
|
||||||
|
mockEnvironmentState.ETHEREUM_PROVIDER_URL;
|
||||||
|
process.env['NX_ETHERSCAN_URL'] = mockEnvironmentState.ETHERSCAN_URL;
|
||||||
process.env['NX_GIT_BRANCH'] = mockEnvironmentState.GIT_BRANCH;
|
process.env['NX_GIT_BRANCH'] = mockEnvironmentState.GIT_BRANCH;
|
||||||
process.env['NX_GIT_ORIGIN_URL'] = mockEnvironmentState.GIT_ORIGIN_URL;
|
process.env['NX_GIT_ORIGIN_URL'] = mockEnvironmentState.GIT_ORIGIN_URL;
|
||||||
process.env['NX_GIT_COMMIT_HASH'] = mockEnvironmentState.GIT_COMMIT_HASH;
|
process.env['NX_GIT_COMMIT_HASH'] = mockEnvironmentState.GIT_COMMIT_HASH;
|
||||||
@ -72,20 +121,18 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// @ts-ignore: typescript doesn't recognise the mocked fetch instance
|
|
||||||
fetch.mockRestore();
|
|
||||||
window.localStorage.clear();
|
|
||||||
|
|
||||||
delete process.env['NX_VEGA_URL'];
|
delete process.env['NX_VEGA_URL'];
|
||||||
delete process.env['NX_VEGA_ENV'];
|
delete process.env['NX_VEGA_ENV'];
|
||||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||||
|
delete process.env['NX_VEGA_NETWORKS'];
|
||||||
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
||||||
delete process.env['NX_ETHERSCAN_URL'];
|
delete process.env['NX_ETHERSCAN_URL'];
|
||||||
delete process.env['NX_VEGA_NETWORKS'];
|
|
||||||
delete process.env['NX_GIT_BRANCH'];
|
delete process.env['NX_GIT_BRANCH'];
|
||||||
delete process.env['NX_GIT_ORIGIN_URL'];
|
delete process.env['NX_GIT_ORIGIN_URL'];
|
||||||
delete process.env['NX_GIT_COMMIT_HASH'];
|
delete process.env['NX_GIT_COMMIT_HASH'];
|
||||||
delete process.env['NX_GITHUB_FEEDBACK_URL'];
|
delete process.env['NX_GITHUB_FEEDBACK_URL'];
|
||||||
|
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useEnvironment hook', () => {
|
describe('useEnvironment hook', () => {
|
||||||
@ -114,21 +161,6 @@ describe('useEnvironment hook', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows for the VEGA_URL to be missing when there is a VEGA_CONFIG_URL present', async () => {
|
|
||||||
delete process.env['NX_VEGA_URL'];
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
|
||||||
wrapper: MockWrapper,
|
|
||||||
});
|
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.error).toBe(undefined);
|
|
||||||
expect(result.current).toEqual({
|
|
||||||
...mockEnvironmentState,
|
|
||||||
VEGA_URL: MOCK_HOST,
|
|
||||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows for the VEGA_NETWORKS to be missing from the environment', async () => {
|
it('allows for the VEGA_NETWORKS to be missing from the environment', async () => {
|
||||||
delete process.env['NX_VEGA_NETWORKS'];
|
delete process.env['NX_VEGA_NETWORKS'];
|
||||||
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||||
@ -214,6 +246,9 @@ describe('useEnvironment hook', () => {
|
|||||||
`(
|
`(
|
||||||
'uses correct default ethereum connection variables in $env',
|
'uses correct default ethereum connection variables in $env',
|
||||||
async ({ env, etherscanUrl, providerUrl }) => {
|
async ({ env, etherscanUrl, providerUrl }) => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() => createMockClient({ network: env }));
|
||||||
|
|
||||||
process.env['NX_VEGA_ENV'] = env;
|
process.env['NX_VEGA_ENV'] = env;
|
||||||
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
||||||
delete process.env['NX_ETHERSCAN_URL'];
|
delete process.env['NX_ETHERSCAN_URL'];
|
||||||
@ -251,4 +286,319 @@ describe('useEnvironment hook', () => {
|
|||||||
`The NX_ETHEREUM_PROVIDER_URL environment variable must be a valid url`
|
`The NX_ETHEREUM_PROVIDER_URL environment variable must be a valid url`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('node selection', () => {
|
||||||
|
it('updates the VEGA_URL from the config when it is missing from the environment', async () => {
|
||||||
|
delete process.env['NX_VEGA_URL'];
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
VEGA_URL: MOCK_HOST,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the VEGA_URL with the quickest node to respond from the config urls', async () => {
|
||||||
|
delete process.env['NX_VEGA_URL'];
|
||||||
|
|
||||||
|
const mockNodes: Record<string, MockRequestConfig> = {
|
||||||
|
'https://mock-node-1.com': { hasError: false, delay: 4 },
|
||||||
|
'https://mock-node-2.com': { hasError: false, delay: 5 },
|
||||||
|
'https://mock-node-3.com': { hasError: false, delay: 8 },
|
||||||
|
'https://mock-node-4.com': { hasError: false, delay: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(
|
||||||
|
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||||
|
);
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||||
|
return createMockClient({ statistics: mockNodes[url] });
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeUrl = getQuickestNode(mockNodes);
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
VEGA_URL: nodeUrl,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores failing nodes and selects the first successful one to use', async () => {
|
||||||
|
delete process.env['NX_VEGA_URL'];
|
||||||
|
|
||||||
|
const mockNodes: Record<string, MockRequestConfig> = {
|
||||||
|
'https://mock-node-1.com': { hasError: true, delay: 4 },
|
||||||
|
'https://mock-node-2.com': { hasError: false, delay: 5 },
|
||||||
|
'https://mock-node-3.com': { hasError: false, delay: 8 },
|
||||||
|
'https://mock-node-4.com': { hasError: true, delay: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(
|
||||||
|
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||||
|
);
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||||
|
return createMockClient({ statistics: mockNodes[url] });
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeUrl = getQuickestNode(mockNodes);
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
VEGA_URL: nodeUrl,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a network error when cannot connect to any nodes', async () => {
|
||||||
|
delete process.env['NX_VEGA_URL'];
|
||||||
|
|
||||||
|
const mockNodes: Record<string, MockRequestConfig> = {
|
||||||
|
'https://mock-node-1.com': { hasError: true, delay: 4 },
|
||||||
|
'https://mock-node-2.com': { hasError: true, delay: 5 },
|
||||||
|
'https://mock-node-3.com': { hasError: true, delay: 8 },
|
||||||
|
'https://mock-node-4.com': { hasError: true, delay: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(
|
||||||
|
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||||
|
);
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||||
|
return createMockClient({ statistics: mockNodes[url] });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
VEGA_URL: undefined,
|
||||||
|
networkError: ErrorType.CONNECTION_ERROR_ALL,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a network error when it cannot fetch the network config and there is no VEGA_URL in the environment', async () => {
|
||||||
|
delete process.env['NX_VEGA_URL'];
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(() => {
|
||||||
|
throw new Error('Cannot fetch');
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
VEGA_URL: undefined,
|
||||||
|
networkError: ErrorType.CONFIG_LOAD_ERROR,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs an error when it cannot fetch the network config and there is a VEGA_URL in the environment', async () => {
|
||||||
|
const consoleWarnSpy = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(noop);
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(() => {
|
||||||
|
throw new Error('Cannot fetch');
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
|
getErrorByType(
|
||||||
|
ErrorType.CONFIG_LOAD_ERROR,
|
||||||
|
mockEnvironmentState.VEGA_ENV
|
||||||
|
)?.headline
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||||
|
// eslint-disable-next-line jest/no-disabled-tests
|
||||||
|
it.skip('has a network error when the config is invalid and there is no VEGA_URL in the environment', async () => {
|
||||||
|
delete process.env['NX_VEGA_URL'];
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ some: 'invalid-object' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
VEGA_URL: undefined,
|
||||||
|
networkError: ErrorType.CONFIG_VALIDATION_ERROR,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||||
|
// eslint-disable-next-line jest/no-disabled-tests
|
||||||
|
it.skip('logs an error when the network config in invalid and there is a VEGA_URL in the environment', async () => {
|
||||||
|
const consoleWarnSpy = jest
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(noop);
|
||||||
|
|
||||||
|
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||||
|
global.fetch.mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ some: 'invalid-object' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
|
getErrorByType(
|
||||||
|
ErrorType.CONFIG_VALIDATION_ERROR,
|
||||||
|
mockEnvironmentState.VEGA_ENV
|
||||||
|
)?.headline
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||||
|
// eslint-disable-next-line jest/no-disabled-tests
|
||||||
|
it.skip('has a network error when the selected node is not a valid url', async () => {
|
||||||
|
process.env['NX_VEGA_URL'] = 'not-url';
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
networkError: ErrorType.INVALID_URL,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a network error when cannot connect to the selected node', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() => {
|
||||||
|
return createMockClient({ statistics: { hasError: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
networkError: ErrorType.CONNECTION_ERROR,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a network error when the selected node is not on the correct network', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() => {
|
||||||
|
return createMockClient({ network: Networks.MAINNET });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
networkError: ErrorType.INVALID_NETWORK,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a network error when the selected node has not ssl available', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() => {
|
||||||
|
return createMockClient({ busEvents: { hasError: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||||
|
wrapper: MockWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.error).toBe(undefined);
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
...mockEnvironmentState,
|
||||||
|
networkError: ErrorType.SSL_ERROR,
|
||||||
|
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useState, createContext, useContext } from 'react';
|
import { useEffect, useState, createContext, useContext } from 'react';
|
||||||
|
|
||||||
import { NodeSwitcherDialog } from '../components/node-switcher-dialog';
|
import { NodeSwitcherDialog } from '../components/node-switcher-dialog';
|
||||||
import { useConfig } from './use-config';
|
import { useConfig } from './use-config';
|
||||||
|
import { useNodes } from './use-nodes';
|
||||||
import { compileEnvironment } from '../utils/compile-environment';
|
import { compileEnvironment } from '../utils/compile-environment';
|
||||||
import { validateEnvironment } from '../utils/validate-environment';
|
import { validateEnvironment } from '../utils/validate-environment';
|
||||||
import type { Environment, RawEnvironment, ConfigStatus } from '../types';
|
import {
|
||||||
|
getErrorType,
|
||||||
|
getErrorByType,
|
||||||
|
getIsNodeLoading,
|
||||||
|
} from '../utils/validate-node';
|
||||||
|
import { ErrorType } from '../types';
|
||||||
|
import type { Environment, RawEnvironment, NodeData } from '../types';
|
||||||
|
|
||||||
type EnvironmentProviderProps = {
|
type EnvironmentProviderProps = {
|
||||||
definitions?: Partial<RawEnvironment>;
|
definitions?: Partial<RawEnvironment>;
|
||||||
@ -13,24 +20,77 @@ type EnvironmentProviderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EnvironmentState = Environment & {
|
export type EnvironmentState = Environment & {
|
||||||
configStatus: ConfigStatus;
|
networkError?: ErrorType;
|
||||||
setNodeSwitcherOpen: () => void;
|
setNodeSwitcherOpen: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvironmentContext = createContext({} as EnvironmentState);
|
const EnvironmentContext = createContext({} as EnvironmentState);
|
||||||
|
|
||||||
|
const hasFinishedLoading = (node: NodeData) =>
|
||||||
|
node.initialized && !getIsNodeLoading(node) && !node.verified;
|
||||||
|
|
||||||
export const EnvironmentProvider = ({
|
export const EnvironmentProvider = ({
|
||||||
definitions,
|
definitions,
|
||||||
children,
|
children,
|
||||||
}: EnvironmentProviderProps) => {
|
}: EnvironmentProviderProps) => {
|
||||||
|
const [networkError, setNetworkError] = useState<undefined | ErrorType>();
|
||||||
const [isNodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
const [isNodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||||
const [environment, updateEnvironment] = useState<Environment>(
|
const [environment, updateEnvironment] = useState<Environment>(
|
||||||
compileEnvironment(definitions)
|
compileEnvironment(definitions)
|
||||||
);
|
);
|
||||||
const { status: configStatus, config } = useConfig(
|
const { loading, config } = useConfig(environment, (errorType) => {
|
||||||
environment,
|
if (!environment.VEGA_URL) {
|
||||||
updateEnvironment
|
setNetworkError(errorType);
|
||||||
);
|
setNodeSwitcherOpen(true);
|
||||||
|
} else {
|
||||||
|
const error = getErrorByType(errorType, environment.VEGA_ENV);
|
||||||
|
error && console.warn(error.headline);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { state: nodes, clients } = useNodes(environment.VEGA_ENV, config);
|
||||||
|
const nodeKeys = Object.keys(nodes);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!environment.VEGA_URL) {
|
||||||
|
const successfulNodeKey = nodeKeys.find(
|
||||||
|
(key) => nodes[key].verified
|
||||||
|
) as keyof typeof nodes;
|
||||||
|
if (successfulNodeKey && nodes[successfulNodeKey]) {
|
||||||
|
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||||
|
updateEnvironment((prevEnvironment) => ({
|
||||||
|
...prevEnvironment,
|
||||||
|
VEGA_URL: nodes[successfulNodeKey].url,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the selected node has errors
|
||||||
|
if (environment.VEGA_URL && nodes[environment.VEGA_URL]) {
|
||||||
|
const errorType = getErrorType(
|
||||||
|
environment.VEGA_ENV,
|
||||||
|
nodes[environment.VEGA_URL]
|
||||||
|
);
|
||||||
|
if (errorType !== null) {
|
||||||
|
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||||
|
setNetworkError(errorType);
|
||||||
|
setNodeSwitcherOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the config doesn't contain nodes the app can connect to
|
||||||
|
if (
|
||||||
|
nodeKeys.length > 0 &&
|
||||||
|
nodeKeys.filter((key) => hasFinishedLoading(nodes[key])).length ===
|
||||||
|
nodeKeys.length
|
||||||
|
) {
|
||||||
|
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||||
|
setNetworkError(ErrorType.CONNECTION_ERROR_ALL);
|
||||||
|
setNodeSwitcherOpen(true);
|
||||||
|
}
|
||||||
|
// prevent infinite render loop by skipping deps which will change as a result
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [environment.VEGA_URL, nodes]);
|
||||||
|
|
||||||
const errorMessage = validateEnvironment(environment);
|
const errorMessage = validateEnvironment(environment);
|
||||||
|
|
||||||
@ -42,20 +102,20 @@ export const EnvironmentProvider = ({
|
|||||||
<EnvironmentContext.Provider
|
<EnvironmentContext.Provider
|
||||||
value={{
|
value={{
|
||||||
...environment,
|
...environment,
|
||||||
configStatus,
|
networkError,
|
||||||
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config && (
|
<NodeSwitcherDialog
|
||||||
<NodeSwitcherDialog
|
dialogOpen={isNodeSwitcherOpen}
|
||||||
dialogOpen={isNodeSwitcherOpen}
|
initialErrorType={networkError}
|
||||||
toggleDialogOpen={setNodeSwitcherOpen}
|
setDialogOpen={setNodeSwitcherOpen}
|
||||||
config={config}
|
loading={loading}
|
||||||
onConnect={(url) =>
|
config={config}
|
||||||
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
onConnect={(url) =>
|
||||||
}
|
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
||||||
/>
|
}
|
||||||
)}
|
/>
|
||||||
{children}
|
{children}
|
||||||
</EnvironmentContext.Provider>
|
</EnvironmentContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,267 +0,0 @@
|
|||||||
import { renderHook, act } from '@testing-library/react-hooks';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
|
|
||||||
import { useNode, STATS_QUERY, TIME_UPDATE_SUBSCRIPTION } from './use-node';
|
|
||||||
|
|
||||||
const MOCK_DURATION = 1073;
|
|
||||||
|
|
||||||
const MOCK_STATISTICS_QUERY_RESULT = {
|
|
||||||
blockHeight: '11',
|
|
||||||
chainId: 'testnet_01234',
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
url: '',
|
|
||||||
responseTime: {
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
block: {
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
ssl: {
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
chain: {
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMockClient = ({
|
|
||||||
failStats = false,
|
|
||||||
failSubscription = false,
|
|
||||||
}: { failStats?: boolean; failSubscription?: boolean } = {}) => {
|
|
||||||
const provider = new MockedProvider({
|
|
||||||
mocks: [
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
query: STATS_QUERY,
|
|
||||||
},
|
|
||||||
result: failStats
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
data: {
|
|
||||||
statistics: {
|
|
||||||
__typename: 'Statistics',
|
|
||||||
...MOCK_STATISTICS_QUERY_RESULT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
query: TIME_UPDATE_SUBSCRIPTION,
|
|
||||||
},
|
|
||||||
result: failSubscription
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
data: {
|
|
||||||
busEvents: {
|
|
||||||
eventId: 'time-0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return provider.state.client;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.performance.getEntriesByName = jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation((url: string) => [
|
|
||||||
{
|
|
||||||
entryType: 'resource',
|
|
||||||
name: url,
|
|
||||||
startTime: 0,
|
|
||||||
toJSON: () => ({}),
|
|
||||||
duration: MOCK_DURATION,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
// @ts-ignore allow deleting the spy function after we're done with the tests
|
|
||||||
delete window.performance.getEntriesByName;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useNode hook', () => {
|
|
||||||
it('returns the default state when no arguments provided', () => {
|
|
||||||
const { result } = renderHook(() => useNode());
|
|
||||||
expect(result.current.state).toEqual(initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the default state when no url provided', () => {
|
|
||||||
const client = createMockClient();
|
|
||||||
const { result } = renderHook(() => useNode(undefined, client));
|
|
||||||
expect(result.current.state).toEqual(initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the default state when no client provided', () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const { result } = renderHook(() => useNode(url, undefined));
|
|
||||||
expect(result.current.state).toEqual({ ...initialState, url });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets loading state while waiting for the results', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient();
|
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
|
||||||
useNode(url, client)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.current.state).toEqual({
|
|
||||||
url,
|
|
||||||
responseTime: {
|
|
||||||
isLoading: true,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
block: {
|
|
||||||
isLoading: true,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
ssl: {
|
|
||||||
isLoading: true,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
chain: {
|
|
||||||
isLoading: true,
|
|
||||||
hasError: false,
|
|
||||||
value: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitForNextUpdate();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets statistics results', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient();
|
|
||||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.block).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.state.chain).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: MOCK_STATISTICS_QUERY_RESULT.chainId,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.state.responseTime).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: MOCK_DURATION,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets subscription result', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient();
|
|
||||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.ssl).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets error when statistics request fails', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient({ failStats: true });
|
|
||||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.block).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: true,
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.state.chain).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: true,
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.state.responseTime).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: true,
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets error when subscription request fails', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient({ failSubscription: true });
|
|
||||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.ssl).toEqual({
|
|
||||||
isLoading: false,
|
|
||||||
hasError: true,
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows updating block values', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient({ failSubscription: true });
|
|
||||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.block.value).toEqual(11);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.updateBlockState(12);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.block.value).toEqual(12);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows resetting the state to defaults', async () => {
|
|
||||||
const url = 'https://some.url';
|
|
||||||
const client = createMockClient();
|
|
||||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.state.block.value).toBe(
|
|
||||||
Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight)
|
|
||||||
);
|
|
||||||
expect(result.current.state.chain.value).toBe(
|
|
||||||
MOCK_STATISTICS_QUERY_RESULT.chainId
|
|
||||||
);
|
|
||||||
expect(result.current.state.responseTime.value).toBe(MOCK_DURATION);
|
|
||||||
expect(result.current.state.ssl.value).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.state).toEqual({ ...initialState, url });
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,188 +0,0 @@
|
|||||||
import { useEffect, useReducer } from 'react';
|
|
||||||
import { produce } from 'immer';
|
|
||||||
import { gql } from '@apollo/client';
|
|
||||||
import type { createClient } from '../utils/apollo-client';
|
|
||||||
import type { NodeData } from '../types';
|
|
||||||
import type { Statistics } from './__generated__/Statistics';
|
|
||||||
|
|
||||||
type StatisticsPayload = {
|
|
||||||
block: NodeData['block']['value'];
|
|
||||||
chain: NodeData['chain']['value'];
|
|
||||||
responseTime: NodeData['responseTime']['value'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const STATS_QUERY = gql`
|
|
||||||
query Statistics {
|
|
||||||
statistics {
|
|
||||||
chainId
|
|
||||||
blockHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TIME_UPDATE_SUBSCRIPTION = gql`
|
|
||||||
subscription BlockTime {
|
|
||||||
busEvents(types: TimeUpdate, batchSize: 1) {
|
|
||||||
eventId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
enum ACTION {
|
|
||||||
GET_STATISTICS,
|
|
||||||
GET_STATISTICS_SUCCESS,
|
|
||||||
GET_STATISTICS_FAILURE,
|
|
||||||
CHECK_SUBSCRIPTION,
|
|
||||||
CHECK_SUBSCRIPTION_SUCCESS,
|
|
||||||
CHECK_SUBSCRIPTION_FAILURE,
|
|
||||||
UPDATE_BLOCK,
|
|
||||||
RESET_STATE,
|
|
||||||
}
|
|
||||||
|
|
||||||
function withData<T>(value?: T) {
|
|
||||||
return {
|
|
||||||
isLoading: false,
|
|
||||||
hasError: false,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function withError<T>(value?: T) {
|
|
||||||
return {
|
|
||||||
isLoading: false,
|
|
||||||
hasError: true,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInitialState = (url?: string): NodeData => ({
|
|
||||||
url: url ?? '',
|
|
||||||
responseTime: withData(),
|
|
||||||
block: withData(),
|
|
||||||
ssl: withData(),
|
|
||||||
chain: withData(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getResponseTime = (url: string) => {
|
|
||||||
const requests = window.performance.getEntriesByName(url);
|
|
||||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
|
||||||
return duration;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActionType<T extends ACTION, P = undefined> = {
|
|
||||||
type: T;
|
|
||||||
payload?: P;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Action =
|
|
||||||
| ActionType<ACTION.GET_STATISTICS>
|
|
||||||
| ActionType<ACTION.GET_STATISTICS_SUCCESS, StatisticsPayload>
|
|
||||||
| ActionType<ACTION.GET_STATISTICS_FAILURE>
|
|
||||||
| ActionType<ACTION.CHECK_SUBSCRIPTION>
|
|
||||||
| ActionType<ACTION.CHECK_SUBSCRIPTION_SUCCESS>
|
|
||||||
| ActionType<ACTION.CHECK_SUBSCRIPTION_FAILURE>
|
|
||||||
| ActionType<ACTION.UPDATE_BLOCK, number>
|
|
||||||
| ActionType<ACTION.RESET_STATE>;
|
|
||||||
|
|
||||||
const reducer = (state: NodeData, action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case ACTION.GET_STATISTICS:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.block.isLoading = true;
|
|
||||||
state.chain.isLoading = true;
|
|
||||||
state.responseTime.isLoading = true;
|
|
||||||
});
|
|
||||||
case ACTION.GET_STATISTICS_SUCCESS:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.block = withData(action.payload?.block);
|
|
||||||
state.chain = withData(action.payload?.chain);
|
|
||||||
state.responseTime = withData(action.payload?.responseTime);
|
|
||||||
});
|
|
||||||
case ACTION.GET_STATISTICS_FAILURE:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.block = withError();
|
|
||||||
state.chain = withError();
|
|
||||||
state.responseTime = withError();
|
|
||||||
});
|
|
||||||
case ACTION.CHECK_SUBSCRIPTION:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.ssl.isLoading = true;
|
|
||||||
});
|
|
||||||
case ACTION.CHECK_SUBSCRIPTION_SUCCESS:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.ssl = withData(true);
|
|
||||||
});
|
|
||||||
case ACTION.CHECK_SUBSCRIPTION_FAILURE:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.ssl = withError();
|
|
||||||
});
|
|
||||||
case ACTION.UPDATE_BLOCK:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.block.value = action.payload;
|
|
||||||
});
|
|
||||||
case ACTION.RESET_STATE:
|
|
||||||
return produce(state, (state) => {
|
|
||||||
state.responseTime = withData();
|
|
||||||
state.block = withData();
|
|
||||||
state.ssl = withData();
|
|
||||||
state.chain = withData();
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useNode = (
|
|
||||||
url?: string,
|
|
||||||
client?: ReturnType<typeof createClient>
|
|
||||||
) => {
|
|
||||||
const [state, dispatch] = useReducer(reducer, getInitialState(url));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (client && url) {
|
|
||||||
dispatch({ type: ACTION.GET_STATISTICS });
|
|
||||||
dispatch({ type: ACTION.CHECK_SUBSCRIPTION });
|
|
||||||
|
|
||||||
client
|
|
||||||
.query<Statistics>({
|
|
||||||
query: STATS_QUERY,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTION.GET_STATISTICS_SUCCESS,
|
|
||||||
payload: {
|
|
||||||
chain: res.data.statistics.chainId,
|
|
||||||
block: Number(res.data.statistics.blockHeight),
|
|
||||||
responseTime: getResponseTime(url),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
dispatch({ type: ACTION.GET_STATISTICS_FAILURE });
|
|
||||||
});
|
|
||||||
|
|
||||||
const subscription = client
|
|
||||||
.subscribe({
|
|
||||||
query: TIME_UPDATE_SUBSCRIPTION,
|
|
||||||
errorPolicy: 'all',
|
|
||||||
})
|
|
||||||
.subscribe({
|
|
||||||
next() {
|
|
||||||
dispatch({ type: ACTION.CHECK_SUBSCRIPTION_SUCCESS });
|
|
||||||
subscription.unsubscribe();
|
|
||||||
},
|
|
||||||
error() {
|
|
||||||
dispatch({ type: ACTION.CHECK_SUBSCRIPTION_FAILURE });
|
|
||||||
subscription.unsubscribe();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [client, url, dispatch]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
updateBlockState: (value: number) =>
|
|
||||||
dispatch({ type: ACTION.UPDATE_BLOCK, payload: value }),
|
|
||||||
reset: () => dispatch({ type: ACTION.RESET_STATE }),
|
|
||||||
};
|
|
||||||
};
|
|
401
libs/environment/src/hooks/use-nodes.spec.tsx
Normal file
401
libs/environment/src/hooks/use-nodes.spec.tsx
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react-hooks';
|
||||||
|
import { ApolloClient } from '@apollo/client';
|
||||||
|
import createClient from '../utils/apollo-client';
|
||||||
|
import { useNodes } from './use-nodes';
|
||||||
|
import { Networks } from '../types';
|
||||||
|
import createMockClient, {
|
||||||
|
getMockStatisticsResult,
|
||||||
|
} from './mocks/apollo-client';
|
||||||
|
|
||||||
|
jest.mock('../utils/apollo-client');
|
||||||
|
|
||||||
|
const MOCK_ENV = Networks.DEVNET;
|
||||||
|
const MOCK_DURATION = 1073;
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
url: '',
|
||||||
|
verified: false,
|
||||||
|
initialized: false,
|
||||||
|
responseTime: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
chain: {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.performance.getEntriesByName = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((url: string) => [
|
||||||
|
{
|
||||||
|
entryType: 'resource',
|
||||||
|
name: url,
|
||||||
|
startTime: 0,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
duration: MOCK_DURATION,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() => createMockClient());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useNodes hook', () => {
|
||||||
|
it('returns the default state when empty config provided', () => {
|
||||||
|
const { result } = renderHook(() => useNodes(MOCK_ENV, { hosts: [] }));
|
||||||
|
|
||||||
|
expect(result.current.state).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets loading state while waiting for the results', async () => {
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state[node]).toEqual({
|
||||||
|
...initialState,
|
||||||
|
url: node,
|
||||||
|
verified: false,
|
||||||
|
initialized: true,
|
||||||
|
responseTime: {
|
||||||
|
...initialState.responseTime,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
...initialState.block,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
chain: {
|
||||||
|
...initialState.chain,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
ssl: {
|
||||||
|
...initialState.ssl,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForNextUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets statistics results', async () => {
|
||||||
|
const mockResult = getMockStatisticsResult();
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: Number(mockResult.statistics.blockHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[node].chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: mockResult.statistics.chainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[node].responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: MOCK_DURATION,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets subscription result', async () => {
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when host in not a valid url', async () => {
|
||||||
|
const node = 'not-url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block.hasError).toBe(true);
|
||||||
|
expect(result.current.state[node].chain.hasError).toBe(true);
|
||||||
|
expect(result.current.state[node].responseTime.hasError).toBe(true);
|
||||||
|
expect(result.current.state[node].responseTime.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when statistics request fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() =>
|
||||||
|
createMockClient({ statistics: { hasError: true } })
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[node].chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[node].responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when subscription request fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() =>
|
||||||
|
createMockClient({ busEvents: { hasError: true } })
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows updating block values', async () => {
|
||||||
|
const mockResult = getMockStatisticsResult();
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block.value).toEqual(
|
||||||
|
Number(mockResult.statistics.blockHeight)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeBlock(node, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block.value).toEqual(12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when calling the block update on a non-existing node', async () => {
|
||||||
|
const mockResult = getMockStatisticsResult();
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block.value).toEqual(
|
||||||
|
Number(mockResult.statistics.blockHeight)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeBlock('https://non-existing.url', 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state['https://non-existing.url']).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds new node', async () => {
|
||||||
|
const node = 'custom-node-key';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [] })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state[node]).toEqual(undefined);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addNode(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node]).toEqual(initialState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets new url for node', async () => {
|
||||||
|
const node = 'https://some.url';
|
||||||
|
const newUrl = 'https://some-other.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [node] })
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeUrl(node, newUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].url).toBe(newUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when custom node has an invalid url', async () => {
|
||||||
|
const node = 'node-key';
|
||||||
|
const url = 'not-url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [] })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state[node]).toBe(undefined);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeUrl(node, url);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].url).toBe(url);
|
||||||
|
expect(result.current.state[node].block.hasError).toBe(true);
|
||||||
|
expect(result.current.state[node].chain.hasError).toBe(true);
|
||||||
|
expect(result.current.state[node].responseTime.hasError).toBe(true);
|
||||||
|
expect(result.current.state[node].ssl.hasError).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when custom node statistics request fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() =>
|
||||||
|
createMockClient({ statistics: { hasError: true } })
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = 'node-key';
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [] })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state[node]).toBe(undefined);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeUrl(node, url);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].block).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[node].chain).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.state[node].responseTime).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when custom node subscription fails', async () => {
|
||||||
|
// @ts-ignore allow adding a mock return value to mocked module
|
||||||
|
createClient.mockImplementation(() =>
|
||||||
|
createMockClient({ busEvents: { hasError: true } })
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = 'node-key';
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [] })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.state[node]).toBe(undefined);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeUrl(node, url);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.state[node].ssl).toEqual({
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes a collection of clients', async () => {
|
||||||
|
const url1 = 'https://some.url';
|
||||||
|
const url2 = 'https://some-other.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [url1, url2] })
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.clients[url1]).toBeInstanceOf(ApolloClient);
|
||||||
|
expect(result.current.clients[url2]).toBeInstanceOf(ApolloClient);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes a client for the custom node', async () => {
|
||||||
|
const node = 'node-key';
|
||||||
|
const url = 'https://some.url';
|
||||||
|
const { result, waitFor } = renderHook(() =>
|
||||||
|
useNodes(MOCK_ENV, { hosts: [] })
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateNodeUrl(node, url);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.clients[url]).toBeInstanceOf(ApolloClient);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
260
libs/environment/src/hooks/use-nodes.tsx
Normal file
260
libs/environment/src/hooks/use-nodes.tsx
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import type { Dispatch } from 'react';
|
||||||
|
import { useState, useEffect, useReducer } from 'react';
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import type createClient from '../utils/apollo-client';
|
||||||
|
import { initializeNode } from '../utils/initialize-node';
|
||||||
|
import { getErrorType, getIsNodeLoading } from '../utils/validate-node';
|
||||||
|
import type { NodeData, Configuration, Networks } from '../types';
|
||||||
|
|
||||||
|
type StatisticsPayload = {
|
||||||
|
block: NodeData['block']['value'];
|
||||||
|
chain: NodeData['chain']['value'];
|
||||||
|
responseTime: NodeData['responseTime']['value'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ACTIONS {
|
||||||
|
GET_STATISTICS,
|
||||||
|
GET_STATISTICS_SUCCESS,
|
||||||
|
GET_STATISTICS_FAILURE,
|
||||||
|
CHECK_SUBSCRIPTION,
|
||||||
|
CHECK_SUBSCRIPTION_SUCCESS,
|
||||||
|
CHECK_SUBSCRIPTION_FAILURE,
|
||||||
|
ADD_NODE,
|
||||||
|
UPDATE_NODE_URL,
|
||||||
|
UPDATE_NODE_BLOCK,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType<T extends ACTIONS, P = undefined> = {
|
||||||
|
type: T;
|
||||||
|
node: string;
|
||||||
|
payload?: P;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Action =
|
||||||
|
| ActionType<ACTIONS.GET_STATISTICS, { url: string }>
|
||||||
|
| ActionType<ACTIONS.GET_STATISTICS_SUCCESS, StatisticsPayload>
|
||||||
|
| ActionType<ACTIONS.GET_STATISTICS_FAILURE>
|
||||||
|
| ActionType<ACTIONS.CHECK_SUBSCRIPTION, { url: string }>
|
||||||
|
| ActionType<ACTIONS.CHECK_SUBSCRIPTION_SUCCESS>
|
||||||
|
| ActionType<ACTIONS.CHECK_SUBSCRIPTION_FAILURE>
|
||||||
|
| ActionType<ACTIONS.ADD_NODE>
|
||||||
|
| ActionType<ACTIONS.UPDATE_NODE_URL, { url: string }>
|
||||||
|
| ActionType<ACTIONS.UPDATE_NODE_BLOCK, number>;
|
||||||
|
|
||||||
|
function withData<T>(value?: T) {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function withError<T>(value?: T) {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
hasError: true,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNodeData = (url?: string): NodeData => ({
|
||||||
|
url: url ?? '',
|
||||||
|
verified: false,
|
||||||
|
initialized: false,
|
||||||
|
responseTime: withData(),
|
||||||
|
block: withData(),
|
||||||
|
ssl: withData(),
|
||||||
|
chain: withData(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getInitialState = (config?: Configuration) =>
|
||||||
|
(config?.hosts ?? []).reduce<Record<string, NodeData>>(
|
||||||
|
(acc, url) => ({
|
||||||
|
...acc,
|
||||||
|
[url]: getNodeData(url),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
type ClientCollection = Record<
|
||||||
|
string,
|
||||||
|
undefined | ReturnType<typeof createClient>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ClientData = {
|
||||||
|
clients: ClientCollection;
|
||||||
|
subscriptions: ReturnType<typeof initializeNode>['unsubscribe'][];
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeNodes = (
|
||||||
|
dispatch: Dispatch<Action>,
|
||||||
|
nodes: Record<string, string>
|
||||||
|
) => {
|
||||||
|
return Object.keys(nodes).reduce<ClientData>(
|
||||||
|
(acc, node) => {
|
||||||
|
const { client, unsubscribe } = initializeNode(
|
||||||
|
dispatch,
|
||||||
|
node,
|
||||||
|
nodes[node]
|
||||||
|
);
|
||||||
|
Object.assign(acc.clients, { [nodes[node]]: client });
|
||||||
|
acc.subscriptions.push(unsubscribe);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clients: {},
|
||||||
|
subscriptions: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer =
|
||||||
|
(env: Networks) => (state: Record<string, NodeData>, action: Action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ACTIONS.GET_STATISTICS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) {
|
||||||
|
state[action.node] = getNodeData(action.payload?.url);
|
||||||
|
}
|
||||||
|
state[action.node].url = action.payload?.url ?? '';
|
||||||
|
state[action.node].initialized = true;
|
||||||
|
state[action.node].block.isLoading = true;
|
||||||
|
state[action.node].chain.isLoading = true;
|
||||||
|
state[action.node].responseTime.isLoading = true;
|
||||||
|
});
|
||||||
|
case ACTIONS.GET_STATISTICS_SUCCESS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].block = withData(action.payload?.block);
|
||||||
|
state[action.node].chain = withData(action.payload?.chain);
|
||||||
|
state[action.node].responseTime = withData(
|
||||||
|
action.payload?.responseTime
|
||||||
|
);
|
||||||
|
state[action.node].verified =
|
||||||
|
!getIsNodeLoading(state[action.node]) &&
|
||||||
|
getErrorType(env, state[action.node]) === null;
|
||||||
|
});
|
||||||
|
case ACTIONS.GET_STATISTICS_FAILURE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].block = withError();
|
||||||
|
state[action.node].chain = withError();
|
||||||
|
state[action.node].responseTime = withError();
|
||||||
|
});
|
||||||
|
case ACTIONS.CHECK_SUBSCRIPTION:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) {
|
||||||
|
state[action.node] = getNodeData(action.payload?.url);
|
||||||
|
}
|
||||||
|
state[action.node].url = action.payload?.url ?? '';
|
||||||
|
state[action.node].ssl.isLoading = true;
|
||||||
|
state[action.node].initialized = true;
|
||||||
|
});
|
||||||
|
case ACTIONS.CHECK_SUBSCRIPTION_SUCCESS:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].ssl = withData(true);
|
||||||
|
state[action.node].verified =
|
||||||
|
!getIsNodeLoading(state[action.node]) &&
|
||||||
|
getErrorType(env, state[action.node]) === null;
|
||||||
|
});
|
||||||
|
case ACTIONS.CHECK_SUBSCRIPTION_FAILURE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].ssl = withError();
|
||||||
|
});
|
||||||
|
case ACTIONS.ADD_NODE:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
state[action.node] = getNodeData();
|
||||||
|
});
|
||||||
|
case ACTIONS.UPDATE_NODE_URL:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
const existingNode = Object.keys(state).find(
|
||||||
|
(node) =>
|
||||||
|
action.node !== node && state[node].url === action.payload?.url
|
||||||
|
);
|
||||||
|
state[action.node] = existingNode
|
||||||
|
? state[existingNode]
|
||||||
|
: getNodeData(action.payload?.url);
|
||||||
|
});
|
||||||
|
case ACTIONS.UPDATE_NODE_BLOCK:
|
||||||
|
return produce(state, (state) => {
|
||||||
|
if (!state[action.node]) return;
|
||||||
|
state[action.node].block.value = action.payload;
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNodes = (env: Networks, config?: Configuration) => {
|
||||||
|
const [clients, setClients] = useState<ClientCollection>({});
|
||||||
|
const [state, dispatch] = useReducer(reducer(env), getInitialState(config));
|
||||||
|
const configCacheKey = config?.hosts.join(';');
|
||||||
|
const allUrls = Object.keys(state).map((node) => state[node].url);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
Object.keys(clients).forEach((url) => clients[url]?.stop());
|
||||||
|
};
|
||||||
|
// stop all created clients on unmount
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nodeUrlMap = (config?.hosts || []).reduce(
|
||||||
|
(acc, url) => ({ ...acc, [url]: url }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const { clients: newClients, subscriptions } = initializeNodes(
|
||||||
|
dispatch,
|
||||||
|
nodeUrlMap
|
||||||
|
);
|
||||||
|
setClients(newClients);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
// use primitive cache key to prevent infinite rerender loop
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [configCacheKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allNodes = Object.keys(state);
|
||||||
|
const initializedUrls = Object.keys(clients);
|
||||||
|
const nodeUrlMap = allUrls
|
||||||
|
.filter((node) => !initializedUrls.includes(node))
|
||||||
|
.reduce<Record<string, string>>((acc, url) => {
|
||||||
|
const node = allNodes.find((key) => state[key].url === url);
|
||||||
|
if (node) {
|
||||||
|
acc[node] = url;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const { clients: newClients, subscriptions } = initializeNodes(
|
||||||
|
dispatch,
|
||||||
|
nodeUrlMap
|
||||||
|
);
|
||||||
|
setClients((prevClients) => ({
|
||||||
|
...prevClients,
|
||||||
|
...newClients,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
// use primitive cache key to prevent infinite rerender loop
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [allUrls.join(';')]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
clients,
|
||||||
|
addNode: (node: string) => dispatch({ type: ACTIONS.ADD_NODE, node }),
|
||||||
|
updateNodeUrl: (node: string, url: string) =>
|
||||||
|
dispatch({ type: ACTIONS.UPDATE_NODE_URL, node, payload: { url } }),
|
||||||
|
updateNodeBlock: (node: string, value: number) =>
|
||||||
|
dispatch({ type: ACTIONS.UPDATE_NODE_BLOCK, node, payload: value }),
|
||||||
|
};
|
||||||
|
};
|
@ -6,6 +6,18 @@ import { Networks, ENV_KEYS } from './utils/validate-environment';
|
|||||||
|
|
||||||
export { ENV_KEYS, Networks };
|
export { ENV_KEYS, Networks };
|
||||||
|
|
||||||
|
export const CUSTOM_NODE_KEY = 'custom';
|
||||||
|
|
||||||
|
export enum ErrorType {
|
||||||
|
INVALID_URL,
|
||||||
|
INVALID_NETWORK,
|
||||||
|
SSL_ERROR,
|
||||||
|
CONNECTION_ERROR,
|
||||||
|
CONNECTION_ERROR_ALL,
|
||||||
|
CONFIG_LOAD_ERROR,
|
||||||
|
CONFIG_VALIDATION_ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
export type Environment = z.infer<typeof envSchema> & {
|
export type Environment = z.infer<typeof envSchema> & {
|
||||||
// provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS
|
// provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS
|
||||||
VEGA_NETWORKS: Partial<Record<Networks, string>>;
|
VEGA_NETWORKS: Partial<Record<Networks, string>>;
|
||||||
@ -17,15 +29,6 @@ export type RawEnvironment = Record<EnvKey, string>;
|
|||||||
|
|
||||||
export type Configuration = z.infer<typeof configSchema>;
|
export type Configuration = z.infer<typeof configSchema>;
|
||||||
|
|
||||||
export type ConfigStatus =
|
|
||||||
| 'idle'
|
|
||||||
| 'success'
|
|
||||||
| 'loading-config'
|
|
||||||
| 'loading-node'
|
|
||||||
| 'error-loading-config'
|
|
||||||
| 'error-validating-config'
|
|
||||||
| 'error-loading-node';
|
|
||||||
|
|
||||||
type NodeCheck<T> = {
|
type NodeCheck<T> = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
@ -34,6 +37,8 @@ type NodeCheck<T> = {
|
|||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
url: string;
|
url: string;
|
||||||
|
verified: boolean;
|
||||||
|
initialized: boolean;
|
||||||
ssl: NodeCheck<boolean>;
|
ssl: NodeCheck<boolean>;
|
||||||
block: NodeCheck<number>;
|
block: NodeCheck<number>;
|
||||||
responseTime: NodeCheck<number>;
|
responseTime: NodeCheck<number>;
|
||||||
|
@ -14,13 +14,14 @@ import { RetryLink } from '@apollo/client/link/retry';
|
|||||||
|
|
||||||
const isBrowser = typeof window !== 'undefined';
|
const isBrowser = typeof window !== 'undefined';
|
||||||
|
|
||||||
export function createClient(base?: string) {
|
export const GQL_PATH = 'query';
|
||||||
|
|
||||||
|
export default function createClient(base?: string) {
|
||||||
if (!base) {
|
if (!base) {
|
||||||
throw new Error('Base must be passed into createClient!');
|
throw new Error('Base must be passed into createClient!');
|
||||||
}
|
}
|
||||||
const gqlPath = 'query';
|
const urlHTTP = new URL(GQL_PATH, base);
|
||||||
const urlHTTP = new URL(gqlPath, base);
|
const urlWS = new URL(GQL_PATH, base);
|
||||||
const urlWS = new URL(gqlPath, base);
|
|
||||||
// Replace http with ws, preserving if its a secure connection eg. https => wss
|
// Replace http with ws, preserving if its a secure connection eg. https => wss
|
||||||
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
|
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
|
||||||
|
|
||||||
|
56
libs/environment/src/utils/initialize-node.tsx
Normal file
56
libs/environment/src/utils/initialize-node.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { Dispatch } from 'react';
|
||||||
|
import { ACTIONS } from '../hooks/use-nodes';
|
||||||
|
import type { Action } from '../hooks/use-nodes';
|
||||||
|
import { requestNode } from './request-node';
|
||||||
|
import { GQL_PATH } from './apollo-client';
|
||||||
|
|
||||||
|
const getResponseTime = (url: string) => {
|
||||||
|
const requestUrl = new URL(GQL_PATH, url);
|
||||||
|
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||||
|
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||||
|
return duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeNode = (
|
||||||
|
dispatch: Dispatch<Action>,
|
||||||
|
node: string,
|
||||||
|
nodeUrl?: string
|
||||||
|
) => {
|
||||||
|
let isMounted = true;
|
||||||
|
const url = nodeUrl ?? node;
|
||||||
|
|
||||||
|
dispatch({ type: ACTIONS.GET_STATISTICS, node, payload: { url } });
|
||||||
|
dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION, node, payload: { url } });
|
||||||
|
|
||||||
|
const client = requestNode(url, {
|
||||||
|
onStatsSuccess: (data) => {
|
||||||
|
isMounted &&
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.GET_STATISTICS_SUCCESS,
|
||||||
|
node,
|
||||||
|
payload: {
|
||||||
|
chain: data.statistics.chainId,
|
||||||
|
block: Number(data.statistics.blockHeight),
|
||||||
|
responseTime: getResponseTime(url),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStatsFailure: () => {
|
||||||
|
isMounted && dispatch({ type: ACTIONS.GET_STATISTICS_FAILURE, node });
|
||||||
|
},
|
||||||
|
onSubscriptionSuccess: () => {
|
||||||
|
isMounted && dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_SUCCESS, node });
|
||||||
|
},
|
||||||
|
onSubscriptionFailure: () => {
|
||||||
|
isMounted && dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_FAILURE, node });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
unsubscribe: () => {
|
||||||
|
client?.stop();
|
||||||
|
isMounted = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -1,22 +0,0 @@
|
|||||||
export function promiseRaceToSuccess<T>(requests: Array<Promise<T>>) {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
let hasResolved = false;
|
|
||||||
const failures = [];
|
|
||||||
|
|
||||||
requests.forEach((req) => {
|
|
||||||
req
|
|
||||||
.then((res) => {
|
|
||||||
if (!hasResolved) {
|
|
||||||
resolve(res);
|
|
||||||
hasResolved = true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
failures.push(err);
|
|
||||||
if (failures.length === requests.length) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
76
libs/environment/src/utils/request-node.ts
Normal file
76
libs/environment/src/utils/request-node.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import createClient from './apollo-client';
|
||||||
|
import type { Statistics } from './__generated__/Statistics';
|
||||||
|
|
||||||
|
export const STATS_QUERY = gql`
|
||||||
|
query Statistics {
|
||||||
|
statistics {
|
||||||
|
chainId
|
||||||
|
blockHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TIME_UPDATE_SUBSCRIPTION = gql`
|
||||||
|
subscription BlockTime {
|
||||||
|
busEvents(types: TimeUpdate, batchSize: 1) {
|
||||||
|
eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Callbacks = {
|
||||||
|
onStatsSuccess: (data: Statistics) => void;
|
||||||
|
onStatsFailure: () => void;
|
||||||
|
onSubscriptionSuccess: () => void;
|
||||||
|
onSubscriptionFailure: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestNode = (
|
||||||
|
url: string,
|
||||||
|
{
|
||||||
|
onStatsSuccess,
|
||||||
|
onStatsFailure,
|
||||||
|
onSubscriptionSuccess,
|
||||||
|
onSubscriptionFailure,
|
||||||
|
}: Callbacks
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
} catch (err) {
|
||||||
|
onStatsFailure();
|
||||||
|
onSubscriptionFailure();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient(url);
|
||||||
|
|
||||||
|
client
|
||||||
|
.query<Statistics>({
|
||||||
|
query: STATS_QUERY,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
onStatsSuccess(res.data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
onStatsFailure();
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscription = client
|
||||||
|
.subscribe({
|
||||||
|
query: TIME_UPDATE_SUBSCRIPTION,
|
||||||
|
errorPolicy: 'all',
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next() {
|
||||||
|
onSubscriptionSuccess();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
onSubscriptionFailure();
|
||||||
|
subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
|
};
|
149
libs/environment/src/utils/validate-node.tsx
Normal file
149
libs/environment/src/utils/validate-node.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { CUSTOM_NODE_KEY, ErrorType } from '../types';
|
||||||
|
import type { Networks, NodeData } from '../types';
|
||||||
|
|
||||||
|
export const getIsNodeLoading = ({
|
||||||
|
chain,
|
||||||
|
responseTime,
|
||||||
|
block,
|
||||||
|
ssl,
|
||||||
|
}: NodeData) => {
|
||||||
|
return (
|
||||||
|
chain.isLoading ||
|
||||||
|
responseTime.isLoading ||
|
||||||
|
block.isLoading ||
|
||||||
|
ssl.isLoading
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHasInvalidChain = (env: Networks, chain = '') => {
|
||||||
|
return !(chain.split('-')[0] === env.toLowerCase() ?? false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsInvalidUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsNodeDisabled = (env: Networks, data?: NodeData) => {
|
||||||
|
return (
|
||||||
|
!!data &&
|
||||||
|
(getIsNodeLoading(data) ||
|
||||||
|
getHasInvalidChain(env, data.chain.value) ||
|
||||||
|
getIsInvalidUrl(data.url) ||
|
||||||
|
data.chain.hasError ||
|
||||||
|
data.responseTime.hasError ||
|
||||||
|
data.block.hasError ||
|
||||||
|
data.ssl.hasError)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsFormDisabled = (
|
||||||
|
currentNode: string | undefined,
|
||||||
|
inputText: string,
|
||||||
|
env: Networks,
|
||||||
|
state: Record<string, NodeData>
|
||||||
|
) => {
|
||||||
|
if (!currentNode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentNode === CUSTOM_NODE_KEY &&
|
||||||
|
state[CUSTOM_NODE_KEY] &&
|
||||||
|
inputText !== state[CUSTOM_NODE_KEY].url
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = state[currentNode];
|
||||||
|
return getIsNodeDisabled(env, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorByType = (
|
||||||
|
errorType: ErrorType | undefined | null,
|
||||||
|
env: Networks,
|
||||||
|
url?: string
|
||||||
|
) => {
|
||||||
|
switch (errorType) {
|
||||||
|
case ErrorType.INVALID_URL:
|
||||||
|
return {
|
||||||
|
headline: t('Error: invalid url'),
|
||||||
|
message: t(url ? `${url} is not a valid url.` : ''),
|
||||||
|
};
|
||||||
|
case ErrorType.INVALID_NETWORK:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: incorrect network`),
|
||||||
|
message: t(`This node is not on the ${env} network.`),
|
||||||
|
};
|
||||||
|
case ErrorType.SSL_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: the node you are reading from does not have SSL`),
|
||||||
|
message: t(
|
||||||
|
url
|
||||||
|
? `${url} does not have SSL. SSL is required to subscribe to data.`
|
||||||
|
: ''
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case ErrorType.CONNECTION_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: can't connect to node`),
|
||||||
|
message: t(url ? `There was an error connecting to ${url}.` : ''),
|
||||||
|
};
|
||||||
|
case ErrorType.CONNECTION_ERROR_ALL:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: can't connect to any of the nodes on the network`),
|
||||||
|
message: t(
|
||||||
|
`Please try entering a custom node address, or try again later.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case ErrorType.CONFIG_VALIDATION_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(
|
||||||
|
`Error: the configuration found for the network ${env} is invalid`
|
||||||
|
),
|
||||||
|
message: t(
|
||||||
|
`Please try entering a custom node address, or try again later.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
case ErrorType.CONFIG_LOAD_ERROR:
|
||||||
|
return {
|
||||||
|
headline: t(`Error: can't load network configuration`),
|
||||||
|
message: t(
|
||||||
|
`You can try entering a custom node address, or try again later.`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getErrorType = (env: Networks, data?: NodeData) => {
|
||||||
|
if (data && !getIsNodeLoading(data) && data.initialized) {
|
||||||
|
if (getIsInvalidUrl(data.url)) {
|
||||||
|
return ErrorType.INVALID_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.chain.hasError ||
|
||||||
|
data.responseTime.hasError ||
|
||||||
|
data.block.hasError
|
||||||
|
) {
|
||||||
|
return ErrorType.CONNECTION_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getHasInvalidChain(env, data.chain.value)) {
|
||||||
|
return ErrorType.INVALID_NETWORK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.ssl.hasError) {
|
||||||
|
return ErrorType.SSL_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -14,6 +14,7 @@
|
|||||||
"**/*.spec.js",
|
"**/*.spec.js",
|
||||||
"**/*.test.jsx",
|
"**/*.test.jsx",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.jsx",
|
||||||
"**/*.d.ts"
|
"**/*.d.ts",
|
||||||
|
"**/__mocks__/*.tsx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
52
libs/fills/src/lib/__generated__/Fills.ts
generated
52
libs/fills/src/lib/__generated__/Fills.ts
generated
@ -9,7 +9,7 @@ import { Pagination, Side } from "@vegaprotocol/types";
|
|||||||
// GraphQL query operation: Fills
|
// GraphQL query operation: Fills
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_buyer {
|
export interface Fills_party_tradesConnection_edges_node_buyer {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
* Party identifier
|
* Party identifier
|
||||||
@ -17,7 +17,7 @@ export interface Fills_party_tradesPaged_edges_node_buyer {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_seller {
|
export interface Fills_party_tradesConnection_edges_node_seller {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
* Party identifier
|
* Party identifier
|
||||||
@ -25,7 +25,7 @@ export interface Fills_party_tradesPaged_edges_node_seller {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_buyerFee {
|
export interface Fills_party_tradesConnection_edges_node_buyerFee {
|
||||||
__typename: "TradeFee";
|
__typename: "TradeFee";
|
||||||
/**
|
/**
|
||||||
* The maker fee, aggressive party to the other party (the one who had an order in the book)
|
* The maker fee, aggressive party to the other party (the one who had an order in the book)
|
||||||
@ -41,7 +41,7 @@ export interface Fills_party_tradesPaged_edges_node_buyerFee {
|
|||||||
liquidityFee: string;
|
liquidityFee: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_sellerFee {
|
export interface Fills_party_tradesConnection_edges_node_sellerFee {
|
||||||
__typename: "TradeFee";
|
__typename: "TradeFee";
|
||||||
/**
|
/**
|
||||||
* The maker fee, aggressive party to the other party (the one who had an order in the book)
|
* The maker fee, aggressive party to the other party (the one who had an order in the book)
|
||||||
@ -57,7 +57,7 @@ export interface Fills_party_tradesPaged_edges_node_sellerFee {
|
|||||||
liquidityFee: string;
|
liquidityFee: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product_settlementAsset {
|
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product_settlementAsset {
|
||||||
__typename: "Asset";
|
__typename: "Asset";
|
||||||
/**
|
/**
|
||||||
* The id of the asset
|
* The id of the asset
|
||||||
@ -73,15 +73,15 @@ export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_in
|
|||||||
decimals: number;
|
decimals: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product {
|
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product {
|
||||||
__typename: "Future";
|
__typename: "Future";
|
||||||
/**
|
/**
|
||||||
* The name of the asset (string)
|
* The name of the asset (string)
|
||||||
*/
|
*/
|
||||||
settlementAsset: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product_settlementAsset;
|
settlementAsset: Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product_settlementAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument {
|
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument {
|
||||||
__typename: "Instrument";
|
__typename: "Instrument";
|
||||||
/**
|
/**
|
||||||
* Uniquely identify an instrument across all instruments available on Vega (string)
|
* Uniquely identify an instrument across all instruments available on Vega (string)
|
||||||
@ -94,18 +94,18 @@ export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_in
|
|||||||
/**
|
/**
|
||||||
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
||||||
*/
|
*/
|
||||||
product: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product;
|
product: Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument {
|
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument {
|
||||||
__typename: "TradableInstrument";
|
__typename: "TradableInstrument";
|
||||||
/**
|
/**
|
||||||
* An instance of or reference to a fully specified instrument.
|
* An instance of or reference to a fully specified instrument.
|
||||||
*/
|
*/
|
||||||
instrument: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument;
|
instrument: Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node_market {
|
export interface Fills_party_tradesConnection_edges_node_market {
|
||||||
__typename: "Market";
|
__typename: "Market";
|
||||||
/**
|
/**
|
||||||
* Market ID
|
* Market ID
|
||||||
@ -141,10 +141,10 @@ export interface Fills_party_tradesPaged_edges_node_market {
|
|||||||
/**
|
/**
|
||||||
* An instance of or reference to a tradable instrument.
|
* An instance of or reference to a tradable instrument.
|
||||||
*/
|
*/
|
||||||
tradableInstrument: Fills_party_tradesPaged_edges_node_market_tradableInstrument;
|
tradableInstrument: Fills_party_tradesConnection_edges_node_market_tradableInstrument;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges_node {
|
export interface Fills_party_tradesConnection_edges_node {
|
||||||
__typename: "Trade";
|
__typename: "Trade";
|
||||||
/**
|
/**
|
||||||
* The hash of the trade data
|
* The hash of the trade data
|
||||||
@ -177,38 +177,38 @@ export interface Fills_party_tradesPaged_edges_node {
|
|||||||
/**
|
/**
|
||||||
* The party that bought
|
* The party that bought
|
||||||
*/
|
*/
|
||||||
buyer: Fills_party_tradesPaged_edges_node_buyer;
|
buyer: Fills_party_tradesConnection_edges_node_buyer;
|
||||||
/**
|
/**
|
||||||
* The party that sold
|
* The party that sold
|
||||||
*/
|
*/
|
||||||
seller: Fills_party_tradesPaged_edges_node_seller;
|
seller: Fills_party_tradesConnection_edges_node_seller;
|
||||||
/**
|
/**
|
||||||
* The fee paid by the buyer side of the trade
|
* The fee paid by the buyer side of the trade
|
||||||
*/
|
*/
|
||||||
buyerFee: Fills_party_tradesPaged_edges_node_buyerFee;
|
buyerFee: Fills_party_tradesConnection_edges_node_buyerFee;
|
||||||
/**
|
/**
|
||||||
* The fee paid by the seller side of the trade
|
* The fee paid by the seller side of the trade
|
||||||
*/
|
*/
|
||||||
sellerFee: Fills_party_tradesPaged_edges_node_sellerFee;
|
sellerFee: Fills_party_tradesConnection_edges_node_sellerFee;
|
||||||
/**
|
/**
|
||||||
* The market the trade occurred on
|
* The market the trade occurred on
|
||||||
*/
|
*/
|
||||||
market: Fills_party_tradesPaged_edges_node_market;
|
market: Fills_party_tradesConnection_edges_node_market;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_edges {
|
export interface Fills_party_tradesConnection_edges {
|
||||||
__typename: "TradeEdge";
|
__typename: "TradeEdge";
|
||||||
node: Fills_party_tradesPaged_edges_node;
|
node: Fills_party_tradesConnection_edges_node;
|
||||||
cursor: string;
|
cursor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged_pageInfo {
|
export interface Fills_party_tradesConnection_pageInfo {
|
||||||
__typename: "PageInfo";
|
__typename: "PageInfo";
|
||||||
startCursor: string;
|
startCursor: string;
|
||||||
endCursor: string;
|
endCursor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesPaged {
|
export interface Fills_party_tradesConnection {
|
||||||
__typename: "TradeConnection";
|
__typename: "TradeConnection";
|
||||||
/**
|
/**
|
||||||
* The total number of trades in this connection
|
* The total number of trades in this connection
|
||||||
@ -217,11 +217,11 @@ export interface Fills_party_tradesPaged {
|
|||||||
/**
|
/**
|
||||||
* The trade in this connection
|
* The trade in this connection
|
||||||
*/
|
*/
|
||||||
edges: Fills_party_tradesPaged_edges[];
|
edges: Fills_party_tradesConnection_edges[];
|
||||||
/**
|
/**
|
||||||
* The pagination information
|
* The pagination information
|
||||||
*/
|
*/
|
||||||
pageInfo: Fills_party_tradesPaged_pageInfo;
|
pageInfo: Fills_party_tradesConnection_pageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party {
|
export interface Fills_party {
|
||||||
@ -230,7 +230,7 @@ export interface Fills_party {
|
|||||||
* Party identifier
|
* Party identifier
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
tradesPaged: Fills_party_tradesPaged;
|
tradesConnection: Fills_party_tradesConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills {
|
export interface Fills {
|
||||||
|
@ -5,7 +5,7 @@ import type { PageInfo, Pagination } from '@vegaprotocol/react-helpers';
|
|||||||
import type { FillFields } from './__generated__/FillFields';
|
import type { FillFields } from './__generated__/FillFields';
|
||||||
import type {
|
import type {
|
||||||
Fills,
|
Fills,
|
||||||
Fills_party_tradesPaged_edges,
|
Fills_party_tradesConnection_edges,
|
||||||
} from './__generated__/Fills';
|
} from './__generated__/Fills';
|
||||||
import type { FillsSub } from './__generated__/FillsSub';
|
import type { FillsSub } from './__generated__/FillsSub';
|
||||||
|
|
||||||
@ -17,19 +17,41 @@ const FILL_FRAGMENT = gql`
|
|||||||
size
|
size
|
||||||
buyOrder
|
buyOrder
|
||||||
sellOrder
|
sellOrder
|
||||||
|
aggressor
|
||||||
buyer {
|
buyer {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
seller {
|
seller {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
buyerFee {
|
||||||
|
makerFee
|
||||||
|
infrastructureFee
|
||||||
|
liquidityFee
|
||||||
|
}
|
||||||
|
sellerFee {
|
||||||
|
makerFee
|
||||||
|
infrastructureFee
|
||||||
|
liquidityFee
|
||||||
|
}
|
||||||
market {
|
market {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
decimalPlaces
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
tradableInstrument {
|
tradableInstrument {
|
||||||
instrument {
|
instrument {
|
||||||
id
|
id
|
||||||
code
|
code
|
||||||
|
product {
|
||||||
|
... on Future {
|
||||||
|
settlementAsset {
|
||||||
|
id
|
||||||
|
symbol
|
||||||
|
decimals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,7 +63,7 @@ export const FILLS_QUERY = gql`
|
|||||||
query Fills($partyId: ID!, $marketId: ID, $pagination: Pagination) {
|
query Fills($partyId: ID!, $marketId: ID, $pagination: Pagination) {
|
||||||
party(id: $partyId) {
|
party(id: $partyId) {
|
||||||
id
|
id
|
||||||
tradesPaged(marketId: $marketId, pagination: $pagination) {
|
tradesConnection(marketId: $marketId, pagination: $pagination) {
|
||||||
totalCount
|
totalCount
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@ -67,7 +89,10 @@ export const FILLS_SUB = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const update = (data: Fills_party_tradesPaged_edges[], delta: FillFields[]) => {
|
const update = (
|
||||||
|
data: Fills_party_tradesConnection_edges[],
|
||||||
|
delta: FillFields[]
|
||||||
|
) => {
|
||||||
return produce(data, (draft) => {
|
return produce(data, (draft) => {
|
||||||
delta.forEach((node) => {
|
delta.forEach((node) => {
|
||||||
const index = draft.findIndex((edge) => edge.node.id === node.id);
|
const index = draft.findIndex((edge) => edge.node.id === node.id);
|
||||||
@ -80,21 +105,23 @@ const update = (data: Fills_party_tradesPaged_edges[], delta: FillFields[]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getData = (responseData: Fills): Fills_party_tradesPaged_edges[] | null =>
|
const getData = (
|
||||||
responseData.party?.tradesPaged.edges || null;
|
responseData: Fills
|
||||||
|
): Fills_party_tradesConnection_edges[] | null =>
|
||||||
|
responseData.party?.tradesConnection.edges || null;
|
||||||
|
|
||||||
const getPageInfo = (responseData: Fills): PageInfo | null =>
|
const getPageInfo = (responseData: Fills): PageInfo | null =>
|
||||||
responseData.party?.tradesPaged.pageInfo || null;
|
responseData.party?.tradesConnection.pageInfo || null;
|
||||||
|
|
||||||
const getTotalCount = (responseData: Fills): number | undefined =>
|
const getTotalCount = (responseData: Fills): number | undefined =>
|
||||||
responseData.party?.tradesPaged.totalCount;
|
responseData.party?.tradesConnection.totalCount;
|
||||||
|
|
||||||
const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || [];
|
const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || [];
|
||||||
|
|
||||||
const append = (
|
const append = (
|
||||||
data: Fills_party_tradesPaged_edges[] | null,
|
data: Fills_party_tradesConnection_edges[] | null,
|
||||||
pageInfo: PageInfo,
|
pageInfo: PageInfo,
|
||||||
insertionData: Fills_party_tradesPaged_edges[] | null,
|
insertionData: Fills_party_tradesConnection_edges[] | null,
|
||||||
insertionPageInfo: PageInfo | null,
|
insertionPageInfo: PageInfo | null,
|
||||||
pagination?: Pagination
|
pagination?: Pagination
|
||||||
) => {
|
) => {
|
||||||
|
@ -6,7 +6,7 @@ import { FillsTable } from './fills-table';
|
|||||||
import type { IGetRowsParams } from 'ag-grid-community';
|
import type { IGetRowsParams } from 'ag-grid-community';
|
||||||
|
|
||||||
import { fillsDataProvider as dataProvider } from './fills-data-provider';
|
import { fillsDataProvider as dataProvider } from './fills-data-provider';
|
||||||
import type { Fills_party_tradesPaged_edges } from './__generated__/Fills';
|
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
|
||||||
import type { FillsSub_trades } from './__generated__/FillsSub';
|
import type { FillsSub_trades } from './__generated__/FillsSub';
|
||||||
|
|
||||||
interface FillsManagerProps {
|
interface FillsManagerProps {
|
||||||
@ -15,11 +15,11 @@ interface FillsManagerProps {
|
|||||||
|
|
||||||
export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const dataRef = useRef<Fills_party_tradesPaged_edges[] | null>(null);
|
const dataRef = useRef<Fills_party_tradesConnection_edges[] | null>(null);
|
||||||
const totalCountRef = useRef<number | undefined>(undefined);
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
({ data }: { data: Fills_party_tradesPaged_edges[] }) => {
|
({ data }: { data: Fills_party_tradesConnection_edges[] }) => {
|
||||||
if (!gridRef.current?.api) {
|
if (!gridRef.current?.api) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
data,
|
data,
|
||||||
totalCount,
|
totalCount,
|
||||||
}: {
|
}: {
|
||||||
data: Fills_party_tradesPaged_edges[];
|
data: Fills_party_tradesConnection_edges[];
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
}) => {
|
}) => {
|
||||||
dataRef.current = data;
|
dataRef.current = data;
|
||||||
@ -48,7 +48,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
|
||||||
const { data, error, loading, load, totalCount } = useDataProvider<
|
const { data, error, loading, load, totalCount } = useDataProvider<
|
||||||
Fills_party_tradesPaged_edges[],
|
Fills_party_tradesConnection_edges[],
|
||||||
FillsSub_trades[]
|
FillsSub_trades[]
|
||||||
>({ dataProvider, update, insert, variables });
|
>({ dataProvider, update, insert, variables });
|
||||||
totalCountRef.current = totalCount;
|
totalCountRef.current = totalCount;
|
||||||
|
@ -16,7 +16,7 @@ Default.args = {
|
|||||||
partyId: 'party-id',
|
partyId: 'party-id',
|
||||||
datasource: {
|
datasource: {
|
||||||
getRows: makeGetRows(
|
getRows: makeGetRows(
|
||||||
fills.party?.tradesPaged.edges.map((e) => e.node) || []
|
fills.party?.tradesConnection.edges.map((e) => e.node) || []
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Side } from '@vegaprotocol/types';
|
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { IGetRowsParams } from 'ag-grid-community';
|
import type { IGetRowsParams } from 'ag-grid-community';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import type {
|
import type {
|
||||||
Fills,
|
Fills,
|
||||||
Fills_party_tradesPaged_edges_node,
|
Fills_party_tradesConnection_edges_node,
|
||||||
} from './__generated__/Fills';
|
} from './__generated__/Fills';
|
||||||
|
import { Side } from '@vegaprotocol/types';
|
||||||
|
|
||||||
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||||
const fills: Fills_party_tradesPaged_edges_node[] = [
|
const fills: Fills_party_tradesConnection_edges_node[] = [
|
||||||
generateFill({
|
generateFill({
|
||||||
buyer: {
|
buyer: {
|
||||||
id: 'party-id',
|
id: 'party-id',
|
||||||
@ -50,7 +50,7 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
const defaultResult: Fills = {
|
const defaultResult: Fills = {
|
||||||
party: {
|
party: {
|
||||||
id: 'buyer-id',
|
id: 'buyer-id',
|
||||||
tradesPaged: {
|
tradesConnection: {
|
||||||
__typename: 'TradeConnection',
|
__typename: 'TradeConnection',
|
||||||
totalCount: 1,
|
totalCount: 1,
|
||||||
edges: fills.map((f) => {
|
edges: fills.map((f) => {
|
||||||
@ -74,9 +74,9 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateFill = (
|
export const generateFill = (
|
||||||
override?: PartialDeep<Fills_party_tradesPaged_edges_node>
|
override?: PartialDeep<Fills_party_tradesConnection_edges_node>
|
||||||
) => {
|
) => {
|
||||||
const defaultFill: Fills_party_tradesPaged_edges_node = {
|
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
||||||
__typename: 'Trade',
|
__typename: 'Trade',
|
||||||
id: '0',
|
id: '0',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@ -135,7 +135,7 @@ export const generateFill = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const makeGetRows =
|
export const makeGetRows =
|
||||||
(data: Fills_party_tradesPaged_edges_node[]) =>
|
(data: Fills_party_tradesConnection_edges_node[]) =>
|
||||||
({ successCallback }: IGetRowsParams) => {
|
({ successCallback }: IGetRowsParams) => {
|
||||||
successCallback(data, data.length);
|
successCallback(data, data.length);
|
||||||
};
|
};
|
||||||
|
@ -66,7 +66,7 @@ export const OrderbookRow = React.memo(
|
|||||||
relativeAsk={cumulativeRelativeAsk}
|
relativeAsk={cumulativeRelativeAsk}
|
||||||
relativeBid={cumulativeRelativeBid}
|
relativeBid={cumulativeRelativeBid}
|
||||||
indicativeVolume={indicativeVolume}
|
indicativeVolume={indicativeVolume}
|
||||||
className="pr-4"
|
className="pr-4 text-black dark:text-white"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -282,7 +282,7 @@ export const Orderbook = ({
|
|||||||
const hasData = renderedRows.data && renderedRows.data.length !== 0;
|
const hasData = renderedRows.data && renderedRows.data.length !== 0;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`h-full overflow-auto relative ${styles['scroll']}`}
|
className={`h-full overflow-auto relative ${styles['scroll']} pl-4 pt-4`}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
ref={scrollElement}
|
ref={scrollElement}
|
||||||
data-testid="scroll"
|
data-testid="scroll"
|
||||||
@ -354,7 +354,7 @@ export const Orderbook = ({
|
|||||||
<select
|
<select
|
||||||
onChange={(e) => onResolutionChange(Number(e.currentTarget.value))}
|
onChange={(e) => onResolutionChange(Number(e.currentTarget.value))}
|
||||||
value={resolution}
|
value={resolution}
|
||||||
className="block bg-black-25 dark:bg-white-25 text-black dark:text-white focus-visible:shadow-focus dark:focus-visible:shadow-focus-dark focus-visible:outline-0 font-mono w-100 text-right w-full h-full"
|
className="block bg-black-10 dark:bg-black-70 text-black dark:text-white focus-visible:shadow-focus dark:focus-visible:shadow-focus-dark focus-visible:outline-0 font-mono w-100 text-right w-full h-full"
|
||||||
data-testid="resolution"
|
data-testid="resolution"
|
||||||
>
|
>
|
||||||
{new Array(3)
|
{new Array(3)
|
||||||
|
@ -29,7 +29,10 @@ export const LandingDialog = ({ open, setOpen }: LandingDialogProps) => {
|
|||||||
intent={Intent.Primary}
|
intent={Intent.Primary}
|
||||||
open={open}
|
open={open}
|
||||||
onChange={setClose}
|
onChange={setClose}
|
||||||
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
|
titleClassNames={
|
||||||
|
'font-bold font-sans text-3xl tracking-tight mb-0 pl-8'
|
||||||
|
}
|
||||||
|
contentClassNames={'md:w-[520px] lg:w-[520px] w-full'}
|
||||||
>
|
>
|
||||||
<SelectMarketList data={data} onSelect={setClose} />
|
<SelectMarketList data={data} onSelect={setClose} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -67,6 +67,7 @@ export const SelectMarketList = ({
|
|||||||
onKeyPress={(event) => handleKeyPress(event, id)}
|
onKeyPress={(event) => handleKeyPress(event, id)}
|
||||||
onClick={() => onSelect(id)}
|
onClick={() => onSelect(id)}
|
||||||
data-testid={`market-link-${id}`}
|
data-testid={`market-link-${id}`}
|
||||||
|
className={`focus:decoration-vega-yellow`}
|
||||||
>
|
>
|
||||||
{marketName}
|
{marketName}
|
||||||
</a>
|
</a>
|
||||||
@ -106,7 +107,10 @@ export const SelectMarketList = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<a className={`${boldUnderlineClassNames} text-ui-small`} href="/markets">
|
<a
|
||||||
|
className={`${boldUnderlineClassNames} text-ui-small focus:decoration-vega-yellow`}
|
||||||
|
href="/markets"
|
||||||
|
>
|
||||||
{t('Or view full market list')}
|
{t('Or view full market list')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
import { MarketState, MarketTradingMode, AuctionTrigger } from "@vegaprotocol/types";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL fragment: MarketDataFields
|
// GraphQL fragment: MarketDataFields
|
||||||
@ -43,4 +43,8 @@ export interface MarketDataFields {
|
|||||||
* the mark price (actually an unsigned int)
|
* the mark price (actually an unsigned int)
|
||||||
*/
|
*/
|
||||||
markPrice: string;
|
markPrice: string;
|
||||||
|
/**
|
||||||
|
* what triggered an auction (if an auction was started)
|
||||||
|
*/
|
||||||
|
trigger: AuctionTrigger;
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
import { MarketState, MarketTradingMode, AuctionTrigger } from "@vegaprotocol/types";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL subscription operation: MarketDataSub
|
// GraphQL subscription operation: MarketDataSub
|
||||||
@ -43,6 +43,10 @@ export interface MarketDataSub_marketData {
|
|||||||
* the mark price (actually an unsigned int)
|
* the mark price (actually an unsigned int)
|
||||||
*/
|
*/
|
||||||
markPrice: string;
|
markPrice: string;
|
||||||
|
/**
|
||||||
|
* what triggered an auction (if an auction was started)
|
||||||
|
*/
|
||||||
|
trigger: AuctionTrigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketDataSub {
|
export interface MarketDataSub {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
import { MarketState, MarketTradingMode, AuctionTrigger } from "@vegaprotocol/types";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: Markets
|
// GraphQL query operation: Markets
|
||||||
@ -43,6 +43,10 @@ export interface Markets_markets_data {
|
|||||||
* the mark price (actually an unsigned int)
|
* the mark price (actually an unsigned int)
|
||||||
*/
|
*/
|
||||||
markPrice: string;
|
markPrice: string;
|
||||||
|
/**
|
||||||
|
* what triggered an auction (if an auction was started)
|
||||||
|
*/
|
||||||
|
trigger: AuctionTrigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset {
|
export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset {
|
||||||
|
@ -4,11 +4,13 @@ import {
|
|||||||
PriceFlashCell,
|
PriceFlashCell,
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
t,
|
t,
|
||||||
|
formatLabel,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
import { AgGridColumn } from 'ag-grid-react';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import type { Markets_markets } from '../__generated__/Markets';
|
import type { Markets_markets } from '../__generated__/Markets';
|
||||||
|
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
|
||||||
|
|
||||||
interface MarketListTableProps {
|
interface MarketListTableProps {
|
||||||
datasource: IDatasource;
|
datasource: IDatasource;
|
||||||
@ -44,13 +46,19 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
|
|||||||
field="tradableInstrument.instrument.product.settlementAsset.symbol"
|
field="tradableInstrument.instrument.product.settlementAsset.symbol"
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('State')}
|
headerName={t('Trading mode')}
|
||||||
field="data"
|
field="data"
|
||||||
valueFormatter={({ value }: ValueFormatterParams) =>
|
minWidth={200}
|
||||||
value === undefined
|
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||||
? value
|
if (!value) return value;
|
||||||
: `${value.market.state} (${value.market.tradingMode})`
|
const { market, trigger } = value;
|
||||||
}
|
return market &&
|
||||||
|
market.tradingMode === MarketTradingMode.MonitoringAuction &&
|
||||||
|
trigger &&
|
||||||
|
trigger !== AuctionTrigger.Unspecified
|
||||||
|
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
|
||||||
|
: formatLabel(market?.tradingMode);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Best bid')}
|
headerName={t('Best bid')}
|
||||||
|
@ -10,6 +10,7 @@ import type {
|
|||||||
Markets_markets_data,
|
Markets_markets_data,
|
||||||
} from '../../components/__generated__/Markets';
|
} from '../../components/__generated__/Markets';
|
||||||
import { marketsDataProvider as dataProvider } from './markets-data-provider';
|
import { marketsDataProvider as dataProvider } from './markets-data-provider';
|
||||||
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
|
|
||||||
export const MarketsContainer = () => {
|
export const MarketsContainer = () => {
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
@ -28,14 +29,15 @@ export const MarketsContainer = () => {
|
|||||||
Markets_markets_data
|
Markets_markets_data
|
||||||
>({ dataProvider, update });
|
>({ dataProvider, update });
|
||||||
dataRef.current = data;
|
dataRef.current = data;
|
||||||
|
|
||||||
const getRows = async ({
|
const getRows = async ({
|
||||||
successCallback,
|
successCallback,
|
||||||
startRow,
|
startRow,
|
||||||
endRow,
|
endRow,
|
||||||
}: IGetRowsParams) => {
|
}: IGetRowsParams) => {
|
||||||
const rowsThisBlock = dataRef.current
|
const rowsThisBlock = dataRef.current
|
||||||
? dataRef.current.slice(startRow, endRow)
|
? dataRef.current
|
||||||
|
.slice(startRow, endRow)
|
||||||
|
.filter((m) => m.data?.market.state !== MarketState.Rejected)
|
||||||
: [];
|
: [];
|
||||||
const lastRow = dataRef.current?.length ?? -1;
|
const lastRow = dataRef.current?.length ?? -1;
|
||||||
successCallback(rowsThisBlock, lastRow);
|
successCallback(rowsThisBlock, lastRow);
|
||||||
|
@ -21,6 +21,7 @@ const MARKET_DATA_FRAGMENT = gql`
|
|||||||
bestBidPrice
|
bestBidPrice
|
||||||
bestOfferPrice
|
bestOfferPrice
|
||||||
markPrice
|
markPrice
|
||||||
|
trigger
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import type {
|
import type {
|
||||||
MarketList,
|
MarketList,
|
||||||
@ -11,22 +12,24 @@ export const lastPrice = ({ candles }: MarketList_markets) =>
|
|||||||
|
|
||||||
export const mapDataToMarketList = ({ markets }: MarketList) =>
|
export const mapDataToMarketList = ({ markets }: MarketList) =>
|
||||||
orderBy(
|
orderBy(
|
||||||
markets?.map((m) => {
|
markets
|
||||||
return {
|
?.filter((m) => m.state !== MarketState.Rejected)
|
||||||
id: m.id,
|
.map((m) => {
|
||||||
decimalPlaces: m.decimalPlaces,
|
return {
|
||||||
marketName: m.tradableInstrument.instrument?.code,
|
id: m.id,
|
||||||
lastPrice: lastPrice(m) ?? m.data?.markPrice,
|
decimalPlaces: m.decimalPlaces,
|
||||||
candles: (m.candles || []).filter((c) => c),
|
marketName: m.tradableInstrument.instrument?.code,
|
||||||
open: m.marketTimestamps.open
|
lastPrice: lastPrice(m) ?? m.data?.markPrice,
|
||||||
? new Date(m.marketTimestamps.open).getTime()
|
candles: (m.candles || []).filter((c) => c),
|
||||||
: null,
|
open: m.marketTimestamps.open
|
||||||
close: m.marketTimestamps.close
|
? new Date(m.marketTimestamps.open).getTime()
|
||||||
? new Date(m.marketTimestamps.close).getTime()
|
: null,
|
||||||
: null,
|
close: m.marketTimestamps.close
|
||||||
state: m.state,
|
? new Date(m.marketTimestamps.close).getTime()
|
||||||
};
|
: null,
|
||||||
}) || [],
|
state: m.state,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
['state', 'open', 'id'],
|
['state', 'open', 'id'],
|
||||||
['asc', 'asc', 'asc']
|
['asc', 'asc', 'asc']
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Link, Lozenge } from '@vegaprotocol/ui-toolkit';
|
import { Link, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
@ -56,7 +57,7 @@ export const NetworkInfo = () => {
|
|||||||
<p className="mb-16">
|
<p className="mb-16">
|
||||||
{t('Known issues and feedback on')}{' '}
|
{t('Known issues and feedback on')}{' '}
|
||||||
{feedbackLinks.map(({ name, url }, index) => (
|
{feedbackLinks.map(({ name, url }, index) => (
|
||||||
<>
|
<Fragment key={index}>
|
||||||
<Link key={index} href={url}>
|
<Link key={index} href={url}>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
@ -66,7 +67,7 @@ export const NetworkInfo = () => {
|
|||||||
{feedbackLinks.length > 1 &&
|
{feedbackLinks.length > 1 &&
|
||||||
index === feedbackLinks.length - 1 &&
|
index === feedbackLinks.length - 1 &&
|
||||||
`, ${t('and')} `}
|
`, ${t('and')} `}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -10,6 +10,7 @@ import type {
|
|||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
||||||
import type { ValidationProps } from './use-order-validation';
|
import type { ValidationProps } from './use-order-validation';
|
||||||
|
import { marketTranslations } from './use-order-validation';
|
||||||
import { useOrderValidation } from './use-order-validation';
|
import { useOrderValidation } from './use-order-validation';
|
||||||
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
|
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
|
||||||
import type { Market } from '../market';
|
import type { Market } from '../market';
|
||||||
@ -73,12 +74,12 @@ const ERROR = {
|
|||||||
'Only limit orders are permitted when market is in auction',
|
'Only limit orders are permitted when market is in auction',
|
||||||
MARKET_CONTINUOUS_TIF:
|
MARKET_CONTINUOUS_TIF:
|
||||||
'Only GTT, GTC and GFA are permitted when market is in auction',
|
'Only GTT, GTC and GFA are permitted when market is in auction',
|
||||||
FIELD_SIZE_REQ: 'An amount needs to be provided',
|
FIELD_SIZE_REQ: 'You need to provide an amount',
|
||||||
FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`,
|
FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`,
|
||||||
FIELD_PRICE_REQ: 'A price needs to be provided',
|
FIELD_PRICE_REQ: 'You need to provide a price',
|
||||||
FIELD_PRICE_MIN: 'The price cannot be negative',
|
FIELD_PRICE_MIN: 'The price cannot be negative',
|
||||||
FIELD_PRICE_STEP_NULL: 'No decimal amounts allowed for this order',
|
FIELD_PRICE_STEP_NULL: 'Order sizes must be in whole numbers for this market',
|
||||||
FIELD_PRICE_STEP_DECIMAL: `The amount field only takes up to ${market.positionDecimalPlaces} decimals`,
|
FIELD_PRICE_STEP_DECIMAL: `The amount field accepts up to ${market.positionDecimalPlaces} decimal places`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function setup(
|
function setup(
|
||||||
@ -90,107 +91,148 @@ function setup(
|
|||||||
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }));
|
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }));
|
||||||
}
|
}
|
||||||
|
|
||||||
it('Returns empty string when given valid data', () => {
|
describe('useOrderValidation', () => {
|
||||||
const { result } = setup();
|
it('Returns empty string when given valid data', () => {
|
||||||
expect(result.current).toEqual('');
|
const { result } = setup();
|
||||||
});
|
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||||
|
|
||||||
it('Returns an error message when no keypair found', async () => {
|
|
||||||
const { result } = setup(defaultOrder, { keypair: null });
|
|
||||||
expect(result.current).toEqual('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Returns an error message when the keypair is tainted', async () => {
|
|
||||||
const { result } = setup(defaultOrder, {
|
|
||||||
keypair: { ...defaultWalletContext.keypair, tainted: true },
|
|
||||||
});
|
});
|
||||||
expect(result.current).toEqual('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each`
|
it('Returns an error message when no keypair found', () => {
|
||||||
state | errorMessage
|
const { result } = setup(defaultOrder, { keypair: null });
|
||||||
${MarketState.Cancelled} | ${ERROR.MARKET_INACTIVE}
|
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||||
${MarketState.Closed} | ${ERROR.MARKET_INACTIVE}
|
|
||||||
${MarketState.Rejected} | ${ERROR.MARKET_INACTIVE}
|
|
||||||
${MarketState.Settled} | ${ERROR.MARKET_INACTIVE}
|
|
||||||
${MarketState.TradingTerminated} | ${ERROR.MARKET_INACTIVE}
|
|
||||||
${MarketState.Suspended} | ${ERROR.MARKET_SUSPENDED}
|
|
||||||
${MarketState.Pending} | ${ERROR.MARKET_WAITING}
|
|
||||||
${MarketState.Proposed} | ${ERROR.MARKET_WAITING}
|
|
||||||
`(
|
|
||||||
'Returns an error message for "$marketState" market',
|
|
||||||
async ({ state, errorMessage }) => {
|
|
||||||
const { result } = setup({ market: { ...defaultOrder.market, state } });
|
|
||||||
expect(result.current).toEqual(errorMessage);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it.each`
|
|
||||||
tradingMode | errorMessage
|
|
||||||
${MarketTradingMode.BatchAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
|
||||||
${MarketTradingMode.MonitoringAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
|
||||||
${MarketTradingMode.OpeningAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
|
||||||
`(
|
|
||||||
'Returns an error message when trying to submit a non-limit order for a "$tradingMode" market',
|
|
||||||
async ({ tradingMode, errorMessage }) => {
|
|
||||||
const { result } = setup({
|
|
||||||
market: { ...defaultOrder.market, tradingMode },
|
|
||||||
orderType: VegaWalletOrderType.Market,
|
|
||||||
});
|
|
||||||
expect(result.current).toEqual(errorMessage);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it.each`
|
|
||||||
tradingMode | orderTimeInForce | errorMessage
|
|
||||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
|
||||||
`(
|
|
||||||
'Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market',
|
|
||||||
async ({ tradingMode, orderTimeInForce, errorMessage }) => {
|
|
||||||
const { result } = setup({
|
|
||||||
market: { ...defaultOrder.market, tradingMode },
|
|
||||||
orderType: VegaWalletOrderType.Limit,
|
|
||||||
orderTimeInForce,
|
|
||||||
});
|
|
||||||
expect(result.current).toEqual(errorMessage);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it.each`
|
|
||||||
fieldName | errorType | errorMessage
|
|
||||||
${'size'} | ${'required'} | ${ERROR.FIELD_SIZE_REQ}
|
|
||||||
${'size'} | ${'min'} | ${ERROR.FIELD_SIZE_MIN}
|
|
||||||
${'price'} | ${'required'} | ${ERROR.FIELD_PRICE_REQ}
|
|
||||||
${'price'} | ${'min'} | ${ERROR.FIELD_PRICE_MIN}
|
|
||||||
`(
|
|
||||||
'Returns an error message when the order $fieldName "$errorType" validation fails',
|
|
||||||
async ({ fieldName, errorType, errorMessage }) => {
|
|
||||||
const { result } = setup({
|
|
||||||
fieldErrors: { [fieldName]: { type: errorType } },
|
|
||||||
});
|
|
||||||
expect(result.current).toEqual(errorMessage);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
it('Returns an error message when the order size incorrectly has decimal values', async () => {
|
|
||||||
const { result } = setup({
|
|
||||||
market: { ...market, positionDecimalPlaces: 0 },
|
|
||||||
fieldErrors: { size: { type: 'validate', message: ERROR_SIZE_DECIMAL } },
|
|
||||||
});
|
});
|
||||||
expect(result.current).toEqual(ERROR.FIELD_PRICE_STEP_NULL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Returns an error message when the order size has more decimals then allowed', async () => {
|
it('Returns an error message when the keypair is tainted', () => {
|
||||||
const { result } = setup({
|
const { result } = setup(defaultOrder, {
|
||||||
fieldErrors: { size: { type: 'validate', message: ERROR_SIZE_DECIMAL } },
|
keypair: { ...defaultWalletContext.keypair, tainted: true },
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
state
|
||||||
|
${MarketState.Settled}
|
||||||
|
${MarketState.Rejected}
|
||||||
|
${MarketState.TradingTerminated}
|
||||||
|
`(
|
||||||
|
'Returns an error message for market state when not accepting orders',
|
||||||
|
({ state }) => {
|
||||||
|
const { result } = setup({ market: { ...defaultOrder.market, state } });
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
message: `This market is ${marketTranslations(
|
||||||
|
state
|
||||||
|
)} and not accepting orders`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
state
|
||||||
|
${MarketState.Suspended}
|
||||||
|
${MarketState.Pending}
|
||||||
|
${MarketState.Cancelled}
|
||||||
|
${MarketState.Proposed}
|
||||||
|
${MarketState.Closed}
|
||||||
|
`(
|
||||||
|
'Returns an error message for market state suspended or pending',
|
||||||
|
({ state }) => {
|
||||||
|
const { result } = setup({
|
||||||
|
market: {
|
||||||
|
...defaultOrder.market,
|
||||||
|
state,
|
||||||
|
tradingMode: MarketTradingMode.BatchAuction,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: false,
|
||||||
|
message: `This market is ${state.toLowerCase()} and only accepting liquidity commitment orders`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
tradingMode | errorMessage
|
||||||
|
${MarketTradingMode.BatchAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||||
|
${MarketTradingMode.MonitoringAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||||
|
${MarketTradingMode.OpeningAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||||
|
`(
|
||||||
|
`Returns an error message when trying to submit a non-limit order for a "$tradingMode" market`,
|
||||||
|
({ tradingMode, errorMessage }) => {
|
||||||
|
const { result } = setup({
|
||||||
|
market: { ...defaultOrder.market, tradingMode },
|
||||||
|
orderType: VegaWalletOrderType.Market,
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
tradingMode | orderTimeInForce | errorMessage
|
||||||
|
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||||
|
`(
|
||||||
|
`Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market`,
|
||||||
|
({ tradingMode, orderTimeInForce, errorMessage }) => {
|
||||||
|
const { result } = setup({
|
||||||
|
market: { ...defaultOrder.market, tradingMode },
|
||||||
|
orderType: VegaWalletOrderType.Limit,
|
||||||
|
orderTimeInForce,
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
fieldName | errorType | errorMessage
|
||||||
|
${`size`} | ${`required`} | ${ERROR.FIELD_SIZE_REQ}
|
||||||
|
${`size`} | ${`min`} | ${ERROR.FIELD_SIZE_MIN}
|
||||||
|
${`price`} | ${`required`} | ${ERROR.FIELD_PRICE_REQ}
|
||||||
|
${`price`} | ${`min`} | ${ERROR.FIELD_PRICE_MIN}
|
||||||
|
`(
|
||||||
|
`Returns an error message when the order $fieldName "$errorType" validation fails`,
|
||||||
|
({ fieldName, errorType, errorMessage }) => {
|
||||||
|
const { result } = setup({
|
||||||
|
fieldErrors: { [fieldName]: { type: errorType } },
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('Returns an error message when the order size incorrectly has decimal values', () => {
|
||||||
|
const { result } = setup({
|
||||||
|
market: { ...market, positionDecimalPlaces: 0 },
|
||||||
|
fieldErrors: { size: { type: `validate`, message: ERROR_SIZE_DECIMAL } },
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
message: ERROR.FIELD_PRICE_STEP_NULL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Returns an error message when the order size has more decimals than allowed', () => {
|
||||||
|
const { result } = setup({
|
||||||
|
fieldErrors: { size: { type: `validate`, message: ERROR_SIZE_DECIMAL } },
|
||||||
|
});
|
||||||
|
expect(result.current).toStrictEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
expect(result.current).toEqual(ERROR.FIELD_PRICE_STEP_DECIMAL);
|
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,15 @@ export type ValidationProps = {
|
|||||||
fieldErrors?: FieldErrors<Order>;
|
fieldErrors?: FieldErrors<Order>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const marketTranslations = (marketState: MarketState) => {
|
||||||
|
switch (marketState) {
|
||||||
|
case MarketState.TradingTerminated:
|
||||||
|
return t('terminated');
|
||||||
|
default:
|
||||||
|
return t(marketState).toLowerCase();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const useOrderValidation = ({
|
export const useOrderValidation = ({
|
||||||
step,
|
step,
|
||||||
market,
|
market,
|
||||||
@ -28,33 +37,120 @@ export const useOrderValidation = ({
|
|||||||
}: ValidationProps) => {
|
}: ValidationProps) => {
|
||||||
const { keypair } = useVegaWallet();
|
const { keypair } = useVegaWallet();
|
||||||
|
|
||||||
const invalidText = useMemo(() => {
|
const { message, isDisabled } = useMemo(() => {
|
||||||
if (!keypair) {
|
if (!keypair) {
|
||||||
return t('No public key selected');
|
return { message: t('No public key selected'), isDisabled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keypair.tainted) {
|
if (keypair.tainted) {
|
||||||
return t('Selected public key has been tainted');
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t('Selected public key has been tainted'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
MarketState.Settled,
|
||||||
|
MarketState.Rejected,
|
||||||
|
MarketState.TradingTerminated,
|
||||||
|
].includes(market.state)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t(
|
||||||
|
`This market is ${marketTranslations(
|
||||||
|
market.state
|
||||||
|
)} and not accepting orders`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
MarketState.Suspended,
|
||||||
|
MarketState.Pending,
|
||||||
|
MarketState.Proposed,
|
||||||
|
MarketState.Cancelled,
|
||||||
|
MarketState.Closed,
|
||||||
|
].includes(market.state)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
isDisabled: false,
|
||||||
|
message: t(
|
||||||
|
`This market is ${marketTranslations(
|
||||||
|
market.state
|
||||||
|
)} and only accepting liquidity commitment orders`
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (market.state !== MarketState.Active) {
|
if (market.state !== MarketState.Active) {
|
||||||
if (market.state === MarketState.Suspended) {
|
if (market.state === MarketState.Suspended) {
|
||||||
return t('Market is currently suspended');
|
if (market.tradingMode === MarketTradingMode.Continuous) {
|
||||||
|
if (orderType !== OrderType.Limit) {
|
||||||
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t(
|
||||||
|
'Only limit orders are permitted when market is in auction'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
OrderTimeInForce.FOK,
|
||||||
|
OrderTimeInForce.IOC,
|
||||||
|
OrderTimeInForce.GFN,
|
||||||
|
].includes(orderTimeInForce)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t(
|
||||||
|
'Only GTT, GTC and GFA are permitted when market is in auction'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDisabled: false,
|
||||||
|
message: t(
|
||||||
|
`This market is ${marketTranslations(
|
||||||
|
market.state
|
||||||
|
)} and only accepting liquidity commitment orders`
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
market.state === MarketState.Proposed ||
|
market.state === MarketState.Proposed ||
|
||||||
market.state === MarketState.Pending
|
market.state === MarketState.Pending
|
||||||
) {
|
) {
|
||||||
return t('Market is not active yet');
|
return {
|
||||||
|
isDisabled: false,
|
||||||
|
message: t(
|
||||||
|
`This market is ${marketTranslations(
|
||||||
|
market.state
|
||||||
|
)} and only accepting liquidity commitment orders`
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return t('Market is no longer active');
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t('This market is no longer active.'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (market.tradingMode !== MarketTradingMode.Continuous) {
|
if (market.tradingMode !== MarketTradingMode.Continuous) {
|
||||||
if (orderType !== OrderType.Limit) {
|
if (orderType !== OrderType.Limit) {
|
||||||
return t('Only limit orders are permitted when market is in auction');
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t(
|
||||||
|
'Only limit orders are permitted when market is in auction'
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -64,26 +160,41 @@ export const useOrderValidation = ({
|
|||||||
OrderTimeInForce.GFN,
|
OrderTimeInForce.GFN,
|
||||||
].includes(orderTimeInForce)
|
].includes(orderTimeInForce)
|
||||||
) {
|
) {
|
||||||
return t(
|
return {
|
||||||
'Only GTT, GTC and GFA are permitted when market is in auction'
|
isDisabled: true,
|
||||||
);
|
message: t(
|
||||||
|
'Only GTT, GTC and GFA are permitted when market is in auction'
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldErrors?.size?.type === 'required') {
|
if (fieldErrors?.size?.type === 'required') {
|
||||||
return t('An amount needs to be provided');
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t('You need to provide an amount'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldErrors?.size?.type === 'min') {
|
if (fieldErrors?.size?.type === 'min') {
|
||||||
return t(`The amount cannot be lower than "${step}"`);
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t(`The amount cannot be lower than "${step}"`),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldErrors?.price?.type === 'required') {
|
if (fieldErrors?.price?.type === 'required') {
|
||||||
return t('A price needs to be provided');
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t('You need to provide a price'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldErrors?.price?.type === 'min') {
|
if (fieldErrors?.price?.type === 'min') {
|
||||||
return t(`The price cannot be negative`);
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t(`The price cannot be negative`),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -91,14 +202,20 @@ export const useOrderValidation = ({
|
|||||||
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
|
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
|
||||||
) {
|
) {
|
||||||
if (market.positionDecimalPlaces === 0) {
|
if (market.positionDecimalPlaces === 0) {
|
||||||
return t('No decimal amounts allowed for this order');
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: t('Order sizes must be in whole numbers for this market'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return t(
|
return {
|
||||||
`The amount field only takes up to ${market.positionDecimalPlaces} decimals`
|
isDisabled: true,
|
||||||
);
|
message: t(
|
||||||
|
`The amount field accepts up to ${market.positionDecimalPlaces} decimal places`
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return { isDisabled: false, message: '' };
|
||||||
}, [
|
}, [
|
||||||
keypair,
|
keypair,
|
||||||
step,
|
step,
|
||||||
@ -110,5 +227,5 @@ export const useOrderValidation = ({
|
|||||||
orderTimeInForce,
|
orderTimeInForce,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return invalidText;
|
return { message, isDisabled };
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import startCase from 'lodash/startCase';
|
||||||
|
|
||||||
export const getUserLocale = () => 'default';
|
export const getUserLocale = () => 'default';
|
||||||
|
|
||||||
export const splitAt = (index: number) => (x: string) =>
|
export const splitAt = (index: number) => (x: string) =>
|
||||||
[x.slice(0, index), x.slice(index)];
|
[x.slice(0, index), x.slice(index)];
|
||||||
|
|
||||||
|
export const formatLabel = (str: string) =>
|
||||||
|
capitalize(startCase(str).toLowerCase());
|
||||||
|
@ -20,13 +20,13 @@ export const PriceCell = React.memo(
|
|||||||
: [value];
|
: [value];
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="font-mono relative text-ui-small"
|
className="font-mono relative text-ui-small text-black dark:text-white"
|
||||||
data-testid={testId || 'price'}
|
data-testid={testId || 'price'}
|
||||||
>
|
>
|
||||||
{valueSplit[0]}
|
{valueSplit[0]}
|
||||||
{valueSplit[1] ? decimalSeparator : null}
|
{valueSplit[1] ? decimalSeparator : null}
|
||||||
{valueSplit[1] ? (
|
{valueSplit[1] ? (
|
||||||
<span className="text-black-muted dark:text-white-muted">
|
<span className="text-black-60 dark:text-white-60">
|
||||||
{valueSplit[1]}
|
{valueSplit[1]}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user