Compare commits

...
Author SHA1 Message Date
asiaznik 1a099d7163 fix(governance): vega wallet e2e 2023-03-10 16:35:43 +01:00
asiaznik 1e5e3016ad fix(governance): vega wallet e2e 2023-03-10 16:08:40 +01:00
asiaznik 2a6d28312c fix(governance): validators cy lint 2023-03-10 15:11:45 +01:00
asiaznik 223cdbd5ed fix(governance): consolidated e2e tests 2023-03-10 15:08:25 +01:00
asiaznik ed443c669e timeout madness 2023-03-10 15:08:25 +01:00
asiaznik eb9fb783b7 fix(governance): build-spec 2023-03-10 15:08:25 +01:00
asiaznik f5e3f96510 fix(trading): e2e fixes 2023-03-10 15:08:25 +01:00
asiaznik 03f3a17813 fix(governance): e2e withdrawal 2023-03-10 15:08:25 +01:00
asiaznik 41be4a57fd fix(governance): e2e fixes 2023-03-10 15:08:24 +01:00
asiaznik 1d438b8a3b fix(explorer): fixed failing e2e 2023-03-10 15:08:24 +01:00
asiaznik 5b04f55890 fix: lint 2023-03-10 15:08:24 +01:00
asiaznik 2685efe55d feat: magnifying glass 2023-03-10 15:08:24 +01:00
asiaznik 6e4f2bee75 fix: search trigger, hide 2023-03-10 15:08:24 +01:00
asiaznik db48e1ae31 fix: ref warning 2023-03-10 15:08:24 +01:00
asiaznik ca3e6f78bd feat(token): token nav 2023-03-10 15:08:24 +01:00
asiaznik 223052b6fa feat(console, ui-toolkit): drawer for console, drawer color scheme 2023-03-10 15:08:24 +01:00
asiaznik 3bb0f7dfc7 feat(console, explorer): console navigation, generic and explorer tweaks 2023-03-10 15:08:24 +01:00
asiaznik 70ef417440 feat(ui-toolkit, explorer): navigation 2023-03-10 15:05:11 +01:00
asiaznik c4668dd3ad feat(ui-toolkit, explorer): navigation 2023-03-10 15:05:11 +01:00
Joe Tsang b3bfb90804 test(governance): new and update market proposal forms (#3131) 2023-03-10 14:03:36 +00:00
m.ray 327ebf3496 fix(trading): deal ticket expiry not wiped out after changing TIF from GTT (#3149) 2023-03-10 15:02:01 +01:00
Matthew Russell 1098accb84 fix(trading,explorer,governance): consistent zustand usage (#3133) 2023-03-10 14:01:51 +00:00
Edd 52183555a9 feat(explorer): improve display of multi cancellations (#3151) 2023-03-10 14:01:26 +00:00
dexturr 51187029aa chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-10 12:09:30 +00:00
dexturr f7f7be59d3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-10 06:10:07 +00:00
Bartłomiej Głownia 49e3baf094 feat(deal-ticket): simplify summary validation (#3144) 2023-03-09 16:12:59 -08:00
Maciek a575b4c502 chore(trading): tweaks and improvements of floating bottom buttons (#3138) 2023-03-09 15:52:38 -08:00
dexturr af2e52d59c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-09 18:09:03 +00:00
daro-maj 2725bef159 test(trading): increase coverage for 0002 wcon (#3137) 2023-03-09 14:11:46 +01:00
dexturr 070f1905e2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-09 12:10:33 +00:00
Bartłomiej Głowniaandmattrussell36 6705eb4398 feat(trading): calculate required margin base on open volume, active … (#2957)
Co-authored-by: mattrussell36 <mattrussell36@users.noreply.github.com>
2023-03-09 10:03:50 +00:00
dexturr 9d3fc04597 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-09 06:09:54 +00:00
dexturr d3929b8d4a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-09 00:17:17 +00:00
dexturr 8a5579b1cc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-08 18:08:10 +00:00
Maciek 197f2e8097 chore(trading): 2825 buttons floating over table rows (#3084) 2023-03-08 15:58:40 +01:00
Matthew RussellandMadalina Raicu 8f5a2276de fix(trading): order store connection to deal ticket (#3100)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-03-08 12:14:56 +00:00
dexturr 46513685c8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-08 12:10:32 +00:00
m.ray e66e96f12d fix(trading): add loss socialisation docs links (#3109) 2023-03-08 09:42:25 +00:00
dexturr 31c8365812 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-08 06:07:09 +00:00
dexturr 460f534c7a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-08 00:17:38 +00:00
dexturr 24cd080dd4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 18:08:33 +00:00
Bartłomiej Głownia f10c33748f fix(orders): orders table flickering (#3105) 2023-03-07 17:13:34 +01:00
dexturr ac163d0194 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 12:08:15 +00:00
Joe Tsang e7616e64f8 test(governance): tranche service (#3099) 2023-03-07 10:42:20 +00:00
Matthew Russell adca4600c2 feat(deposits): deposit flow (#3062) 2023-03-07 08:17:02 +00:00
dexturr a8eef1cb53 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 06:07:42 +00:00
dexturr 58d4bd1459 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 00:16:09 +00:00
dexturr 26020b5f71 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 18:07:54 +00:00
daro-maj 16d345eb4a test(trading): revert nonparallel tests option (#3098) 2023-03-06 17:48:33 +01:00
m.ray 3b3fcab4f4 fix(trading): fills table fees display (#3071) 2023-03-06 17:24:18 +01:00
Matthew Russell f54a629179 feat(positions): positions losses and maintenance warnings (#2985) 2023-03-06 16:01:31 +00:00
dexturr 56f48671cc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 12:09:18 +00:00
Bartłomiej Głownia a65ee358ff fix(market-depth): orderbook row height calculation (#3068) 2023-03-06 10:21:45 +00:00
dexturr 7b8d814724 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 06:07:28 +00:00
dexturr cd237dc491 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 00:17:15 +00:00
dexturr 9ad00adc5a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 18:07:24 +00:00
Bartłomiej Głownia fab055bb34 chore(orders): fix order table rows flickering on filter change (#3088) 2023-03-05 14:08:16 +00:00
dexturr d66956d080 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 12:07:46 +00:00
dexturr 9848c8ffff chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 06:08:12 +00:00
dexturr b0e8ed62d2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 00:19:08 +00:00
dexturr 289ff3b5b3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 18:09:11 +00:00
dexturr 1d965187dc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 12:10:27 +00:00
dexturr 8bf3615001 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 06:09:19 +00:00
dexturr 1b77800b62 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 00:16:57 +00:00
dexturr 979dc2904b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 18:09:26 +00:00
m.ray 0fec3bcf3c fix(trading): update order on market trade header (#3082) (#3087) 2023-03-03 17:37:46 +00:00
Edd 4542e25ec9 feat(explorer): proposal transaction view (#3060) 2023-03-03 14:27:24 +00:00
Edd acd96fbf21 chore(smart-contracts): remove redundant ascii to hex in favour of ethers hexlify (#3066) 2023-03-03 12:21:22 +00:00
Matthew Russell 96bf12d4dd fix(trading): add a timeout to show subscriptions as failed (#3073) 2023-03-03 12:20:51 +00:00
Edd 47b81188d4 fix(explorer): update vega_networks for all explorer envs (#3078) 2023-03-03 12:20:41 +00:00
dexturr efada06256 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 12:10:27 +00:00
daro-maj 311ef41138 test(trading): tests parallel (#3075) 2023-03-03 11:42:12 +01:00
Maciek 5dd0cafeca chore(trading): catch ethereum errors and capture to sentry (#3064) 2023-03-03 08:39:20 +01:00
dexturr f3d6aa1129 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 06:09:30 +00:00
dexturr 93dbc32f22 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 01:56:44 +00:00
dexturr fd1827ef67 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-02 18:09:24 +00:00
Sam Keen 9c9948e4c1 feat(governance): final wallet not connected views (#3037) 2023-03-02 14:36:04 +00:00
Sam Keen 79b0868ff7 feat(governance): remove graph from tranches page (#3049) 2023-03-02 14:35:46 +00:00
dexturr a8401d4c6f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-02 12:07:58 +00:00
Maciek c030264211 chore(react-helpers): improve error catching in useResizeObserver (#3058) 2023-03-02 10:32:42 +01:00
Maciek 068f7b3fc3 chore(trading): tweaking catching no market data error (#3044) 2023-03-02 08:25:57 +01:00
dexturr 4430f0c5f6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-02 06:09:50 +00:00
dexturr 0a14000708 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-02 00:15:49 +00:00
Matthew Russell 9ca6fe3cbf fix(trading): env vars for validator testnet (#3061) 2023-03-01 10:08:45 -08:00
dexturr 4f96472375 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-01 18:07:09 +00:00
m.ray cddf2ba00b fix(trading): edit order strikethrough (#3057) 2023-03-01 16:36:12 +00:00
Ciaran McGhie d01d6f5815 chore(ci): add utils and i18n libs as options for publish to npm workflow (#3055) 2023-03-01 16:35:49 +00:00
Sam Keen 2998e165f6 feat(governance): new env file for multisig on validator-testnet (#3056) 2023-03-01 14:29:47 +00:00
m.ray 886b0cb6d3 fix(trading): deal ticket limit price input and margin warning (#3050) 2023-03-01 14:23:21 +00:00
Edd 6799a06acd chore(explorer): remove commented out query (#3045) 2023-03-01 14:04:54 +00:00
Matthew Russell 47a6bcff78 chore(trading): delete unused styles (#3038) 2023-03-01 14:16:31 +01:00
Edd 06dcd00b53 fix(explorer): add size in market component for remaining order size (#3046) 2023-03-01 14:13:22 +01:00
Edd fd7aa5bd78 fix(explorer): fix inconsistent wss urls for tm (#3053) 2023-03-01 14:12:56 +01:00
Joe Tsang 384cfbc70b chore(governance): update test run files (#3051) 2023-03-01 12:35:13 +00:00
dexturr 9d14f603b3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-01 12:07:47 +00:00
335 changed files with 10200 additions and 5071 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ on:
type: choice
options:
- explorer-e2e
- token-e2e
- governance-e2e
- trading-e2e
tags:
description: 'Test tags to run'
+1 -1
View File
@@ -10,5 +10,5 @@ jobs:
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: '["explorer-e2e","token-e2e","trading-e2e"]'
projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
tags: '@smoke @regression @slow'
+2 -2
View File
@@ -42,10 +42,10 @@ jobs:
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected"
projects=""
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV
+2
View File
@@ -12,6 +12,8 @@ on:
- react-helpers
- tailwindcss-config
- types
- utils
- i18n
jobs:
publish:
+1 -1
View File
@@ -161,7 +161,7 @@ In order to setup and run vegawallet for e2e capsule tests, in a separate termin
bash setup-vegawallet.sh
```
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/token-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/governance-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
+7 -13
View File
@@ -1,33 +1,27 @@
context('Blocks page', { tags: '@regression' }, function () {
before('visit token home page', function () {
cy.visit('/');
});
describe('Verify elements on page', function () {
const blockNavigation = 'a[href="/blocks"]';
beforeEach(() => {
cy.visit('/blocks');
});
const blockHeight = '[data-testid="block-height"]';
const blockTime = '[data-testid="block-time"]';
const blockHeader = '[data-testid="block-header"]';
const previousBlockBtn = '[data-testid="previous-block"]';
const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]';
beforeEach('navigate to blocks page', function () {
cy.get(blockNavigation).click();
});
it('Blocks page is displayed', function () {
validateBlocksDisplayed();
});
it('Blocks page is displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(blockNavigation).click();
cy.switchToMobile();
validateBlocksDisplayed();
});
it('Block validator page is displayed', function () {
waitForBlocksResponse();
cy.get(blockHeight).eq(0).click();
cy.get(blockHeight).eq(0).find('a').click({ force: true });
cy.get('[data-testid="block-validator"]').should('not.be.empty');
cy.get(blockTime).should('not.be.empty');
//TODO: Add assertion for transactions when txs are added
@@ -35,7 +29,7 @@ context('Blocks page', { tags: '@regression' }, function () {
it('Navigate to previous block', function () {
waitForBlocksResponse();
cy.get(blockHeight).eq(0).click();
cy.get(blockHeight).eq(0).find('a').click({ force: true });
cy.get(blockHeader)
.invoke('text')
.then(($blockHeaderTxt) => {
@@ -2,17 +2,14 @@ context('Network parameters page', { tags: '@smoke' }, function () {
before('navigate to network parameter page', function () {
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat');
});
describe('Verify elements on page', function () {
const networkParametersNavigation = 'a[href="/network-parameters"]';
beforeEach(() => {
cy.visit('/network-parameters');
});
const networkParametersHeader = '[data-testid="network-param-header"]';
const tableRows = '[data-testid="key-value-table-row"]';
before('navigate to network parameter page', function () {
cy.visit('/');
cy.get(networkParametersNavigation).click();
});
it('should show network parameter heading at top of page', function () {
cy.get(networkParametersHeader)
.should('have.text', 'Network Parameters')
@@ -201,55 +198,8 @@ context('Network parameters page', { tags: '@smoke' }, function () {
});
});
it('should be able to switch network parameter page - between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not already set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
it.skip('should be able to see network parameters - on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(networkParametersNavigation).click();
it('should be able to see network parameters - on mobile', function () {
cy.switchToMobile();
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
+1 -1
View File
@@ -1,7 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
-2
View File
@@ -1,9 +1,7 @@
# App configuration variables
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
# App flags
+1 -3
View File
@@ -1,10 +1,8 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.devnet1.vega.xyz/websocket
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=DEVNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
+2 -4
View File
@@ -1,10 +1,8 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=MAINNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
+1 -3
View File
@@ -1,10 +1,8 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=MIRROR
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
+1 -2
View File
@@ -5,8 +5,7 @@ NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.sandbox.vega.xyz/websocket
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
-1
View File
@@ -4,7 +4,6 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
+1 -3
View File
@@ -1,9 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
+2 -5
View File
@@ -1,13 +1,10 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/websocket/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=TESTNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_NETWORKS={}
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
+1 -3
View File
@@ -3,12 +3,10 @@ NX_VEGA_ENV=VALIDATOR_TESTNET
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/validator-testnet-network.json
NX_VEGA_URL=https://api.validators-testnet.vega.xyz/graphql
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.validators-testnet.vega.xyz/websocket/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
-1
View File
@@ -2,6 +2,5 @@
NX_TENDERMINT_URL=http://localhost:26607/
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
+5
View File
@@ -0,0 +1,5 @@
function ReactMarkdown({ children }) {
return <div>{children}</div>;
}
export default ReactMarkdown;
+31 -25
View File
@@ -1,6 +1,4 @@
import classnames from 'classnames';
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
import { Nav } from './components/nav';
import { Header } from './components/header';
import { Main } from './components/main';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
@@ -11,6 +9,7 @@ import {
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import classNames from 'classnames';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@@ -25,35 +24,42 @@ const DialogsContainer = () => {
);
};
function App() {
const layoutClasses = classnames(
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
'min-h-[100vh] mx-auto my-0',
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
'bg-white dark:bg-black',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
);
const MainnetSimAd = () => (
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Mainnet sim 2 is live!</span>
<ExternalLink href="https://fairground.wtf/">
Come help stress test the network
</ExternalLink>
</div>
</AnnouncementBanner>
);
function App() {
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Mainnet sim 2 is live!</span>
<ExternalLink href="https://fairground.wtf/">
Come help stress test the network
</ExternalLink>
<div
className={classNames(
'max-w-[1500px] min-h-[100vh]',
'mx-auto my-0',
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
)}
>
<div>
<Header />
<MainnetSimAd />
</div>
<div>
<Main />
</div>
<div>
<Footer />
</div>
</AnnouncementBanner>
<div className={layoutClasses}>
<Header />
<Nav />
<Main />
<Footer />
</div>
<DialogsContainer />
</NetworkLoader>
</TendermintWebsocketProvider>
@@ -22,29 +22,3 @@ query ExplorerFutureEpoch {
}
}
}
# query ExplorerEpoch($id: ID!) {
#
##### This could be useful for calculating roughly when a future epoch will
##### occur, but epoch not exist results in a total error
# networkParameter(key: "validators.epoch.length") {
# value
# }
#
##### This could be useful for relating where we are in time, but as above
##### the total failure caused by epoch(id) not existing
##### means this is useful
# currentEpoch: epoch {
# id
# }
#
# epoch(id: $id) {
# id
# timestamps {
# start
# end
# firstBlock
# lastBlock
# }
# }
#}
@@ -16,7 +16,7 @@ export const Footer = () => {
return (
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
@@ -19,12 +19,10 @@ const renderComponent = () => (
);
describe('Header', () => {
it('should render heading', () => {
it('should render navigation', () => {
render(renderComponent());
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
'Vega Explorer'
);
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
});
it('should render search', () => {
render(renderComponent());
@@ -1,43 +1,108 @@
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
import { matchPath, useLocation } from 'react-router-dom';
import {
ThemeSwitcher,
Navigation,
NavigationList,
NavigationItem,
NavigationLink,
NavigationBreakpoint,
NavigationTrigger,
NavigationContent,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { Search } from '../search';
import { Routes } from '../../routes/route-names';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import { useNavStore } from '../nav';
import type { Navigable } from '../../routes/router-config';
import routerConfig from '../../routes/router-config';
import { useMemo } from 'react';
import compact from 'lodash/compact';
import { Search } from '../search';
const routeToNavigationItem = (r: Navigable) => (
<NavigationItem key={r.name}>
<NavigationLink to={r.path}>{r.text}</NavigationLink>
</NavigationItem>
);
export const Header = () => {
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
const headerClasses = classnames(
'md:col-span-2',
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
'p-4 gap-2 md:gap-4',
'border-b border-neutral-700 dark:border-neutral-300 bg-black',
'dark text-white'
const mainItems = compact(
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
routerConfig.find((r) => r.path === n)
)
);
const groupedItems = compact(
[
Routes.PARTIES,
Routes.ASSETS,
Routes.MARKETS,
Routes.GOVERNANCE,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].map((n) => routerConfig.find((r) => r.path === n))
);
const { pathname } = useLocation();
/**
* Because the grouped items are displayed in a sub menu under an "Other" item
* we need to determine whether any underlying item is active to highlight the
* trigger in the same fashion as any other top-level `NavigationLink`.
* This function checks whether the current location pathname is one of the
* underlying NavigationLinks.
*/
const isOnOther = useMemo(() => {
for (const path of groupedItems.map((r) => r.path)) {
const matched = matchPath(`${path}/*`, pathname);
if (matched) return true;
}
return false;
}, [groupedItems, pathname]);
return (
<header className={headerClasses}>
<div className="flex h-full items-center sm:items-stretch gap-4">
<Link to={Routes.HOME}>
<h1
className="text-white text-3xl font-alpha uppercase calt mb-0"
data-testid="explorer-header"
>
{t('Vega Explorer')}
</h1>
</Link>
<NetworkSwitcher />
</div>
<button
data-testid="open-menu"
className="md:hidden text-white"
onClick={() => toggle()}
<Navigation
appName="Explorer"
theme="system"
breakpoints={[490, 900]}
actions={
<>
<ThemeSwitcher />
<Search />
</>
}
onResize={(width, el) => {
if (width < 1157) {
// switch to magnifying glass trigger when width < 1157
el.classList.remove('nav-search-full');
el.classList.add('nav-search-compact');
} else {
el.classList.remove('nav-search-compact');
el.classList.add('nav-search-full');
}
}}
>
<NavigationList hide={[NavigationBreakpoint.Small]}>
<NavigationItem>
<NetworkSwitcher />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
>
<Icon name={open ? 'cross' : 'menu'} />
</button>
<Search />
<ThemeSwitcher className="-my-4" />
</header>
{mainItems.map(routeToNavigationItem)}
{groupedItems && (
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnOther)}>
{t('Other')}
</NavigationTrigger>
<NavigationContent>
<NavigationList>
{groupedItems.map(routeToNavigationItem)}
</NavigationList>
</NavigationContent>
</NavigationItem>
)}
</NavigationList>
</Navigation>
);
};
@@ -4,13 +4,14 @@ import { ENV } from '../../../config/env';
import Hash from '../hash';
export type ProposalLinkProps = {
id: string;
text?: string;
};
/**
* Given a proposal ID, generates an external link over to
* the Governance page for more information
*/
const ProposalLink = ({ id }: ProposalLinkProps) => {
const ProposalLink = ({ id, text }: ProposalLinkProps) => {
const { data } = useExplorerProposalQuery({
variables: { id },
});
@@ -20,7 +21,7 @@ const ProposalLink = ({ id }: ProposalLinkProps) => {
return (
<ExternalLink href={`${base}/proposals/${id}`}>
<Hash text={label} />
{text ? text : <Hash text={label} />}
</ExternalLink>
);
};
@@ -4,9 +4,8 @@ import {
getMarketExpiryDateFormatted,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoNoCandlesQuery } from '@vegaprotocol/market-info';
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info';
import pick from 'lodash/pick';
import {
MarketStateMapping,
MarketTradingModeMapping,
@@ -17,11 +16,7 @@ import BigNumber from 'bignumber.js';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
export const MarketDetails = ({
market,
}: {
market: MarketInfoNoCandlesQuery['market'];
}) => {
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
@@ -32,7 +27,9 @@ export const MarketDetails = ({
if (!market) return null;
const keyDetails = {
...pick(market, 'decimalPlaces', 'positionDecimalPlaces', 'tradingMode'),
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
tradingMode: market.tradingMode,
state: MarketStateMapping[market.state],
};
const assetDecimals =
@@ -1 +0,0 @@
export * from './nav';
@@ -1,181 +0,0 @@
import { NavLink, useLocation } from 'react-router-dom';
import type { Navigable } from '../../routes/router-config';
import routerConfig from '../../routes/router-config';
import classnames from 'classnames';
import { create } from 'zustand';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import { Icon } from '@vegaprotocol/ui-toolkit';
import first from 'lodash/first';
import last from 'lodash/last';
import { BREAKPOINT_MD } from '../../config/breakpoints';
type NavStore = {
open: boolean;
toggle: () => void;
hide: () => void;
};
export const useNavStore = create<NavStore>((set, get) => ({
open: false,
toggle: () => set({ open: !get().open }),
hide: () => set({ open: false }),
}));
const NavLinks = ({ links }: { links: Navigable[] }) => {
const navLinks = links.map((r) => (
<li key={r.name}>
<NavLink
to={r.path}
className={({ isActive }) =>
classnames(
'block mb-2 px-2',
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
{
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
isActive,
}
)
}
>
{r.text}
</NavLink>
</li>
));
return <ul className="pr-8 md:pr-0">{navLinks}</ul>;
};
export const Nav = () => {
const [open, hide] = useNavStore((state) => [state.open, state.hide]);
const location = useLocation();
const navRef = useRef<HTMLElement>(null);
const btnRef = useRef<HTMLButtonElement>(null);
const focusable = useMemo(
() =>
navRef.current
? [
...(navRef.current.querySelectorAll(
'a, button'
) as NodeListOf<HTMLElement>),
]
: [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[navRef.current] // do not remove `navRef.current` from deps
);
const closeNav = useCallback(() => {
hide();
console.log(focusable);
focusable.forEach((fe) =>
fe.setAttribute(
'tabindex',
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
)
);
}, [focusable, hide]);
// close navigation when location changes
useEffect(() => {
closeNav();
}, [closeNav, location]);
useLayoutEffect(() => {
if (open) {
focusable.forEach((fe) => fe.setAttribute('tabindex', '0'));
}
document.body.style.overflow = open ? 'hidden' : '';
const offset =
document.querySelector('header')?.getBoundingClientRect().top || 0;
if (navRef.current) {
navRef.current.style.height = `calc(100vh - ${offset}px)`;
}
// focus current by default
if (navRef.current && open) {
(navRef.current.querySelector('a[aria-current]') as HTMLElement)?.focus();
}
const closeOnEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeNav();
}
};
// tabbing loop
const focusLast = (e: FocusEvent) => {
e.preventDefault();
const isNavElement =
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
if (!isNavElement && open) {
last(focusable)?.focus();
}
};
const focusFirst = (e: FocusEvent) => {
e.preventDefault();
const isNavElement =
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
if (!isNavElement && open) {
first(focusable)?.focus();
}
};
const resetOnDesktop = () => {
focusable.forEach((fe) =>
fe.setAttribute(
'tabindex',
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
)
);
};
window.addEventListener('resize', resetOnDesktop);
first(focusable)?.addEventListener('focusout', focusLast);
last(focusable)?.addEventListener('focusout', focusFirst);
document.addEventListener('keydown', closeOnEsc);
return () => {
window.removeEventListener('resize', resetOnDesktop);
document.removeEventListener('keydown', closeOnEsc);
first(focusable)?.removeEventListener('focusout', focusLast);
last(focusable)?.removeEventListener('focusout', focusFirst);
};
}, [closeNav, focusable, open]);
return (
<nav
ref={navRef}
className={classnames(
'absolute top-0 z-20 overflow-y-auto',
'transition-[right]',
{
'right-[-200vw] h-full': !open,
'right-0 h-[100vh]': open,
},
'w-full p-4 border-neutral-700 dark:border-neutral-300',
'bg-white dark:bg-black',
'md:static md:border-r'
)}
>
<NavLinks links={routerConfig} />
<button
ref={btnRef}
className="absolute top-0 right-0 p-4 md:hidden"
onClick={() => {
closeNav();
}}
>
<Icon name="cross" />
</button>
</nav>
);
};
@@ -101,7 +101,7 @@ const DeterministicOrderDetails = ({
{t('Remaining')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
{o.remaining}
<SizeInMarket size={o.remaining} marketId={o.market.id} />
</h5>
</div>
)}
@@ -0,0 +1,26 @@
import { t } from '@vegaprotocol/i18n';
interface CancelSummaryProps {
orderId?: string;
marketId?: string;
}
/**
* Simple component for rendering a reasonable string from an order cancellation
*/
export const CancelSummary = ({ orderId, marketId }: CancelSummaryProps) => {
return <span className="font-bold">{getLabel(orderId, marketId)}</span>;
};
export function getLabel(
orderId: string | undefined,
marketId: string | undefined
): string {
if (!orderId && !marketId) {
return t('All orders');
} else if (marketId && !orderId) {
return t('All in market');
}
return '-';
}
@@ -5,11 +5,31 @@ import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { getSearchType, SearchTypes, toHex } from './detect-search';
import { Routes } from '../../routes/route-names';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
interface FormFields {
search: string;
}
const MagnifyingGlass = () => (
<svg
className="w-4 h-4"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12.8202"
y1="13.1798"
x2="17.0629"
y2="17.4224"
stroke="currentColor"
/>
<circle cx="8" cy="8" r="7.5" stroke="currentColor" />
</svg>
);
export const Search = () => {
const { register, handleSubmit } = useForm<FormFields>();
const navigate = useNavigate();
@@ -49,39 +69,95 @@ export const Search = () => {
[navigate]
);
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full md:max-w-[620px] justify-self-end"
>
<label htmlFor="search" className="sr-only">
{t('Search by block number or transaction hash')}
</label>
<div className="flex items-stretch gap-2">
<div className="flex grow relative">
<Input
{...register('search')}
id="search"
data-testid="search"
className="text-white"
hasError={Boolean(error?.message)}
type="text"
placeholder={t(
'Enter block number, public key or transaction hash'
)}
/>
{error?.message && (
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b text-black">
<InputError data-testid="search-error" intent="danger">
{error.message}
</InputError>
</div>
const searchForm = (
<form className="block min-w-[290px]" onSubmit={handleSubmit(onSubmit)}>
<div className="flex relative items-stretch gap-2 text-xs">
<label htmlFor="search" className="sr-only">
{t('Search by block number or transaction hash')}
</label>
<button
className={classNames(
'absolute top-[50%] translate-y-[-50%] left-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
</div>
<Button type="submit" size="sm" data-testid="search-button">
>
<MagnifyingGlass />
</button>
<Input
{...register('search')}
id="search"
data-testid="search"
className={classNames(
'peer',
'pl-8 py-2 text-xs',
'border rounded border-vega-light-200 dark:border-vega-dark-200'
)}
hasError={Boolean(error?.message)}
type="text"
placeholder={t('Enter block number, public key or transaction hash')}
/>
{error?.message && (
<div
className={classNames(
'hidden peer-focus:block',
'bg-white dark:bg-black',
'border rounded-b border-t-0 border-vega-light-200 dark:border-vega-dark-200',
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
)}
>
<InputError
data-testid="search-error"
intent="danger"
className="text-xs"
>
{error.message}
</InputError>
</div>
)}
<Button
className="hidden [.search-dropdown_&]:block"
type="submit"
size="xs"
data-testid="search-button"
>
{t('Search')}
</Button>
</div>
</form>
);
const searchTrigger = (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="text-vega-light-300 dark:text-vega-dark-300 data-open:text-black dark:data-open:text-white flex items-center">
<MagnifyingGlass />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className={classNames(
'search-dropdown',
'p-2 min-w-[290px] z-20',
'text-vega-light-300 dark:text-vega-dark-300',
'bg-white dark:bg-black',
'border rounded border-vega-light-200 dark:border-vega-dark-200',
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]'
)}
align="end"
sideOffset={10}
>
{searchForm}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
return (
<>
<div className="hidden [.nav-search-full_&]:block">{searchForm}</div>
<div className="hidden [.nav-search-compact_&]:block">
{searchTrigger}
</div>
</>
);
};
@@ -2,6 +2,7 @@ import type { BatchCancellationInstruction } from '../../../../routes/types/bloc
import { TxOrderType } from '../../tx-order-type';
import { MarketLink } from '../../../links';
import OrderSummary from '../../../order-summary/order-summary';
import { CancelSummary } from '../../../order-summary/order-cancellation';
interface BatchCancelProps {
index: number;
@@ -19,7 +20,14 @@ export const BatchCancel = ({ index, submission }: BatchCancelProps) => {
<TxOrderType orderType={'OrderCancellation'} />
</td>
<td>
<OrderSummary id={submission.orderId} modifier="cancelled" />
{submission.orderId ? (
<OrderSummary id={submission.orderId} modifier="cancelled" />
) : (
<CancelSummary
orderId={submission.orderId}
marketId={submission.marketId}
/>
)}
</td>
<td>
<MarketLink id={submission.marketId} />
@@ -0,0 +1,7 @@
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
}
}
@@ -0,0 +1,19 @@
query ExplorerNewAssetSignatureBundle($id: ID!) {
erc20ListAssetBundle(assetId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
query ExplorerUpdateAssetSignatureBundle($id: ID!) {
erc20SetAssetLimitsBundle(proposalId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
@@ -0,0 +1,50 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerProposalStatusQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
}
}
`;
/**
* __useExplorerProposalStatusQuery__
*
* To run a query within a React component, call `useExplorerProposalStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerProposalStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerProposalStatusQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerProposalStatusQuery(baseOptions: Apollo.QueryHookOptions<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>(ExplorerProposalStatusDocument, options);
}
export function useExplorerProposalStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>(ExplorerProposalStatusDocument, options);
}
export type ExplorerProposalStatusQueryHookResult = ReturnType<typeof useExplorerProposalStatusQuery>;
export type ExplorerProposalStatusLazyQueryHookResult = ReturnType<typeof useExplorerProposalStatusLazyQuery>;
export type ExplorerProposalStatusQueryResult = Apollo.QueryResult<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>;
@@ -0,0 +1,98 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerNewAssetSignatureBundleQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerNewAssetSignatureBundleQuery = { __typename?: 'Query', erc20ListAssetBundle?: { __typename?: 'Erc20ListAssetBundle', signatures: string, nonce: string } | null, asset?: { __typename?: 'Asset', status: Types.AssetStatus } | null };
export type ExplorerUpdateAssetSignatureBundleQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerUpdateAssetSignatureBundleQuery = { __typename?: 'Query', erc20SetAssetLimitsBundle: { __typename?: 'ERC20SetAssetLimitsBundle', signatures: string, nonce: string }, asset?: { __typename?: 'Asset', status: Types.AssetStatus } | null };
export const ExplorerNewAssetSignatureBundleDocument = gql`
query ExplorerNewAssetSignatureBundle($id: ID!) {
erc20ListAssetBundle(assetId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
`;
/**
* __useExplorerNewAssetSignatureBundleQuery__
*
* To run a query within a React component, call `useExplorerNewAssetSignatureBundleQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerNewAssetSignatureBundleQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerNewAssetSignatureBundleQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerNewAssetSignatureBundleQuery(baseOptions: Apollo.QueryHookOptions<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>(ExplorerNewAssetSignatureBundleDocument, options);
}
export function useExplorerNewAssetSignatureBundleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>(ExplorerNewAssetSignatureBundleDocument, options);
}
export type ExplorerNewAssetSignatureBundleQueryHookResult = ReturnType<typeof useExplorerNewAssetSignatureBundleQuery>;
export type ExplorerNewAssetSignatureBundleLazyQueryHookResult = ReturnType<typeof useExplorerNewAssetSignatureBundleLazyQuery>;
export type ExplorerNewAssetSignatureBundleQueryResult = Apollo.QueryResult<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>;
export const ExplorerUpdateAssetSignatureBundleDocument = gql`
query ExplorerUpdateAssetSignatureBundle($id: ID!) {
erc20SetAssetLimitsBundle(proposalId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
`;
/**
* __useExplorerUpdateAssetSignatureBundleQuery__
*
* To run a query within a React component, call `useExplorerUpdateAssetSignatureBundleQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerUpdateAssetSignatureBundleQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerUpdateAssetSignatureBundleQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerUpdateAssetSignatureBundleQuery(baseOptions: Apollo.QueryHookOptions<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>(ExplorerUpdateAssetSignatureBundleDocument, options);
}
export function useExplorerUpdateAssetSignatureBundleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>(ExplorerUpdateAssetSignatureBundleDocument, options);
}
export type ExplorerUpdateAssetSignatureBundleQueryHookResult = ReturnType<typeof useExplorerUpdateAssetSignatureBundleQuery>;
export type ExplorerUpdateAssetSignatureBundleLazyQueryHookResult = ReturnType<typeof useExplorerUpdateAssetSignatureBundleLazyQuery>;
export type ExplorerUpdateAssetSignatureBundleQueryResult = Apollo.QueryResult<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>;
@@ -0,0 +1,70 @@
import { t } from '@vegaprotocol/i18n';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
type Terms = components['schemas']['vegaProposalTerms'];
export function format(date: string | undefined, def: string) {
if (!date) {
return def;
}
return new Date().toLocaleDateString() || def;
}
export function getDate(
data: ExplorerProposalStatusQuery | undefined,
terms: Terms
): string {
const DEFAULT = t('Unknown');
if (!data?.proposal?.state) {
return DEFAULT;
}
switch (data.proposal.state) {
case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED':
return `${t('Vote passed on')}: ${format(
terms.enactmentTimestamp,
DEFAULT
)}`;
case 'STATE_FAILED':
return `${t('Failed on')}: ${format(terms.validationTimestamp, DEFAULT)}`;
case 'STATE_OPEN':
return `${t('Open until')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_PASSED':
return `${t('Passed on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_REJECTED':
return `${t('Rejected on submission')}`;
case 'STATE_WAITING_FOR_NODE_VOTE':
return `${t('Opening')}...`;
default:
return DEFAULT;
}
}
interface ProposalDateProps {
id: string;
terms: Terms;
}
/**
* Shows the most relevant date for the proposal summary view. Depending on the
* state returned by GraphQL, we show either the validation, closing or enactment
* timestamp
*/
export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
const { data } = useExplorerProposalStatusQuery({
variables: {
id,
},
});
return (
<Lozenge className="font-sans text-xs float-right">
{getDate(data, terms)}
</Lozenge>
);
};
@@ -0,0 +1,117 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import type * as Apollo from '@apollo/client';
import type * as Types from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps {
id: string;
}
type IconAndLabel = {
icon: IconProps['name'];
label: string;
};
/**
* Select an icon for a given query result. Tolerates queries that don't return
* any data
*
* @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name
*/
export function getIconAndLabelForStatus(
res: ProposalQueryResult
): IconAndLabel {
const DEFAULT: IconAndLabel = {
icon: 'error',
label: t('Proposal state unknown'),
};
if (res.loading) {
return {
icon: 'more',
label: t('Loading data'),
};
}
if (!res?.data?.proposal || res.error) {
return {
icon: 'error',
label: res.error?.message || DEFAULT.label,
};
}
switch (res.data.proposal.state) {
case 'STATE_DECLINED':
return {
icon: 'stop',
label: t('Proposal did not have enough participation to be valid'),
};
case 'STATE_ENACTED':
return {
icon: 'tick-circle',
label: t('Vote passed and the proposal has been enacted'),
};
case 'STATE_FAILED':
return {
icon: 'thumbs-down',
label: t('Proposal became invalid and was not processed'),
};
case 'STATE_OPEN':
return {
// A checklist to indicate in progress
icon: 'form',
label: t('Voting is in progress'),
};
case 'STATE_PASSED':
return {
icon: 'thumbs-up',
label: t(
'Voting is complete and this proposal was approved. It is not yet enacted.'
),
};
case 'STATE_REJECTED':
return {
icon: 'disable',
label: t('The proposal was invalid'),
};
case 'STATE_WAITING_FOR_NODE_VOTE':
return {
// A sparkly thing indicating it's new
icon: 'clean',
label: t('Proposal is being checked by validators'),
};
default:
return DEFAULT;
}
}
/**
*/
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = getIconAndLabelForStatus(
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
return (
<div className="float-left mr-3">
<Tooltip description={<p>{label}</p>}>
<Icon name={icon} />
</Tooltip>
</div>
);
};
@@ -0,0 +1,42 @@
import { Loader } from '@vegaprotocol/ui-toolkit';
import { BundleError } from './signature-bundle/bundle-error';
import { BundleExists } from './signature-bundle/bundle-exists';
import { useExplorerNewAssetSignatureBundleQuery } from './__generated__/SignatureBundle';
export interface ProposalSignatureBundleByTypeProps {
id: string;
}
/**
* If a proposal needs a signature bundle, AND that signature bundle exists
* AND that proposal is a New Asset Proposal, render an overview of the
* signature bundle. Or an error.
*
* There is an almost identical component, ProposalSignatureBundleUpdateAsset
*/
export const ProposalSignatureBundleNewAsset = ({
id,
}: ProposalSignatureBundleByTypeProps) => {
const { data, error, loading } = useExplorerNewAssetSignatureBundleQuery({
variables: {
id,
},
});
if (loading) {
return <Loader />;
}
if (data?.erc20ListAssetBundle?.signatures) {
return (
<BundleExists
signatures={data.erc20ListAssetBundle.signatures}
nonce={data.erc20ListAssetBundle.nonce}
status={data.asset?.status}
proposalId={id}
/>
);
} else {
return <BundleError status={data?.asset?.status} error={error} />;
}
};
@@ -0,0 +1,39 @@
import { Loader } from '@vegaprotocol/ui-toolkit';
import type { ProposalSignatureBundleByTypeProps } from './signature-bundle-new';
import { BundleError } from './signature-bundle/bundle-error';
import { BundleExists } from './signature-bundle/bundle-exists';
import { useExplorerUpdateAssetSignatureBundleQuery } from './__generated__/SignatureBundle';
/**
* If a proposal needs a signature bundle, AND that signature bundle exists
* AND that proposal is a Asset Limits Proposal, render an overview of the
* signature bundle. Or an error.
*
* There is an almost identical component, ProposalSignatureBundleNewAsset
*/
export const ProposalSignatureBundleUpdateAsset = ({
id,
}: ProposalSignatureBundleByTypeProps) => {
const { data, error, loading } = useExplorerUpdateAssetSignatureBundleQuery({
variables: {
id,
},
});
if (loading) {
return <Loader />;
}
if (data?.erc20SetAssetLimitsBundle?.signatures) {
return (
<BundleExists
signatures={data.erc20SetAssetLimitsBundle.signatures}
nonce={data.erc20SetAssetLimitsBundle.nonce}
status={data.asset?.status}
proposalId={id}
/>
);
} else {
return <BundleError status={data?.asset?.status} error={error} />;
}
};
@@ -0,0 +1,31 @@
import { ProposalSignatureBundleNewAsset } from './signature-bundle-new';
import { ProposalSignatureBundleUpdateAsset } from './signature-bundle-update';
export function format(date: string | undefined, def: string) {
if (!date) {
return def;
}
return new Date().toLocaleDateString() || def;
}
interface ProposalSignatureBundleProps {
id: string;
type: 'NewAsset' | 'UpdateAsset';
}
/**
* Some proposals, if enacted, generate a signature bundle.
* The queries have to be split due to the way the API returns
* errors, hence this slightly redundant feeling switcher.
*/
export const ProposalSignatureBundle = ({
id,
type,
}: ProposalSignatureBundleProps) => {
return type === 'NewAsset' ? (
<ProposalSignatureBundleNewAsset id={id} />
) : (
<ProposalSignatureBundleUpdateAsset id={id} />
);
};
@@ -0,0 +1,67 @@
import type { ApolloError } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { BundleError } from './bundle-error';
describe('Bundle Error', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
it.each(NON_ENABLED_STATUS)(
'shows the apollo error if not enabled and a message is provided',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleError
error={{ message: 'test-error-message' } as ApolloError}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('test-error-message')).toBeInTheDocument();
}
);
it.each(NON_ENABLED_STATUS)(
'shows some fallback error message if the bundle has not been used and there is no error',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleError status={status} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('No bundle for proposal ID')).toBeInTheDocument();
}
);
it.each(ENABLED_STATUS)(
'hides ProposalLink if the status is enabled',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleError
error={{ message: 'irrelevant ' } as ApolloError}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Asset already enabled')).toBeInTheDocument();
}
);
});
@@ -0,0 +1,34 @@
import type { ApolloError } from '@apollo/client';
import type { AssetStatus } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import Hash from '../../../../links/hash';
import { IconForBundleStatus } from './bundle-icon';
export interface BundleErrorProps {
status?: AssetStatus;
error?: ApolloError;
}
/**
* Renders if a proposal signature bundle cannot be found.
* It is also possible that a data node has dropped the bundle
* from its retention so there is a backup case where we check
* the status - if it's already enabled, pretend this isn't an error
*/
export const BundleError = ({ status, error }: BundleErrorProps) => {
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<IconForBundleStatus status={status} />
<h1 className="text-xl pb-1">{t('No signature bundle found')}</h1>
<p>
{status === 'STATUS_ENABLED' ? (
t('Asset already enabled')
) : (
<Hash text={error ? error.message : t('No bundle for proposal ID')} />
)}
</p>
</div>
);
};
@@ -0,0 +1,64 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { BundleExists } from './bundle-exists';
describe('Bundle Exists', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
const MOCK_SIGNATURES =
'0x1760ce4efec01d5bb9fe57c0660a39a27e0b5a1bd39b4d3fc0452bf142a4ef733baaf4dbedd74cd676c759f96ac7f412ec05e136bbff8a81d9f0c2428df8e099004c4d166ece0527d86e202386e8758580790d0a46f6429baaa268b4bf5c11c9e3402e175a9022ae8295e543853c01383ef17cd58458ddfc9c706fca0ecffa0d3d0160eb5234e63a78b9649428ca7659eb693fdde86edfc6c2707ef2e8fbf4c76a9f0eebe183da571a58837e2fa995d7b10955ecf04c138dc0ce964f17c3de1f5c6d0060ec132cc515cf974ead1da38e2ff8d4b870335865e8d4d8c982182bddea18bb513e2d37fd32de7fedc0c4f694e6bcdcf20f8547f19e7d9d25ce32c6ca9ea51e01ad3b92475778d9da7251d3943071f59107c9cd7ad9dd1923c06e88d3352869d919a591bf30732bad2c3fcf30a9f664dbb7a9c65a64875a48cbeb5741bd0a853901';
const MOCK_NONCE =
'18250011763873610289536200551900545467959221115607409799241178172533618346952';
const MOCK_PROPOSAL_ID =
'285923fed8c66ffb416b163e8ec72d3a87b9b8e2570e7ee7fe97d7092a918bc8';
const PROPOSAL_LINK_TEXT = 'Visit our Governance site to submit this';
it.each(NON_ENABLED_STATUS)(
'shows a handy ProposalLink if the status is anything except enabled',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleExists
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText(PROPOSAL_LINK_TEXT)).toBeInTheDocument();
}
);
it.each(ENABLED_STATUS)(
'hides ProposalLink if the status is enabled',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleExists
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.queryAllByText(PROPOSAL_LINK_TEXT)).toEqual([]);
}
);
});
@@ -0,0 +1,46 @@
import { t } from '@vegaprotocol/i18n';
import type { AssetStatus } from '@vegaprotocol/types';
import ProposalLink from '../../../../links/proposal-link/proposal-link';
import { IconForBundleStatus } from './bundle-icon';
import { ProposalSignatureBundleDetails } from './details';
export interface BundleExistsProps {
signatures: string;
nonce: string;
status?: AssetStatus;
proposalId: string;
}
/**
* If a proposal needs a signature bundle, AND that signature bundle exists,
* this component renders that signature bundle.
*
*/
export const BundleExists = ({
signatures,
nonce,
status,
proposalId,
}: BundleExistsProps) => {
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<IconForBundleStatus status={status} />
<h1 className="text-xl pb-1">
{status === 'STATUS_ENABLED'
? t('Asset added to bridge')
: t('Signature bundle generated')}
</h1>
<ProposalSignatureBundleDetails signatures={signatures} nonce={nonce} />
{status !== 'STATUS_ENABLED' ? (
<p className="mt-5">
<ProposalLink
id={proposalId}
text={t('Visit our Governance site to submit this')}
/>
</p>
) : null}
</div>
);
};
@@ -0,0 +1,33 @@
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { IconForBundleStatus } from './bundle-icon';
describe('Bundle status icon', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
it.each(NON_ENABLED_STATUS)(
'show a sparkle icon if the bundle is unused',
(status) => {
const screen = render(<IconForBundleStatus status={status} />);
const i = screen.getByRole('img');
expect(i).toHaveAttribute('aria-label');
expect(i.getAttribute('aria-label')).toMatch(/clean/);
}
);
it.each(ENABLED_STATUS)(
'shows a tick if the bundle is already used',
(status) => {
const screen = render(<IconForBundleStatus status={status} />);
const i = screen.getByRole('img');
expect(i).toHaveAttribute('aria-label');
expect(i.getAttribute('aria-label')).toMatch(/tick-circle/);
}
);
});
@@ -0,0 +1,17 @@
import type { AssetStatus } from '@vegaprotocol/types';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import { Icon } from '@vegaprotocol/ui-toolkit';
export interface IconForBundleStatusProps {
status?: AssetStatus;
}
/**
* Naively select an icon for an asset. If it is enabled, we show a tick - anything
* else is assumed to be 'in progress'. There should only be a signature bundle or the
* asset should not exist
*/
export const IconForBundleStatus = ({ status }: IconForBundleStatusProps) => {
const i: IconName = status === 'STATUS_ENABLED' ? 'tick-circle' : 'clean';
return <Icon className="float-left mt-2 mr-3" name={i} />;
};
@@ -0,0 +1,41 @@
import { t } from '@vegaprotocol/i18n';
export interface ProposalSignatureBundleDetailsProps {
signatures: string;
nonce: string;
}
export const ProposalSignatureBundleDetails = ({
signatures,
nonce,
}: ProposalSignatureBundleDetailsProps) => {
return (
<details className="mt-5">
<summary>{t('Signature bundle details')}</summary>
<div className="ml-4">
<h2 className="text-lg mt-2 mb-2">{t('Signatures')}</h2>
<p>
<textarea
className="font-mono bg-neutral-300 text-[11px] leading-3 text-gray-900 w-full p-2 max-w-[615px]"
readOnly={true}
rows={12}
cols={120}
value={signatures}
/>
</p>
<h2 className="text-lg mt-5 mb-2">{t('Nonce')}</h2>
<p>
<textarea
className="font-mono bg-neutral-300 text-[11px] leading-3 text-gray-900 w-full p-2 max-w-[615px]"
readOnly={true}
rows={2}
cols={120}
value={nonce}
/>
</p>
</div>
</details>
);
};
@@ -0,0 +1,90 @@
import type { ProposalTerms } from '../tx-proposal';
import { useState } from 'react';
import type { components } from '../../../../../types/explorer';
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
import ProposalLink from '../../../links/proposal-link/proposal-link';
import truncate from 'lodash/truncate';
import { ProposalStatusIcon } from './proposal-status-icon';
import ReactMarkdown from 'react-markdown';
import { ProposalDate } from './proposal-date';
import { t } from '@vegaprotocol/i18n';
type Rationale = components['schemas']['vegaProposalRationale'];
type ProposalTermsDialog = {
open: boolean;
title: string;
content: unknown;
};
interface ProposalSummaryProps {
id: string;
rationale?: Rationale;
terms?: ProposalTerms;
}
/**
* Effectively a 'preview' for what the proposal is about, and a link to the
* Token site for full breakdown of votes
*/
export const ProposalSummary = ({
id,
rationale,
terms,
}: ProposalSummaryProps) => {
const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false,
title: '',
content: null,
});
const openDialog = () => {
if (!terms) return;
setDialog({
open: true,
title: rationale?.title || t('Proposal details'),
content: terms ? terms : {},
});
};
const md =
rationale && rationale.description
? truncate(rationale.description, {
// Limits the description to roughly 5 lines, maximum
length: 350,
})
: '';
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5">
{id && <ProposalStatusIcon id={id} />}
{rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>}
{rationale?.description && (
<p className="pt-2 text-sm leading-tight">
<ReactMarkdown
className="react-markdown-container"
skipHtml={true}
disallowedElements={['img']}
linkTarget="_blank"
>
{md}
</ReactMarkdown>
</p>
)}
<p className="pt-5">
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
{t('View terms')}
</button>{' '}
<ProposalLink id={id} text={t('Full details')} />
{terms && <ProposalDate terms={terms} id={id} />}
</p>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
title={dialog.title}
content={dialog.content}
/>
</div>
);
};
@@ -55,7 +55,7 @@ export const TxDetailsShared = ({
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
<TableCell>
<Hash text={txData.hash} />
<Hash text={txData.hash.toLowerCase()} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -24,6 +24,7 @@ import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxProposal } from './tx-proposal';
import { TxDetailsTransfer } from './tx-transfer';
interface TxDetailsWrapperProps {
@@ -88,6 +89,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsOrderAmend;
case 'Validator Heartbeat':
return TxDetailsHeartbeat;
case 'Proposal':
return TxProposal;
case 'Vote on Proposal':
return TxProposalVote;
case 'Batch Market Instructions':
@@ -5,6 +5,8 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
import { CancelSummary } from '../../order-summary/order-cancellation';
import Hash from '../../links/hash';
interface TxDetailsOrderCancelProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -24,8 +26,8 @@ export const TxDetailsOrderCancel = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const marketId: string = txData.command.orderCancellation.marketId || '-';
const orderId: string = txData.command.orderCancellation.orderId || '-';
const marketId: string = txData.command.orderCancellation.marketId;
const orderId: string = txData.command.orderCancellation.orderId;
return (
<>
@@ -38,18 +40,24 @@ export const TxDetailsOrderCancel = ({
<TableRow modifier="bordered">
<TableCell>{t('Order')}</TableCell>
<TableCell>
<code>{orderId}</code>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
{orderId ? (
<Hash text={orderId} />
) : (
<CancelSummary orderId={orderId} marketId={marketId} />
)}
</TableCell>
</TableRow>
{marketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
{orderId !== '-' ? <DeterministicOrderDetails id={orderId} /> : null}
{orderId ? <DeterministicOrderDetails id={orderId} /> : null}
</>
);
};
@@ -0,0 +1,115 @@
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import has from 'lodash/has';
import { ProposalSummary } from './proposal/summary';
import Hash from '../../links/hash';
import { ProposalSignatureBundle } from './proposal/signature-bundle';
import { t } from '@vegaprotocol/i18n';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
interface TxProposalProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Detects if a given proposal requires a signature bundle to be emitted by the validators for
* the proposal to be completed after it is accepted by the governance vote
*
* @param t The details of the proposal
* @returns boolean True if a signature bundle is required. Used to fetch a signature bundle
*/
export function proposalRequiresSignatureBundle(proposal?: Proposal): boolean {
if (!proposal?.terms) {
return false;
}
return !!['newAsset', 'updateAsset'].filter((requiredIfExists) =>
has(proposal.terms, requiredIfExists)
);
}
/**
* Given a proposal, returns a nice text label for the proposal type
*
* @param terms The details of the proposal
* @returns string Proposal type
*/
export function proposalTypeLabel(terms?: ProposalTerms): string {
if (has(terms, 'newAsset')) {
return t('New asset proposal');
} else if (has(terms, 'updateAsset')) {
return t('Update asset proposal');
} else if (has(terms, 'newMarket')) {
return t('New market proposal');
} else if (has(terms, 'updateMarket')) {
return t('Update market proposal');
} else if (has(terms, 'updateNetworkParameter')) {
return t('Update network parameter');
} else if (has(terms, 'newFreeform')) {
return t('Freeform proposal');
}
// The list above contains all currently known types. This will be triggered if a new
// unrecognised proposal type is added.
return t('Governance proposal');
}
/**
* A proposal. It's more simplistic than man other views because there are already other, better
* ways to view things about a proposal!
*
*/
export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
if (!txData || !txData.command.proposalSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
let deterministicId = '';
const proposal: Proposal = txData.command.proposalSubmission;
const sig = txData?.signature?.value;
if (sig) {
deterministicId = txSignatureToDeterministicId(sig);
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{proposalTypeLabel(proposal.terms)}</TableCell>
</TableRow>
{/* TODO: Disable type row */}
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>
<Hash text={deterministicId} />
</TableCell>
</TableRow>
</TableWithTbody>
<ProposalSummary
id={deterministicId}
rationale={proposal.rationale}
terms={proposal?.terms}
/>
{proposalRequiresSignatureBundle(proposal) && (
<ProposalSignatureBundle
id={deterministicId}
type={proposal.terms?.newAsset ? 'NewAsset' : 'UpdateAsset'}
/>
)}
</>
);
};
@@ -12,6 +12,7 @@ export const Proposals = () => {
const { data, loading, error } = useDataProvider({
dataProvider: proposalsDataProvider,
variables: {},
});
useDocumentTitle([t('Governance Proposals')]);
@@ -1,14 +1,14 @@
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { MarketDetails } from '../../components/markets/market-details';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import compact from 'lodash/compact';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
import { marketInfoProvider } from '@vegaprotocol/market-info';
import { PageTitle } from '../../components/page-helpers/page-title';
export const MarketPage = () => {
@@ -16,24 +16,17 @@ export const MarketPage = () => {
const { marketId } = useParams<{ marketId: string }>();
const variables = useMemo(
() => ({
marketId,
}),
[marketId]
);
const { data, loading, error } = useDataProvider({
dataProvider: marketInfoNoCandlesDataProvider,
dataProvider: marketInfoProvider,
skipUpdates: true,
variables,
variables: {
marketId: marketId || '',
skip: !marketId,
},
});
useDocumentTitle(
compact([
'Market details',
data?.market?.tradableInstrument.instrument.name,
])
compact(['Market details', data?.tradableInstrument.instrument.name])
);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
@@ -43,10 +36,10 @@ export const MarketPage = () => {
<section className="relative">
<PageTitle
data-testid="markets-heading"
title={data?.market?.tradableInstrument.instrument.name || ''}
title={data?.tradableInstrument.instrument.name || ''}
actions={
<Button
disabled={!data?.market}
disabled={!data}
size="xs"
onClick={() => setDialogOpen(true)}
>
@@ -60,14 +53,14 @@ export const MarketPage = () => {
loading={loading}
error={error}
>
<MarketDetails market={data?.market} />
{data && <MarketDetails market={data} />}
</AsyncRenderer>
</section>
<JsonViewerDialog
open={dialogOpen}
onChange={(isOpen) => setDialogOpen(isOpen)}
title={data?.market?.tradableInstrument.instrument.name || ''}
content={data?.market}
title={data?.tradableInstrument.instrument.name || ''}
content={data}
/>
</>
);
@@ -13,6 +13,7 @@ export const MarketsPage = () => {
const { data, loading, error } = useDataProvider({
dataProvider: marketsProvider,
variables: undefined,
skipUpdates: true,
});
+17 -12
View File
@@ -23,14 +23,18 @@ import { NetworkParameters } from './network-parameters';
import type { RouteObject } from 'react-router-dom';
import { MarketPage, MarketsPage } from './markets';
export type Navigable = { path: string; name: string; text: string };
export type Navigable = {
path: string;
name: string;
text: string;
};
type Route = RouteObject & Navigable;
const partiesRoutes: Route[] = flags.parties
? [
{
path: Routes.PARTIES,
name: 'Parties',
name: t('Parties'),
text: t('Parties'),
element: <Party />,
children: [
@@ -52,7 +56,7 @@ const assetsRoutes: Route[] = flags.assets
{
path: Routes.ASSETS,
text: t('Assets'),
name: 'Assets',
name: t('Assets'),
children: [
{
index: true,
@@ -71,7 +75,7 @@ const genesisRoutes: Route[] = flags.genesis
? [
{
path: Routes.GENESIS,
name: 'Genesis',
name: t('Genesis'),
text: t('Genesis Parameters'),
element: <Genesis />,
},
@@ -82,7 +86,7 @@ const governanceRoutes: Route[] = flags.governance
? [
{
path: Routes.GOVERNANCE,
name: 'Governance proposals',
name: t('Governance proposals'),
text: t('Governance Proposals'),
element: <Proposals />,
},
@@ -93,7 +97,7 @@ const marketsRoutes: Route[] = flags.markets
? [
{
path: Routes.MARKETS,
name: 'Markets',
name: t('Markets'),
text: t('Markets'),
children: [
{
@@ -113,17 +117,18 @@ const networkParametersRoutes: Route[] = flags.networkParameters
? [
{
path: Routes.NETWORK_PARAMETERS,
name: 'NetworkParameters',
name: t('NetworkParameters'),
text: t('Network Parameters'),
element: <NetworkParameters />,
},
]
: [];
const validators: Route[] = flags.validators
? [
{
path: Routes.VALIDATORS,
name: 'Validators',
name: t('Validators'),
text: t('Validators'),
element: <ValidatorsPage />,
},
@@ -133,14 +138,14 @@ const validators: Route[] = flags.validators
const routerConfig: Route[] = [
{
path: Routes.HOME,
name: 'Home',
name: t('Home'),
text: t('Home'),
element: <Home />,
index: true,
},
{
path: Routes.TX,
name: 'Txs',
name: t('Txs'),
text: t('Transactions'),
element: <Txs />,
children: [
@@ -160,7 +165,7 @@ const routerConfig: Route[] = [
},
{
path: Routes.BLOCKS,
name: 'Blocks',
name: t('Blocks'),
text: t('Blocks'),
element: <BlockPage />,
children: [
@@ -176,7 +181,7 @@ const routerConfig: Route[] = [
},
{
path: Routes.ORACLES,
name: 'Oracles',
name: t('Oracles'),
text: t('Oracles'),
element: <OraclePage />,
children: [
@@ -9,7 +9,7 @@ import type {
// Note: Long enough that there is a truncated output and a full output
const pubKey =
'67755549e43e95f0697f83b2bf419c6ccc18eee32a8a61b8ba6f59471b86fbef';
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
const hash = '7416753a30622a9e24a06f0172d6c33a95186b36806d96345c6dc5a23fa3f283';
const height = '52987';
const txData: BlockExplorerTransactionResult = {
+10
View File
@@ -3,3 +3,13 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
Object.defineProperty(window, 'ResizeObserver', {
writable: false,
value: jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
connect: jest.fn(),
disconnect: jest.fn(),
})),
});
+9
View File
@@ -2,3 +2,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.react-markdown-container a {
color: #ff077f;
text-decoration: underline;
}
.react-markdown-container a:before {
content: '🔗 ';
}
+2 -2
View File
@@ -19,8 +19,8 @@ module.exports = defineConfig({
supportFile: './src/support/index.js',
video: false,
videoUploadOnPasses: false,
videosFolder: '../../dist/cypress/apps/token-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/token-e2e/screenshots',
videosFolder: '../../dist/cypress/apps/governance-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/governance-e2e/screenshots',
chromeWebSecurity: false,
viewportWidth: 1440,
viewportHeight: 900,
@@ -0,0 +1,83 @@
export const trancheData = {
'0': {
tranche_id: 0,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'1': {
tranche_id: 1,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 1670422330,
duration: 1670453883,
},
'107': {
tranche_id: 107,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'110': {
tranche_id: 110,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'154': {
tranche_id: 154,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'2': {
tranche_id: 2,
users: [
'0x8716c75bb3abe54b959adc529e6355f0422454ed',
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
'0xf73e7ad8aa300a7f86bcc4d55e6c4ea25546e057',
],
initial_balance: 111300000000000000000,
current_balance: 111297025049610000000,
cliff_start: 1677578461,
duration: 15209600,
},
'3': {
tranche_id: 3,
users: [
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
'0x77a0b7e247b7b8ec99e068a24c401783d6c4dfff',
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
],
initial_balance: 21000000000000000000,
current_balance: 21000000000000000000,
cliff_start: 1677578461,
duration: 18628800,
},
'66': {
tranche_id: 66,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
};
@@ -22,7 +22,7 @@
"filters": [
{
"key": {
"name": "prices.ETH.value",
"name": "prices.BTC.value",
"type": "TYPE_INTEGER",
"numberDecimalPlaces": "0"
},
@@ -38,30 +38,34 @@
}
},
"dataSourceSpecForTradingTermination": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
"external": {
"oracle": {
"signers": [
{
"operator": "OPERATOR_EQUALS",
"value": "true"
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
{
"operator": "OPERATOR_EQUALS",
"value": "true"
}
]
}
]
}
]
}
},
"dataSourceSpecBinding": {
"settlementPriceProperty": "prices.BTC.value",
"settlementDataProperty": "prices.BTC.value",
"tradingTerminationProperty": "trading.terminated.ETH5"
}
}
@@ -0,0 +1,86 @@
{
"lpPriceRange": "11",
"instrument": {
"code": "Token.24h",
"future": {
"quoteName": "fBTC",
"dataSourceSpecForSettlementData": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "prices.BTC.value",
"type": "TYPE_INTEGER"
},
"conditions": [
{
"operator": "OPERATOR_GREATER_THAN",
"value": "0"
}
]
}
]
}
}
},
"dataSourceSpecForTradingTermination": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
{
"operator": "OPERATOR_GREATER_THAN_OR_EQUAL",
"value": "1648684800000000000"
}
]
}
]
}
}
},
"dataSourceSpecBinding": {
"settlementDataProperty": "prices.BTC.value",
"tradingTerminationProperty": "trading.terminated.ETH5"
}
}
},
"metadata": ["sector:energy", "sector:food", "source:docs.vega.xyz"],
"priceMonitoringParameters": {
"triggers": [
{
"horizon": "43200",
"probability": "0.9999999",
"auctionExtension": "600"
}
]
},
"logNormal": {
"tau": 0.0001140771161,
"riskAversionParameter": 0.001,
"params": {
"mu": 0,
"r": 0.016,
"sigma": 0.3
}
}
}
@@ -1,62 +1,76 @@
{
"lpPriceRange": "10",
"instrument": {
"code": "TEST.24h",
"future": {
"quoteName": "fUSDC",
"settlementDataDecimals": 5,
"dataSourceSpecForSettlementData": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "prices.ETH.value",
"type": "TYPE_INTEGER"
},
"conditions": [
"external": {
"oracle": {
"signers": [
{
"operator": "OPERATOR_GREATER_THAN",
"value": "0"
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "prices.ETH.value",
"type": "TYPE_INTEGER",
"numberDecimalPlaces": "0"
},
"conditions": [
{
"operator": "OPERATOR_GREATER_THAN",
"value": "0"
}
]
}
]
}
]
}
},
"dataSourceSpecForTradingTermination": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
"external": {
"oracle": {
"signers": [
{
"operator": "OPERATOR_GREATER_THAN_OR_EQUAL",
"value": "1648684800000000000"
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
{
"operator": "OPERATOR_EQUALS",
"value": "true"
}
]
}
]
}
]
}
},
"dataSourceSpecBinding": {
"settlementPriceProperty": "prices.ETH.value",
"settlementDataProperty": "prices.ETH.value",
"tradingTerminationProperty": "trading.terminated.ETH5"
}
}
},
"metadata": ["sector:energy", "sector:food", "source:docs.vega.xyz"],
"metadata": [
"sector:energy",
"sector:food",
"source:docs.vega.xyz",
"test:update"
],
"priceMonitoringParameters": {
"triggers": [
{
@@ -66,6 +80,14 @@
}
]
},
"liquidityMonitoringParameters": {
"targetStakeParameters": {
"timeWindow": "3600",
"scalingFactor": 10
},
"triggeringRatio": "0.7",
"auctionExtension": "1"
},
"logNormal": {
"tau": 0.0001140771161,
"riskAversionParameter": 0.001,
@@ -26,6 +26,10 @@ const enactmentDeadlineError =
'[data-testid="enactment-before-voting-deadline"]';
const proposalDownloadBtn = '[data-testid="proposal-download-json"]';
const feedbackError = '[data-testid="Error"]';
const viewProposalBtn = 'view-proposal-btn';
const liquidityVoteStatus = 'liquidity-votes-status';
const tokenVoteStatus = 'token-votes-status';
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const epochTimeout = Cypress.env('epochTimeout');
const proposalTimeout = { timeout: 14000 };
@@ -45,6 +49,7 @@ context(
{ tags: '@slow' },
function () {
before('connect wallets and set approval limit', function () {
cy.createMarket();
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
});
@@ -65,7 +70,7 @@ context(
// 3002-PROP-007
cy.get(newProposalDescription).type('E2E test for proposals');
cy.get(proposalParameterSelect).find('option').should('have.length', 116);
cy.get(proposalParameterSelect).find('option').should('have.length', 117);
cy.get(proposalParameterSelect).select(
// 3007-PNEC-002
'governance_proposal_asset_minEnact'
@@ -175,9 +180,8 @@ context(
cy.get(enactmentDeadlineError).should('not.exist');
});
// Skipping because unclear what the required json is yet for new market proposal, will update once docs have been updated
// 3003-todo-PMAN-001
it.skip('Able to submit valid new market proposal', function () {
// 3003-PMAN-001
it('Able to submit valid new market proposal', function () {
cy.go_to_make_new_proposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
@@ -199,6 +203,7 @@ context(
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
newMarketProposal.invalid = 'I am an invalid field';
let newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
@@ -209,11 +214,66 @@ context(
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Invalid params: the transaction is malformed'
'Invalid params: the transaction is not a valid Vega command: unknown field "invalid" in vega.NewMarket'
);
});
it.skip('Able to submit update market proposal', function () {
// Will fail if run after 'Able to submit update market proposal and vote for proposal'
// 3002-PROP-022
it('Unable to submit update market proposal without equity-like share in the market', function () {
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal - rejected');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.get(proposalMarketSelect).select('Test market 1');
cy.fixture('/proposals/update-market').then((updateMarketProposal) => {
let newUpdateMarketProposal = JSON.stringify(updateMarketProposal);
cy.get(newProposalTerms).type(newUpdateMarketProposal, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
cy.getByTestId('dialog-content')
.find('p')
.should('have.text', 'PROPOSAL_ERROR_INSUFFICIENT_EQUITY_LIKE_SHARE');
cy.ensure_specified_unstaked_tokens_are_associated('1');
});
// 3002-PROP-020
it('Unable to submit update market proposal without minimum amount of tokens', function () {
cy.vega_wallet_teardown();
cy.vega_wallet_faucet_assets_without_check(
'fUSDC',
'1000000',
vegaWalletPublicKey
);
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal - rejected');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.get(proposalMarketSelect).select('Test market 1');
cy.fixture('/proposals/update-market').then((updateMarketProposal) => {
let newUpdateMarketProposal = JSON.stringify(updateMarketProposal);
cy.get(newProposalTerms).type(newUpdateMarketProposal, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Network error: the network blocked the transaction through the spam protection: party has insufficient associated governance tokens in their staking account to submit proposal request (ABCI code 89)'
);
});
// 3001-VOTE-092
it('Able to submit update market proposal and vote for proposal', function () {
cy.vega_wallet_faucet_assets_without_check(
'fUSDC',
'1000000',
vegaWalletPublicKey
);
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
@@ -222,6 +282,10 @@ context(
cy.get('dd').eq(0).should('have.text', 'Test market 1');
cy.get('dd').eq(1).should('have.text', 'TEST.24h');
cy.get('dd').eq(2).should('not.be.empty');
cy.get('dd').eq(2).invoke('text').as('EnactedMarketId');
});
cy.get('@EnactedMarketId').then((marketId) => {
cy.VegaWalletSubmitLiquidityProvision(marketId, '1');
});
cy.fixture('/proposals/update-market').then((updateMarketProposal) => {
let newUpdateMarketProposal = JSON.stringify(updateMarketProposal);
@@ -232,6 +296,34 @@ context(
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
cy.navigate_to('proposals');
cy.get('@EnactedMarketId').then((marketId) => {
cy.contains(marketId)
.parentsUntil(proposalListItem)
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
});
cy.getByTestId(liquidityVoteStatus).should(
'contain.text',
'Currently expected to fail'
);
cy.getByTestId(tokenVoteStatus).should(
'contain.text',
'Currently expected to fail'
);
cy.vote_for_proposal('for');
cy.getByTestId(liquidityVoteStatus).should(
'contain.text',
'Currently expected to pass'
);
cy.getByTestId(tokenVoteStatus).should(
'contain.text',
'Currently expected to pass'
);
cy.get_proposal_information_from_table('Expected to pass')
.contains('👍 by Token vote')
.should('be.visible');
});
// 3001-VOTE-026 3001-VOTE-027 3001-VOTE-028 3001-VOTE-095 3001-VOTE-096 3005-PASN-001
@@ -298,7 +390,7 @@ context(
.parentsUntil(proposalListItem)
.within(() => {
cy.get(proposalDetails).should('contain.text', assetId); // 3001-VOTE-029
cy.getByTestId('view-proposal-btn').click();
cy.getByTestId(viewProposalBtn).click();
});
});
cy.get_proposal_information_from_table('Proposed enactment') // 3001-VOTE-044
@@ -14,13 +14,14 @@ const stakeShare = '[data-testid="stake-percentage"]';
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
'[data-testid="vega-wallet-balance-unstaked"]:visible';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const ethWalletAssociatedBalances =
'[data-testid="eth-wallet-associated-balances"]';
const ethWalletTotalAssociatedBalance = '[data-testid="currency-locked"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const ethWalletTotalAssociatedBalance =
'[data-testid="currency-locked"]:visible';
const ethWalletContainer = '[data-testid="ethereum-wallet"]:visible';
const vegaWallet = '[data-testid="vega-wallet"]';
const partValidatorId = '…';
const txTimeout = Cypress.env('txTimeout');
@@ -1,60 +1,10 @@
const navSection = 'nav';
const navSupply = '[href="/token/tranches"]';
const navToken = '[href="/token"]';
const navStaking = '[href="/validators"]';
const navRewards = '[href="/rewards"]';
const navWithdraw = '[href="/token/withdraw"]';
const navGovernance = '[href="/proposals"]';
const navRedeem = '[href="/token/redeem"]';
context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
before('visit token home page', function () {
cy.visit('/');
cy.get('nav', { timeout: 10000 }).should('be.visible');
});
describe('with wallets disconnected', function () {
before('wait for page to load', function () {
cy.get(navSection, { timeout: 10000 }).should('be.visible');
});
describe('Navigation tabs', function () {
it('should have proposals tab', function () {
cy.get(navSection).within(() => {
cy.get(navGovernance).should('be.visible');
});
});
it('should have validators tab', function () {
cy.get(navSection).within(() => {
cy.get(navStaking).should('be.visible');
});
});
it('should have rewards tab', function () {
cy.get(navSection).within(() => {
cy.get(navRewards).should('be.visible');
});
});
describe('Token dropdown', function () {
before('click on token dropdown', function () {
cy.get(navSection).within(() => {
cy.getByTestId('state-trigger').realClick();
});
});
it('should have token dropdown', function () {
cy.get(navToken).should('be.visible');
});
it('should have supply & vesting dropdown', function () {
cy.get(navSupply).should('be.visible');
});
it('should have withdraw dropdown', function () {
cy.get(navWithdraw).should('be.visible');
});
it('should have redeem dropdown', function () {
cy.get(navRedeem).should('be.visible');
});
});
});
describe('Links and buttons', function () {
it('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => {
@@ -0,0 +1,284 @@
context(
'Landing pages - verifies required elements',
{ tags: '@smoke' },
() => {
const navbar = 'nav .navbar';
const mobileNav = '[data-testid="menu-drawer"]';
const topLevelLinks = [
{
name: 'Proposals',
selector: '[href="/proposals"]',
tests: () => {
it('should be able to see a working link for - find out more about Vega governance', function () {
const governanceDocsUrl = 'https://vega.xyz/governance';
const proposalDocumentationLink =
'[data-testid="proposal-documentation-link"]';
// 3001-VOTE-001
cy.get(proposalDocumentationLink)
.should('be.visible')
.and('have.text', 'Find out more about Vega governance')
.and('have.attr', 'href')
.and('equal', governanceDocsUrl);
// 3002-PROP-001
cy.request(governanceDocsUrl)
.its('body')
.then((body) => {
if (!body.includes('Govern the network')) {
assert.include(
body,
'Govern the network',
`Checking that governance link destination includes 'Govern the network' text`
);
}
});
});
it('should be able to see button for - new proposal', function () {
// 3001-VOTE-002
const newProposalLink = '[data-testid="new-proposal-link"]';
cy.get(newProposalLink)
.should('be.visible')
.and('have.text', 'New proposal')
.and('have.attr', 'href')
.and('equal', '/proposals/propose');
});
},
},
{
name: 'Validators',
selector: '[href="/validators"]',
tests: () => {
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get('[data-testid="staking-guide-link"]')
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
},
},
{
name: 'Rewards',
selector: '[href="/rewards"]',
header: 'Rewards and fees',
tests: () => {
it('should have epoch warning', () => {
cy.get('[data-testid="callout"]')
.should('be.visible')
.and(
'have.text',
'Rewards are credited 5 minutes after the epoch ends.This delay is set by a network parameter'
);
});
it('should have toggle for seeing total vs individual rewards', () => {
cy.get('[data-testid="epoch-reward-view-toggle-total"]').should(
'be.visible'
);
});
},
},
];
const secondLevelLinks = [
{
trigger: true,
name: 'Token',
selector: '[data-testid="state-trigger"]',
},
{
name: 'Token',
selector: '[href="/token"]',
header: 'The $VEGA token',
},
{
name: 'Supply & Vesting',
selector: '[href="/token/tranches"]',
header: 'Vesting tranches',
},
{
name: 'Withdraw',
selector: '[href="/token/withdraw"]',
header: 'Withdrawals',
tests: () => {
it('should have connect Vega wallet button', function () {
cy.get('[data-testid="connect-to-vega-wallet-btn"]')
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
});
},
},
{
name: 'Redeem',
selector: '[href="/token/redeem"]',
header: 'Vesting',
tests: () => {
// 1005-VEST-018
it('should have connect Eth wallet button', function () {
cy.get('[data-testid="connect-to-eth-btn"]')
.should('be.visible')
.and('have.text', 'Connect Ethereum wallet');
});
},
},
{
name: 'Associate',
selector: '[href="/token/associate"]',
header: 'Associate $VEGA tokens with Vega Key',
},
{
name: 'Disassociate',
selector: '[href="/token/disassociate"]',
header: 'Disassociate $VEGA tokens from a Vega key',
},
];
const expand = () => {
const trigger = secondLevelLinks.find((l) => l.trigger).selector;
cy.get(trigger).then((el) => {
if (el.attr('aria-expanded') === 'false') {
el.trigger('click');
}
});
};
const collapse = () => {
const trigger = secondLevelLinks.find((l) => l.trigger).selector;
cy.get(trigger).then((el) => {
if (el.attr('aria-expanded') === 'true') {
el.trigger('click');
}
});
};
const ensureHeader = (text) => {
cy.get('main header h1').should('have.text', text);
};
before(() => {
// goes to HOME
cy.visit('/');
// and waits for it to load
cy.get(navbar, { timeout: 10000 }).should('be.visible');
});
describe('Navigation (desktop)', () => {
for (const { name, selector } of topLevelLinks) {
it(`should have ${name} nav link`, () => {
cy.get(navbar).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
});
});
}
for (const { name, selector, trigger } of secondLevelLinks) {
it(`should have ${name} ${
trigger ? 'as trigger button' : ''
} second level nav link`, () => {
cy.get(navbar).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
if (trigger) cy.get(selector).click();
});
});
}
after(() => {
collapse();
});
});
describe('Navigation (mobile)', () => {
beforeEach(() => {
// iphone xr
cy.viewport(414, 896);
});
it('should have burger button', () => {
cy.get('[data-testid="button-menu-drawer"]').should('be.visible');
cy.get('[data-testid="button-menu-drawer"]').click();
cy.get(mobileNav).should('be.visible');
});
for (const { name, selector } of topLevelLinks) {
it(`should have ${name} nav link`, () => {
cy.get(mobileNav).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
});
});
}
for (const { name, selector, trigger } of secondLevelLinks) {
it(`should have ${name} ${
trigger ? 'as trigger button' : ''
} second level nav link`, () => {
cy.get(mobileNav).within(() => {
cy.get(selector).should('be.visible');
cy.get(selector).should('have.text', name);
});
});
}
after(() => {
cy.get('[data-testid="button-menu-drawer"]').click();
cy.viewport(
Cypress.config('viewportWidth'),
Cypress.config('viewportHeight')
);
});
});
describe('Elements', () => {
for (const { name, selector, header, tests } of topLevelLinks) {
describe(`${name} page`, () => {
it(`navigates to ${name}`, () => {
cy.get(navbar).within(() => {
cy.log(`goes to ${name}`);
cy.get(selector).click();
cy.log(`ensures ${name} is highlighted`);
cy.get(selector).should('have.attr', 'aria-current');
});
});
it('displays header', () => {
ensureHeader(header || name);
});
if (tests) tests.apply(this);
});
}
for (const { name, selector, header, tests } of secondLevelLinks.filter(
(l) => !l.trigger
)) {
describe(`${name} page`, () => {
it(`navigates to ${name}`, () => {
cy.get(navbar).within(() => {
expand();
cy.log(`goes to ${name}`);
cy.get(selector).click();
expand();
cy.log(`ensures ${name} is highlighted`);
cy.get(selector).should('have.attr', 'aria-current');
});
});
it('displays header', () => {
ensureHeader(header || name);
});
if (tests) tests.apply(this);
});
}
after(() => {
collapse();
});
});
}
);
@@ -1,68 +0,0 @@
const proposalDocumentationLink = '[data-testid="proposal-documentation-link"]';
const newProposalButton = '[data-testid="new-proposal-link"]';
const newProposalLink = '[data-testid="new-proposal-link"]';
const governanceDocsUrl = 'https://vega.xyz/governance';
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
context(
'Governance Page - verify elements on page',
{ tags: '@smoke' },
function () {
before('navigate to governance page', function () {
cy.visit('/').navigate_to('proposals');
});
describe('with no network change proposals', function () {
it('should have governance tab highlighted', function () {
cy.verify_tab_highlighted('proposals');
});
it('should have GOVERNANCE header visible', function () {
cy.verify_page_header('Proposals');
});
it('should be able to see a working link for - find out more about Vega governance', function () {
// 3001-VOTE-001
cy.get(proposalDocumentationLink)
.should('be.visible')
.and('have.text', 'Find out more about Vega governance')
.and('have.attr', 'href')
.and('equal', governanceDocsUrl);
// 3002-PROP-001
cy.request(governanceDocsUrl)
.its('body')
.then((body) => {
if (!body.includes('Govern the network')) {
assert.include(
body,
'Govern the network',
`Checking that governance link destination includes 'Govern the network' text`
);
}
});
});
it('should be able to see button for - new proposal', function () {
// 3001-VOTE-002
cy.get(newProposalLink)
.should('be.visible')
.and('have.text', 'New proposal')
.and('have.attr', 'href')
.and('equal', '/proposals/propose');
});
// Skipping this test for now, the new proposal button no longer takes a user directly
// to a proposal form, instead it takes them to a page where they can select a proposal type.
// Keeping this test here for now as it can be repurposed to test the new proposal forms.
it.skip('should be able to see a connect wallet button - if vega wallet disconnected and new proposal button selected', function () {
cy.get(newProposalButton).should('be.visible').click();
cy.get(connectToVegaWalletButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.navigate_to('proposals');
cy.wait_for_spinner();
});
});
}
);
@@ -50,7 +50,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
});
it('Able to disconnect via wallet', function () {
cy.getByTestId('manage-vega-wallet').click();
cy.get('aside [data-testid="manage-vega-wallet"]').click();
cy.getByTestId('disconnect').click();
cy.getByTestId(banner).should('not.exist');
});
@@ -1,35 +0,0 @@
const viewToggle = '[data-testid="epoch-reward-view-toggle-total"]';
const warning = '[data-testid="callout"]';
context(
'Rewards Page - verify elements on page',
{ tags: '@regression' },
function () {
before('navigate to rewards page', function () {
cy.visit('/').navigate_to('rewards');
});
describe('with wallets disconnected', function () {
it('should have REWARDS tab highlighted', function () {
cy.verify_tab_highlighted('rewards');
});
it('should have rewards header visible', function () {
cy.verify_page_header('Rewards and fees');
});
it('should have epoch warning', function () {
cy.get(warning)
.should('be.visible')
.and(
'have.text',
'Rewards are credited 5 minutes after the epoch ends.This delay is set by a network parameter'
);
});
it('should have toggle for seeing total vs individual rewards', function () {
cy.get(viewToggle).should('be.visible');
});
});
}
);
@@ -17,8 +17,7 @@ const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
context('Verify elements on Token page', { tags: '@smoke' }, function () {
before('Visit token page', function () {
cy.visit('/');
cy.navigate_to('token');
cy.visit('/token');
});
describe('THE $VEGA TOKEN table', function () {
it('should have TOKEN ADDRESS', function () {
@@ -0,0 +1,91 @@
import { trancheData } from '../../fixtures/mocks/tranches';
const tranches = trancheData;
context(
'Tranches page - verify elements on the page',
{ tags: '@smoke' },
function () {
before('visit homepage', function () {
cy.intercept('GET', '**/tranches/stats', { tranches });
cy.visit('/token/tranches');
});
// 1005-VEST-001
// 1005-VEST-002
it('Able to view tranches', function () {
cy.getByTestId('tranche-item')
.should('have.length', 2)
.first()
.within(() => {
cy.get('a')
.should('have.text', 'Tranche 2') // 1005-VEST-003
.and('have.attr', 'href', '/token/tranches/2');
cy.get('span').eq(1).should('have.text', '111.30'); // 1005-VEST-005
cy.contains('Unlocking starts') // 1005-VEST-008
.parent()
.should('contain.text', '28 Feb 2023');
cy.contains('Fully unlocked')
.parent()
.should('contain.text', '23 Aug 2023');
cy.getByTestId('progress-bar').should('exist');
cy.getByTestId('currency-locked') // 1005-VEST-006
.invoke('text')
.should('not.be.empty');
cy.getByTestId('currency-unlocked') // 1005-VEST-007
.invoke('text')
.should('not.be.empty');
});
});
it('Able to see individual tranche data', function () {
cy.get('[href="/token/tranches/2"]').click();
cy.getByTestId('redeemed-tranche-tokens').within(() => {
cy.get('span').eq(1).should('have.text', 0);
});
cy.getByTestId('key-value-table').within(() => {
cy.getByTestId('link')
.should('have.length', 8)
.each((ethLink) => {
cy.wrap(ethLink)
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
});
cy.getByTestId('redeem-link')
.should('have.length', 8)
.each((redeemLink) => {
cy.wrap(redeemLink)
.should('have.attr', 'href')
.and('contain', '/token/redeem/');
});
});
});
it('Able to view tranches with less than 10 vega', function () {
cy.navigate_to('supply');
cy.getByTestId('show-all-tranches').click();
cy.getByTestId('tranche-item')
.should('have.length', 8)
.first()
.within(() => {
cy.get('a')
.should('have.text', 'Tranche 0')
.and('have.attr', 'href', '/token/tranches/0');
cy.get('span').eq(1).should('have.text', '0.00');
cy.contains('Unlocking starts')
.parent()
.should('contain.text', '01 Jan 1970');
cy.contains('Fully unlocked')
.parent()
.should('contain.text', '01 Jan 1970');
cy.getByTestId('progress-bar').should('exist');
cy.getByTestId('currency-locked')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('currency-unlocked')
.invoke('text')
.should('not.be.empty');
});
});
}
);
@@ -1,5 +1,4 @@
/// <reference types="cypress" />
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
const validatorId = '[data-testid="validator-id"]';
const validatorPubKey = '[data-testid="validator-public-key"]';
@@ -25,31 +24,7 @@ const stakeNumberRegex = /^\d*\.?\d*$/;
context('Staking Page - verify elements on page', function () {
before('navigate to staking page', function () {
cy.visit('/').navigate_to('validators');
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
describe('description section', function () {
it('Should have validators tab highlighted', function () {
cy.verify_tab_highlighted('validators');
});
it('Should have validators ON VEGA header visible', function () {
cy.verify_page_header('Validators');
});
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
});
cy.visit('/validators');
});
describe(
@@ -1,44 +1,111 @@
const connectButton = '[data-testid="connect-to-eth-btn"]';
const lockedTokensInVestingContract = '6,499,972.30';
context(
'Vesting Page - verify elements on page',
{ tags: '@smoke' },
function () {
describe('with wallets disconnected', function () {
before('navigate to vesting page', function () {
cy.visit('/').navigate_to('vesting');
});
it('should have vesting tab highlighted', function () {
cy.verify_tab_highlighted('token');
});
it('should have VESTING header visible', function () {
cy.verify_page_header('Vesting');
});
it('should have connect Eth wallet info', function () {
cy.get(connectButton).should('be.visible');
});
it('should have connect Eth wallet button', function () {
cy.get(connectButton)
.should('be.visible')
.and('have.text', 'Connect Ethereum wallet');
});
before('navigate to vesting page', function () {
cy.visit('/token/redeem');
});
describe('with eth wallet connected', function () {
describe('With Eth wallet connected', function () {
before('connect eth wallet', function () {
cy.ethereum_wallet_connect();
cy.visit('/');
cy.getByTestId('view-connected-eth-btn').click();
});
// 1005-VEST-001
// 1005-VEST-002
it('Able to view tranches', function () {
cy.navigate_to('supply');
cy.url().should('include', '/token/tranches');
cy.get('h1').should('contain.text', 'Vesting tranches');
// 1005-VEST-020 1005-VEST-021
it('Tokens in vesting contract for eth wallet is displayed on wallet window', function () {
cy.getByTestId('vega-in-vesting-contract').within(() => {
cy.getByTestId('currency-title')
.should('contain.text', 'VEGA')
.and('contain.text', 'In vesting contract');
cy.get('[data-testid="currency-value"]:visible').should(
'have.text',
lockedTokensInVestingContract
);
cy.get('[data-testid="currency-locked"]:visible').should(
'have.text',
lockedTokensInVestingContract
);
cy.get('[data-testid="currency-unlocked"]:visible').should(
'have.text',
'0.00'
);
});
});
// 1005-VEST-022 1005-VEST-023
it('Tokens amount displayed in vesting page', function () {
cy.getByTestId(
'redemption-description',
Cypress.env('txTimeout')
).should('exist');
const redemptionText =
'The connected Ethereum wallet (0xEe7D…d94F) has 6,499,972.30 $VEGA tokens in 1 tranche(s) of the vesting contract.';
cy.getByTestId('redemption-description').should(
'have.text',
redemptionText
);
cy.getByTestId('vesting-table').within(() => {
cy.getByTestId('key-value-table-row')
.eq(0)
.within(() => {
cy.get('dt').should('have.text', 'Vesting VEGA');
cy.get('dd').should('have.text', lockedTokensInVestingContract);
});
cy.getByTestId('key-value-table-row')
.eq(1)
.within(() => {
cy.get('dt').should('have.text', 'Locked');
cy.get('dd').should('have.text', lockedTokensInVestingContract);
});
cy.getByTestId('key-value-table-row')
.eq(2)
.within(() => {
cy.get('dt').should('have.text', 'Unlocked');
cy.get('dd').should('have.text', '0.00');
});
cy.getByTestId('key-value-table-row')
.eq(3)
.within(() => {
cy.get('dt').should('have.text', 'Associated');
cy.get('dd').should('have.text', '0.00');
});
});
});
// 1005-VEST-024 1005-VEST-025 1005-VEST-026 1005-VEST-036 1005-VEST-037
it('Tokens locked in individual tranches are displayed', function () {
cy.getByTestId('tranche-table-footer').should(
'have.text',
'All the tokens in this tranche are locked and must be assigned to a tranche before they can be redeemed.'
);
cy.getByTestId('tranche-item').within(() => {
cy.get('a')
.should('have.text', 'Tranche 0')
.and('have.attr', 'href', '/token/tranches/0');
cy.get('span')
.eq(1)
.should('have.text', lockedTokensInVestingContract);
cy.contains('Unlocking starts') // 1005-VEST-008
.parent()
.should('contain.text', '01 Jan 1970');
cy.contains('Fully unlocked')
.parent()
.should('contain.text', '01 Jan 1970');
cy.getByTestId('progress-bar').should('exist');
cy.getByTestId('currency-locked') // 1005-VEST-006
.should('have.text', lockedTokensInVestingContract);
cy.getByTestId('currency-unlocked') // 1005-VEST-007
.invoke('text')
.should('not.be.empty');
cy.getByTestId('tranche-item-footer').should(
'have.text',
'All the tokens in this tranche are locked and can not be redeemed yet.'
);
});
cy.connectVegaWallet();
});
});
}
@@ -1,18 +1,19 @@
const walletContainer = '[data-testid="ethereum-wallet"]';
const walletContainer = 'aside [data-testid="ethereum-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
const connectToEthButton = '[data-testid="connect-to-eth-wallet-button"]';
const connectToEthButton =
'[data-testid="connect-to-eth-wallet-button"]:visible';
const connectorList = '[data-testid="web3-connector-list"]';
const associate = '[href="/token/associate"]';
const disassociate = '[href="/token/disassociate"]';
const disconnect = '[data-testid="disconnect-from-eth-wallet-button"]';
const accountNo = '[data-testid="ethereum-account-truncated"]';
const currencyTitle = '[data-testid="currency-title"]';
const currencyValue = '[data-testid="currency-value"]';
const vegaInVesting = '[data-testid="vega-in-vesting-contract"]';
const vegaInWallet = '[data-testid="vega-in-wallet"]';
const progressBar = '[data-testid="progress-bar"]';
const currencyLocked = '[data-testid="currency-locked"]';
const currencyUnlocked = '[data-testid="currency-unlocked"]';
const currencyTitle = '[data-testid="currency-title"]:visible';
const currencyValue = '[data-testid="currency-value"]:visible';
const vegaInVesting = '[data-testid="vega-in-vesting-contract"]:visible';
const vegaInWallet = '[data-testid="vega-in-wallet"]:visible';
const progressBar = '[data-testid="progress-bar"]:visible';
const currencyLocked = '[data-testid="currency-locked"]:visible';
const currencyUnlocked = '[data-testid="currency-unlocked"]:visible';
const dialog = '[role="dialog"]';
const dialogHeader = '[data-testid="dialog-title"]';
const dialogCloseBtn = '[data-testid="dialog-close"]';
@@ -1,6 +1,6 @@
import { truncateByChars } from '@vegaprotocol/utils';
const walletContainer = '[data-testid="vega-wallet"]';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
const connectButton = '[data-testid="connect-vega-wallet"]';
const getVegaLink = '[data-testid="link"]';
@@ -14,7 +14,6 @@ const restWallet = '#wallet';
const restPassphrase = '#passphrase';
const restConnectBtn = '[type="submit"]';
const accountNo = '[data-testid="vega-account-truncated"]';
const walletName = '[data-testid="wallet-name"]';
const currencyTitle = '[data-testid="currency-title"]';
const currencyValue = '[data-testid="currency-value"]';
const vegaUnstaked = '[data-testid="vega-wallet-balance-unstaked"] .text-right';
@@ -28,44 +27,24 @@ const vegaWalletCurrencyTitle = '[data-testid="currency-title"]';
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const txTimeout = Cypress.env('txTimeout');
const faucetAssets = {
BTCFake: 'fBTC',
DAIFake: 'fDAI',
EUROFake: 'fEURO',
USDCFake: 'fUSDC',
};
context(
'Vega Wallet - verify elements on widget',
{ tags: '@regression' },
function () {
before('visit token home page', function () {
() => {
before('visit token home page', () => {
cy.visit('/');
cy.get(walletContainer, { timeout: 60000 }).should('be.visible');
});
describe('with wallets disconnected', function () {
before('wait for widget to load', function () {
cy.get(walletContainer, { timeout: 10000 }).should('be.visible');
});
it('should have VEGA WALLET header visible', function () {
describe('with wallets disconnected', () => {
it('should have required elements visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletHeader)
.should('be.visible')
.and('have.text', 'Vega Wallet');
});
});
it('should have Connect Vega button visible', function () {
cy.get(walletContainer).within(() => {
cy.get(connectButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet to use associated $VEGA');
});
});
it('should have Get a Vega wallet link visible', function () {
cy.get(walletContainer).within(() => {
cy.get(getVegaLink)
.should('be.visible')
.and('have.text', 'Get a Vega wallet')
@@ -74,14 +53,14 @@ context(
});
});
describe('when connect button clicked', function () {
before('click connect vega wallet button', function () {
describe('when connect button clicked', () => {
before('click connect vega wallet button', () => {
cy.get(walletContainer).within(() => {
cy.get(connectButton).click();
});
});
it('should have Connect Vega header visible', function () {
it('should have Connect Vega header visible', () => {
cy.get(dialog).within(() => {
cy.get(walletDialogHeader)
.should('be.visible')
@@ -175,14 +154,6 @@ context(
}
);
it.skip('should have wallet name visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletName)
.should('be.visible')
.and('have.text', `${Cypress.env('vegaWalletName')} key 1`);
});
});
it('should have Vega Associated currency title visible', function () {
cy.get(walletContainer).within(() => {
cy.get(currencyTitle)
@@ -299,113 +270,71 @@ context(
});
// 2002-SINC-016
describe('when assets exist in vegawallet', function () {
before('send-faucet assets to connected vega wallet', function () {
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.USDCFake,
'1000000',
vegaWalletPublicKey
);
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.BTCFake,
'600000',
vegaWalletPublicKey
);
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.EUROFake,
'800000',
vegaWalletPublicKey
);
cy.vega_wallet_faucet_assets_without_check(
faucetAssets.DAIFake,
'200000',
vegaWalletPublicKey
);
describe('Vega wallet with assets', function () {
const assets = [
{
id: 'fUSDC',
name: 'USDC (fake)',
amount: '1000000',
expectedAmount: '10.00',
},
{
id: 'fDAI',
name: 'DAI (fake)',
amount: '200000',
expectedAmount: '2.00',
},
{
id: 'fBTC',
name: 'BTC (fake)',
amount: '600000',
expectedAmount: '6.00',
},
{
id: 'fEURO',
name: 'EURO (fake)',
amount: '800000',
expectedAmount: '8.00',
},
];
before('faucet assets to connected vega wallet', function () {
for (const { id, amount } of assets) {
cy.vega_wallet_faucet_assets_without_check(
id,
amount,
vegaWalletPublicKey
);
}
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
});
it('should see fUSDC assets - within vega wallet', function () {
let currency = { id: faucetAssets.USDCFake, name: 'USDC (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
for (const { id, name, expectedAmount } of assets) {
it(`should see ${id} within vega wallet`, () => {
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.invoke('text')
.should('not.be.empty');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
cy.get(vegaWalletCurrencyTitle)
.contains(id)
.parent()
.siblings()
.within((el) => {
const value = parseFloat(el.text());
cy.wrap(value).should('be.gte', parseFloat(expectedAmount));
});
cy.get(vegaWalletCurrencyTitle)
.contains(id)
.parent()
.contains(name);
});
});
});
it('should see fBTC assets - within vega wallet', function () {
let currency = { id: faucetAssets.BTCFake, name: 'BTC (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.within(() => cy.contains_exactly('6.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
});
});
it('should see fEURO assets - within vega wallet', function () {
let currency = { id: faucetAssets.EUROFake, name: 'EURO (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.within(() => cy.contains_exactly('8.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
});
});
it('should see fDAI assets - within vega wallet', function () {
let currency = { id: faucetAssets.DAIFake, name: 'DAI (fake)' };
cy.get(walletContainer).within(() => {
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id, txTimeout)
.should('be.visible');
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.siblings()
.within(() => cy.contains_exactly('2.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
});
});
}
});
});
}
@@ -1,27 +0,0 @@
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
context(
'Withdraw Page - verify elements on page',
{ tags: '@smoke' },
function () {
before('navigate to withdrawals page', function () {
cy.visit('/').navigate_to('withdraw');
});
describe('with wallets disconnected', function () {
it('should have withdraw tab highlighted', function () {
cy.verify_tab_highlighted('token');
});
it('should have WITHDRAW header visible', function () {
cy.verify_page_header('Withdrawals');
});
it('should have connect Vega wallet button', function () {
cy.get(connectToVegaBtn)
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
});
});
}
);
@@ -22,25 +22,32 @@ const navigation = {
};
const topLevelRoutes = ['proposals', 'validators', 'rewards'];
const tokenDropDown = 'state-trigger';
Cypress.Commands.add('navigate_to', (page) => {
const tokenDropDown = 'state-trigger';
if (!topLevelRoutes.includes(page)) {
cy.getByTestId(tokenDropDown, { timeout: 10000 }).click();
cy.getByTestId('token-dropdown').within(() => {
cy.get(navigation[page]).click();
// FIXME: Timeout madness
cy.getByTestId(tokenDropDown, { timeout: 60000 }).eq(0).click();
cy.get('[data-testid="token-dropdown"]:visible').within(() => {
cy.get(navigation[page]).eq(0).click();
});
} else {
return cy.get(navigation.section, { timeout: 10000 }).within(() => {
cy.get(navigation[page]).click();
cy.get(navigation[page]).eq(0).click();
});
}
});
Cypress.Commands.add('verify_tab_highlighted', (page) => {
return cy.get(navigation.section).within(() => {
cy.get(navigation[page]).should('have.attr', 'aria-current');
if (!topLevelRoutes.includes(page)) {
cy.getByTestId(tokenDropDown, { timeout: 10000 }).eq(0).click();
cy.get('[data-testid="token-dropdown"]:visible').within(() => {
cy.get(navigation[page]).should('have.attr', 'aria-current');
});
} else {
cy.get(navigation[page]).should('have.attr', 'aria-current');
}
});
});
@@ -59,7 +66,7 @@ export function associateTokenStartOfTests() {
cy.highlight(`Associating tokens for first time`);
cy.ethereum_wallet_connect();
cy.connectVegaWallet();
cy.get('[href="/token/associate"]').first().click();
cy.get('[href="/token/associate"]:visible').first().click();
cy.getByTestId('associate-radio-wallet', { timeout: 30000 }).click();
cy.getByTestId('token-amount-input', epochTimeout).type('1');
cy.getByTestId('token-input-submit-button', txTimeout)
@@ -82,9 +82,10 @@ Cypress.Commands.add(
Cypress.Commands.add(
'get_submitted_proposal_from_proposal_list',
(proposalTitle) => {
cy.get_proposal_id_from_list(proposalTitle);
cy.get('@proposalIdText').then((proposalId) => {
return cy.get(`#${proposalId}`);
cy.get_proposal_id_from_list(proposalTitle).then(() => {
cy.get('@proposalIdText').then((proposalId) => {
return cy.get(`#${proposalId}`);
});
});
}
);
@@ -3,10 +3,10 @@ const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
const tokenInputApprove = '[data-testid="token-input-approve-button"]';
const addStakeRadioButton = '[data-testid="add-stake-radio"]';
const removeStakeRadioButton = '[data-testid="remove-stake-radio"]';
const ethWalletAssociateButton = '[href="/token/associate"]';
const ethWalletDissociateButton = '[href="/token/disassociate"]';
const ethWalletAssociateButton = '[href="/token/associate"]:visible';
const ethWalletDissociateButton = '[href="/token/disassociate"]:visible';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
'[data-testid="vega-wallet-balance-unstaked"]:visible';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const associateContractRadioButton = '[data-testid="associate-radio-contract"]';
@@ -1,5 +1,6 @@
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const connectToEthButton = '[data-testid="connect-to-eth-wallet-button"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]:visible';
const connectToEthButton =
'[data-testid="connect-to-eth-wallet-button"]:visible';
const capsuleWalletConnectButton = '[data-testid="web3-connector-Unknown"]';
Cypress.Commands.add('ethereum_wallet_connect', () => {
@@ -7,7 +7,7 @@ import {
} from '@vegaprotocol/smart-contracts';
import { ethers, Wallet } from 'ethers';
const vegaWalletContainer = '[data-testid="vega-wallet"]';
const vegaWalletContainer = 'aside [data-testid="vega-wallet"]';
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
@@ -95,7 +95,7 @@ Cypress.Commands.add('faucet_asset', function (assetEthAddress) {
});
Cypress.Commands.add('vega_wallet_teardown', function () {
cy.get('[data-testid="associated-amount"]')
cy.get('aside [data-testid="associated-amount"]')
.should('be.visible')
.invoke('text')
.as('associatedAmount');
@@ -11,7 +11,7 @@ export const EthConnectPrompt = () => {
const { appDispatch } = useAppState();
return (
<Button
variant="primary"
variant="default"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
@@ -1,126 +0,0 @@
import classNames from 'classnames';
import { NavLink } from 'react-router-dom';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import * as Dialog from '@radix-ui/react-dialog';
import { EthWallet } from '../eth-wallet';
import { VegaWallet } from '../vega-wallet';
import { useTranslation } from 'react-i18next';
interface Route {
name: string;
path: string;
}
const DrawerSection = ({ children }: { children: React.ReactNode }) => (
<div className="px-4 my-4">{children}</div>
);
const IconLine = ({ inverted }: { inverted: boolean }) => (
<span className={`block w-6 h-[2px] ${inverted ? 'bg-black' : 'bg-white'}`} />
);
const DrawerNavLinks = ({
isInverted,
routes,
}: {
isInverted?: boolean;
routes: Route[];
}) => {
const { appDispatch } = useAppState();
const { t } = useTranslation();
const linkProps = {
end: true,
onClick: () =>
appDispatch({ type: AppStateActionType.SET_DRAWER, isOpen: false }),
};
const navClasses = classNames('flex flex-col');
return (
<nav className={navClasses}>
{routes.map(({ name, path }) => {
return (
<NavLink
{...linkProps}
to={{ pathname: path }}
className={({ isActive }) =>
classNames({
'bg-vega-yellow text-black': !isInverted && isActive,
'bg-transparent text-white hover:text-vega-yellow':
!isInverted && !isActive,
'bg-black text-white': isInverted && isActive,
'bg-transparent text-black hover:text-white':
isInverted && !isActive,
'border-t border-white p-4': true,
})
}
>
{t(name)}
</NavLink>
);
})}
</nav>
);
};
export const NavDrawer = ({
inverted,
routes,
}: {
inverted: boolean;
routes: Route[];
}) => {
const { appState, appDispatch } = useAppState();
const drawerContentClasses = classNames(
'drawer-content', // needed for css animation
// Positions the modal in the center of screen
'fixed w-[80vw] max-w-[420px] top-0 right-0',
'flex flex-col flex-nowrap justify-between h-full bg-banner overflow-y-scroll border-l border-white',
'bg-black text-neutral-200'
);
return (
<>
<button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_DRAWER,
isOpen: true,
})
}
className="flex flex-col flex-nowrap gap-1"
>
<IconLine inverted={inverted} />
<IconLine inverted={inverted} />
<IconLine inverted={inverted} />
</button>
<Dialog.Root
open={appState.drawerOpen}
onOpenChange={(isOpen) =>
appDispatch({
type: AppStateActionType.SET_DRAWER,
isOpen,
})
}
>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-white/15" />
<Dialog.Content className={drawerContentClasses}>
<div>
<DrawerSection>
<EthWallet />
</DrawerSection>
<DrawerSection>
<VegaWallet />
</DrawerSection>
</div>
<DrawerNavLinks routes={routes} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</>
);
};
@@ -1,50 +0,0 @@
import { useState } from 'react';
import Routes, { TOKEN_DROPDOWN_ROUTES } from '../../routes/routes';
import { useTranslation } from 'react-i18next';
import type { NavbarTheme } from './nav-link';
import { AppNavLink } from './nav-link';
import {
NavDropdownMenu,
NavDropdownMenuContent,
NavDropdownMenuItem,
NavDropdownMenuTrigger,
} from '@vegaprotocol/ui-toolkit';
export const NavDropDown = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
const { t } = useTranslation();
const [isOpen, setOpen] = useState(false);
return (
<NavDropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
<AppNavLink
name={
<NavDropdownMenuTrigger
className="w-auto flex items-center -m-3 p-3 cursor-pointer"
data-testid="state-trigger"
onClick={() => setOpen(!isOpen)}
>
{t('Token')}
</NavDropdownMenuTrigger>
}
testId="token-dd"
path={Routes.TOKEN}
navbarTheme={navbarTheme}
/>
<NavDropdownMenuContent data-testid="token-dropdown">
{TOKEN_DROPDOWN_ROUTES.map((r) => (
<NavDropdownMenuItem key={r.name} onClick={() => setOpen(false)}>
<AppNavLink
testId={r.name}
name={t(r.name)}
path={r.path}
navbarTheme={'inherit'}
subNav={true}
end={true}
fullWidth={true}
/>
</NavDropdownMenuItem>
))}
</NavDropdownMenuContent>
</NavDropdownMenu>
);
};
@@ -1,57 +0,0 @@
import classNames from 'classnames';
import { NavLink } from 'react-router-dom';
import type { HTMLAttributeAnchorTarget, ReactNode } from 'react';
import { getNavLinkClassNames } from '@vegaprotocol/ui-toolkit';
export type NavbarTheme = 'inherit' | 'dark' | 'yellow';
interface AppNavLinkProps {
name: ReactNode | string;
path: string;
navbarTheme: NavbarTheme;
testId?: string;
target?: HTMLAttributeAnchorTarget;
end?: boolean;
fullWidth?: boolean;
subNav?: boolean;
}
export const AppNavLink = ({
name,
path,
navbarTheme,
target,
testId,
end = false,
fullWidth = false,
subNav = false,
}: AppNavLinkProps) => {
const borderClasses = classNames(
'absolute h-0.5 w-full bottom-[-1px] left-0',
{
'bg-black dark:bg-vega-yellow': navbarTheme !== 'yellow',
'bg-black': navbarTheme === 'yellow',
}
);
return (
<NavLink
key={path}
data-testid={testId}
to={{ pathname: path }}
className={getNavLinkClassNames(navbarTheme, fullWidth, subNav)}
target={target}
end={end}
>
{({ isActive }) => {
return (
<div className={subNav ? 'inline-block relative pb-1' : undefined}>
{name}
{isActive && (
<span data-testid="link-active" className={borderClasses} />
)}
</div>
);
}}
</NavLink>
);
};
@@ -1,25 +0,0 @@
@keyframes slideIn {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideOut {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
.drawer-content[data-state='open'] {
animation: slideIn 150ms ease-out forwards;
}
.drawer-content[data-state='closed'] {
animation: slideOut 150ms ease-in forwards;
}
@@ -1,56 +0,0 @@
import { render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { Nav } from './nav';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
NetworkSwitcher: () => <div data-testid="network-switcher" />,
useEnvironment: () => ({ VEGA_ENV: 'MAINNET' }),
}));
const renderComponent = (initialEntries?: string[]) => {
return render(
<MemoryRouter initialEntries={initialEntries}>
<Nav />
</MemoryRouter>
);
};
describe('nav', () => {
it('Renders logo with link to home', () => {
renderComponent();
expect(screen.getByTestId('logo-link')).toHaveProperty(
'href',
'http://localhost/'
);
});
it('Renders network switcher', () => {
renderComponent();
expect(screen.getByTestId('network-switcher')).toBeInTheDocument();
});
it('Renders all top level routes', () => {
renderComponent();
expect(screen.getByTestId('Proposals')).toHaveProperty(
'href',
'http://localhost/proposals'
);
expect(screen.getByTestId('Validators')).toHaveProperty(
'href',
'http://localhost/validators'
);
expect(screen.getByTestId('Rewards')).toHaveProperty(
'href',
'http://localhost/rewards'
);
});
it('Shows active state on dropdown trigger when on home route for subroutes', () => {
const { getByTestId } = renderComponent(['/token']);
const dd = getByTestId('token-dd');
expect(within(dd).getByTestId('link-active')).toBeInTheDocument();
});
it('Shows active state on dropdown trigger when on sub route of dropdown', () => {
const { getByTestId } = renderComponent(['/token/withdraw']);
const dd = getByTestId('token-dd');
expect(within(dd).getByTestId('link-active')).toBeInTheDocument();
});
});
+77 -73
View File
@@ -1,81 +1,85 @@
import { Link } from 'react-router-dom';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import { useEffect, useState } from 'react';
import { TOP_LEVEL_ROUTES } from '../../routes/routes';
import { TOKEN_DROPDOWN_ROUTES, TOP_LEVEL_ROUTES } from '../../routes/routes';
import { useTranslation } from 'react-i18next';
import logoWhiteText from '../../images/logo-white-text.png';
import logoBlackText from '../../images/logo-black-text.png';
import debounce from 'lodash/debounce';
import { NavDrawer } from './nav-draw';
import { Nav as ToolkitNav } from '@vegaprotocol/ui-toolkit';
import { AppNavLink } from './nav-link';
import { NavDropDown } from './nav-dropdown';
const useDebouncedResize = () => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResizeDebounced = debounce(() => {
setWindowWidth(window.innerWidth);
}, 300);
window.addEventListener('resize', handleResizeDebounced);
return () => {
window.removeEventListener('resize', handleResizeDebounced);
};
}, []);
return {
windowWidth,
};
};
type NavbarTheme = 'inherit' | 'dark' | 'yellow';
interface NavbarProps {
navbarTheme?: NavbarTheme;
}
export const Nav = ({ navbarTheme = 'inherit' }: NavbarProps) => {
const { windowWidth } = useDebouncedResize();
const isDesktop = windowWidth > 995;
import type { NavigationProps } from '@vegaprotocol/ui-toolkit';
import { useNavigationDrawer } from '@vegaprotocol/ui-toolkit';
import {
Navigation,
NavigationBreakpoint,
NavigationContent,
NavigationItem,
NavigationLink,
NavigationList,
NavigationTrigger,
} from '@vegaprotocol/ui-toolkit';
import { EthWallet } from '../eth-wallet';
import { VegaWallet } from '../vega-wallet';
import { useLocation, useMatch } from 'react-router-dom';
import { useEffect } from 'react';
export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
const { t } = useTranslation();
const isYellow = navbarTheme === 'yellow';
const setDrawerOpen = useNavigationDrawer((state) => state.setDrawerOpen);
const location = useLocation();
const isOnToken = useMatch('token/*');
useEffect(() => {
setDrawerOpen(false);
}, [location, setDrawerOpen]);
const topLevel = TOP_LEVEL_ROUTES.map(({ name, path }) => (
<NavigationItem key={name}>
<NavigationLink to={path}>{name}</NavigationLink>
</NavigationItem>
));
const secondLevel = TOKEN_DROPDOWN_ROUTES.map(({ name, path, end }) => (
<NavigationItem key={name}>
<NavigationLink to={path} end={Boolean(end)}>
{name}
</NavigationLink>
</NavigationItem>
));
return (
<ToolkitNav
navbarTheme={navbarTheme}
icon={
<Link to="/" data-testid="logo-link">
<img
alt="Vega"
src={navbarTheme === 'yellow' ? logoBlackText : logoWhiteText}
height={30}
width={250}
/>
</Link>
}
title={undefined}
titleContent={<NetworkSwitcher />}
>
{isDesktop ? (
<nav className="flex items-center flex-1 px-4">
{TOP_LEVEL_ROUTES.map((r) => (
<AppNavLink
key={r.path}
testId={r.name}
name={t(r.name)}
path={r.path}
navbarTheme={navbarTheme}
/>
))}
<NavDropDown navbarTheme={navbarTheme} />
</nav>
) : (
<nav className="flex items-center flex-1 px-2 justify-end">
<NavDrawer inverted={isYellow} routes={TOP_LEVEL_ROUTES} />
</nav>
)}
</ToolkitNav>
<Navigation appName="Governance" theme={theme} breakpoints={[458, 959]}>
<NavigationList
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
hide={[NavigationBreakpoint.Small]}
>
<NavigationItem className="[.drawer-content_&]:w-full">
<NetworkSwitcher className="[.drawer-content_&]:w-full" />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
{topLevel}
<NavigationItem>
<NavigationTrigger
data-testid="state-trigger"
isActive={Boolean(isOnToken)}
>
{t('Token')}
</NavigationTrigger>
<NavigationContent>
<NavigationList data-testid="token-dropdown">
{secondLevel}
</NavigationList>
</NavigationContent>
</NavigationItem>
</NavigationList>
<NavigationList
hide={true}
className="[.drawer-content_&]:border-t [.drawer-content_&]:border-t-vega-light-200 dark:[.drawer-content_&]:border-t-vega-dark-200 [.drawer-content_&]:pt-8 [.drawer-content_&]:mt-4"
>
<NavigationItem className="[.drawer-content_&]:w-full">
<EthWallet />
</NavigationItem>
<NavigationItem className="[.drawer-content_&]:w-full">
<VegaWallet />
</NavigationItem>
</NavigationList>
</Navigation>
);
};
@@ -27,7 +27,7 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
</ExternalLink>
</div>
</AnnouncementBanner>
<Nav navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}
@@ -37,9 +37,6 @@ export interface AppState {
/** Whether or not the connect to Ethereum wallet overlay is open */
ethConnectOverlay: boolean;
/** Whether or not the mobile drawer is open. Only relevant on screens smaller than 960 */
drawerOpen: boolean;
/** Whether or not the transaction modal is open */
transactionOverlay: boolean;
/**
@@ -17,7 +17,6 @@ const initialAppState: AppState = {
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
};
@@ -36,7 +35,6 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
return {
...state,
vegaWalletOverlay: action.isOpen,
drawerOpen: action.isOpen ? false : state.drawerOpen,
};
}
case AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY: {
@@ -44,20 +42,17 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
...state,
vegaWalletManageOverlay: action.isOpen,
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
drawerOpen: action.isOpen ? false : state.drawerOpen,
};
}
case AppStateActionType.SET_ETH_WALLET_OVERLAY: {
return {
...state,
ethConnectOverlay: action.isOpen,
drawerOpen: action.isOpen ? false : state.drawerOpen,
};
}
case AppStateActionType.SET_DRAWER: {
return {
...state,
drawerOpen: action.isOpen,
vegaWalletOverlay: false,
};
}
@@ -9,26 +9,28 @@ export type PendingTxsStore = {
resetPendingTxs: () => void;
};
export const usePendingBalancesStore = create<PendingTxsStore>((set, get) => ({
pendingBalances: [],
addPendingTxs: (event: Event[]) => {
set({
pendingBalances: uniqBy(
[...get().pendingBalances, ...event],
'transactionHash'
),
});
},
removePendingTx: (event: Event) => {
set({
pendingBalances: [
...get().pendingBalances.filter(
({ transactionHash }) => transactionHash !== event.transactionHash
export const usePendingBalancesStore = create<PendingTxsStore>()(
(set, get) => ({
pendingBalances: [],
addPendingTxs: (event: Event[]) => {
set({
pendingBalances: uniqBy(
[...get().pendingBalances, ...event],
'transactionHash'
),
],
});
},
resetPendingTxs: () => {
set({ pendingBalances: [] });
},
}));
});
},
removePendingTx: (event: Event) => {
set({
pendingBalances: [
...get().pendingBalances.filter(
({ transactionHash }) => transactionHash !== event.transactionHash
),
],
});
},
resetPendingTxs: () => {
set({ pendingBalances: [] });
},
})
);
@@ -336,6 +336,7 @@
"withdrawLpNoneDeposited": "You have no SLP tokens deposited or rewards accumulated",
"withdrawAllLpSuccessCalloutTitle": "Your SLP tokens and rewards have been sent to your Ethereum address",
"Dissociate VEGA tokens": "Disassociate $VEGA tokens",
"DisassociateVegaTokensFromWallet": "Disassociate $VEGA tokens from your Vega wallet",
"Early Investors": "Early Investors",
"Team": "Team",
"Community": "Community",
@@ -482,6 +483,7 @@
"withdrawFormSubmitButtonIdle": "Withdraw {{amount}} {{symbol}} tokens",
"withdrawFormSubmitButtonPending": "Preparing",
"withdrawalsTitle": "Withdrawals",
"withdrawalsSubtitle": "Connect your Vega wallet to complete a withdrawal. These withdrawals will be completed with an Ethereum transaction.",
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
"withdrawalsNone": "You don't have any pending withdrawals.",
"withdrawalsCompleteButton": "Finish withdrawal",
@@ -552,9 +554,6 @@
"resourceNotFound": "Resource Not Found",
"Or": "Or",
"addTokenToWallet": "Show this token in your Ethereum wallet",
"chartBelow": "*Certain activities, for example community incentive tokens, do not have specific dates to be credited and so an approximation has been used. ",
"chartTitle": "Token unlocking schedule",
"chartAbove": "This chart shows the approximate* schedule for how tokens will unlock from vesting tranches over time.",
"date": "Date",
"noEthereumProviderError": "No Ethereum browser extension detected, install MetaMask on desktop or visit from a dApp browser on mobile",
"unsupportedChainIdError": "You're connected to an unsupported network",
@@ -38,7 +38,7 @@ export interface RefreshBalances {
vestingAssociatedBalance: BigNumber;
}
export const useBalances = create<BalancesStore>((set) => ({
export const useBalances = create<BalancesStore>()((set) => ({
associationBreakdown: {
stakingAssociations: {},
vestingAssociations: {},
@@ -1,7 +1,7 @@
import { toBigNum } from '@vegaprotocol/utils';
import type { TrancheServiceResponse } from '@vegaprotocol/smart-contracts';
import type BigNumber from 'bignumber.js';
import create from 'zustand';
import { create } from 'zustand';
import { ENV } from '../../config';
export interface Tranche {
@@ -36,7 +36,7 @@ export type TranchesStore = {
const secondsToDate = (seconds: number) => new Date(seconds * 1000);
export const useTranches = create<TranchesStore>((set) => ({
export const useTranches = create<TranchesStore>()((set) => ({
tranches: null,
loading: false,
error: null,
@@ -54,7 +54,6 @@ const mockAppState: AppState = {
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
drawerOpen: false,
transactionOverlay: false,
bannerMessage: '',
};
@@ -56,7 +56,7 @@ export const VoteDetails = ({
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<section>
<SubHeading title={t('liquidityVotes')} />
<p>
<p data-testid="liquidity-votes-status">
<span>
<CurrentProposalStatus proposal={proposal} />
</span>
@@ -105,7 +105,7 @@ export const VoteDetails = ({
)}
<section data-testid="votes-table">
<SubHeading title={t('tokenVotes')} />
<p>
<p data-testid="token-votes-status">
<span>
<CurrentProposalStatus proposal={proposal} />
</span>

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