Compare commits

...
Author SHA1 Message Date
Dariusz Majcherczyk 5017224fcd test: update live e2e tests 2023-03-29 14:52:12 +02:00
dexturr e306d1c9fa chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-28 12:08:16 +00:00
dexturr 737dec4f5f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-28 06:08:37 +00:00
dexturr 6780b2f618 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-28 00:15:54 +00:00
Penny Andrews 08dc60086d chore(ci): update name of release issue template (#3279) 2023-03-27 11:47:35 -07:00
Bartłomiej Głownia 4595db07b0 fix(trading): throttle deal ticket submit (#3282) 2023-03-27 11:46:40 -07:00
dexturr c3da31146f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 18:08:26 +00:00
dexturr b6a9faaa7c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 12:10:42 +00:00
daro-maj 477d523582 test(trading): fixed capsule tests (#3281) 2023-03-27 12:50:22 +02:00
Maciek a2b112d898 fix(positions): make positions table smoothly updated (again) (#3266) 2023-03-27 12:30:55 +02:00
Mikołaj Młodzikowski 621ba497e8 feat: remove env vars file after setting up js file 2023-03-27 11:03:06 +02:00
dexturr a6964bc3cf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 06:06:57 +00:00
dexturr 48a9998441 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-27 00:16:27 +00:00
dexturr bd1bd94ee7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 18:07:12 +00:00
dexturr 2fdebbd694 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 12:10:22 +00:00
dexturr 96af8035d0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 06:07:03 +00:00
dexturr 051e3da366 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-26 00:17:44 +00:00
dexturr af5e666d5e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 18:09:35 +00:00
dexturr 333a7d6206 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 12:10:21 +00:00
dexturr cb6b988466 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 06:09:33 +00:00
dexturr 806ea74f7e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-25 00:14:18 +00:00
Matthew Russell 8693f3bfd8 chore(trading): dont show faucet button until connected (#3251) 2023-03-24 17:06:22 -07:00
Bartłomiej Głownia 58c6652e29 feat(trading): use mini scroll in deal ticket, market info for and landing dialog (#3269) 2023-03-24 17:01:12 -07:00
Bartłomiej Głownia e8adff25c4 fix(trading): handle lack of approval in trading deposit flow (#3278) 2023-03-24 16:57:00 -07:00
Penny Andrews 3e594a783a chore(ci): add release issue template (#3272) 2023-03-24 16:55:22 -07:00
dexturr 1f7117075b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 18:09:07 +00:00
Bartłomiej Głownia 4c850ecead fix(trading): change etherscan link label on toast (#3262) 2023-03-24 17:57:59 +01:00
Mikołaj Młodzikowski 19f8fa909e feat(explorer,trading,governance): use nx to know which apps needs building (#3267) 2023-03-24 16:42:33 +01:00
Sam Keen 8214858685 feat(governance): lp vote now reflected on proposal cards (#3256) 2023-03-24 14:56:56 +00:00
dexturr 868f8e21dc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 12:08:16 +00:00
dexturr 6ead84b57c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 06:09:30 +00:00
dexturr 3975477985 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 00:14:29 +00:00
Art 8a0c15ac11 feat(explorer): breadcrumbs (#3254) 2023-03-23 19:11:34 +00:00
dexturr a4f512f946 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-23 18:08:43 +00:00
dexturr 88f3c278f8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-23 12:08:36 +00:00
Mikołaj Młodzikowski 34d5f3fd13 feat(explorer): build docker image on pull request + refactor dockerfile for ipfs (#3245) 2023-03-23 10:50:23 +01:00
daro-maj a060f94146 test(trading): 0003-WTXN submit vega transaction e2e tests (#3246) 2023-03-23 09:46:28 +01:00
MaciekandMatthew Russell f649d78565 chore(orders): remove infinite scroll from order table (#3247)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-03-23 09:13:07 +01:00
dexturr b611d30b31 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-23 06:08:50 +00:00
dexturr 545324ccd6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-23 00:16:17 +00:00
Bartłomiej Głownia 8c16759621 fix(trading): use unique keys in LocalStoragePersistTabs (#3255) 2023-03-22 16:34:58 -07:00
Matthew Russell e724aac2f1 feat(trading): add resources dropdown to console nav (#3249) 2023-03-22 16:33:29 -07:00
dexturr e3a6b75130 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-22 18:09:03 +00:00
Joe Tsang 8d8f6b4928 test(governance): governance tests refactor (#3059) 2023-03-22 17:35:00 +00:00
m.ray 6c1c5bf2a9 feat(trading): pin collateral row (#3218) 2023-03-22 13:05:05 +01:00
Ciaran McGhie 74a3aa8566 refactor(ui-toolkit): move healthbar to ui-toolkit (#3121) 2023-03-22 11:33:46 +00:00
dexturr 4cbd7a4928 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-22 06:09:20 +00:00
dexturr 201cf07a01 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-22 00:13:57 +00:00
Dexter EdwardsandMatthew Russell 90ceeebf15 fix(ui-toolkit): anchor button should be same size as button by default (#3172)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-03-21 15:24:34 -07:00
dexturr 965f7f6e2d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-21 18:08:39 +00:00
Ciaran McGhie 18633efec3 chore(ui-toolkit): rework intent colours (#3202) 2023-03-21 16:40:39 +00:00
a0b758334a fix(trading): auction triggers liquidity API change in develop for core 0.69.0 (#3197)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2023-03-21 09:19:21 -07:00
Bartłomiej Głownia 9f3026aa4b fix(market-depth): fix depth chart data upate, use market depth as mid price source (#3242) 2023-03-21 08:46:04 -07:00
Maciek faa359e2fb chore(trading): 2764 link to mainnet incident reports (#3235) 2023-03-21 08:19:54 -07:00
Bartłomiej Głownia 1d178b940f feat(trading): store current tab selection (#3148) 2023-03-21 15:50:31 +01:00
dexturr b2dd053621 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-21 12:10:18 +00:00
dexturr 77f5193b7a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-21 06:07:47 +00:00
dexturr 055a0070db chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-21 00:13:48 +00:00
dexturr 29b221c86a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 18:09:57 +00:00
Dexter Edwards 3e619acd4b feat(ui-toolkit): button icons (#3233) 2023-03-20 14:40:04 +00:00
botond c720719ac5 chore(governance): update announcement banner text (#3234) 2023-03-20 13:52:03 +00:00
Art c676a7c1e2 chore(explorer): move banner up (#3237) 2023-03-20 13:38:31 +00:00
dexturr ca63a671c8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 12:09:43 +00:00
Maciek 4165f3905f chore(static): update domains in network conf files (#3219) 2023-03-20 09:58:05 +00:00
dexturr a359ccb755 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 06:07:41 +00:00
dexturr e584737005 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 00:17:40 +00:00
dexturr b12c5ed39f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 18:08:07 +00:00
dexturr d3dbd64cf1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 12:07:36 +00:00
dexturr 2df62bca61 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 06:07:48 +00:00
dexturr fd2a434b46 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 00:17:10 +00:00
dexturr 4e7a78f988 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 18:08:23 +00:00
dexturr 327619652a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 12:07:24 +00:00
dexturr d2339205cc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 06:06:50 +00:00
dexturr 63df4cf465 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 00:13:54 +00:00
dexturr 5e34178291 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 18:06:54 +00:00
dexturr 0c44664aa5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 12:07:53 +00:00
Bartłomiej Głownia 7ef1d75529 chore(trading): remove red background from trades table (#3214) 2023-03-17 09:52:40 +00:00
Maciek 84795b2d6a chore(trading): split bottom panel into two parts (#3205) 2023-03-17 09:50:43 +00:00
dexturr 300019f108 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 06:09:09 +00:00
dexturr 2d0aebd3fe chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 00:14:43 +00:00
dexturr 9e86d2aff5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 18:09:41 +00:00
daro-maj 0f7e76ae01 test(trading): increase coverage for 0004-ewal (#3210) 2023-03-16 13:29:40 +01:00
dexturr 56732d1503 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 12:08:59 +00:00
Joe Tsang 5c19263653 chore(governance): include slippage factor to proposals (#3204) 2023-03-16 10:41:18 +00:00
dexturr 0d911b3029 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 06:08:43 +00:00
Matthew RussellandEdd 9f059e7862 chore(explorer,governance): update be endpoints (#3183)
Co-authored-by: Edd <edd@vega.xyz>
2023-03-15 20:12:04 -07:00
dexturr 92ea93dadb chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 00:16:50 +00:00
daro-maj 6165aefad7 test(trading): fixed settlement asset displayed test (#3209) 2023-03-15 18:38:57 +00:00
dexturr c95369d914 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-15 18:08:52 +00:00
Maciek 5dbc5d7997 chore(trading): make loading masks in tabs consistent along the app (#3155) 2023-03-15 16:08:48 +01:00
dexturr 47b3d5b55f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-15 12:07:50 +00:00
Maciek da7c0b84f7 chore(trading): display latency in block height progressing as warn or error (#3169) 2023-03-15 12:49:31 +01:00
Art 67e602ebb1 feat(explorer): try again for tx details (#3195) 2023-03-15 08:24:42 +00:00
Art f407110e95 chore(environment): etherscan link (#3193) 2023-03-15 08:24:20 +00:00
Art 66a85cce2a chore(explorer): market info shared panels (#3192) 2023-03-15 08:21:12 +00:00
Art 93f8450ecb fix(utils): missing flags (#3194) 2023-03-15 08:20:41 +00:00
dexturr 0447a6cf5d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-15 06:09:39 +00:00
dexturr 498be838c7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-15 00:17:07 +00:00
dexturr fc3b1bd9ab chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-14 18:09:59 +00:00
Joe Tsang 090d850647 test(governance): e2e rewards (#3190) 2023-03-14 16:10:58 +00:00
Maciek 95280f5b26 chore(environment): fetch networks configuration from repo (#3145) 2023-03-14 16:02:43 +01:00
m.ray 101ae00a57 fix(trading): hide transfer button in view as mode (#3189) 2023-03-14 15:39:47 +01:00
Bartłomiej Głownia 613cce3683 feat(orders): use default filters in orders list (#3143) 2023-03-14 13:53:51 +01:00
dexturr 41fa18ed45 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-14 12:10:12 +00:00
Sam Keen c941893cdd fix(governance): ensure validators tables sorting on develop (#3176) 2023-03-14 09:16:12 +00:00
m.ray d049b471d8 chore(trading): mainnet sim 3 live banners (#3184) 2023-03-14 09:04:19 +00:00
m.ray 010ffc0f46 fix(trading): dropdown fix from testnet and validator-testnet (#3185) 2023-03-14 08:55:45 +00:00
dexturr dd56e4083b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-14 06:08:23 +00:00
dexturr 09d1ba68ba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-14 00:16:43 +00:00
Matthew Russell 3959daa2b7 chore(trading,explorer,governance): update validator testnet nodes (#3182) 2023-03-13 16:17:10 -07:00
Matthew Russell c599b02c89 fix(trading): add missing block explorer link for complete transfer (#3181) 2023-03-13 18:00:51 -04:00
m.ray e5ebaf02d2 fix(trading): ledger entries table formatting (#3180) 2023-03-13 19:40:08 +00:00
Matthew Russell ae54616c1f chore(trading): move banner above nav (#3161) 2023-03-13 18:38:16 +00:00
m.ray 8f3cf390c5 fix(trading): remove liquidation from positions & format price bounds with market dp (#3173) 2023-03-13 18:17:00 +00:00
m.ray 64cf0c90a7 feat(trading): clarify collateral and breakdown tables (#3113) 2023-03-13 18:15:53 +00:00
dexturr 3edd91eadb chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-13 18:06:59 +00:00
m.ray 676aeb07a4 fix(trading): deal ticket validation on zero account balance and margin warnings (#3177) 2023-03-13 17:58:51 +00:00
dexturr 52ee998bf0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-13 12:07:45 +00:00
Eddandasiaznik 727e2cd2ff fix(explorer): neaten validators mobile view (#3160)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-03-13 11:08:53 +00:00
dexturr 06b2dd18f9 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-13 06:07:23 +00:00
dexturr a189f38cdc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-13 00:17:26 +00:00
dexturr d18681a5f0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-12 18:06:57 +00:00
dexturr 9e2d03853a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-12 12:07:33 +00:00
dexturr 18c36d5e6b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-12 06:07:33 +00:00
dexturr dcfb8f9527 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-12 00:16:12 +00:00
dexturr a64acecf4b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-11 18:06:56 +00:00
dexturr 0998952196 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-11 12:08:29 +00:00
dexturr 2fd4d379cf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-11 06:08:34 +00:00
dexturr c99d055b24 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-11 00:15:06 +00:00
Edd f1fa13e0bd feat(explorer): show signers in proposal multisig bundles (#3122) 2023-03-10 18:19:37 +00:00
dexturr ae020a8dca chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-10 18:07:16 +00:00
Edd fa871a49c7 fix(explorer): format els as percentage in network param view (#3157) 2023-03-10 16:14:56 +00:00
Edd 4205dd471a fix(explorer,trading): asset details should show quantum in asset dps (#3158) 2023-03-10 16:14:36 +00:00
Eddandasiaznik 1040077c2b feat(explorer,trading,governance): update mainnet sims banner (#3156)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-03-10 16:12:30 +00:00
Art 9d346d7846 feat(ui-toolkit): navigation (#3069) 2023-03-10 16:46:51 +01:00
b68136ba3f chore(deposits): update faucet feedback i18n (#3132)
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
2023-03-10 14:43:32 +00:00
daro-maj f5ad2de5a5 test(trading): skip send report to artifact if tests are passed (#3154) 2023-03-10 15:37:49 +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
406 changed files with 15808 additions and 12729 deletions
+4
View File
@@ -3,3 +3,7 @@ apps/**/node_modules/*
tmp/*
.dockerignore
dockerfiles
node_modules
.git
.github
.vscode
+22
View File
@@ -0,0 +1,22 @@
---
name: Release
about:
A template to outline the steps needed to for a successful release of our frontend apps
title: 'Release [add dapp version]-core-[add core version]'
labels:
assignees: ''
---
### Tasks
- [ ] Review [link to core release](xxx)
- [ ] Tag frontend-monorepo
- [ ] Create release and generate release notes
- [ ] Run `@smoke` tests
- [ ] Run `@regression` tests
- [ ] Run `@slow` tests
- [ ] Explorative testing of key flows
- [ ] Set `release/[network]` to tagged commit
- [ ] Verify builds (on Netlify and Fleek) are successful
- [ ] Verify build has been deployed
- [ ] Smoke testing on deployed app
+3 -1
View File
@@ -1,3 +1,4 @@
name: Cypress Run
on:
workflow_call:
inputs:
@@ -17,6 +18,7 @@ jobs:
fail-fast: false
matrix:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 30
steps:
@@ -87,7 +89,7 @@ jobs:
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
- uses: actions/upload-artifact@v3
if: ${{ always() }}
if: ${{ failure() }}
with:
name: logs-${{ matrix.project }}
path: /home/runner/.vegacapsule/testnet/logs
@@ -1,4 +1,4 @@
name: Cypress tests - PR
name: PR Validations
on:
push:
@@ -20,14 +20,16 @@ jobs:
with:
fetch-depth: 0
- name: Check node version
id: node-version
run: |
npmVersion=$(cat .nvmrc | head -n 1)
echo ::set-output name=npmVersion::${npmVersion}
- name: Setup node
uses: actions/setup-node@v3
- name: Remove package.json & yarn.lock to avoid installing everything
run: rm package.json yarn.lock
- name: Install nx
run: yarn add nx
with:
node-version: ${{ steps.node-version.outputs.npmVersion }}
# Check SHAs
- name: Derive appropriate SHAs for base and head for `nx affected` commands
@@ -39,38 +41,51 @@ jobs:
# See affected apps
- name: See affected apps
run: |
nx_version=$(cat package.json | grep '"nx"' | cut -d ':' -f 2 | tr -d '",[:space:]')
rm package.json yarn.lock
yarn add nx@$nx_version
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected"
projects=""
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV
projects_e2e=""
if [[ $affected == *"governance"* ]]; then projects_e2e+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects_e2e+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects_e2e+='"explorer-e2e" '; fi
if [[ -z "$projects_e2e" ]]; then projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects_e2e=${projects_e2e%?}
projects_e2e=[${projects_e2e// /,}]
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }}
run:
run-cypress:
needs: pr
if: ${{ needs.pr.outputs.projects != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.pr.outputs.projects }}
projects: ${{ needs.pr.outputs.projects-e2e }}
tags: '@smoke @regression'
run-docker-build:
needs: pr
if: ${{ needs.pr.outputs.projects != '[]' }}
uses: ./.github/workflows/publish-docker-containers.yml
secrets: inherit
with:
projects: ${{ needs.pr.outputs.projects }}
# Report single result at the end, to avoid mess with required checks in PR
result:
if: ${{ always() }}
needs: run
needs: run-cypress
runs-on: ubuntu-latest
name: Cypress result
steps:
- run: |
result="${{ needs.run.result }}"
result="${{ needs.run-cypress.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
+44 -76
View File
@@ -1,54 +1,23 @@
name: Publish docker containers
name: Docker build
'on':
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
on:
workflow_call:
inputs:
publish:
description: 'Publish tag to Docker Hub & GitHub Registry'
required: false
type: boolean
default: false
tag:
description: 'Git Tag to build and publish'
required: false
projects:
required: true
type: string
default: ''
apps:
description: 'Applications to build and publish'
required: false
type: choice
options:
- '["explorer", "token", "trading"]'
- '["explorer"]'
- '["token"]'
- '["trading"]'
archs:
description: 'Architecture to build and publish'
required: false
type: choice
options:
- linux/amd64, linux/arm64
- linux/amd64
- linux/arm64
jobs:
master:
strategy:
fail-fast: false
matrix:
app: ${{ fromJson(inputs.apps || '["explorer", "token", "trading"]') }}
name: Build the ${{ matrix.app }} image
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
with:
ref: ${{ inputs.tag }}
- name: Set up QEMU
id: quemu
@@ -60,67 +29,66 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
# https://github.com/actions/checkout#Checkout-pull-request-HEAD-commit-instead-of-merge-commit
- name: Determine Docker Image tag
id: tags
run: |
hash=$(git rev-parse HEAD|cut -b1-8)
versionTag=${{ inputs.tag || startsWith(github.ref, 'refs/tags/') && github.ref_name || '${hash}' }}
npmVersion=$(cat .nvmrc | head -n 1)
versionTag=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.pull_request.head.sha }}
echo ::set-output name=npmVersion::${npmVersion}
echo ::set-output name=version::${versionTag}
echo ::set-output name=npmVersion::$(cat dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }} | grep FROM | head -n 1 | awk '{print $2}' | cut -d ':' -f 2 | cut -d '-' -f 1 )
- name: Print config
run: |
git rev-parse --verify HEAD
git status
echo "inputs.tag=${{ inputs.tag }}"
echo "inputs.publish=${{ inputs.publish }}"
echo "inputs.apps=${{ inputs.apps }}"
echo "inputs.archs=${{ inputs.archs }}"
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
- uses: actions/setup-node@v3
with:
node-version: ${{ steps.tags.outputs.npmVersion }}
- name: Build frontend dists
run: |
yarn --verbose --pure-lockfile
yarn nx ${{ matrix.app =='trading' && 'export' || 'build' }} ${{ matrix.app }} --pure-lockfile
- name: Build and export to local Docker
uses: docker/build-push-action@v3
with:
context: .
push: false
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: APP=${{ matrix.app }}
load: true
tags: vegaprotocol/${{ matrix.app }}:local
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Sanity check docker image
run: |
docker run --rm vegaprotocol/${{ matrix.app }}:local cat .env
docker run --rm vegaprotocol/${{ matrix.app }}:local ls -lah
echo "Check .env file"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat .env
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
- name: Build and push to DockerHub
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
push: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: APP=${{ matrix.app }}
platforms: ${{ inputs.archs || 'linux/amd64, linux/arm64' }}
push: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
tags: |
vegaprotocol/${{ matrix.app }}:latest
vegaprotocol/${{ matrix.app }}:${{ steps.tags.outputs.version }}
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:latest
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ steps.tags.outputs.version }}
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' }}
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+33
View File
@@ -0,0 +1,33 @@
# Build container
ARG NODE_VERSION
FROM --platform=amd64 node:${NODE_VERSION}-alpine3.16 as build
WORKDIR /app
# Argument to allow building of different apps
ARG APP
RUN apk add --update --no-cache \
python3 \
make \
gcc \
g++
COPY . ./
RUN yarn --network-timeout 100000 --pure-lockfile
# work around for different build process in trading
RUN sh ./docker-build.sh
# Server environment
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
# this is to ensure that we run always same version of alpine to make sure ipfs is indempotent
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
ARG APP
# configuration of system
RUN apk add --no-cache bash go-ipfs
EXPOSE 80
COPY entrypoint.sh /entrypoint.sh
CMD ["/entrypoint.sh"]
# Copy dist
WORKDIR /usr/share/nginx/html
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/apps/${APP} /usr/share/nginx/html
COPY ./apps/${APP}/.env .env
RUN ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash
+1 -1
View File
@@ -3,7 +3,7 @@ NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_ENV=CUSTOM
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/capsule-network.json
NX_VEGA_CONFIG_URL=
CYPRESS_VEGA_TENDERMINT_URL=http://localhost:26617
@@ -22,7 +22,6 @@
"governance.proposal.updateAsset.minProposerBalance",
"governance.proposal.updateAsset.minVoterBalance",
"governance.proposal.updateAsset.requiredParticipation",
"governance.proposal.updateMarket.minProposerEquityLikeShare",
"market.fee.factors.infrastructureFee",
"market.fee.factors.makerFee",
"market.liquidity.bondPenaltyParameter",
@@ -77,6 +76,7 @@
"governance.proposal.updateMarket.requiredParticipationLP",
"governance.proposal.updateNetParam.requiredMajority",
"governance.proposal.updateNetParam.requiredParticipation",
"governance.proposal.updateMarket.minProposerEquityLikeShare",
"validators.vote.required"
],
"duration": [
+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) => {
+8 -87
View File
@@ -6,18 +6,10 @@ context('Home Page', function () {
describe('Stats page', { tags: '@smoke' }, function () {
const statsValue = '[data-testid="stats-value"]';
it('Should show connected environment', function () {
const deployedEnv = Cypress.env('environment').toUpperCase();
cy.get('[data-testid="stats-environment"]').should(
'have.text',
`/ ${deployedEnv}`
);
});
it('should show connected environment stats', function () {
const statTitles = {
0: 'Status',
1: 'Height',
1: 'Block height',
2: 'Uptime',
3: 'Total nodes',
4: 'Total staked',
@@ -36,27 +28,27 @@ context('Home Page', function () {
cy.get('[data-testid="stats-title"]')
.each(($list, index) => {
cy.wrap($list).should('have.text', statTitles[index]);
cy.wrap($list).should('contain.text', statTitles[index]);
})
.then(($list) => {
cy.wrap($list).should('have.length', 16);
});
cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
cy.get(statsValue).eq(0).should('contain.text', 'CONNECTED');
cy.get(statsValue).eq(1).should('not.be.empty');
cy.get(statsValue)
.eq(2)
.invoke('text')
.should('match', /\d+d \d+h \d+m \d+s/i);
cy.get(statsValue).eq(3).should('have.text', '2');
cy.get(statsValue).eq(3).should('contain.text', '2');
cy.get(statsValue)
.eq(4)
.invoke('text')
.should('match', /\d+\.\d\d(?!\d)/i);
cy.get(statsValue).eq(5).should('have.text', '0');
cy.get(statsValue).eq(6).should('have.text', '0');
cy.get(statsValue).eq(7).should('have.text', '0');
cy.get(statsValue).eq(8).should('have.text', '0');
cy.get(statsValue).eq(5).should('contain.text', '0');
cy.get(statsValue).eq(6).should('contain.text', '0');
cy.get(statsValue).eq(7).should('contain.text', '0');
cy.get(statsValue).eq(8).should('contain.text', '0');
cy.get(statsValue).eq(9).should('not.be.empty');
cy.get(statsValue).eq(10).should('not.be.empty');
cy.get(statsValue).eq(11).should('not.be.empty');
@@ -86,75 +78,4 @@ context('Home Page', function () {
});
});
});
describe('Git info', function () {
it('git info is rendered on the footer of the page', function () {
cy.getByTestId('git-info').within(() => {
cy.getByTestId('git-network-data').within(() => {
cy.contains('Reading network data from').should('be.visible');
cy.get('span').should('have.text', Cypress.env('networkQueryUrl'));
cy.getByTestId('link').should('be.visible');
});
cy.getByTestId('git-eth-data').within(() => {
cy.contains('Reading Ethereum data from').should('be.visible');
cy.get('span').should('have.text', Cypress.env('ethUrl'));
});
cy.getByTestId('git-commit-hash').within(() => {
cy.contains('Version/commit hash:').should('be.visible');
cy.getByTestId('link').should('have.text', Cypress.env('commitHash'));
});
});
});
});
describe('Search bar', function () {
it('Successful search for specific id by block id', function () {
const blockId = '973624';
search(blockId);
cy.url().should('include', blockId);
});
it('Successful search for specific id by tx hash', function () {
const txHash =
'9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8';
search(txHash);
cy.url().should('include', txHash);
});
it('Successful search for specific id by tx id', function () {
const txId =
'0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898';
search(txId);
cy.url().should('include', txId);
});
it('Error message displayed when invalid search by wrong string length', function () {
search('9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8');
validateSearchError("Something doesn't look right");
});
it('Error message displayed when invalid search by invalid hash', function () {
search(
'9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8'
);
validateSearchError('Transaction is not hexadecimal');
});
it('Error message displayed when searching empty field', function () {
cy.get('[data-testid="search"]').clear();
cy.get('[data-testid="search-button"]').click();
validateSearchError('Search required');
});
function search(searchTxt) {
cy.get('[data-testid="search"]').clear().type(searchTxt);
cy.get('[data-testid="search-button"]').click();
}
function validateSearchError(expectedError) {
cy.get('[data-testid="search-error"]').should('have.text', expectedError);
}
});
});
@@ -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,6 +1,6 @@
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_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
+2 -2
View File
@@ -1,10 +1,10 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_VEGA_ENV=DEVNET
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
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet1-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
+2 -2
View File
@@ -1,8 +1,8 @@
# App configuration variables
NX_TENDERMINT_URL=https://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_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
+1 -1
View File
@@ -1,7 +1,7 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_ENV=MIRROR
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
+1 -1
View File
@@ -1,6 +1,6 @@
# App configuration variables
NX_VEGA_ENV=SANDBOX
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
+1 -1
View File
@@ -1,7 +1,7 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
+1 -1
View File
@@ -1,6 +1,6 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
NX_VEGA_ENV=STAGNET3
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
+2 -2
View File
@@ -2,9 +2,9 @@
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
+5 -5
View File
@@ -1,12 +1,12 @@
# App configuration variables
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_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_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_URL=https://tm-be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
+9 -48
View File
@@ -1,60 +1,21 @@
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';
import { Footer } from './components/footer/footer';
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
asJson={asJson}
open={isOpen}
onChange={setOpen}
/>
);
};
const splashLoading = (
<Splash>
<Loader />
</Splash>
);
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'
);
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>
</AnnouncementBanner>
<div className={layoutClasses}>
<Header />
<Nav />
<Main />
<Footer />
</div>
<DialogsContainer />
<RouterProvider router={router} fallbackElement={splashLoading} />
</NetworkLoader>
</TendermintWebsocketProvider>
);
@@ -10,13 +10,13 @@ export const Footer = () => {
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl'].includes(screenSize),
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
[screenSize]
);
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">
@@ -13,19 +13,18 @@ jest.mock('../search', () => ({
}));
const renderComponent = () => (
<MemoryRouter>
<MemoryRouter initialEntries={['/txs']}>
<Header />
</MemoryRouter>
);
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,111 @@
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
import { matchPath, useLocation, useMatch } 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 { isNavigable } from '../../routes/router-config';
import { routerConfig } from '../../routes/router-config';
import { useMemo } from 'react';
import compact from 'lodash/compact';
import { Search } from '../search';
const routeToNavigationItem = (r: Navigable) => (
<NavigationItem key={r.handle.name}>
<NavigationLink to={r.path}>{r.handle.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 isHome = Boolean(useMatch(Routes.HOME));
const pages = routerConfig[0].children || [];
const mainItems = compact(
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
pages.find((r) => r.path === n)
)
).filter(isNavigable);
const groupedItems = compact(
[
Routes.PARTIES,
Routes.ASSETS,
Routes.MARKETS,
Routes.GOVERNANCE,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].map((n) => pages.find((r) => r.path === n))
).filter(isNavigable);
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 />
{!isHome && <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 && groupedItems.length > 0 && (
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnOther)}>
{t('Other')}
</NavigationTrigger>
<NavigationContent>
<NavigationList>
{groupedItems.map(routeToNavigationItem)}
</NavigationList>
</NavigationContent>
</NavigationItem>
)}
</NavigationList>
</Navigation>
);
};
@@ -1,9 +0,0 @@
import { AppRouter } from '../../routes';
export const Main = () => {
return (
<main className="p-4">
<AppRouter />
</main>
);
};
@@ -1,143 +1,52 @@
import {
addDecimalsFormatNumber,
formatNumberPercentage,
getMarketExpiryDateFormatted,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info';
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
import {
MarketStateMapping,
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
import { useMemo } from 'react';
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info';
import { Link } from 'react-router-dom';
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
);
const { data: asset } = useAssetDataProvider(assetId ?? '');
if (!market) return null;
const keyDetails = {
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
tradingMode: market.tradingMode,
state: MarketStateMapping[market.state],
};
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const panels = [
{
title: t('Key details'),
content: (
<MarketInfoTable
noBorder={false}
data={{
name: market.tradableInstrument.instrument.name,
marketID: market.id,
tradingMode:
keyDetails.tradingMode &&
MarketTradingModeMapping[keyDetails.tradingMode],
marketDecimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}}
/>
),
content: <KeyDetailsInfoPanel noBorder={false} market={market} />,
},
{
title: t('Instrument'),
content: (
<MarketInfoTable
noBorder={false}
data={{
marketName: market.tradableInstrument.instrument.name,
code: market.tradableInstrument.instrument.code,
productType:
market.tradableInstrument.instrument.product.__typename,
...market.tradableInstrument.instrument.product,
}}
/>
),
content: <InstrumentInfoPanel noBorder={false} market={market} />,
},
{
title: t('Settlement asset'),
content: asset ? (
<AssetDetailsTable
asset={asset}
inline={true}
noBorder={false}
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/>
) : (
<Splash>{t('No data')}</Splash>
),
content: <SettlementAssetInfoPanel market={market} noBorder={false} />,
},
{
title: t('Metadata'),
content: (
<MarketInfoTable
noBorder={false}
data={{
expiryDate: getMarketExpiryDateFormatted(
market.tradableInstrument.instrument.metadata.tags
),
...market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
}}
/>
),
content: <MetadataInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk model'),
content: (
<MarketInfoTable
noBorder={false}
data={market.tradableInstrument.riskModel}
unformatted={true}
omits={[]}
/>
),
content: <RiskModelInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk parameters'),
content: (
<MarketInfoTable
noBorder={false}
data={market.tradableInstrument.riskModel.params}
unformatted={true}
omits={[]}
/>
),
content: <RiskParametersInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk factors'),
content: (
<MarketInfoTable
noBorder={false}
data={market.riskFactors}
unformatted={true}
omits={['market', '__typename']}
/>
),
content: <RiskFactorsInfoPanel noBorder={false} market={market} />,
},
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
(trigger, i) => ({
@@ -158,7 +67,10 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
<MarketInfoTable
noBorder={false}
data={{ referencePrice: trigger.referencePrice }}
decimalPlaces={assetDecimals}
decimalPlaces={
market.tradableInstrument.instrument.product.settlementAsset
.decimals
}
/>
</>
),
@@ -166,64 +78,26 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
{
title: t('Liquidity monitoring parameters'),
content: (
<MarketInfoTable
<LiquidityMonitoringParametersInfoPanel
noBorder={false}
data={{
triggeringRatio:
market.liquidityMonitoringParameters.triggeringRatio,
...market.liquidityMonitoringParameters.targetStakeParameters,
}}
market={market}
/>
),
},
{
title: t('Liquidity'),
content: <LiquidityInfoPanel market={market} noBorder={false} />,
},
{
title: t('Liquidity price range'),
content: (
<>
<p className="text-xs mb-4">
{`For liquidity orders to count towards a commitment, they must be
within the liquidity monitoring bounds.`}
</p>
<p className="text-xs mb-4">
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
price.`}
</p>
<MarketInfoTable
noBorder={false}
data={{
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
lowestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${quoteUnit}`,
}}
></MarketInfoTable>
</>
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
),
},
{
title: t('Oracle'),
content: (
<MarketInfoTable
noBorder={false}
data={
market.tradableInstrument.instrument.product.dataSourceSpecBinding
}
>
<OracleInfoPanel noBorder={false} market={market}>
<Link
className="text-xs hover:underline"
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
@@ -236,7 +110,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
>
{t('View termination oracle specification')}
</Link>
</MarketInfoTable>
</OracleInfoPanel>
),
},
];
@@ -244,7 +118,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
return (
<>
{panels.map((p) => (
<div className="mb-3">
<div key={p.title} className="mb-3">
<h2 className="font-alpha calt text-xl">{p.title}</h2>
{p.content}
</div>
@@ -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>
);
};
@@ -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 '-';
}
@@ -1,4 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Button } from '@vegaprotocol/ui-toolkit';
import { StatusMessage } from '../status-message';
interface RenderFetchedProps {
@@ -7,6 +8,7 @@ interface RenderFetchedProps {
loading: boolean | undefined;
className?: string;
errorMessage?: string;
refetch?: () => void;
}
export const RenderFetched = ({
@@ -15,6 +17,7 @@ export const RenderFetched = ({
children,
className,
errorMessage = t('Error retrieving data'),
refetch,
}: RenderFetchedProps) => {
if (loading) {
return (
@@ -23,7 +26,20 @@ export const RenderFetched = ({
}
if (error) {
return <StatusMessage className={className}>{errorMessage}</StatusMessage>;
return (
<>
<StatusMessage className={className}>{errorMessage}</StatusMessage>
{refetch && (
<Button
onClick={() => {
refetch();
}}
>
{t('Try again')}
</Button>
)}
</>
);
}
return children;
@@ -1,32 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import React from 'react';
interface RouteErrorBoundaryProps {
children: React.ReactElement;
}
export class RouteErrorBoundary extends React.Component<
RouteErrorBoundaryProps,
{ hasError: boolean }
> {
constructor(props: RouteErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
override componentDidCatch(error: Error) {
console.log(`Error caught in App error boundary ${error.message}`, error);
}
override render() {
if (this.state.hasError) {
return <h1>{t('Something went wrong')}</h1>;
}
return this.props.children;
}
}
@@ -1,189 +1,70 @@
import {
detectTypeByFetching,
detectTypeFromQuery,
getSearchType,
determineType,
isBlock,
isHexadecimal,
isNetworkParty,
isNonHex,
SearchTypes,
toHex,
toNonHex,
isHash,
} from './detect-search';
import { DATA_SOURCES } from '../../config';
global.fetch = jest.fn();
describe('Detect Search', () => {
it("should detect that it's a hexadecimal", () => {
const expected = true;
const testString =
'0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isHexadecimal(testString);
expect(actual).toBe(expected);
});
it("should detect that it's not hexadecimal", () => {
const expected = true;
const testString =
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isNonHex(testString);
expect(actual).toBe(expected);
it.each([
['0000000000000000000000000000000000000000000000000000000000000000', true],
['0000000000000000000000000000000000000000000000000000000000000001', true],
[
'LOOONG0000000000000000000000000000000000000000000000000000000000000000',
false,
],
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', false],
['something else', false],
])("should detect that it's a hash", (input, expected) => {
expect(isHash(input)).toBe(expected);
});
it("should detect that it's a network party", () => {
const expected = true;
const testString = 'network';
const actual = isNetworkParty(testString);
expect(actual).toBe(expected);
expect(isNetworkParty('network')).toBe(true);
expect(isNetworkParty('web')).toBe(false);
});
it("should detect that it's a block", () => {
const expected = true;
const testString = '3188';
const actual = isBlock(testString);
expect(actual).toBe(expected);
expect(isBlock('123')).toBe(true);
expect(isBlock('x123')).toBe(false);
});
it('should convert from non-hex to hex', () => {
const expected = '0x123';
const testString = '123';
const actual = toHex(testString);
expect(actual).toBe(expected);
});
it('should convert from hex to non-hex', () => {
const expected = '123';
const testString = '0x123';
const actual = toNonHex(testString);
expect(actual).toBe(expected);
});
it("should detect type client side from query if it's a hexadecimal", () => {
const expected = [SearchTypes.Party, SearchTypes.Transaction];
const testString =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a non hex", () => {
const expected = [SearchTypes.Party, SearchTypes.Transaction];
const testString =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a network party", () => {
const expected = [SearchTypes.Party];
const testString = 'network';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a block (number)", () => {
const expected = [SearchTypes.Block];
const testString = '23432';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
const query = '0xabc';
const type = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await detectTypeByFetching(query);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
);
expect(result).toBe(type);
});
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
const query = 'abc';
const type = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await detectTypeByFetching(query);
expect(result).toBe(type);
});
it('getSearchType should return party from fetch response', async () => {
const query =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return transaction from fetch response', async () => {
const query =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return undefined from transaction response', async () => {
const query = 'u';
const expected = undefined;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return block if query is number', async () => {
const query = '123';
const expected = SearchTypes.Block;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return party if query is network', async () => {
const query = 'network';
const expected = SearchTypes.Party;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it.each([
[
'0000000000000000000000000000000000000000000000000000000000000000',
SearchTypes.Transaction,
],
[
'0000000000000000000000000000000000000000000000000000000000000001',
SearchTypes.Party,
],
['123', SearchTypes.Block],
['network', SearchTypes.Party],
['something else', SearchTypes.Unknown],
])(
"detectTypeByFetching should call fetch with non-hex query it's a transaction",
async (input, type) => {
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok:
input ===
'0000000000000000000000000000000000000000000000000000000000000000',
json: () =>
Promise.resolve({
transaction: {
hash: input,
},
}),
})
)
);
const result = await determineType(input);
expect(result).toBe(type);
}
);
});
@@ -1,3 +1,4 @@
import { remove0x } from '@vegaprotocol/utils';
import { DATA_SOURCES } from '../../config';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
@@ -6,15 +7,20 @@ export enum SearchTypes {
Party = 'party',
Block = 'block',
Order = 'order',
Unknown = 'unknown',
}
export const TX_LENGTH = 64;
export const HASH_LENGTH = 64;
export const isHash = (value: string) =>
/[0-9a-fA-F]+/.test(remove0x(value)) &&
remove0x(value).length === HASH_LENGTH;
export const isHexadecimal = (search: string) =>
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
search.startsWith('0x') && search.length === 2 + HASH_LENGTH;
export const isNonHex = (search: string) =>
!search.startsWith('0x') && search.length === TX_LENGTH;
!search.startsWith('0x') && search.length === HASH_LENGTH;
export const isBlock = (search: string) => !Number.isNaN(Number(search));
@@ -23,121 +29,44 @@ export const isNetworkParty = (search: string) => search === 'network';
export const toHex = (query: string) =>
isHexadecimal(query) ? query : `0x${query}`;
export const toNonHex = (query: string) =>
isNonHex(query) ? query : `${query.replace('0x', '')}`;
export const toNonHex = remove0x;
export const detectTypeFromQuery = (
query: string
): SearchTypes[] | undefined => {
const i = query.toLowerCase();
if (isHexadecimal(i) || isNonHex(i)) {
return [SearchTypes.Party, SearchTypes.Transaction];
} else if (isNetworkParty(i)) {
return [SearchTypes.Party];
} else if (isBlock(i)) {
return [SearchTypes.Block];
/**
* Determine the type of the given query
*/
export const determineType = async (query: string): Promise<SearchTypes> => {
const value = query.toLowerCase();
if (isHash(value)) {
// it can be either `SearchTypes.Party` or `SearchTypes.Transaction`
if (await isTransactionHash(value)) {
return SearchTypes.Transaction;
} else {
return SearchTypes.Party;
}
} else if (isNetworkParty(value)) {
return SearchTypes.Party;
} else if (isBlock(value)) {
return SearchTypes.Block;
}
return undefined;
return SearchTypes.Unknown;
};
export const detectTypeByFetching = async (
query: string
): Promise<SearchTypes | undefined> => {
const hash = toNonHex(query);
/**
* Checks if given input is a transaction hash by querying the transactions
* endpoint
*/
export const isTransactionHash = async (input: string): Promise<boolean> => {
const hash = remove0x(input);
const request = await fetch(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
);
if (request?.ok) {
const body: BlockExplorerTransaction = await request.json();
if (body?.transaction) {
return SearchTypes.Transaction;
return true;
}
}
return SearchTypes.Party;
};
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
// export const detectTypeByFetching = async (
// query: string,
// type: SearchTypes
// ): Promise<SearchTypes | undefined> => {
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
//
// if (!TYPES.includes(type)) {
// throw new Error('Search type provided not recognised');
// }
//
// if (type === SearchTypes.Transaction) {
// const hash = toNonHex(query);
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
// );
//
// if (request?.ok) {
// const body: BlockExplorerTransaction = await request.json();
//
// if (body?.transaction) {
// return SearchTypes.Transaction;
// }
// }
// } else if (type === SearchTypes.Party) {
// const party = toNonHex(query);
//
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
// );
//
// if (request.ok) {
// const body: BlockExplorerTransactions = await request.json();
//
// if (body?.transactions?.length) {
// return SearchTypes.Party;
// }
// }
// }
//
// return undefined;
// };
// export const getSearchType = async (
// query: string
// ): Promise<SearchTypes | undefined> => {
// const searchTypes = detectTypeFromQuery(query);
// const hasResults = searchTypes?.length;
//
// if (hasResults) {
// if (hasResults > 1) {
// const promises = searchTypes.map((type) =>
// detectTypeByFetching(query, type)
// );
// const results = await Promise.all(promises);
// return results.find((result) => result !== undefined);
// }
//
// return searchTypes[0];
// }
//
// return undefined;
// };
export const getSearchType = async (
query: string
): Promise<SearchTypes | undefined> => {
const searchTypes = detectTypeFromQuery(query);
const hasResults = searchTypes?.length;
if (hasResults) {
if (hasResults > 1) {
return await detectTypeByFetching(query);
}
return searchTypes[0];
}
return undefined;
return false;
};
@@ -1,157 +1,82 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Search } from './search';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { SearchForm } from './search';
import { MemoryRouter } from 'react-router-dom';
import { Routes } from '../../routes/route-names';
import { SearchTypes, getSearchType } from './detect-search';
global.fetch = jest.fn();
const mockedNavigate = jest.fn();
const mockGetSearchType = getSearchType as jest.MockedFunction<
typeof getSearchType
>;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedNavigate,
}));
jest.mock('./detect-search', () => ({
...jest.requireActual('./detect-search'),
getSearchType: jest.fn(),
}));
beforeEach(() => {
mockedNavigate.mockClear();
});
const renderComponent = () => (
<MemoryRouter>
<Search />
</MemoryRouter>
);
const renderComponent = () =>
render(
<MemoryRouter>
<SearchForm />
</MemoryRouter>
);
const getInputs = () => ({
input: screen.getByTestId('search'),
button: screen.getByTestId('search-button'),
});
describe('Search', () => {
describe('SearchForm', () => {
it('should render search input and button', () => {
render(renderComponent());
renderComponent();
expect(screen.getByTestId('search')).toBeInTheDocument();
expect(screen.getByTestId('search-button')).toHaveTextContent('Search');
});
it('should render error if input is not known', async () => {
render(renderComponent());
const { button, input } = getInputs();
fireEvent.change(input, { target: { value: 'asd' } });
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Transaction type is not recognised'
it.each([
[
Routes.TX,
'0000000000000000000000000000000000000000000000000000000000000000',
],
[
Routes.PARTIES,
'0000000000000000000000000000000000000000000000000000000000000001',
],
[Routes.BLOCKS, '123'],
[undefined, 'something else'],
])('should redirect to %s', async (route, input) => {
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok:
input ===
'0000000000000000000000000000000000000000000000000000000000000000',
json: () =>
Promise.resolve({
transaction: {
hash: input,
},
}),
})
)
);
});
it('should render error if no input is given', async () => {
render(renderComponent());
const { button } = getInputs();
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Search query required'
);
});
it('should redirect to transactions page', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
fireEvent.change(input, {
target: {
value:
'0x1234567890123456789012345678901234567890123456789012345678901234',
},
renderComponent();
await act(async () => {
fireEvent.change(screen.getByTestId('search'), {
target: {
value: input,
},
});
fireEvent.click(screen.getByTestId('search-button'));
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to transactions page without proceeding 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
fireEvent.change(input, {
target: {
value:
'1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to parties page', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
fireEvent.change(input, {
target: {
value:
'0x1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to parties page without proceeding 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
fireEvent.change(input, {
target: {
value:
'1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to blocks page if passed a number', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Block);
fireEvent.change(input, {
target: {
value: '123',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(`${Routes.BLOCKS}/123`);
expect(mockedNavigate).toBeCalledTimes(route ? 1 : 0);
if (route) {
// eslint-disable-next-line jest/no-conditional-expect
expect(mockedNavigate).toBeCalledWith(`${route}/${input}`);
}
});
});
});
@@ -1,84 +1,211 @@
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import { Button, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { getSearchType, SearchTypes, toHex } from './detect-search';
import { Routes } from '../../routes/route-names';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import { remove0x } from '@vegaprotocol/utils';
import { determineType, isBlock, isHash, SearchTypes } from './detect-search';
interface FormFields {
search: string;
}
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>
);
const Clear = () => (
<svg
className="w-3 h-3"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.3748 1.37478L1.37478 11.3748L0.625244 10.6252L10.6252 0.625244L11.3748 1.37478Z"
fill="currentColor"
/>
<path
d="M1.37478 0.625244L11.3748 10.6252L10.6252 11.3748L0.625244 1.37478L1.37478 0.625244Z"
fill="currentColor"
/>
</svg>
);
export const Search = () => {
const { register, handleSubmit } = useForm<FormFields>();
const navigate = useNavigate();
const [error, setError] = useState<Error | null>(null);
const searchForm = <SearchForm />;
const onSubmit = useCallback(
async (fields: FormFields) => {
setError(null);
const query = fields.search;
if (!query) {
return setError(new Error(t('Search query required')));
}
const result = await getSearchType(query);
const urlAsHex = toHex(query);
const unrecognisedError = new Error(
t('Transaction type is not recognised')
);
if (result) {
switch (result) {
case SearchTypes.Party:
return navigate(`${Routes.PARTIES}/${urlAsHex}`);
case SearchTypes.Transaction:
return navigate(`${Routes.TX}/${urlAsHex}`);
case SearchTypes.Block:
return navigate(`${Routes.BLOCKS}/${Number(query)}`);
default:
return setError(unrecognisedError);
}
}
return setError(unrecognisedError);
},
[navigate]
const 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 (
<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">
<>
<div className="hidden [.nav-search-full_&]:block min-w-[290px]">
{searchForm}
</div>
<div className="hidden [.nav-search-compact_&]:block">
{searchTrigger}
</div>
</>
);
};
export const SearchForm = () => {
const {
register,
handleSubmit,
setValue,
setError,
clearErrors,
formState,
watch,
} = useForm<FormFields>();
const navigate = useNavigate();
const onSubmit = useCallback(
async (fields: FormFields) => {
clearErrors();
const type = await determineType(fields.search);
if (type) {
switch (type) {
case SearchTypes.Party:
return navigate(`${Routes.PARTIES}/${remove0x(fields.search)}`);
case SearchTypes.Transaction:
return navigate(`${Routes.TX}/${remove0x(fields.search)}`);
case SearchTypes.Block:
return navigate(`${Routes.BLOCKS}/${Number(fields.search)}`);
}
}
setError('search', new Error(t('The search term is not a valid query')));
},
[clearErrors, navigate, setError]
);
const searchQuery = watch('search', '');
return (
<form className="block min-w-[200px]" onSubmit={handleSubmit(onSubmit)}>
<div className="flex relative items-stretch gap-2 text-xs">
<div className="relative w-full">
<label htmlFor="search" className="sr-only">
{t('Search by block number or transaction hash')}
</label>
<button
className={classNames(
'absolute top-[50%] translate-y-[-50%] left-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
>
<MagnifyingGlass />
</button>
<button
onClick={(e) => {
e.preventDefault();
setValue('search', '');
clearErrors();
}}
className={classNames(
{ hidden: searchQuery.length === 0 },
'absolute top-[50%] translate-y-[-50%] right-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
>
<Clear />
</button>
<Input
{...register('search')}
{...register('search', {
required: t('Search query is required'),
validate: (value) =>
isHash(value) ||
isBlock(value) ||
t('Search query has to be a number or a 64 character hash'),
onBlur: () => clearErrors('search'),
})}
id="search"
data-testid="search"
className="text-white"
hasError={Boolean(error?.message)}
className={classNames(
'pl-8 py-2 text-xs',
{ 'pr-8': searchQuery.length > 1 },
'border rounded border-vega-light-200 dark:border-vega-dark-200',
{
'border-vega-pink dark:border-vega-pink': Boolean(
formState.errors.search
),
}
)}
hasError={Boolean(formState.errors.search)}
type="text"
placeholder={t(
'Enter block number, public key or transaction hash'
)}
/>
{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>
)}
</div>
<Button type="submit" size="sm" data-testid="search-button">
{formState.errors.search && (
<div
className={classNames(
'[nav_&]:border [nav_&]:rounded [nav_&]:border-vega-light-300 [nav_&]:dark:border-vega-light-300',
'[.search-dropdown_&]:border [.search-dropdown_&]:rounded [.search-dropdown_&]:border-vega-light-300 [.search-dropdown_&]:dark:border-vega-light-300',
'bg-white dark:bg-black',
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
)}
>
<InputError
data-testid="search-error"
intent="danger"
className="text-xs"
>
{formState.errors.search.message}
</InputError>
</div>
)}
<Button
variant="primary"
type="submit"
size="xs"
data-testid="search-button"
className="[nav_&]:hidden"
>
{t('Search')}
</Button>
</div>
@@ -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} />
@@ -5,7 +5,6 @@ import type { components } from '../../../../../types/explorer';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { TxDetailsChainMultisigSigner } from './tx-multisig-signer';
import { getBlockTime } from './lib/get-block-time';
type Added = components['schemas']['vegaERC20SignerAdded'];
type Removed = components['schemas']['vegaERC20SignerRemoved'];
@@ -61,10 +60,7 @@ describe('Chain Event: multisig signer change', () => {
expect(screen.getByText(t('Add signer'))).toBeInTheDocument();
expect(screen.getByText(`${addedMock.newSigner}`)).toBeInTheDocument();
const expectedDate = getBlockTime(mockBlockTime);
expect(screen.getByText(t('Signer change at'))).toBeInTheDocument();
expect(screen.getByText(expectedDate)).toBeInTheDocument();
});
it('Renders TableRows if all data is provided', () => {
@@ -93,9 +89,6 @@ describe('Chain Event: multisig signer change', () => {
expect(screen.getByText(t('Remove signer'))).toBeInTheDocument();
expect(screen.getByText(`${removedMock.oldSigner}`)).toBeInTheDocument();
const expectedDate = getBlockTime(mockBlockTime);
expect(screen.getByText(t('Signer change at'))).toBeInTheDocument();
expect(screen.getByText(expectedDate)).toBeInTheDocument();
});
});
@@ -6,7 +6,6 @@ import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { TxDetailsChainMultisigThreshold } from './tx-multisig-threshold';
import omit from 'lodash/omit';
import { getBlockTime } from './lib/get-block-time';
type Threshold =
components['schemas']['vegaERC20MultiSigEvent']['thresholdSet'];
@@ -74,9 +73,6 @@ describe('Chain Event: multisig threshold change', () => {
expect(screen.getByText(t('Threshold'))).toBeInTheDocument();
expect(screen.getByText(`66.7%`)).toBeInTheDocument();
const expectedDate = getBlockTime(mockBlockTime);
expect(screen.getByText(t('Threshold change date'))).toBeInTheDocument();
expect(screen.getByText(expectedDate)).toBeInTheDocument();
});
});
@@ -5,6 +5,11 @@ query ExplorerNewAssetSignatureBundle($id: ID!) {
}
asset(id: $id) {
status
source {
... on ERC20 {
contractAddress
}
}
}
}
@@ -15,5 +20,10 @@ query ExplorerUpdateAssetSignatureBundle($id: ID!) {
}
asset(id: $id) {
status
source {
... on ERC20 {
contractAddress
}
}
}
}
@@ -8,14 +8,14 @@ export type ExplorerNewAssetSignatureBundleQueryVariables = Types.Exact<{
}>;
export type ExplorerNewAssetSignatureBundleQuery = { __typename?: 'Query', erc20ListAssetBundle?: { __typename?: 'Erc20ListAssetBundle', signatures: string, nonce: string } | null, asset?: { __typename?: 'Asset', status: Types.AssetStatus } | null };
export type ExplorerNewAssetSignatureBundleQuery = { __typename?: 'Query', erc20ListAssetBundle?: { __typename?: 'Erc20ListAssetBundle', signatures: string, nonce: string } | null, asset?: { __typename?: 'Asset', status: Types.AssetStatus, source: { __typename?: 'BuiltinAsset' } | { __typename?: 'ERC20', contractAddress: string } } | 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 type ExplorerUpdateAssetSignatureBundleQuery = { __typename?: 'Query', erc20SetAssetLimitsBundle: { __typename?: 'ERC20SetAssetLimitsBundle', signatures: string, nonce: string }, asset?: { __typename?: 'Asset', status: Types.AssetStatus, source: { __typename?: 'BuiltinAsset' } | { __typename?: 'ERC20', contractAddress: string } } | null };
export const ExplorerNewAssetSignatureBundleDocument = gql`
@@ -26,6 +26,11 @@ export const ExplorerNewAssetSignatureBundleDocument = gql`
}
asset(id: $id) {
status
source {
... on ERC20 {
contractAddress
}
}
}
}
`;
@@ -65,6 +70,11 @@ export const ExplorerUpdateAssetSignatureBundleDocument = gql`
}
asset(id: $id) {
status
source {
... on ERC20 {
contractAddress
}
}
}
}
`;
@@ -110,7 +110,9 @@ export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
return (
<div className="float-left mr-3">
<Tooltip description={<p>{label}</p>}>
<Icon name={icon} />
<div>
<Icon name={icon} />
</div>
</Tooltip>
</div>
);
@@ -1,10 +1,12 @@
import { Loader } from '@vegaprotocol/ui-toolkit';
import type { ProposalTerms } from '../tx-proposal';
import { BundleError } from './signature-bundle/bundle-error';
import { BundleExists } from './signature-bundle/bundle-exists';
import { useExplorerNewAssetSignatureBundleQuery } from './__generated__/SignatureBundle';
export interface ProposalSignatureBundleByTypeProps {
id: string;
tx?: ProposalTerms['newAsset'] | ProposalTerms['updateAsset'];
}
/**
@@ -16,6 +18,7 @@ export interface ProposalSignatureBundleByTypeProps {
*/
export const ProposalSignatureBundleNewAsset = ({
id,
tx,
}: ProposalSignatureBundleByTypeProps) => {
const { data, error, loading } = useExplorerNewAssetSignatureBundleQuery({
variables: {
@@ -24,7 +27,20 @@ export const ProposalSignatureBundleNewAsset = ({
});
if (loading) {
return <Loader />;
return (
<div className="w-auto max-w-lg p-5 mt-5">
<Loader />
</div>
);
}
if (
!tx?.changes?.erc20 ||
!tx?.changes?.erc20 ||
!('contractAddress' in tx.changes.erc20) ||
tx.changes.erc20.contractAddress === undefined
) {
return null;
}
if (data?.erc20ListAssetBundle?.signatures) {
@@ -32,8 +48,10 @@ export const ProposalSignatureBundleNewAsset = ({
<BundleExists
signatures={data.erc20ListAssetBundle.signatures}
nonce={data.erc20ListAssetBundle.nonce}
assetAddress={tx.changes.erc20.contractAddress}
status={data.asset?.status}
proposalId={id}
tx={tx}
/>
);
} else {
@@ -13,6 +13,7 @@ import { useExplorerUpdateAssetSignatureBundleQuery } from './__generated__/Sign
*/
export const ProposalSignatureBundleUpdateAsset = ({
id,
tx,
}: ProposalSignatureBundleByTypeProps) => {
const { data, error, loading } = useExplorerUpdateAssetSignatureBundleQuery({
variables: {
@@ -24,11 +25,16 @@ export const ProposalSignatureBundleUpdateAsset = ({
return <Loader />;
}
if (data?.asset?.source?.__typename !== 'ERC20') {
return null;
}
if (data?.erc20SetAssetLimitsBundle?.signatures) {
return (
<BundleExists
signatures={data.erc20SetAssetLimitsBundle.signatures}
nonce={data.erc20SetAssetLimitsBundle.nonce}
assetAddress={data.asset.source.contractAddress}
status={data.asset?.status}
proposalId={id}
/>
@@ -1,31 +0,0 @@
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,17 @@
query ExplorerBundleSigners {
networkParameter(key: "blockchains.ethereumConfig") {
value
}
nodesConnection(pagination: { first: 25 }) {
edges {
node {
id
name
status
ethereumAddress
pubkey
tmPubkey
}
}
}
}
@@ -0,0 +1,57 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerBundleSignersQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerBundleSignersQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, status: Types.NodeStatus, ethereumAddress: string, pubkey: string, tmPubkey: string } } | null> | null } };
export const ExplorerBundleSignersDocument = gql`
query ExplorerBundleSigners {
networkParameter(key: "blockchains.ethereumConfig") {
value
}
nodesConnection(pagination: {first: 25}) {
edges {
node {
id
name
status
ethereumAddress
pubkey
tmPubkey
}
}
}
}
`;
/**
* __useExplorerBundleSignersQuery__
*
* To run a query within a React component, call `useExplorerBundleSignersQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerBundleSignersQuery` 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 } = useExplorerBundleSignersQuery({
* variables: {
* },
* });
*/
export function useExplorerBundleSignersQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerBundleSignersQuery, ExplorerBundleSignersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerBundleSignersQuery, ExplorerBundleSignersQueryVariables>(ExplorerBundleSignersDocument, options);
}
export function useExplorerBundleSignersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerBundleSignersQuery, ExplorerBundleSignersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerBundleSignersQuery, ExplorerBundleSignersQueryVariables>(ExplorerBundleSignersDocument, options);
}
export type ExplorerBundleSignersQueryHookResult = ReturnType<typeof useExplorerBundleSignersQuery>;
export type ExplorerBundleSignersLazyQueryHookResult = ReturnType<typeof useExplorerBundleSignersLazyQuery>;
export type ExplorerBundleSignersQueryResult = Apollo.QueryResult<ExplorerBundleSignersQuery, ExplorerBundleSignersQueryVariables>;
@@ -8,14 +8,16 @@ import { BundleError } from './bundle-error';
describe('Bundle Error', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
];
const NOT_SHOWN_STATUS: AssetStatus[] = [
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',
it.each(NOT_SHOWN_STATUS)(
'does not render for proposed or rejected bundles',
(status) => {
const screen = render(
<MemoryRouter>
@@ -28,7 +30,24 @@ describe('Bundle Error', () => {
</MemoryRouter>
);
expect(screen.getByText('test-error-message')).toBeInTheDocument();
expect(screen.container).toBeEmptyDOMElement();
}
);
it.each(NON_ENABLED_STATUS)(
'shows the apollo error in a syntax highlighter 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('No signature bundle')).toBeInTheDocument();
}
);
@@ -43,7 +62,7 @@ describe('Bundle Error', () => {
</MemoryRouter>
);
expect(screen.getByText('No bundle for proposal ID')).toBeInTheDocument();
expect(screen.getByText('No signature bundle')).toBeInTheDocument();
}
);
@@ -2,8 +2,8 @@ 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';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
export interface BundleErrorProps {
status?: AssetStatus;
@@ -17,18 +17,33 @@ export interface BundleErrorProps {
* the status - if it's already enabled, pretend this isn't an error
*/
export const BundleError = ({ status, error }: BundleErrorProps) => {
if (!status || status === 'STATUS_PROPOSED' || status === 'STATUS_REJECTED') {
// If there is no status, there is no asset and no bundle - ProposalDetails will make it clear why.
// If the asset exists but is just proposed, or rejected, there won't be a signature bundle yet
return null;
}
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>
<h1 className="text-xl pb-1">{t('No signature bundle')}</h1>
<p>
<p className="my-4">
{t(
'No signature bundle was generated as a result of this proposal, or the signature bundle could not be found.'
)}
</p>
<div>
{status === 'STATUS_ENABLED' ? (
t('Asset already enabled')
) : (
<Hash text={error ? error.message : t('No bundle for proposal ID')} />
<details>
<summary>{t('Show server error message')}</summary>
<SyntaxHighlighter data={error} size="smaller" />
</details>
)}
</p>
</div>
</div>
);
};
@@ -32,6 +32,7 @@ describe('Bundle Exists', () => {
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
assetAddress={'0x123413423'}
status={status}
/>
</MockedProvider>
@@ -52,6 +53,7 @@ describe('Bundle Exists', () => {
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
assetAddress={'0x123413423'}
status={status}
/>
</MockedProvider>
@@ -1,14 +1,17 @@
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';
import type { AssetStatus } from '@vegaprotocol/types';
import type { ProposalTerms } from '../../tx-proposal';
import { BundleSigners } from './bundle-signers';
export interface BundleExistsProps {
signatures: string;
nonce: string;
status?: AssetStatus;
assetAddress: string;
proposalId: string;
tx?: ProposalTerms['newAsset'] | ProposalTerms['updateAsset'];
}
/**
@@ -21,7 +24,11 @@ export const BundleExists = ({
nonce,
status,
proposalId,
assetAddress,
tx,
}: BundleExistsProps) => {
// Note if this is wrong, the wrong decoder will be used which will give incorrect data
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<IconForBundleStatus status={status} />
@@ -31,7 +38,42 @@ export const BundleExists = ({
: t('Signature bundle generated')}
</h1>
<ProposalSignatureBundleDetails signatures={signatures} nonce={nonce} />
<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>
<BundleSigners
signatures={signatures}
nonce={nonce}
tx={tx}
id={proposalId}
assetAddress={assetAddress}
/>
{status !== 'STATUS_ENABLED' ? (
<p className="mt-5">
@@ -1,33 +1,34 @@
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { IconForBundleStatus } from './bundle-icon';
import { getIcon } from './bundle-icon';
describe('Bundle status icon', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ERROR_STATUS: AssetStatus[] = [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/);
expect(getIcon(status)).toEqual('clean');
}
);
it.each(ERROR_STATUS)(
'show an error icon if the bundle is unavailable',
(status) => {
expect(getIcon(status)).toEqual('disable');
}
);
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/);
expect(getIcon(status)).toEqual('tick-circle');
}
);
});
@@ -12,6 +12,26 @@ export interface IconForBundleStatusProps {
* 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} />;
const i = getIcon(status);
return (
<Icon
className="float-left mt-2 mr-3"
name={i}
data-testid={i}
ariaLabel={status}
/>
);
};
export function getIcon(status?: AssetStatus): IconName {
switch (status) {
case 'STATUS_ENABLED':
return 'tick-circle';
case undefined:
case 'STATUS_REJECTED':
return 'disable';
default:
return 'clean';
}
}
@@ -0,0 +1,94 @@
import type { EncodeListAssetParameters } from '../../../../../lib/encoders/abis/list-asset';
import type { BridgeFunction } from './bundle-signers';
import {
getBridgeAddressFromNetworkParameter,
getSigners,
} from './bundle-signers';
describe('Bundle Signers helpers', () => {
it('getBridgeAddressFromNetworkParameter handles invalid json', () => {
expect(getBridgeAddressFromNetworkParameter('hi')).toEqual(null);
expect(getBridgeAddressFromNetworkParameter('{hi]')).toEqual(null);
expect(getBridgeAddressFromNetworkParameter('{"hi"}')).toEqual(null);
expect(
getBridgeAddressFromNetworkParameter(false as unknown as string)
).toEqual(null);
});
it('getBridgeAddressFromNetworkParameter returns null if bridge adderss is not in expected place', () => {
expect(
getBridgeAddressFromNetworkParameter(`{
"NetworkParamter": false
}`)
).toEqual(null);
expect(
getBridgeAddressFromNetworkParameter(`{
"network_id": "11155111",
"chain_id": "11155111",
"confirmations": 3,
"staking_bridge_contract": {
"address": "0xFFb0A0d4806502ceF491aF1141f66669A1Bd0D03",
"deployment_block_height": 2011705
},
"token_vesting_contract": {
"address": "0x680fF88252FA7071CAce7398e77872d54D781d0B",
"deployment_block_height": 2011709
},
"multisig_control_contract": {
"address": "0x6eBc32d66277D94DB8FF2ccF86E36f37F29a52D3",
"deployment_block_height": 2011699
}
}`)
).toEqual(null);
});
it('getBridgeAddressFromNetworkParameter returns address if the collateral_bridge_contract has an address', () => {
expect(
getBridgeAddressFromNetworkParameter(`{
"network_id": "11155111",
"chain_id": "11155111",
"confirmations": 3,
"collateral_bridge_contract": {
"address": "0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799"
},
"staking_bridge_contract": {
"address": "0xFFb0A0d4806502ceF491aF1141f66669A1Bd0D03",
"deployment_block_height": 2011705
},
"token_vesting_contract": {
"address": "0x680fF88252FA7071CAce7398e77872d54D781d0B",
"deployment_block_height": 2011709
},
"multisig_control_contract": {
"address": "0x6eBc32d66277D94DB8FF2ccF86E36f37F29a52D3",
"deployment_block_height": 2011699
}
}`)
).toEqual('0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799');
});
it('getSigners to return [] in the case of bad inputs', () => {
expect(
getSigners('list_asset', '123', '', {
assetERC20: '123',
assetId: '456',
limit: 'bad',
threshold: 'data',
nonce: 'here',
})
).toEqual([]);
expect(
getSigners('nothing' as unknown as BridgeFunction, '123', '', {
nonce: 'here',
} as unknown as EncodeListAssetParameters)
).toEqual([]);
expect(
getSigners('set_asset_limits', '0x123', '0x456', {
nonce: 'here',
} as unknown as EncodeListAssetParameters)
).toEqual([]);
});
});
@@ -0,0 +1,197 @@
import { encodeListAssetBridgeTx } from '../../../../../lib/encoders/abis/list-asset';
import { recoverAddress } from 'ethers/lib/utils';
import { useExplorerBundleSignersQuery } from './__generated__/BundleSigners';
import type { ProposalTerms } from '../../tx-proposal';
import { DApp, TOKEN_VALIDATOR, useLinks } from '@vegaprotocol/environment';
import { ExternalLink, Icon } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { IconNames } from '@blueprintjs/icons';
import { encodeUpdateAssetBridgeTx } from '../../../../../lib/encoders/abis/update-asset';
import { prepend0x } from '@vegaprotocol/smart-contracts';
import type { EncodeListAssetParameters } from '../../../../../lib/encoders/abis/list-asset';
import omit from 'lodash/omit';
export type BridgeFunction = 'list_asset' | 'set_asset_limits';
export interface BundleSignersProps {
signatures: string;
assetAddress: string;
nonce: string;
tx?: ProposalTerms['updateAsset'] | ProposalTerms['newAsset'];
id: string;
}
/**
* A logic-heavy component that takes in a signature bundle and returns
* the list of validators that signed the bundle. To do this it requires
* data from quite a few places - a network parameter, the signature bundle,
* the asset that has been modified
*/
export const BundleSigners = ({
signatures,
nonce,
assetAddress,
tx,
id,
}: BundleSignersProps) => {
const tokenLink = useLinks(DApp.Token);
const bridgeFunction: BridgeFunction =
tx?.changes?.erc20 && 'contractAddress' in tx.changes.erc20
? 'list_asset'
: 'set_asset_limits';
const { data } = useExplorerBundleSignersQuery();
const bridgeAddress = getBridgeAddressFromNetworkParameter(
data?.networkParameter?.value
);
const allEthereumKeys =
data?.nodesConnection?.edges
?.filter((n) => n?.node.status === 'NODE_STATUS_VALIDATOR')
.map((s) => s?.node) || [];
if (!tx || !tx.changes?.erc20) {
return null;
}
const { lifetimeLimit, withdrawThreshold } = tx.changes.erc20;
if (
!id ||
allEthereumKeys.length === 0 ||
!bridgeAddress ||
!lifetimeLimit ||
!withdrawThreshold
) {
return null;
}
const signersLowerCase = getSigners(
bridgeFunction,
bridgeAddress,
signatures,
{
assetERC20: assetAddress,
assetId: prepend0x(id),
limit: lifetimeLimit,
threshold: withdrawThreshold,
nonce,
}
);
return (
<>
<h2 className="mt-4 mb-2 text-lg">{t('Signed by validators')}</h2>
<ul>
{allEthereumKeys?.map((n) => {
if (!n) {
return null;
}
const validatorPage = tokenLink(TOKEN_VALIDATOR.replace(':id', n.id));
return signersLowerCase?.indexOf(
n?.ethereumAddress.toLowerCase() || '??'
) !== -1 ? (
<li key={n?.pubkey}>
<ExternalLink href={validatorPage}>
<Icon name={IconNames.ENDORSED} className="ml-1 mr-2" />
{n?.name}
<Icon size={3} name={IconNames.SHARE} className="ml-2" />
</ExternalLink>
</li>
) : (
<li>
<ExternalLink href={validatorPage}>
<Icon name={IconNames.MINUS} className="ml-1 mr-2" />
{n?.name}
<Icon size={3} name={IconNames.SHARE} className="ml-2" />
</ExternalLink>
</li>
);
})}
</ul>
</>
);
};
/**
* Given all of the collated information, this function creates an equivalent unsigned bundle
* and recovers the signers from it, In the case of an error, it returns an empty array.
*
* @param bridgeFunction Decides which data goes in to the digest
* @param bridgeAddress ERC20 bridge address
* @param signatures Long string of signatures
* @param params The object containing all data that the bridge requires for New or Updating assets
* @returns String[] Empty if there was an error or no signers were recovered, otherwise lowercased ETH addresses
*/
export function getSigners(
bridgeFunction: BridgeFunction,
bridgeAddress: string,
signatures: string,
params: EncodeListAssetParameters
): string[] {
try {
if (bridgeFunction === 'list_asset') {
const digest = encodeListAssetBridgeTx(params, bridgeAddress);
// Recover Address from digest can return null, which is handled as an empty array
return recoverAddressesFromDigest(digest, signatures) || [];
} else {
// The params bundles are so similar, rather than force the component to make two different
// styles, just delete the one different property
const p = omit(params, 'assetId');
const digest = encodeUpdateAssetBridgeTx(p, bridgeAddress);
return recoverAddressesFromDigest(digest, signatures) || [];
}
} catch (e) {
// In the worst case, no signing addresses are recovered. This means that all nodes will
// be rendered as if they had not signed the bundle.
return [];
}
}
/**
* Querying for the network parameter value gets us all of the contract details for this network
* encoded as a JSON object. This function pulls out the address for the bridge, or returns null
* in any of the many cases where it may fail
*
* @param networkParameterAsString the stringified JSON object
* @returns null or bridge address as a string
*/
export function getBridgeAddressFromNetworkParameter(
networkParameterAsString: string | undefined
): string | null {
if (!networkParameterAsString) {
return null;
}
try {
const networkParameter = JSON.parse(networkParameterAsString);
return networkParameter.collateral_bridge_contract.address;
} catch (e) {
// There is no good recovery state so return null
return null;
}
}
export function recoverAddressesFromDigest(
digest: string,
unprefixedBundle: string
) {
// Remove 0x from bundle, then split it in to signatures
const sigs = unprefixedBundle.substring(2).match(/.{1,130}/g);
// Convert each of the signatures from hex to a string
const hexSigs = sigs?.map((s) => `0x${s.toString()}`);
if (!hexSigs) {
return null;
}
// toLowerCase is a hack - something somewhere is lowercasing some
// pubkeys
return hexSigs.map((h) => recoverAddress(digest, h).toLowerCase());
}
@@ -1,41 +0,0 @@
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>
);
};
@@ -61,7 +61,7 @@ export const ProposalSummary = ({
{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">
<div className="pt-2 text-sm leading-tight">
<ReactMarkdown
className="react-markdown-container"
skipHtml={true}
@@ -70,15 +70,15 @@ export const ProposalSummary = ({
>
{md}
</ReactMarkdown>
</p>
</div>
)}
<p className="pt-5">
<div 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>
</div>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
@@ -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}
</>
);
};
@@ -7,8 +7,9 @@ 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';
import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new';
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -78,6 +79,12 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
deterministicId = txSignatureToDeterministicId(sig);
}
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
const SignatureBundleComponent = proposal.terms?.newAsset
? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset;
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -105,10 +112,7 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
terms={proposal?.terms}
/>
{proposalRequiresSignatureBundle(proposal) && (
<ProposalSignatureBundle
id={deterministicId}
type={proposal.terms?.newAsset ? 'NewAsset' : 'UpdateAsset'}
/>
<SignatureBundleComponent id={deterministicId} tx={tx} />
)}
</>
);
@@ -0,0 +1,2 @@
// The subset of ABI types that we use in relevant types
export type AbiType = 'address' | 'bytes' | 'bytes32' | 'uint256' | 'string';
@@ -0,0 +1,31 @@
import { keccak256, defaultAbiCoder, isAddress } from 'ethers/lib/utils';
import type { AbiType } from './abi-types';
export const BRIDGE_COMMAND: AbiType[] = [
// The abi encoded bytes of the message
'bytes',
// The address of the bridge
'address',
];
/**
* ABI encode values for a bridge call, getting back its digest
*
* @param bytes The packed bytes of the command for the bridge
* @param address the Ethereum address of the ERC20 bridge
* @param raw defaults to false. If set, does not keccak256 the output
*/
export function encodeBridgeCommand(
bytes: string,
address: string,
raw = false
) {
if (!isAddress(address)) {
throw new Error('Bridge address must be a hex value');
}
const values = [bytes, address];
const value = defaultAbiCoder.encode(BRIDGE_COMMAND, values);
return raw === true ? value : keccak256(value);
}
@@ -0,0 +1,31 @@
import { encodeBridgeCommand } from './bridge-command';
describe('Bridge command encoder', () => {
const VALID_BYTES =
'0x000000000000000000000000b063f5504610ba4b8db230d9f884bfadc1e31da00b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b3790000000000000000000000000000000000000000000000487a9a30453944000000000000000000000000000000000000000000000000000000000000000000010b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b37900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000a6c6973745f617373657400000000000000000000000000000000000000000000';
const VALID_ADDRESS = '0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799';
it('rejects non valid bridge addresses', () => {
expect(() => {
encodeBridgeCommand(VALID_BYTES, '456789');
}).toThrowError('Bridge address must be a hex value');
});
it('throws if the bytes are not bytes-like', () => {
expect(() => {
encodeBridgeCommand('hello', VALID_ADDRESS);
}).toThrowError(/invalid/);
});
it('keccac256s the value by default', () => {
const res = encodeBridgeCommand(VALID_BYTES, VALID_ADDRESS);
// Magic number: Known output, including 0x
expect(res.length).toEqual(66);
});
it('Does not keccac256 the value if third param is set', () => {
const res = encodeBridgeCommand(VALID_BYTES, VALID_ADDRESS, true);
// Magic number: Known output
expect(res.length).toEqual(706);
});
});
@@ -0,0 +1,76 @@
import { encodeListAsset, encodeListAssetBridgeTx } from './list-asset';
describe('List Asset ABI encoder', () => {
it('throws if asset erc20 address is invalid', () => {
expect(() => {
encodeListAsset({
assetERC20: '123',
assetId: '0x456',
limit: '1',
threshold: '1',
nonce: '1',
});
}).toThrowError('Asset ERC20 and assetID must be hex values');
});
it('throws if assetId is not hex encoded', () => {
expect(() => {
encodeListAsset({
assetERC20: '0x123',
assetId: '456',
limit: '1',
threshold: '1',
nonce: '1',
});
}).toThrowError('Asset ERC20 and assetID must be hex values');
});
it('throws if values to not match expected format', () => {
expect(() => {
encodeListAsset({
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
assetId: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
limit: 'not a valid number',
threshold: '1',
nonce: '1',
});
}).toThrowError(/incorrect data length/);
});
it('returns an ABI encoded value if inputs are valid', () => {
const EXPECTED_OUTPUT =
'0x000000000000000000000000b063f5504610ba4b8db230d9f884bfadc1e31da00b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b37900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000a6c6973745f617373657400000000000000000000000000000000000000000000';
const res = encodeListAsset({
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
assetId:
'0x0b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b379',
limit: '1',
threshold: '1',
nonce: '1',
});
expect(res).toEqual(EXPECTED_OUTPUT);
});
it('encodeListAssetBridge returns a keccak256 hash of the bridge tx', () => {
const EXPECTED_OUTPUT =
'0xe0e62b27fe4490025d312bb2e37486f56935a3d9442dc34c2b918b2a28a386f2';
const res = encodeListAssetBridgeTx(
{
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
assetId:
'0x0b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b379',
limit: '1',
threshold: '1',
nonce: '1',
},
'0xb063f5504610ba4b8db230d9f884bfadc1e31da0'
);
// Magic number: keccak256 hash length + '0x'
expect(res.length).toEqual(66);
expect(res).toEqual(EXPECTED_OUTPUT);
});
});
@@ -0,0 +1,76 @@
import { defaultAbiCoder, isAddress, isHexString } from 'ethers/lib/utils';
import { encodeBridgeCommand } from './bridge-command';
import type { AbiType } from './abi-types';
export const METHOD_NAME = 'list_asset';
export const LIST_ASSET_ABI: AbiType[] = [
// Asset address
'address',
// Asset ID on Vega
'bytes32',
// Lifetime limit
'uint256',
// Withdraw threshold
'uint256',
// Nonce
'uint256',
// Contract method name
'string',
];
export interface EncodeListAssetParameters {
// The ETH address of the ERC20 asset
assetERC20: string;
// The Vega ID of the asset, 0x prefixed
assetId: string;
// The number as a string of the asset
limit: string;
// THe number-as-a-string of the withdraw threshold
threshold: string;
// The n-once supplied to the contract
nonce: string;
}
/**
* Generates an ABI encoded function call to list an asset. This is
* used in the Signature Bundle view on some proposals to recover
* which validators signed a multisig bundle. It does this by recovering
* the ERC20 addresses of the signers, then comparing those to the list
* of signers on the bundle. In order to do this, we recreate the signed
* data from the values we know from the transaction. That last part
* is what this function does.
*
* @param EncodeListAssetParameters The arguments for the ABI call
* @returns string encoded message
*/
export function encodeListAsset({
assetERC20,
assetId,
limit,
threshold,
nonce,
}: EncodeListAssetParameters) {
if (!isAddress(assetERC20) || !isHexString(assetId)) {
throw new Error('Asset ERC20 and assetID must be hex values');
}
const values = [assetERC20, assetId, limit, threshold, nonce, METHOD_NAME];
return defaultAbiCoder.encode(LIST_ASSET_ABI, values);
}
/**
* Convenience function that encodes and packs the message as it is encoded by the
* validators in a multisig bundle
*
* @param params Parameters for the List Asset call
* @param bridgeAddress Bridge address for the appropiate network
* @returns keccak256 encoded message digest
*/
export function encodeListAssetBridgeTx(
params: EncodeListAssetParameters,
bridgeAddress: string
) {
return encodeBridgeCommand(encodeListAsset(params), bridgeAddress);
}
@@ -0,0 +1,58 @@
import { encodeUpdateAsset, encodeUpdateAssetBridgeTx } from './update-asset';
describe('Update Asset ABI encoder', () => {
it('throws if asset erc20 address is invalid', () => {
expect(() => {
encodeUpdateAsset({
assetERC20: '123',
limit: '1',
threshold: '1',
nonce: '1',
});
}).toThrowError('Asset ERC20 must be a valid address');
});
it('throws if an input is invalid', () => {
expect(() => {
encodeUpdateAsset({
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
limit: 'hello',
threshold: '1',
nonce: '1',
});
}).toThrowError(/invalid BigNumber/);
});
it('returns an ABI encoded value if inputs are valid', () => {
const EXPECTED_OUTPUT =
'0x000000000000000000000000b063f5504610ba4b8db230d9f884bfadc1e31da000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000107365745f61737365745f6c696d69747300000000000000000000000000000000';
const res = encodeUpdateAsset({
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
limit: '1',
threshold: '1',
nonce: '1',
});
expect(res).toEqual(EXPECTED_OUTPUT);
});
it('encodeUpdateAssetBridge returns a keccak256 hash of the bridge tx', () => {
const EXPECTED_OUTPUT =
'0xeb240131c4558aebfab3da0ddbea1ac0447b9f5670899af2d78795867631d877';
const res = encodeUpdateAssetBridgeTx(
{
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
limit: '1',
threshold: '1',
nonce: '1',
},
'0xb063f5504610ba4b8db230d9f884bfadc1e31da0'
);
// Magic number: keccak256 hash length + '0x'
expect(res.length).toEqual(66);
expect(res).toEqual(EXPECTED_OUTPUT);
});
});
@@ -0,0 +1,65 @@
import { defaultAbiCoder, isAddress } from 'ethers/lib/utils';
import { encodeBridgeCommand } from './bridge-command';
import type { AbiType } from './abi-types';
export const METHOD_NAME = 'set_asset_limits';
export const LIST_ASSET_ABI: AbiType[] = [
// Asset address
'address',
// Lifetime limit
'uint256',
// Withdraw threshold
'uint256',
// Nonce
'uint256',
// Contract method name
'string',
];
export interface EncodeUpdateAssetParameters {
// The ETH address of the ERC20 asset
assetERC20: string;
// The number as a string of the asset
limit: string;
// THe number-as-a-string of the withdraw threshold
threshold: string;
// The n-once supplied to the contract
nonce: string;
}
/**
* Generates an ABI encoded function call to list an asset
*
* @param EncodeListAssetParameters The arguments for the ABI call
* @returns string encoded message
*/
export function encodeUpdateAsset({
assetERC20,
limit,
threshold,
nonce,
}: EncodeUpdateAssetParameters) {
if (!isAddress(assetERC20)) {
throw new Error('Asset ERC20 must be a valid address');
}
const values = [assetERC20, limit, threshold, nonce, METHOD_NAME];
return defaultAbiCoder.encode(LIST_ASSET_ABI, values);
}
/**
* Convenience function that encodes and packs the message as it is encoded by the
* validators in a multisig bundle
*
* @param params Parameters for the List Asset call
* @param bridgeAddress Bridge address for the appropiate network
* @returns keccak256 encoded message digest
*/
export function encodeUpdateAssetBridgeTx(
params: EncodeUpdateAssetParameters,
bridgeAddress: string
) {
return encodeBridgeCommand(encodeUpdateAsset(params), bridgeAddress);
}
+8 -3
View File
@@ -1,14 +1,19 @@
import { StatsManager } from '@vegaprotocol/network-stats';
import { SearchForm } from '../../components/search';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Home = () => {
const classnames = 'mt-4 grid grid-cols-1 lg:grid-cols-2 lg:gap-4';
const classnames = 'mt-4 mb-4';
useDocumentTitle();
return (
<section>
<StatsManager className={classnames} />
<div className="p-20 max-sm:py-10 max-sm:px-0">
<SearchForm />
</div>
<div className="px-20 max-sm:px-0">
<StatsManager className={classnames} />
</div>
</section>
);
};
-26
View File
@@ -1,26 +0,0 @@
import React from 'react';
import { useRoutes } from 'react-router-dom';
import { RouteErrorBoundary } from '../components/router-error-boundary';
import routerConfig from './router-config';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
export interface RouteChildProps {
name: string;
}
export const AppRouter = () => {
const routes = useRoutes(routerConfig);
const splashLoading = (
<Splash>
<Loader />
</Splash>
);
return (
<RouteErrorBoundary>
<React.Suspense fallback={splashLoading}>{routes}</React.Suspense>
</RouteErrorBoundary>
);
};
+187
View File
@@ -0,0 +1,187 @@
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import {
AnnouncementBanner,
BackgroundVideo,
BreadcrumbsContainer,
ButtonLink,
ExternalLink,
Icon,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useState } from 'react';
import {
isRouteErrorResponse,
Link,
Outlet,
useMatch,
useRouteError,
} from 'react-router-dom';
import { Footer } from '../components/footer/footer';
import { Header } from '../components/header';
import { Routes } from './route-names';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
asJson={asJson}
open={isOpen}
onChange={setOpen}
/>
);
};
const MainnetSimAd = () => {
const [shouldDisplayBanner, setShouldDisplayBanner] = useState<boolean>(true);
// Return an empty div so that the grid layout in _app.page.ts
// renders correctly
if (!shouldDisplayBanner) {
return <div />;
}
return (
<AnnouncementBanner>
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
<button
className="flex items-center"
onClick={() => setShouldDisplayBanner(false)}
>
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
</button>
<div>
<span className="pr-4">Mainnet sim 3 is live!</span>
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
</div>
</div>
</AnnouncementBanner>
);
};
export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME));
return (
<>
<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>
<MainnetSimAd />
<Header />
</div>
<div>
<main className="p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet />
</main>
</div>
<div>
<Footer />
</div>
</div>
<DialogsContainer />
</>
);
};
export const ErrorBoundary = () => {
const error = useRouteError();
const errorTitle = isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: t('Something went wrong');
const errorMessage = isRouteErrorResponse(error)
? error.error?.message
: (error as Error).message || JSON.stringify(error);
return (
<>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
{errorTitle}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto break-all font-mono">
{errorMessage}
</div>
<div>
<ButtonLink onClick={() => window.location.reload()}>
{t('Try refreshing')}
</ButtonLink>{' '}
{t('or go back to')}{' '}
<Link className="underline" to="/">
{t('Home')}
</Link>
</div>
</div>
</>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 56 85"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M41 0.5H3V60.5H41V0.5Z" fill="white" />
<path d="M15 18.5H13V20.5H15V18.5Z" fill="black" />
<path d="M17 20.5H15V22.5H17V20.5Z" fill="black" />
<path d="M19 18.5H17V20.5H19V18.5Z" fill="black" />
<path d="M15 22.5H13V24.5H15V22.5Z" fill="black" />
<path d="M19 22.5H17V24.5H19V22.5Z" fill="black" />
<path d="M29 28.5H15V30.5H29V28.5Z" fill="black" />
<path d="M27 18.5H25V20.5H27V18.5Z" fill="black" />
<path d="M29 20.5H27V22.5H29V20.5Z" fill="black" />
<path d="M31 18.5H29V20.5H31V18.5Z" fill="black" />
<path d="M27 22.5H25V24.5H27V22.5Z" fill="black" />
<path d="M31 22.5H29V24.5H31V22.5Z" fill="black" />
<path d="M31 26.5H29V28.5H31V26.5Z" fill="black" />
<path d="M19 60.5H17V84.5H19V60.5Z" fill="black" />
<path d="M27 60.5H25V84.5H27V60.5Z" fill="black" />
<path
d="M3 42.5V58.64V60.5V64.5H21V60.5H23V64.5H41V60.5V58.64V42.5H3Z"
fill="#FF077F"
/>
<path d="M35 46.5H41V42.5H3V46.5H31H35Z" fill="#CB0666" />
<path d="M3 32.32V29.5L0 32.5V60.5H2V33.33L3 32.32Z" fill="black" />
<path d="M41 31.8V29.49L54.79 21.53L55.79 23.26L41 31.8Z" fill="black" />
<path d="M36 54.5H35V55.5H36V54.5Z" fill="black" />
<path d="M35 53.5H34V54.5H35V53.5Z" fill="black" />
<path d="M34 48.5H33V53.5H34V48.5Z" fill="black" />
<path d="M38 48.5H37V52.5H38V48.5Z" fill="black" />
<path d="M37 53.5H36V54.5H37V53.5Z" fill="black" />
<path d="M39 52.5H38V53.5H39V52.5Z" fill="black" />
<path
d="M55.7901 23.27L53.0601 22.54L45.1001 8.75L46.8301 7.75L55.7901 23.27Z"
fill="black"
/>
</svg>
);
@@ -8,7 +8,7 @@ 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 { marketInfoProvider } from '@vegaprotocol/market-info';
import { marketInfoWithDataProvider } from '@vegaprotocol/market-info';
import { PageTitle } from '../../components/page-helpers/page-title';
export const MarketPage = () => {
@@ -17,7 +17,7 @@ export const MarketPage = () => {
const { marketId } = useParams<{ marketId: string }>();
const { data, loading, error } = useDataProvider({
dataProvider: marketInfoProvider,
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: marketId || '',
@@ -31,6 +31,7 @@ const PERCENTAGE_PARAMS = [
'governance.proposal.updateMarket.requiredParticipationLP',
'governance.proposal.updateNetParam.requiredMajority',
'governance.proposal.updateNetParam.requiredParticipation',
'governance.proposal.updateMarket.minProposerEquityLikeShare',
'validators.vote.required',
];
+205 -76
View File
@@ -8,7 +8,6 @@ import { Oracle } from './oracles/id';
import Party from './parties';
import { Parties } from './parties/home';
import { Party as PartySingle } from './parties/id';
import Txs from './txs';
import { ValidatorsPage } from './validators';
import Genesis from './genesis';
import { Block } from './blocks/id';
@@ -20,19 +19,55 @@ import flags from '../config/flags';
import { t } from '@vegaprotocol/i18n';
import { Routes } from './route-names';
import { NetworkParameters } from './network-parameters';
import type { RouteObject } from 'react-router-dom';
import type { Params, RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { MarketPage, MarketsPage } from './markets';
import type { ReactNode } from 'react';
import { ErrorBoundary, Layout } from './layout';
import compact from 'lodash/compact';
import { AssetLink, MarketLink } from '../components/links';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { remove0x } from '@vegaprotocol/utils';
export type Navigable = { path: string; name: string; text: string };
type Route = RouteObject & Navigable;
export type Navigable = {
path: string;
handle: {
name: string;
text: string;
};
};
export const isNavigable = (item: RouteObject): item is Navigable =>
(item as Navigable).path !== undefined &&
(item as Navigable).handle !== undefined &&
(item as Navigable).handle.name !== undefined &&
(item as Navigable).handle.text !== undefined;
export type Breadcrumbable = {
handle: { breadcrumb: (data?: Params<string>) => ReactNode | string };
};
export const isBreadcrumbable = (item: RouteObject): item is Breadcrumbable =>
(item as Breadcrumbable).handle !== undefined &&
(item as Breadcrumbable).handle.breadcrumb !== undefined;
type RouteItem =
| RouteObject
| (RouteObject & Navigable)
| (RouteObject & Breadcrumbable);
type Route = RouteItem & {
children?: RouteItem[];
};
const partiesRoutes: Route[] = flags.parties
? [
{
path: Routes.PARTIES,
name: 'Parties',
text: t('Parties'),
element: <Party />,
handle: {
name: t('Parties'),
text: t('Parties'),
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
},
children: [
{
index: true,
@@ -41,6 +76,13 @@ const partiesRoutes: Route[] = flags.parties
{
path: ':party',
element: <PartySingle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
},
],
},
@@ -51,8 +93,11 @@ const assetsRoutes: Route[] = flags.assets
? [
{
path: Routes.ASSETS,
text: t('Assets'),
name: 'Assets',
handle: {
name: t('Assets'),
text: t('Assets'),
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
},
children: [
{
index: true,
@@ -61,6 +106,11 @@ const assetsRoutes: Route[] = flags.assets
{
path: ':assetId',
element: <AssetPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<AssetLink assetId={params.assetId as string} />
),
},
},
],
},
@@ -71,8 +121,13 @@ const genesisRoutes: Route[] = flags.genesis
? [
{
path: Routes.GENESIS,
name: 'Genesis',
text: t('Genesis Parameters'),
handle: {
name: t('Genesis'),
text: t('Genesis Parameters'),
breadcrumb: () => (
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
),
},
element: <Genesis />,
},
]
@@ -82,8 +137,13 @@ const governanceRoutes: Route[] = flags.governance
? [
{
path: Routes.GOVERNANCE,
name: 'Governance proposals',
text: t('Governance Proposals'),
handle: {
name: t('Governance proposals'),
text: t('Governance Proposals'),
breadcrumb: () => (
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
),
},
element: <Proposals />,
},
]
@@ -93,8 +153,11 @@ const marketsRoutes: Route[] = flags.markets
? [
{
path: Routes.MARKETS,
name: 'Markets',
text: t('Markets'),
handle: {
name: t('Markets'),
text: t('Markets'),
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
},
children: [
{
index: true,
@@ -103,6 +166,11 @@ const marketsRoutes: Route[] = flags.markets
{
path: ':marketId',
element: <MarketPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<MarketLink id={params.marketId as string} />
),
},
},
],
},
@@ -113,90 +181,151 @@ const networkParametersRoutes: Route[] = flags.networkParameters
? [
{
path: Routes.NETWORK_PARAMETERS,
name: 'NetworkParameters',
text: t('Network Parameters'),
handle: {
name: t('NetworkParameters'),
text: t('Network Parameters'),
breadcrumb: () => (
<Link to={Routes.NETWORK_PARAMETERS}>
{t('Network Parameters')}
</Link>
),
},
element: <NetworkParameters />,
},
]
: [];
const validators: Route[] = flags.validators
? [
{
path: Routes.VALIDATORS,
name: 'Validators',
text: t('Validators'),
handle: {
name: t('Validators'),
text: t('Validators'),
breadcrumb: () => (
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
),
},
element: <ValidatorsPage />,
},
]
: [];
const routerConfig: Route[] = [
const linkTo = (...segments: (string | undefined)[]) =>
compact(segments).join('/');
export const routerConfig: Route[] = [
{
path: Routes.HOME,
name: 'Home',
text: t('Home'),
element: <Home />,
index: true,
},
{
path: Routes.TX,
name: 'Txs',
text: t('Transactions'),
element: <Txs />,
children: [
{
path: 'pending',
element: <PendingTxs />,
},
{
path: ':txHash',
element: <Tx />,
},
{
index: true,
element: <TxsList />,
},
],
},
{
path: Routes.BLOCKS,
name: 'Blocks',
text: t('Blocks'),
element: <BlockPage />,
element: <Layout />,
handle: {
name: t('Home'),
text: t('Home'),
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
},
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Blocks />,
element: <Home />,
},
{
path: ':block',
element: <Block />,
path: Routes.TX,
handle: {
name: t('Txs'),
text: t('Transactions'),
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
},
children: [
{
path: 'pending',
element: <PendingTxs />,
handle: {
breadcrumb: () => (
<Link to={linkTo(Routes.TX, 'pending')}>
{t('Pending transactions')}
</Link>
),
},
},
{
path: ':txHash',
element: <Tx />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.TX, params.txHash)}>
{truncateMiddle(remove0x(params.txHash as string))}
</Link>
),
},
},
{
index: true,
element: <TxsList />,
},
],
},
{
path: Routes.BLOCKS,
handle: {
name: t('Blocks'),
text: t('Blocks'),
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
},
element: <BlockPage />,
children: [
{
index: true,
element: <Blocks />,
},
{
path: ':block',
element: <Block />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.BLOCKS, params.block)}>
{params.block}
</Link>
),
},
},
],
},
{
path: Routes.ORACLES,
handle: {
name: t('Oracles'),
text: t('Oracles'),
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
},
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.ORACLES, params.id)}>
{truncateMiddle(params.id as string)}
</Link>
),
},
},
],
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
],
},
{
path: Routes.ORACLES,
name: 'Oracles',
text: t('Oracles'),
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
},
],
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
];
export default routerConfig;
export const router = createBrowserRouter(routerConfig);
+3 -16
View File
@@ -1,5 +1,4 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { remove0x } from '@vegaprotocol/utils';
import { useFetch } from '@vegaprotocol/react-helpers';
import { DATA_SOURCES } from '../../../config';
@@ -8,9 +7,6 @@ import { TxDetails } from './tx-details';
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
import { toNonHex } from '../../../components/search/detect-search';
import { PageHeader } from '../../../components/page-header';
import { Routes } from '../../../routes/route-names';
import { IconNames } from '@blueprintjs/icons';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const Tx = () => {
@@ -22,6 +18,7 @@ const Tx = () => {
const {
state: { data, loading, error },
refetch,
} = useFetch<BlockExplorerTransaction>(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(hash)}`
);
@@ -34,17 +31,6 @@ const Tx = () => {
return (
<section>
<Link
className="font-normal underline underline-offset-4 block mb-5"
to={`/${Routes.TX}`}
>
<Icon
className="text-vega-light-150 dark:text-vega-light-150"
name={IconNames.CHEVRON_LEFT}
/>
All Transactions
</Link>
<PageHeader
title="transaction"
truncateStart={5}
@@ -56,6 +42,7 @@ const Tx = () => {
error={error}
loading={loading}
errorMessage={errorMessage}
refetch={refetch}
>
<TxDetails
className="mb-28"
@@ -21,7 +21,7 @@ import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { PageTitle } from '../../components/page-helpers/page-title';
import BigNumber from 'bignumber.js';
import {
ContractAddressLink,
EtherscanLink,
DApp,
TOKEN_VALIDATOR,
useLinks,
@@ -176,25 +176,14 @@ export const ValidatorsPage = () => {
const validatorName =
v.name && v.name.length > 0 ? v.name : truncateMiddle(v.id);
return (
<li className="mb-5" key={v.id}>
<li className="mb-5 relative" key={v.id}>
<div
data-testid="validator-tile"
validator-id={v.id}
className="border border-vega-light-200 dark:border-vega-dark-200 rounded p-2 overflow-hidden relative flex gap-2 items-start justify-between"
className="border border-vega-light-200 dark:border-vega-dark-200 rounded p-2 overflow-hidden"
>
{v.avatarUrl && (
<div className="w-20">
<ExternalLink href={validatorPage}>
<img
className="w-full"
src={v.avatarUrl}
alt={validatorName}
/>
</ExternalLink>
</div>
)}
<div className="w-full">
<h2 className="font-alpha text-2xl">
<h2 className="font-alpha text-2xl leading-[60px]">
<ExternalLink href={validatorPage}>
{validatorName}
</ExternalLink>
@@ -235,7 +224,7 @@ export const ValidatorsPage = () => {
<KeyValueTableRow>
<div>{t('Ethereum address')}</div>
<div className="break-all text-xs">
<ContractAddressLink address={v.ethereumAddress} />{' '}
<EtherscanLink address={v.ethereumAddress} />{' '}
<CopyWithTooltip text={v.ethereumAddress}>
<button title={t('Copy address to clipboard')}>
<Icon size={3} name="duplicate" />
@@ -315,6 +304,23 @@ export const ValidatorsPage = () => {
</div>
</div>
</KeyValueTableRow>
{v.avatarUrl && (
<KeyValueTableRow>
<div>{t('Avatar')}</div>
<div>
<ExternalLink
href={validatorPage}
className="mx-auto"
>
<img
className="max-w-[75px] md:max-w-[200px] max-h-[80px]"
src={v.avatarUrl}
alt={validatorName}
/>
</ExternalLink>
</div>
</KeyValueTableRow>
)}
</KeyValueTable>
</div>
</div>
+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(),
})),
});
+1 -4
View File
@@ -1,5 +1,4 @@
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './styles.css';
import App from './app/app';
@@ -10,8 +9,6 @@ const root = rootElement && createRoot(rootElement);
root?.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
<App />
</StrictMode>
);
@@ -2,6 +2,8 @@
"changes": {
"decimalPlaces": "5",
"positionDecimalPlaces": "5",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"lpPriceRange": "10",
"instrument": {
"name": "Token test market",
@@ -22,7 +24,7 @@
"filters": [
{
"key": {
"name": "prices.ETH.value",
"name": "prices.BTC.value",
"type": "TYPE_INTEGER",
"numberDecimalPlaces": "0"
},
@@ -38,30 +40,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,78 @@
{
"lpPriceRange": "10",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"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 +82,14 @@
}
]
},
"liquidityMonitoringParameters": {
"targetStakeParameters": {
"timeWindow": "3600",
"scalingFactor": 10
},
"triggeringRatio": "0.7",
"auctionExtension": "1"
},
"logNormal": {
"tau": 0.0001140771161,
"riskAversionParameter": 0.001,
@@ -1,9 +1,28 @@
import { associateTokenStartOfTests } from '../../support/common.functions';
import {
navigateTo,
waitForSpinner,
navigation,
} from '../../support/common.functions';
import {
convertUnixTimestampToDateformat,
createRawProposal,
createTenDigitUnixTimeStampForSpecifiedDays,
enterUniqueFreeFormProposalBody,
generateFreeFormProposalTitle,
getGovernanceProposalDateFormatForSpecifiedDays,
getProposalIdFromList,
getProposalInformationFromTable,
getSubmittedProposalFromProposalList,
goToMakeNewProposal,
governanceProposalType,
} from '../../support/governance.functions';
voteForProposal,
waitForProposalSubmitted,
waitForProposalSync,
} from '../../../../governance-e2e/src/support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../../../governance-e2e/src/support/staking.functions';
import { ethereumWalletConnect } from '../../../../governance-e2e/src/support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../../../governance-e2e/src/support/wallet-teardown.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
const proposalVoteProgressForPercentage =
'[data-testid="vote-progress-indicator-percentage-for"]';
@@ -25,24 +44,23 @@ describe(
function () {
before('connect wallets and set approval limit', function () {
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
associateTokenStartOfTests();
vegaWalletSetSpecifiedApprovalAmount('1000');
});
beforeEach('visit proposals tab', function () {
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.navigate_to_page_if_not_already_loaded('proposals');
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
navigateTo(navigation.proposals);
});
// 3001-VOTE-055
it('Newly created raw proposal details - shows proposal title and full description', function () {
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_proposal_id_from_list(rawProposal.rationale.title);
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalIdFromList(rawProposal.rationale.title);
cy.get('@proposalIdText').then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get(`#${proposalId}`).within(() => {
@@ -69,30 +87,26 @@ describe(
it('Newly created freeform proposal details - shows proposed and closing dates', function () {
const closingVoteHrs = '72';
const proposalTitle = generateFreeFormProposalTitle();
const proposalTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(3);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.create_ten_digit_unix_timestamp_for_specified_days('3').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(closingVoteHrs, proposalTitle);
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
cy.get_submitted_proposal_from_proposal_list(proposalTitle).within(
() => cy.get(viewProposalButton).click()
);
cy.convert_unix_timestamp_to_governance_data_table_date_format(
closingDateTimestamp
).then((closingDate) => {
cy.get_proposal_information_from_table('Closes on')
.contains(closingDate)
.should('be.visible');
});
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody(closingVoteHrs, proposalTitle);
waitForProposalSubmitted();
waitForProposalSync();
navigateTo(navigation.proposals);
getSubmittedProposalFromProposalList(proposalTitle).within(() =>
cy.get(viewProposalButton).click()
);
convertUnixTimestampToDateformat(proposalTimeStamp).then(
(closingDate) => {
getProposalInformationFromTable('Closes on')
.contains(closingDate)
.should('be.visible');
}
);
cy.get_governance_proposal_date_format_for_specified_days('0').then(
getGovernanceProposalDateFormatForSpecifiedDays(0).then(
(proposalDate) => {
cy.get_proposal_information_from_table('Proposed on')
getProposalInformationFromTable('Proposed on')
.contains(proposalDate)
.should('be.visible');
}
@@ -104,25 +118,25 @@ describe(
// 3001-VOTE-040
// 3001-VOTE-067
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
cy.contains('Participation: Not Met 0.00 0.00%(0.00% Required)').should(
'be.visible'
);
cy.get_proposal_information_from_table('Expected to pass')
getProposalInformationFromTable('Expected to pass')
.contains('👎')
.should('be.visible');
// 3001-VOTE-062
// 3001-VOTE-040
// 3001-VOTE-070
cy.get_proposal_information_from_table('Token majority met')
getProposalInformationFromTable('Token majority met')
.contains('👎')
.should('be.visible');
// 3001-VOTE-068
cy.get_proposal_information_from_table('Token participation met')
getProposalInformationFromTable('Token participation met')
.contains('👎')
.should('be.visible');
});
@@ -130,28 +144,27 @@ describe(
// 3001-VOTE-080 3001-VOTE-090 3001-VOTE-069 3001-VOTE-072 3001-VOTE-073
it('Newly created proposal details - ability to vote for and against proposal - with minimum required tokens associated', function () {
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
// 3001-VOTE-080
cy.getByTestId('vote-buttons').contains('against').should('be.visible');
cy.getByTestId('vote-buttons').contains('for').should('be.visible');
cy.vote_for_proposal('for');
cy.get_governance_proposal_date_format_for_specified_days(
'0',
'shortMonth'
).then((votedDate) => {
// 3001-VOTE-051
// 3001-VOTE-093
cy.contains('You voted:')
.siblings()
.contains('For')
.siblings()
.contains(votedDate)
.should('be.visible');
});
voteForProposal('for');
getGovernanceProposalDateFormatForSpecifiedDays(0, 'shortMonth').then(
(votedDate) => {
// 3001-VOTE-051
// 3001-VOTE-093
cy.contains('You voted:')
.siblings()
.contains('For')
.siblings()
.contains(votedDate)
.should('be.visible');
}
);
cy.get(proposalVoteProgressForPercentage) // 3001-VOTE-072
.contains('100.00%')
.and('be.visible');
@@ -162,38 +175,38 @@ describe(
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(1).toFixed(2))
getProposalInformationFromTable('Tokens for proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
getProposalInformationFromTable('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
// 3001-VOTE-061
cy.get_proposal_information_from_table('Participation required')
.contains(0.001)
getProposalInformationFromTable('Participation required')
.contains('0.00%')
.should('be.visible');
// 3001-VOTE-066
cy.get_proposal_information_from_table('Majority Required') // 3001-VOTE-073
.contains(`${parseFloat(100).toFixed(2)}%`)
getProposalInformationFromTable('Majority Required') // 3001-VOTE-073
.contains(`${(66).toFixed(2)}%`)
.should('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
getProposalInformationFromTable('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('for');
voteForProposal('for');
// 3001-VOTE-064
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(1).toFixed(2))
getProposalInformationFromTable('Tokens for proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('against');
voteForProposal('against');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('100.00%')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', parseFloat(1).toFixed(2))
getProposalInformationFromTable('Tokens against proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
getProposalInformationFromTable('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
});
@@ -201,30 +214,31 @@ describe(
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
it('Newly created proposal details - ability to increase associated tokens - by voting again after association', function () {
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
)
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(rawProposal.rationale.title)
.as('submittedProposal')
.within(() => cy.get(viewProposalButton).click());
});
cy.vote_for_proposal('for');
voteForProposal('for');
// 3001-VOTE-079
cy.contains('You voted: For').should('be.visible');
cy.get(proposalVoteProgressForTokens).contains('1').and('be.visible');
cy.get_proposal_information_from_table('Total Supply')
getProposalInformationFromTable('Total Supply')
.invoke('text')
.then((totalSupply) => {
let tokensRequiredToAchieveResult = parseFloat(
(totalSupply.replace(/,/g, '') * 0.001) / 100
const tokensRequiredToAchieveResult = (
(Number(totalSupply.replace(/,/g, '')) * 0.001) /
100
).toFixed(2);
cy.ensure_specified_unstaked_tokens_are_associated(
ensureSpecifiedUnstakedTokensAreAssociated(
tokensRequiredToAchieveResult
);
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(viewProposalButton).click()
);
navigateTo(navigation.proposals);
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(rawProposal.rationale.title)
.as('submittedProposal')
.within(() => cy.get(viewProposalButton).click());
});
cy.get(proposalVoteProgressForPercentage)
.contains('100.00%')
.and('be.visible');
@@ -233,38 +247,36 @@ describe(
.and('be.visible');
// 3001-VOTE-065
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('for');
voteForProposal('for');
cy.get(proposalVoteProgressForTokens)
.contains(tokensRequiredToAchieveResult)
.and('be.visible');
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
cy.get_proposal_information_from_table(
'Total tokens voted percentage'
)
getProposalInformationFromTable('Total tokens voted percentage')
.should('have.text', '0.00%')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
getProposalInformationFromTable('Tokens for proposal')
.should('have.text', tokensRequiredToAchieveResult)
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
getProposalInformationFromTable('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
getProposalInformationFromTable('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get_proposal_information_from_table('Expected to pass')
getProposalInformationFromTable('Expected to pass')
.contains('👍')
.should('be.visible');
// 3001-VOTE-062
cy.get_proposal_information_from_table('Token majority met')
getProposalInformationFromTable('Token majority met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Token participation met')
getProposalInformationFromTable('Token participation met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
getProposalInformationFromTable('Tokens for proposal')
.contains(tokensRequiredToAchieveResult)
.and('be.visible');
});
@@ -1,10 +1,21 @@
/// <reference types="cypress" />
import { associateTokenStartOfTests } from '../../support/governance.functions';
import {
navigateTo,
navigation,
waitForSpinner,
} from '../../support/common.functions';
import {
getProposalInformationFromTable,
voteForProposal,
} from '../../support/governance.functions';
import {
createUpdateNetworkProposalTxBody,
createFreeFormProposalTxBody,
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const closedProposals = '[data-testid="closed-proposals"]';
const proposalStatus = '[data-testid="proposal-status"]';
@@ -21,20 +32,19 @@ context(
function () {
before('Connect wallets and set approval', function () {
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
associateTokenStartOfTests();
vegaWalletSetSpecifiedApprovalAmount('1000');
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
cy.clearLocalStorage();
});
beforeEach('visit proposals', function () {
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.navigate_to('proposals');
ethereumWalletConnect();
navigateTo(navigation.proposals);
});
// 3001-VOTE-006
@@ -43,7 +53,7 @@ context(
cy.createMarket();
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.get(closedProposals).within(() => {
cy.contains(proposalTitle)
.parentsUntil('[data-testid="proposals-list-item"]')
@@ -53,7 +63,7 @@ context(
});
});
cy.getByTestId('proposal-type').should('have.text', 'New market');
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Enacted')
.and('be.visible');
cy.get(votesTable).within(() => {
@@ -68,22 +78,22 @@ context(
const proposalTx = createUpdateNetworkProposalTxBody();
cy.VegaWalletSubmitProposal(proposalTx);
cy.navigate_to('proposals');
navigateTo(navigation.proposals);
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.get(openProposals).within(() => {
cy.contains(proposalTitle)
.parentsUntil('[data-testid="proposals-list-item"]')
.within(() => cy.get(viewProposalButton).click());
});
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Open')
.and('be.visible');
cy.vote_for_proposal('for');
cy.get_proposal_information_from_table('State') // 3001-VOTE-047
voteForProposal('for');
getProposalInformationFromTable('State') // 3001-VOTE-047
.contains('Passed', proposalTimeout)
.and('be.visible');
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Enacted', proposalTimeout)
.and('be.visible');
cy.get(votesTable).within(() => {
@@ -101,19 +111,19 @@ context(
const proposalTx = createFreeFormProposalTxBody();
cy.VegaWalletSubmitProposal(proposalTx);
cy.navigate_to('proposals');
navigateTo(navigation.proposals);
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.get(openProposals).within(() => {
cy.contains(proposalTitle)
.parentsUntil('[data-testid="proposals-list-item"]')
.within(() => cy.get(viewProposalButton).click());
});
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Open')
.and('be.visible');
cy.vote_for_proposal('for');
cy.get_proposal_information_from_table('State')
voteForProposal('for');
getProposalInformationFromTable('State')
.contains('Enacted', proposalTimeout)
.and('be.visible');
});
@@ -123,21 +133,21 @@ context(
const proposalTitle = 'Add New free form proposal with short enactment';
const proposalTx = createFreeFormProposalTxBody();
cy.VegaWalletSubmitProposal(proposalTx);
cy.navigate_to('proposals');
navigateTo(navigation.proposals);
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.get(openProposals).within(() => {
cy.contains(proposalTitle)
.parentsUntil('[data-testid="proposals-list-item"]')
.within(() => cy.get(viewProposalButton).click());
});
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Open')
.and('be.visible');
cy.get_proposal_information_from_table('State') // 3001-VOTE-047
getProposalInformationFromTable('State') // 3001-VOTE-047
.contains('Declined', proposalTimeout)
.and('be.visible');
cy.get_proposal_information_from_table('Rejection reason')
getProposalInformationFromTable('Rejection reason')
.contains('PROPOSAL_ERROR_PARTICIPATION_THRESHOLD_NOT_REACHED')
.and('be.visible');
});
@@ -1,17 +1,42 @@
/// <reference types="cypress" />
import {
createRawProposal,
createTenDigitUnixTimeStampForSpecifiedDays,
enterRawProposalBody,
enterUniqueFreeFormProposalBody,
generateFreeFormProposalTitle,
getProposalInformationFromTable,
getSubmittedProposalFromProposalList,
goToMakeNewProposal,
governanceProposalType,
voteForProposal,
waitForProposalSubmitted,
waitForProposalSync,
} from '../../support/governance.functions';
import { associateTokenStartOfTests } from '../../support/common.functions';
import {
verifyUnstakedBalance,
waitForSpinner,
navigateTo,
navigation,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
closeStakingDialog,
ensureSpecifiedUnstakedTokensAreAssociated,
stakingPageDisassociateTokens,
stakingValidatorPageAddStake,
} from '../../support/staking.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletAssociatedBalance = '[data-testid="associated-amount"]';
const vegaWalletNameElement = '[data-testid="wallet-name"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
@@ -40,30 +65,31 @@ context(
cy.visit('/');
cy.get_network_parameters().then((network_parameters) => {
cy.wrap(
network_parameters['spam.protection.proposal.min.tokens'] /
Number(network_parameters['spam.protection.proposal.min.tokens']) /
1000000000000000000
).as('minProposerBalance');
cy.wrap(
network_parameters['spam.protection.voting.min.tokens'] /
Number(network_parameters['spam.protection.voting.min.tokens']) /
1000000000000000000
).as('minVoterBalance');
cy.wrap(
network_parameters['governance.proposal.freeform.requiredMajority'] *
100
Number(
network_parameters['governance.proposal.freeform.requiredMajority']
) * 100
).as('requiredMajority');
});
cy.vega_wallet_set_specified_approval_amount('1000');
associateTokenStartOfTests();
vegaWalletSetSpecifiedApprovalAmount('1000');
cy.associateTokensToVegaWallet('1');
});
beforeEach('visit governance tab', function () {
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.navigate_to_page_if_not_already_loaded('proposals');
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
navigateTo(navigation.proposals);
});
it('Should be able to see that no proposals exist', function () {
@@ -80,7 +106,7 @@ context(
// 3002-PROP-003
it('Submit a proposal form - shows how many vega tokens are required to make a proposal', function () {
// 3002-PROP-005
cy.go_to_make_new_proposal(governanceProposalType.NEW_MARKET);
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.contains(
`You must have at least 1 VEGA associated to make a proposal`
).should('be.visible');
@@ -88,7 +114,7 @@ context(
// 3002-PROP-011
it('Able to submit a valid freeform proposal - with minimum required tokens associated', function () {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
goToMakeNewProposal(governanceProposalType.FREEFORM);
cy.get(minVoteButton).should('be.visible'); // 3002-PROP-008
cy.get(maxVoteButton).should('be.visible');
cy.get(votingDate).should('not.be.empty');
@@ -96,40 +122,31 @@ context(
'contain.text',
'we add 2 minutes of extra time'
);
cy.enter_unique_freeform_proposal_body(
'50',
generateFreeFormProposalTitle()
);
enterUniqueFreeFormProposalBody('50', generateFreeFormProposalTitle());
// 3002-PROP-012
// 3002-PROP-016
cy.wait_for_proposal_submitted();
waitForProposalSubmitted();
});
it('Able to submit a valid freeform proposal - with minimum required tokens associated - but also staked', function () {
cy.ensure_specified_unstaked_tokens_are_associated('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', '2');
cy.navigate_to('validators');
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.close_staking_dialog();
ensureSpecifiedUnstakedTokensAreAssociated('2');
verifyUnstakedBalance(2);
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
closeStakingDialog();
cy.get(vegaWalletStakedBalances, txTimeout).should('contain', '2');
cy.navigate_to('proposals');
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
'50',
generateFreeFormProposalTitle()
);
cy.wait_for_proposal_submitted();
navigateTo(navigation.proposals);
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody('50', generateFreeFormProposalTitle());
waitForProposalSubmitted();
});
it('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
'0.1',
generateFreeFormProposalTitle()
);
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody('0.1', generateFreeFormProposalTitle());
cy.contains('Awaiting network confirmation', epochTimeout).should(
'not.exist'
);
@@ -139,8 +156,8 @@ context(
});
it('Creating a proposal - proposal rejected - when closing time later than system default', function () {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody(
'100000',
generateFreeFormProposalTitle()
);
@@ -154,22 +171,18 @@ context(
// 3001-VOTE-006
it('Creating a proposal - proposal rejected - able to access rejected proposals', function () {
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('1000').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as('rawProposal');
}
);
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(1000));
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
waitForProposalSync();
navigateTo(navigation.proposals);
cy.get(rejectProposalsLink).click();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(
rawProposal.rationale.title
).within(() => {
cy.contains('Rejected').should('be.visible');
@@ -177,78 +190,67 @@ context(
cy.get(viewProposalButton).click();
});
});
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Rejected')
.and('be.visible');
cy.get_proposal_information_from_table('Rejection reason')
getProposalInformationFromTable('Rejection reason')
.contains('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
.and('be.visible');
cy.get_proposal_information_from_table('Error details')
getProposalInformationFromTable('Error details')
.contains('proposal closing time too late')
.and('be.visible');
});
// 0005-ETXN-004
it('Unable to create a proposal - when no tokens are associated', function () {
cy.vega_wallet_teardown();
const errorMsg =
'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)';
vegaWalletTeardown();
cy.get(vegaWalletAssociatedBalance, txTimeout).contains(
'0.00',
txTimeout
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as;
}
);
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
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'
);
cy.get(feedbackError).should('have.text', errorMsg);
cy.get(dialogCloseButton).click();
});
// 3002-PROP-009
it('Unable to create a proposal - when some but not enough tokens are associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(0.000001);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp);
}
);
const errorMsg =
'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)';
ensureSpecifiedUnstakedTokensAreAssociated('0.000001');
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
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'
);
cy.get(feedbackError).should('have.text', errorMsg);
cy.get(dialogCloseButton).click();
});
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
const errorMsg =
'Invalid params: the transaction does not use a valid Vega command: unknown field unexpected" in vega.commands.v1.ProposalSubmission';
// 3001-VOTE-038 3002-PROP-013 3002-PROP-014
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.fixture('/proposals/raw.json').then((freeformProposal) => {
freeformProposal.terms.closingTimestamp = closingDateTimestamp;
freeformProposal.unexpected = `i shouldn't be here`;
let proposalPayload = JSON.stringify(freeformProposal);
cy.get(rawProposalData).type(proposalPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
}
);
goToMakeNewProposal(governanceProposalType.RAW);
cy.fixture('/proposals/raw.json').then((freeformProposal) => {
freeformProposal.terms.closingTimestamp =
createTenDigitUnixTimeStampForSpecifiedDays(8);
freeformProposal.unexpected = `i shouldn't be here`;
const proposalPayload = JSON.stringify(freeformProposal);
cy.get(rawProposalData).type(proposalPayload, {
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',
'Invalid params: the transaction is malformed'
);
cy.get(feedbackError).should('have.text', errorMsg);
cy.get(dialogCloseButton).click();
cy.get(rawProposalData)
.invoke('val')
@@ -257,71 +259,64 @@ context(
it('Unable to create a freeform proposal - when json terms section contains unexpected field', function () {
// 3001-VOTE-038
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.fixture('/proposals/raw.json').then((rawProposal) => {
rawProposal.terms.closingTimestamp = closingDateTimestamp;
rawProposal.terms.unexpectedField = `i shouldn't be here`;
let proposalPayload = JSON.stringify(rawProposal);
const errorMsg =
'Invalid params: the transaction does not use a valid Vega command: unknown field "unexpectedField" in vega.ProposalTerms';
cy.get(rawProposalData).type(proposalPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
}
);
goToMakeNewProposal(governanceProposalType.RAW);
cy.fixture('/proposals/raw.json').then((rawProposal) => {
rawProposal.terms.closingTimestamp =
createTenDigitUnixTimeStampForSpecifiedDays(8);
rawProposal.terms.unexpectedField = `i shouldn't be here`;
const proposalPayload = JSON.stringify(rawProposal);
cy.get(rawProposalData).type(proposalPayload, {
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',
'Invalid params: the transaction is malformed'
);
cy.get(feedbackError).should('have.text', errorMsg);
cy.get(dialogCloseButton).click();
});
// 1005-PROP-009
it.skip(
'Unable to vote on a freeform proposal - when some but not enough vega associated',
{ tags: '@smoke' },
function () {
const proposalTitle = generateFreeFormProposalTitle();
it('Unable to vote on a freeform proposal - when some but not enough vega associated', function () {
const proposalTitle = generateFreeFormProposalTitle();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('50', proposalTitle);
cy.wait_for_proposal_submitted();
cy.staking_page_disassociate_tokens('0.0001');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance).should('have.length', 1);
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.9999'
);
});
cy.navigate_to('proposals');
cy.get_submitted_proposal_from_proposal_list(proposalTitle).within(() =>
cy.get(viewProposalButton).click()
ensureSpecifiedUnstakedTokensAreAssociated('1');
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody('50', proposalTitle);
waitForProposalSubmitted();
stakingPageDisassociateTokens('0.0001');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.9999'
);
cy.contains('Vote breakdown').should('be.visible', {
timeout: 10000,
});
cy.get(voteButtons).should('not.exist');
cy.getByTestId('min-proposal-requirements').should(
'have.text',
`You must have at least ${this.minVoterBalance} VEGA associated to vote on this proposal`
);
}
);
});
navigateTo(navigation.proposals);
getSubmittedProposalFromProposalList(proposalTitle).within(() =>
cy.get(viewProposalButton).click()
);
cy.contains('Vote breakdown').should('be.visible', {
timeout: 10000,
});
cy.get(voteButtons).should('not.exist');
cy.getByTestId('min-proposal-requirements').should(
'have.text',
`You must have at least ${this.minVoterBalance} VEGA associated to vote on this proposal`
);
});
it('Unable to vote on a proposal - when vega wallet disconnected - option to connect from within', function () {
createRawProposal();
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="disconnect"]').click();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
@@ -339,7 +334,7 @@ context(
'1.00',
txTimeout
);
cy.vote_for_proposal('against');
voteForProposal('against');
// 3001-VOTE-079
cy.contains('You voted: Against').should('be.visible');
});
@@ -1,3 +1,22 @@
import {
navigateTo,
navigation,
waitForSpinner,
} from '../../support/common.functions';
import {
getProposalInformationFromTable,
goToMakeNewProposal,
voteForProposal,
waitForProposalSubmitted,
} from '../../support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const openProposals = '[data-testid="open-proposals"]';
const proposalType = '[data-testid="proposal-type"]';
@@ -26,6 +45,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,27 +68,28 @@ context(
{ tags: '@slow' },
function () {
before('connect wallets and set approval limit', function () {
cy.createMarket();
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
vegaWalletSetSpecifiedApprovalAmount('1000');
});
beforeEach('visit governance tab', function () {
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated('1');
cy.navigate_to('proposals');
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
navigateTo(navigation.proposals);
});
it('Able to submit valid update network parameter proposal', function () {
cy.go_to_make_new_proposal(governanceProposalType.NETWORK_PARAMETER);
goToMakeNewProposal(governanceProposalType.NETWORK_PARAMETER);
// 3002-PROP-006
cy.get(newProposalTitle).type('Test update network parameter proposal');
// 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'
@@ -73,12 +97,12 @@ context(
cy.get(currentParameterValue).should('have.value', '2s');
cy.get(newProposedParameterValue).type('5s'); // 3007-PNEC-003
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
waitForProposalSubmitted();
});
it('Unable to submit network parameter with missing/invalid fields', function () {
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.go_to_make_new_proposal(governanceProposalType.NETWORK_PARAMETER);
navigateTo(navigation.proposals);
goToMakeNewProposal(governanceProposalType.NETWORK_PARAMETER);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.get(inputError).should('have.length', 3);
cy.get(newProposalTitle).type(
@@ -103,7 +127,7 @@ context(
it('Able to download network param proposal json', function () {
const downloadFolder = './cypress/downloads/';
cy.go_to_make_new_proposal(governanceProposalType.NETWORK_PARAMETER);
goToMakeNewProposal(governanceProposalType.NETWORK_PARAMETER);
cy.log('Download proposal file');
cy.get(proposalDownloadBtn)
.should('be.visible')
@@ -151,8 +175,8 @@ context(
});
it('Unable to submit network parameter proposal with vote deadline above enactment deadline', function () {
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.go_to_make_new_proposal(governanceProposalType.NETWORK_PARAMETER);
navigateTo(navigation.proposals);
goToMakeNewProposal(governanceProposalType.NETWORK_PARAMETER);
cy.get(newProposalTitle).type('Test update network parameter proposal');
cy.get(newProposalDescription).type('invalid deadlines');
cy.get(proposalParameterSelect).select(
@@ -175,31 +199,34 @@ 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 () {
cy.go_to_make_new_proposal(governanceProposalType.NEW_MARKET);
// 3003-PMAN-001
it('Able to submit valid new market proposal', function () {
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
let newMarketPayload = JSON.stringify(newMarketProposal);
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
waitForProposalSubmitted();
});
it('Unable to submit new market proposal with missing/invalid fields', function () {
cy.go_to_make_new_proposal(governanceProposalType.NEW_MARKET);
const errorMsg =
'Invalid params: the transaction is not a valid Vega command: unknown field "filters" in vega.DataSourceDefinition';
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.get(inputError).should('have.length', 3);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
let newMarketPayload = JSON.stringify(newMarketProposal);
newMarketProposal.invalid = 'I am an invalid field';
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
@@ -207,14 +234,66 @@ context(
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should('have.text', errorMsg);
});
// 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 () {
goToMakeNewProposal(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) => {
const 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');
ensureSpecifiedUnstakedTokensAreAssociated('1');
});
// 3002-PROP-020
it('Unable to submit update market proposal without minimum amount of tokens', function () {
vegaWalletTeardown();
vegaWalletFaucetAssetsWithoutCheck(
'fUSDC',
'1000000',
vegaWalletPublicKey
);
goToMakeNewProposal(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) => {
const 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',
'Invalid params: the transaction is malformed'
'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)'
);
});
it.skip('Able to submit update market proposal', function () {
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_MARKET);
// 3001-VOTE-092
it('Able to submit update market proposal and vote for proposal', function () {
vegaWalletFaucetAssetsWithoutCheck(
'fUSDC',
'1000000',
vegaWalletPublicKey
);
goToMakeNewProposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.get(proposalMarketSelect).select('Test market 1');
@@ -222,25 +301,57 @@ 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(String(marketId), '1');
});
cy.fixture('/proposals/update-market').then((updateMarketProposal) => {
let newUpdateMarketProposal = JSON.stringify(updateMarketProposal);
const newUpdateMarketProposal = JSON.stringify(updateMarketProposal);
cy.get(newProposalTerms).type(newUpdateMarketProposal, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
waitForProposalSubmitted();
navigateTo(navigation.proposals);
cy.get('@EnactedMarketId').then((marketId) => {
cy.contains(String(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'
);
voteForProposal('for');
cy.getByTestId(liquidityVoteStatus).should(
'contain.text',
'Currently expected to pass'
);
cy.getByTestId(tokenVoteStatus).should(
'contain.text',
'Currently expected to pass'
);
getProposalInformationFromTable('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
it('Able to submit new asset proposal using min deadlines', function () {
cy.go_to_make_new_proposal(governanceProposalType.NEW_ASSET);
goToMakeNewProposal(governanceProposalType.NEW_ASSET);
cy.get(newProposalTitle).type('Test new asset proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-asset').then((newAssetProposal) => {
let newAssetPayload = JSON.stringify(newAssetProposal);
const newAssetPayload = JSON.stringify(newAssetProposal);
cy.get(newProposalTerms).type(newAssetPayload, {
parseSpecialCharSequences: false,
delay: 2,
@@ -269,7 +380,7 @@ context(
});
it('Unable to submit new asset proposal with missing/invalid fields', function () {
cy.go_to_make_new_proposal(governanceProposalType.NEW_ASSET);
goToMakeNewProposal(governanceProposalType.NEW_ASSET);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.get(inputError).should('have.length', 3);
cy.get(newProposalTitle).type('Invalid new asset proposal');
@@ -285,38 +396,38 @@ context(
const assetId =
'ebcd94151ae1f0d39a4bde3b21a9c7ae81a80ea4352fb075a92e07608d9c953d';
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_ASSET);
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
enterUpdateAssetProposalDetails();
cy.get(minVoteDeadline).click();
cy.get(minEnactDeadline).click();
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
cy.navigate_to('proposals');
waitForProposalSubmitted();
navigateTo(navigation.proposals);
cy.get(openProposals).within(() => {
cy.get(proposalType)
.contains('Update asset')
.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
getProposalInformationFromTable('Proposed enactment') // 3001-VOTE-044
.invoke('text')
.should('not.be.empty');
});
it('Able to submit update asset proposal using max deadline', function () {
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_ASSET);
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
enterUpdateAssetProposalDetails();
cy.get(maxVoteDeadline).click();
cy.get(maxEnactDeadline).click();
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
waitForProposalSubmitted();
});
it('Unable to submit edit asset proposal with missing/invalid fields', function () {
cy.go_to_make_new_proposal(governanceProposalType.UPDATE_ASSET);
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.get(inputError).should('have.length', 3);
});
@@ -336,7 +447,7 @@ context(
cy.get(newProposalTitle).type('Test update asset proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/update-asset').then((newAssetProposal) => {
let newAssetPayload = JSON.stringify(newAssetProposal);
const newAssetPayload = JSON.stringify(newAssetProposal);
cy.get(newProposalTerms).type(newAssetPayload, {
parseSpecialCharSequences: false,
delay: 2,
@@ -1,9 +1,28 @@
import type { testFreeformProposal } from '../../support/common-interfaces';
import {
navigateTo,
navigation,
waitForSpinner,
} from '../../support/common.functions';
import {
createFreeformProposal,
createRawProposal,
createTenDigitUnixTimeStampForSpecifiedDays,
enterRawProposalBody,
generateFreeFormProposalTitle,
getProposalIdFromList,
getProposalInformationFromTable,
getSortOrderOfSuppliedArray,
getSubmittedProposalFromProposalList,
goToMakeNewProposal,
governanceProposalType,
voteForProposal,
waitForProposalSubmitted,
waitForProposalSync,
} from '../../support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const openProposals = '[data-testid="open-proposals"]';
@@ -12,17 +31,17 @@ const viewProposalButton = '[data-testid="view-proposal-btn"]';
describe('Governance flow for proposal list', { tags: '@slow' }, function () {
before('connect wallets and set approval limit', function () {
cy.vega_wallet_set_specified_approval_amount('1000');
vegaWalletSetSpecifiedApprovalAmount('1000');
cy.visit('/');
});
beforeEach('visit proposals tab', function () {
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.navigate_to_page_if_not_already_loaded('proposals');
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
navigateTo(navigation.proposals);
});
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
@@ -30,32 +49,30 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
const maxCloseDays = 3;
// 3001-VOTE-005
let proposalDays = [
const proposalDays = [
minCloseDays + 1,
maxCloseDays,
minCloseDays + 3,
minCloseDays + 2,
];
for (var index = 0; index < proposalDays.length; index++) {
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days(
proposalDays[index]
).then((closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp);
});
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
for (let index = 0; index < proposalDays.length; index++) {
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(
createTenDigitUnixTimeStampForSpecifiedDays(proposalDays[index])
);
waitForProposalSubmitted();
waitForProposalSync();
}
let arrayOfProposals = [];
const arrayOfProposals: string[] = [];
cy.navigate_to('proposals');
navigateTo(navigation.proposals);
cy.get(proposalDetailsTitle)
.each((proposalTitleElement) => {
arrayOfProposals.push(proposalTitleElement.text());
})
.then(() => {
cy.get_sort_order_of_supplied_array(arrayOfProposals).should(
cy.wrap(getSortOrderOfSuppliedArray(arrayOfProposals)).should(
'equal',
'descending'
);
@@ -67,7 +84,7 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
const proposalTitle = generateFreeFormProposalTitle();
createFreeformProposal(proposalTitle);
cy.get_proposal_id_from_list(proposalTitle);
getProposalIdFromList(proposalTitle);
cy.get('@proposalIdText').then((proposalId) => {
cy.get('[data-testid="set-proposals-filter-visible"]').click();
cy.get('[data-testid="filter-input"]').type(proposerId);
@@ -77,8 +94,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
it('Newly created proposals list - shows title and portion of summary', function () {
createRawProposal(this.minProposerBalance); // 3001-VOTE-052
cy.get('@rawProposal').then((rawProposal) => {
cy.get_proposal_id_from_list(rawProposal.rationale.title);
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalIdFromList(rawProposal.rationale.title);
cy.get('@proposalIdText').then((proposalId) => {
cy.get(openProposals).within(() => {
// 3001-VOTE-008
@@ -102,21 +119,22 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
// 3001-VOTE-004
// 3001-VOTE-035
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(rawProposal.rationale.title).within(
() => {
cy.get(viewProposalButton).should('be.visible').click();
}
);
cy.get('@proposalIdText').then((proposalId) => {
cy.get_proposal_information_from_table('ID')
.contains(proposalId)
getProposalInformationFromTable('ID')
.contains(String(proposalId))
.and('be.visible');
});
cy.get_proposal_information_from_table('State')
getProposalInformationFromTable('State')
.contains('Open')
.and('be.visible');
cy.get_proposal_information_from_table('Type')
getProposalInformationFromTable('Type')
.contains('Freeform')
.and('be.visible');
});
@@ -125,38 +143,21 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
// 3001-VOTE-071
it('Newly created freeform proposals list - shows proposal participation - both met and not', function () {
const proposalTitle = generateFreeFormProposalTitle();
const requiredParticipation = 0.001;
createFreeformProposal(proposalTitle);
cy.get_submitted_proposal_from_proposal_list(proposalTitle)
.as('submittedProposal')
.within(() => {
// 3001-VOTE-039
cy.get(voteStatus).should('have.text', 'Participation not reached');
cy.get(viewProposalButton).click();
});
cy.vote_for_proposal('for');
cy.get_proposal_information_from_table('Total Supply')
.invoke('text')
.then((totalSupply) => {
let tokensRequiredToAchieveResult = parseFloat(
(totalSupply.replace(/,/g, '') * requiredParticipation) / 100
).toFixed(2);
cy.ensure_specified_unstaked_tokens_are_associated(
tokensRequiredToAchieveResult
);
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(viewProposalButton).click()
);
cy.get_proposal_information_from_table('Token participation met')
.contains('👍')
.should('be.visible');
cy.navigate_to('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(voteStatus).should('have.text', 'Set to pass')
);
});
getSubmittedProposalFromProposalList(proposalTitle).within(() => {
// 3001-VOTE-039
cy.get(voteStatus).should('have.text', 'Participation not reached');
cy.get(viewProposalButton).click();
});
voteForProposal('for');
navigateTo(navigation.proposals);
getSubmittedProposalFromProposalList(proposalTitle).within(() => {
cy.get(voteStatus).should('have.text', 'Set to pass');
cy.get(viewProposalButton).click();
});
getProposalInformationFromTable('Token participation met')
.contains('👍')
.should('be.visible');
});
});
@@ -0,0 +1,105 @@
import {
navigateTo,
navigation,
waitForSpinner,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
closeStakingDialog,
stakingPageAssociateTokens,
stakingValidatorPageAddStake,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
depositAsset,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const rewardsTable = 'epoch-total-rewards-table';
const txTimeout = Cypress.env('txTimeout');
const rewardsTimeOut = { timeout: 60000 };
context('rewards - flow', { tags: '@slow' }, function () {
before('set up environment to allow rewards', function () {
cy.visit('/');
waitForSpinner();
depositAsset(vegaAssetAddress, '1000');
cy.validatorsSelfDelegate();
ethereumWalletConnect();
cy.connectVegaWallet();
cy.VegaWalletTopUpRewardsPool(30, 200);
navigateTo(navigation.validators);
vegaWalletTeardown();
stakingPageAssociateTokens('6000');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
'6,000.0',
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('3000');
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('3000');
closeStakingDialog();
navigateTo(navigation.rewards);
});
it('Should display rewards per epoch', function () {
cy.getByTestId(rewardsTable, rewardsTimeOut).should('exist');
cy.getByTestId(rewardsTable)
.first()
.within(() => {
cy.getByTestId('asset').should('have.text', 'Vega');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD').should('have.text', '1');
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE').should(
'have.text',
'0'
);
cy.getByTestId('total').should('have.text', '1');
});
});
it('Should update when epoch starts', function () {
cy.getByTestId(rewardsTable)
.first()
.within(() => {
cy.get('h2').first().invoke('text').as('epochNumber');
});
waitForBeginningOfEpoch();
cy.get('@epochNumber').then((epochNumber) => {
cy.getByTestId(rewardsTable)
.first()
.within(() => {
cy.get('h2').first().invoke('text').should('not.equal', epochNumber);
});
});
});
// 2002-SINC-009 2002-SINC-010 2002-SINC-011 2002-SINC-012
it('Should display table of rewards earned by connected vega wallet', function () {
cy.getByTestId('epoch-reward-view-toggle-individual').click();
cy.getByTestId('connected-vega-key')
.find('span')
.should('have.text', Cypress.env('vegaWalletPublicKey'));
cy.getByTestId('epoch-individual-rewards-table')
.first()
.within(() => {
cy.get('h2').first().should('contain.text', 'EPOCH');
cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')
.should('contain.text', '0.1177')
.and('contain.text', '(11.7733%)');
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')
.should('contain.text', '0.0001')
.and('contain.text', '(11.7733%)');
cy.getByTestId('total').should('have.text', '0.1179');
});
});
});
@@ -1,875 +0,0 @@
/// <reference types="cypress" />
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
const stakeValidatorListValidatorStake = '[col-id="stake"] > div > span';
const stakeRemoveStakeRadioButton = '[data-testid="remove-stake-radio"]';
const stakeTokenAmountInputBox = '[data-testid="token-amount-input"]';
const stakeTokenSubmitButton = '[data-testid="token-input-submit-button"]';
const stakeNextEpochValue = '[data-testid="stake-next-epoch"]';
const stakeThisEpochValue = '[data-testid="stake-this-epoch"]';
const stakeAddStakeRadioButton = '[data-testid="add-stake-radio"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const totalStake = '[data-testid="total-stake"]';
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"]';
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 vegaWallet = '[data-testid="vega-wallet"]';
const partValidatorId = '…';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
context(
'Staking Tab - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
// 2001-STKE-002, 2001-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.vega_wallet_teardown();
cy.navigate_to('validators');
}
);
it('Able to stake against a validator - using vega from wallet', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('3.0', txTimeout)
.should('be.visible');
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort, txTimeout)
.parent()
.should('contain', 3.0, txTimeout);
cy.get('button').contains('Select a validator to nominate').click();
// 2001-STKE-031
cy.click_on_validator_from_list(0);
// 2001-STKE-033, 2001-STKE-034, 2001-STKE-037
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
// 2001-STKE-039
cy.get(vegaWalletStakedBalances, txTimeout)
.should('contain', 2.0, txTimeout)
.and('contain', partValidatorId);
cy.get(stakeNextEpochValue, epochTimeout) // 2001-STKE-016 2001-STKE-038
.contains(2.0, epochTimeout)
.should('be.visible');
cy.get(stakeThisEpochValue, epochTimeout) // 2001-STKE-013
.contains(2.0, epochTimeout)
.should('be.visible');
cy.close_staking_dialog();
cy.navigate_to('validators');
// 2002-SINC-007
cy.validate_validator_list_total_stake_and_share(
'0',
'2.00',
'100.00%'
);
});
it('Able to stake against a validator - using vega from vesting contract', function () {
cy.staking_page_associate_tokens('3', { type: 'contract' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('3.0', txTimeout)
.should('be.visible');
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort, txTimeout)
.parent()
.should('contain', 3.0, txTimeout);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout)
.should('contain', 2.0, txTimeout)
.and('contain', partValidatorId);
cy.get(stakeNextEpochValue, epochTimeout)
.contains(2.0, epochTimeout)
.should('be.visible');
cy.get(stakeThisEpochValue, epochTimeout)
.contains(2.0, epochTimeout)
.should('be.visible');
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share(
'0',
'2.00',
'100.00%'
);
});
it('Able to stake against a validator - using vega from both wallet and vesting contract', function () {
cy.staking_page_associate_tokens('3', { type: 'contract' });
cy.navigate_to('validators');
cy.staking_page_associate_tokens('4', { type: 'wallet' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
7.0,
txTimeout
);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('3.0', txTimeout)
.should('be.visible');
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('4.0', txTimeout)
.should('be.visible');
cy.get(ethWalletAssociatedBalances, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get(ethWalletAssociatedBalances, txTimeout).should(
'contain',
4.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('6');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout)
.should('contain', 6.0, txTimeout)
.and('contain', partValidatorId);
cy.get(stakeNextEpochValue, epochTimeout)
.contains(6.0, epochTimeout)
.should('be.visible');
cy.get(stakeThisEpochValue, epochTimeout)
.contains(6.0, epochTimeout)
.should('be.visible');
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share(
'0',
'6.00',
'100.00%'
);
});
it('Able to stake against multiple validators', function () {
cy.staking_page_associate_tokens('5');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
5.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout)
.parent()
.should('contain', 2.0, txTimeout);
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.click_on_validator_from_list(1);
cy.staking_validator_page_add_stake('1');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout)
.should('have.length', 2, txTimeout)
.eq(0)
.should('contain', 2.0, txTimeout);
cy.get(vegaWalletStakedBalances, txTimeout)
.eq(1)
.should('contain', 1.0, txTimeout);
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.get(`[row-id="${0}"]`).within(() => {
cy.get(stakeValidatorListTotalStake)
.should('have.text', '2.00')
.and('be.visible');
cy.get(stakeValidatorListTotalShare)
.should('have.text', '66.67%')
.and('be.visible');
cy.get(stakeValidatorListValidatorStake)
.scrollIntoView()
.should('have.text', '2.00')
.and('be.visible');
});
cy.get(`[row-id="${1}"]`).within(() => {
cy.get(stakeValidatorListTotalStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
cy.get(stakeValidatorListTotalShare)
.should('have.text', '33.33%')
.and('be.visible');
cy.get(stakeValidatorListValidatorStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
});
});
// 2001-STKE-041
it(
'Able to remove part of a stake against a validator',
{ tags: '@smoke' },
function () {
cy.staking_page_associate_tokens('4');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
4.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('3');
cy.get(stakeNextEpochValue, epochTimeout)
.contains(3.0, epochTimeout)
.should('be.visible');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.close_staking_dialog();
cy.navigate_to('validators');
// 2001-STKE-040
cy.click_on_validator_from_list(0);
// 2001-STKE-044, 2001-STKE-048
cy.staking_validator_page_remove_stake('1');
// 2001-STKE-049
cy.get(stakeNextEpochValue, epochTimeout).contains(2.0, epochTimeout);
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.get(stakeNextEpochValue, epochTimeout)
.contains(2.0, epochTimeout)
.should('be.visible');
cy.get(stakeThisEpochValue, epochTimeout)
.contains(2.0, epochTimeout)
.should('be.visible');
cy.get(totalStake, epochTimeout).should('contain.text', '2');
cy.get(stakeShare, epochTimeout).should('have.text', '100%');
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share(
'0',
'2.00',
'100.00%'
);
}
);
// 2001-STKE-045
it('Able to remove a full stake against a validator', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('1');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.click_on_validator_from_list('0');
cy.staking_validator_page_remove_stake('1');
cy.get(stakeNextEpochValue, epochTimeout)
.contains(0.0, epochTimeout)
.should('be.visible');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get(stakeNextEpochValue, epochTimeout)
.contains(0.0, epochTimeout)
.should('be.visible');
cy.get(stakeThisEpochValue, epochTimeout)
.contains(0.0, epochTimeout)
.should('be.visible');
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share('0', '0.00', '0.00%');
});
it('Unable to remove a stake with a negative value for a validator', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(stakeNextEpochValue, epochTimeout)
.contains(2.0, epochTimeout)
.should('be.visible');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.click_on_validator_from_list(0);
cy.get(stakeRemoveStakeRadioButton, txTimeout).click();
cy.get(stakeTokenAmountInputBox).type('-0.1');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.get(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove -0.1 $VEGA tokens at the end of epoch`)
.and('be.visible');
});
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(stakeNextEpochValue, epochTimeout)
.contains(2.0, epochTimeout)
.should('be.visible');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.close_staking_dialog();
cy.navigate_to('validators');
cy.click_on_validator_from_list(0);
cy.get(stakeRemoveStakeRadioButton).click();
cy.get(stakeTokenAmountInputBox).type(4);
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.get(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove 4 $VEGA tokens at the end of epoch`)
.and('be.visible');
});
it('Disassociating all wallet tokens max - removes all staked tokens', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list('1');
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_disassociate_all_tokens('wallet');
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('0.0', txTimeout)
.should('be.visible');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share('0', '0.00', '0.00%');
});
it('Disassociating all vesting contract tokens max - removes all staked tokens', function () {
cy.staking_page_associate_tokens('3', { type: 'contract' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list('1');
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_disassociate_all_tokens('contract');
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('0.0', txTimeout)
.should('be.visible');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share('0', '0.00', '0.00%');
});
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
2.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_disassociate_tokens('1');
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('2.0', txTimeout)
.should('be.visible');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'2.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout)
.should('contain', 2.0, txTimeout)
.and('contain', partValidatorId);
cy.navigate_to('validators');
cy.validate_validator_list_total_stake_and_share(
'0',
'2.00',
'100.00%'
);
});
it('Associating wallet tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('3');
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_associate_tokens('4');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
0.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
7.0,
txTimeout
);
});
it('Associating vesting contract tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
cy.staking_page_associate_tokens('3', { type: 'contract' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('3');
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_associate_tokens('4', { type: 'contract' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
0.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
7.0,
txTimeout
);
});
it('Associating vesting contract tokens - when wallet tokens already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
cy.staking_page_associate_tokens('3', { type: 'wallet' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('3');
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_associate_tokens('4', { type: 'contract' });
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
0.0,
txTimeout
);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'contain',
7.0,
txTimeout
);
});
it('Associating tokens - with multiple validators already staked - auto stakes to staked validators - abiding by existing stake ratio', function () {
// 2001-STKE-004
cy.staking_page_associate_tokens('6');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
6.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
0.0,
txTimeout
);
cy.close_staking_dialog();
cy.click_on_validator_from_list(1);
cy.staking_validator_page_add_stake('4');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
0.0,
txTimeout
);
cy.close_staking_dialog();
cy.staking_page_associate_tokens('6');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'12.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout)
.should('contain', '4.0', txTimeout)
.and('contain', partValidatorId);
cy.get(vegaWalletStakedBalances, txTimeout)
.should('contain', '8.0')
.and('contain', partValidatorId);
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
0.0,
txTimeout
);
});
it('Selecting use maximum where tokens are already staked - suggests the unstaked token amount', function () {
cy.staking_page_associate_tokens('3');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
3.0,
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
1.0,
txTimeout
);
cy.close_staking_dialog();
cy.click_on_validator_from_list(0);
cy.get(stakeAddStakeRadioButton).click();
cy.get(stakeMaximumTokens, { timeout: 60000 }).click();
cy.get(stakeTokenSubmitButton).should('contain', 'Add 1 $VEGA tokens');
});
after('teardown wallet', function () {
cy.vega_wallet_teardown();
});
});
}
);
@@ -0,0 +1,454 @@
/// <reference types="cypress" />
import {
verifyUnstakedBalance,
verifyStakedBalance,
verifyEthWalletTotalAssociatedBalance,
verifyEthWalletAssociatedBalance,
waitForSpinner,
navigateTo,
navigation,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
closeStakingDialog,
ensureSpecifiedUnstakedTokensAreAssociated,
stakingPageAssociateTokens,
stakingPageDisassociateAllTokens,
stakingPageDisassociateTokens,
stakingValidatorPageAddStake,
stakingValidatorPageRemoveStake,
validateValidatorListTotalStakeAndShare,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
const stakeValidatorListValidatorStake = '[col-id="stake"] > div > span';
const stakeRemoveStakeRadioButton = '[data-testid="remove-stake-radio"]';
const stakeTokenAmountInputBox = '[data-testid="token-amount-input"]';
const stakeTokenSubmitButton = '[data-testid="token-input-submit-button"]';
const stakeAddStakeRadioButton = '[data-testid="add-stake-radio"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const totalStake = '[data-testid="total-stake"]';
const stakeShare = '[data-testid="stake-percentage"]';
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
context(
'Staking Tab - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
// 2001-STKE-002, 2001-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
ethereumWalletConnect();
// this is a workaround for #2422 which can be removed once issue is resolved
cy.associateTokensToVegaWallet('4');
vegaWalletSetSpecifiedApprovalAmount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
cy.reload();
waitForSpinner();
cy.connectVegaWallet();
ethereumWalletConnect();
navigateTo(navigation.validators);
}
);
it('Able to stake against a validator - using vega from wallet', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
// 2001-STKE-031
clickOnValidatorFromList(0);
// 2001-STKE-033, 2001-STKE-034, 2001-STKE-037
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
// 2001-STKE-039
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0); // 2001-STKE-016 2001-STKE-038
verifyThisEpochValue(2.0); // 2001-STKE-013
closeStakingDialog();
navigateTo(navigation.validators);
// 2002-SINC-007
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
});
it('Able to stake against a validator - using vega from vesting contract', function () {
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0);
verifyThisEpochValue(2.0);
closeStakingDialog();
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
});
it('Able to stake against a validator - using vega from both wallet and vesting contract', function () {
stakingPageAssociateTokens('3', { type: 'contract' });
navigateTo(navigation.validators);
stakingPageAssociateTokens('4', { type: 'wallet' });
verifyUnstakedBalance(7.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletTotalAssociatedBalance('4.0');
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletTotalAssociatedBalance('4.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('6');
verifyUnstakedBalance(1.0);
verifyStakedBalance(6.0);
verifyNextEpochValue(6.0);
verifyThisEpochValue(6.0);
closeStakingDialog();
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '6.00', '100.00%');
});
it('Able to stake against multiple validators', function () {
stakingPageAssociateTokens('5');
verifyUnstakedBalance(5.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(3.0);
cy.get(vegaWalletStakedBalances, txTimeout)
.parent()
.should('contain', 2.0, txTimeout);
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('1');
verifyUnstakedBalance(2.0);
cy.get(vegaWalletStakedBalances, txTimeout)
.should('have.length', 4, txTimeout)
.eq(0)
.should('contain', 2.0, txTimeout);
cy.get(vegaWalletStakedBalances, txTimeout)
.eq(1)
.should('contain', 1.0, txTimeout);
closeStakingDialog();
navigateTo(navigation.validators);
cy.get(`[row-id="${0}"]`).within(() => {
cy.get(stakeValidatorListTotalStake)
.should('have.text', '2.00')
.and('be.visible');
cy.get(stakeValidatorListTotalShare)
.should('have.text', '66.67%')
.and('be.visible');
cy.get(stakeValidatorListValidatorStake)
.scrollIntoView()
.should('have.text', '2.00')
.and('be.visible');
});
cy.get(`[row-id="${1}"]`).within(() => {
cy.get(stakeValidatorListTotalStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
cy.get(stakeValidatorListTotalShare)
.should('have.text', '33.33%')
.and('be.visible');
cy.get(stakeValidatorListValidatorStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
});
});
// 2001-STKE-041
it(
'Able to remove part of a stake against a validator',
{ tags: '@smoke' },
function () {
ensureSpecifiedUnstakedTokensAreAssociated('4');
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('3');
verifyNextEpochValue(3.0);
verifyUnstakedBalance(1.0);
closeStakingDialog();
navigateTo(navigation.validators);
// 2001-STKE-040
clickOnValidatorFromList(0);
// 2001-STKE-044, 2001-STKE-048
stakingValidatorPageRemoveStake('1');
// 2001-STKE-049
verifyNextEpochValue(2.0);
verifyUnstakedBalance(2.0);
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0);
verifyThisEpochValue(2.0);
cy.get(totalStake, epochTimeout).should('contain.text', '2');
waitForBeginningOfEpoch();
cy.get(stakeShare).should('have.text', '100%');
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
}
);
// 2001-STKE-045
it('Able to remove a full stake against a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('1');
verifyUnstakedBalance(2.0);
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
stakingValidatorPageRemoveStake('1');
verifyNextEpochValue(0.0);
verifyUnstakedBalance(3.0);
verifyNextEpochValue(0.0);
verifyThisEpochValue(0.0);
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '0.00', '0.00%');
});
it('Unable to remove a stake with a negative value for a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyNextEpochValue(2.0);
verifyUnstakedBalance(1.0);
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
cy.get(stakeRemoveStakeRadioButton, txTimeout).click();
cy.get(stakeTokenAmountInputBox).type('-0.1');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.get(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove -0.1 $VEGA tokens at the end of epoch`)
.and('be.visible');
});
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyNextEpochValue(2.0);
verifyUnstakedBalance(3.0);
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
cy.get(stakeRemoveStakeRadioButton).click();
cy.get(stakeTokenAmountInputBox).type('4');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.get(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove 4 $VEGA tokens at the end of epoch`)
.and('be.visible');
});
it('Disassociating all wallet tokens max - removes all staked tokens', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(3.0);
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateAllTokens();
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
verifyEthWalletTotalAssociatedBalance('0.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '0.00', '0.00%');
});
it('Disassociating all vesting contract tokens max - removes all staked tokens', function () {
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateAllTokens('contract');
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
verifyEthWalletTotalAssociatedBalance('0.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '0.00', '0.00%');
});
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateTokens('1');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'2.00'
);
});
verifyStakedBalance(2.0);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
});
it('Associating wallet tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('3');
verifyStakedBalance(3.0);
closeStakingDialog();
stakingPageAssociateTokens('4');
verifyUnstakedBalance(0.0);
verifyStakedBalance(7.0);
});
it('Associating vesting contract tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('3');
verifyStakedBalance(3.0);
closeStakingDialog();
stakingPageAssociateTokens('4', { type: 'contract' });
verifyUnstakedBalance(0.0);
verifyStakedBalance(7.0);
});
it('Associating vesting contract tokens - when wallet tokens already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
stakingPageAssociateTokens('3', { type: 'wallet' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('3');
verifyStakedBalance(3.0);
closeStakingDialog();
stakingPageAssociateTokens('4', { type: 'contract' });
verifyUnstakedBalance(0.0);
verifyStakedBalance(7.0);
});
it('Associating tokens - with multiple validators already staked - auto stakes to staked validators - abiding by existing stake ratio', function () {
// 2001-STKE-004
stakingPageAssociateTokens('6');
verifyUnstakedBalance(6.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(0.0);
closeStakingDialog();
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('4');
verifyUnstakedBalance(0.0);
closeStakingDialog();
stakingPageAssociateTokens('6');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'12.00'
);
});
verifyStakedBalance(4.0);
verifyStakedBalance(8.0);
verifyUnstakedBalance(0.0);
});
it('Selecting use maximum where tokens are already staked - suggests the unstaked token amount', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
closeStakingDialog();
clickOnValidatorFromList(0);
cy.get(stakeAddStakeRadioButton).click();
cy.get(stakeMaximumTokens, { timeout: 60000 }).click();
cy.get(stakeTokenSubmitButton).should('contain', 'Add 1 $VEGA tokens');
});
afterEach('Teardown Wallet', function () {
vegaWalletTeardown();
});
function verifyNextEpochValue(amount: number) {
cy.getByTestId('stake-next-epoch', epochTimeout)
.contains(amount, epochTimeout)
.should('be.visible');
}
function verifyThisEpochValue(amount: number) {
cy.getByTestId('stake-this-epoch', epochTimeout) // 2001-STKE-013
.contains(amount, epochTimeout)
.should('be.visible');
}
});
}
);
@@ -1,13 +1,31 @@
import {
verifyEthWalletTotalAssociatedBalance,
verifyEthWalletAssociatedBalance,
waitForSpinner,
navigateTo,
navigation,
} from '../../support/common.functions';
import {
stakingPageAssociateTokens,
stakingPageDisassociateAllTokens,
stakingPageDisassociateTokens,
validateWalletCurrency,
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletAssociate,
vegaWalletDisassociate,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const ethWalletAssociatedBalances =
'[data-testid="eth-wallet-associated-balances"]';
const ethWalletTotalAssociatedBalance = '[data-testid="currency-locked"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const txTimeout = Cypress.env('txTimeout');
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const ethWalletAssociateButton = '[href="/token/associate"]';
const ethWalletAssociateButton = '[data-testid="associate-btn"]';
const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
@@ -28,7 +46,7 @@ context(
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
// 0005-ETXN-001
cy.vega_wallet_set_specified_approval_amount('1000');
vegaWalletSetSpecifiedApprovalAmount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
@@ -36,11 +54,10 @@ context(
'teardown wallet & drill into a specific validator',
function () {
cy.reload();
cy.wait_for_spinner();
cy.ethereum_wallet_connect();
waitForSpinner();
cy.connectVegaWallet();
cy.vega_wallet_teardown();
cy.navigate_to('validators');
ethereumWalletConnect();
vegaWalletTeardown();
}
);
@@ -57,26 +74,21 @@ context(
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
cy.staking_page_associate_tokens('2', { skipConfirmation: true });
stakingPageAssociateTokens('2', { skipConfirmation: true });
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
);
cy.validate_wallet_currency('Associated', '0.00');
cy.validate_wallet_currency('Pending association', '2.00');
cy.validate_wallet_currency('Total associated after pending', '2.00');
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
// 0005-ETXN-002
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent(txTimeout)
.should('contain', 2.0);
verifyEthWalletAssociatedBalance('2.0');
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('2.0', txTimeout)
.should('be.visible');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
@@ -92,50 +104,30 @@ context(
// 1004-ASSO-029
// 1004-ASSO-031
cy.staking_page_associate_tokens('2');
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent(txTimeout)
.should('contain', 2.0);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('2.0', txTimeout)
.should('be.visible');
stakingPageAssociateTokens('2');
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get('button').contains('Select a validator to nominate').click();
cy.staking_page_disassociate_tokens('2');
stakingPageDisassociateTokens('2');
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
);
cy.validate_wallet_currency('Associated', '2.00');
cy.validate_wallet_currency('Pending association', '2.00');
cy.validate_wallet_currency('Total associated after pending', '0.00');
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.get(ethWalletAssociatedBalances, txTimeout).should('not.exist');
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('0.00', txTimeout)
.should('be.visible');
cy.getByTestId('eth-wallet-associated-balances', txTimeout).should(
'not.exist'
);
verifyEthWalletTotalAssociatedBalance('0.00');
});
it('Able to associate more tokens than the approved amount of 1000 - requires re-approval', function () {
//1004-ASSO-011
cy.staking_page_associate_tokens('1001', { approve: true });
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent()
.should('contain', '1,001.00', txTimeout);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('1,001.00', txTimeout)
.should('be.visible');
stakingPageAssociateTokens('1001', { approve: true });
verifyEthWalletAssociatedBalance('1,001.00');
verifyEthWalletTotalAssociatedBalance('1,001.00');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
@@ -145,26 +137,14 @@ context(
});
it('Able to disassociate a partial amount of tokens currently associated', function () {
cy.staking_page_associate_tokens('2');
stakingPageAssociateTokens('2');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
cy.get('button').contains('Select a validator to nominate').click();
cy.staking_page_disassociate_tokens('1');
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent(txTimeout)
.should('contain', 1.0);
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent(txTimeout)
.should('contain', 1.0);
stakingPageDisassociateTokens('1');
verifyEthWalletAssociatedBalance('1.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 1.0);
});
@@ -174,32 +154,24 @@ context(
// 1004-ASSO-026
const warningText =
'Warning: Any tokens that have been nominated to a node will sacrifice rewards they are due for the current epoch. If you do not wish to sacrifice these, you should remove stake from a node at the end of an epoch before disassociation.';
cy.staking_page_associate_tokens('2');
stakingPageAssociateTokens('2');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
cy.get('button').contains('Select a validator to nominate').click();
cy.get(ethWalletDissociateButton).click();
cy.get(disassociationWarning).should('contain', warningText);
cy.staking_page_disassociate_all_tokens();
stakingPageDisassociateAllTokens();
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, { timeout: 20000 }).should(
'not.exist'
);
});
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, { timeout: 20000 }).should(
'not.exist'
);
});
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 0.0);
});
@@ -212,7 +184,7 @@ context(
// 1004-ASSO-024
// 1004-ASSO-023
cy.staking_page_associate_tokens('2', {
stakingPageAssociateTokens('2', {
type: 'contract',
skipConfirmation: true,
});
@@ -221,47 +193,30 @@ context(
'have.length.above',
3
);
cy.validate_wallet_currency('Associated', '0.00');
cy.validate_wallet_currency('Pending association', '2.00');
cy.validate_wallet_currency('Total associated after pending', '2.00');
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent(txTimeout)
.should('contain', 2.0);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('2.0', txTimeout)
.should('be.visible');
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
cy.staking_page_disassociate_tokens('1', {
stakingPageDisassociateTokens('1', {
type: 'contract',
skipConfirmation: true,
});
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
);
cy.validate_wallet_currency('Associated', '2.00');
cy.validate_wallet_currency('Pending association', '1.00');
cy.validate_wallet_currency('Total associated after pending', '1.00');
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '1.00');
validateWalletCurrency('Total associated after pending', '1.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.get(ethWalletAssociatedBalances, txTimeout)
.contains(vegaWalletPublicKeyShort)
.parent(txTimeout)
.should('contain', 1.0);
cy.get(ethWalletTotalAssociatedBalance, txTimeout)
.contains('1.0', txTimeout)
.should('be.visible');
verifyEthWalletAssociatedBalance('1.0');
verifyEthWalletTotalAssociatedBalance('1.0');
});
it('Able to associate & disassociate both wallet and vesting contract tokens', function () {
@@ -269,11 +224,9 @@ context(
// 1004-ASSO-020
// 1004-ASSO-021
// 1004-ASSO-022
cy.staking_page_associate_tokens('21', { type: 'wallet' });
stakingPageAssociateTokens('21', { type: 'wallet' });
cy.get('button').contains('Select a validator to nominate').click();
cy.staking_page_associate_tokens('37', { type: 'contract' });
stakingPageAssociateTokens('37', { type: 'contract' });
cy.get(vestingContractSection).within(() => {
cy.get(associatedKey).should(
'contain',
@@ -281,7 +234,6 @@ context(
);
cy.get(associatedAmount, txTimeout).should('contain', 37);
});
cy.get(vegaInWalletSection).within(() => {
cy.get(associatedKey).should(
'contain',
@@ -289,27 +241,21 @@ context(
);
cy.get(associatedAmount, txTimeout).should('contain', 21);
});
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 58);
});
cy.staking_page_disassociate_tokens('6', { type: 'contract' });
stakingPageDisassociateTokens('6', { type: 'contract' });
cy.get(vestingContractSection).within(() => {
cy.get(associatedAmount, txTimeout).should('contain', 31);
});
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 52);
});
cy.navigate_to('validators');
cy.staking_page_disassociate_tokens('9', { type: 'wallet' });
navigateTo(navigation.validators);
stakingPageDisassociateTokens('9', { type: 'wallet' });
cy.get(vegaInWalletSection).within(() => {
cy.get(associatedAmount, txTimeout).should('contain', 12);
});
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 43);
});
@@ -319,42 +265,41 @@ context(
// 1004-ASSO-008
// 1004-ASSO-010
// No warning visible as described in AC, but the button is disabled
cy.get(ethWalletAssociateButton).first().click();
cy.get(associateWalletRadioButton, { timeout: 30000 }).click();
cy.get(tokenSubmitButton, txTimeout).should('be.disabled'); // button disabled with no input
cy.get(tokenAmountInputBox, { timeout: 10000 }).type(6500000);
cy.get(tokenAmountInputBox, { timeout: 10000 }).type('6500000');
cy.get(tokenSubmitButton, txTimeout).should('be.disabled');
});
// 1004-ASSO-004
it('Pending association outside of app is shown', function () {
cy.vega_wallet_associate('2');
vegaWalletAssociate('2');
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
);
cy.validate_wallet_currency('Associated', '0.00');
cy.validate_wallet_currency('Pending association', '2.00');
cy.validate_wallet_currency('Total associated after pending', '2.00');
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.validate_wallet_currency('Associated', '2.00');
validateWalletCurrency('Associated', '2.00');
});
it('Disassociation outside of app is shown', function () {
cy.staking_page_associate_tokens('2');
cy.validate_wallet_currency('Associated', '2.00');
cy.vega_wallet_disassociate('2');
stakingPageAssociateTokens('2');
cy.wrap(validateWalletCurrency('Associated', '2.00')).then(() => {
vegaWalletDisassociate('2');
});
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
);
cy.validate_wallet_currency('Associated', '2.00');
cy.validate_wallet_currency('Pending association', '2.00');
cy.validate_wallet_currency('Total associated after pending', '0.00');
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.validate_wallet_currency('Associated', '0.00');
validateWalletCurrency('Associated', '0.00');
});
it('Able to associate tokens to different public key of connected vega wallet', function () {
@@ -371,7 +316,7 @@ context(
'have.text',
Cypress.env('vegaWalletPublicKey2')
);
cy.staking_page_associate_tokens('2');
stakingPageAssociateTokens('2');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
@@ -381,7 +326,7 @@ context(
'vegaWalletPublicKey2Short'
)} can now participate in governance and nominate a validator with your associated $VEGA.`
);
cy.staking_page_disassociate_all_tokens();
stakingPageDisassociateAllTokens();
});
});
}
@@ -1,5 +1,16 @@
import {
navigateTo,
navigation,
waitForSpinner,
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
depositAsset,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
const withdraw = 'withdraw';
const selectAsset = 'select-asset';
const withdrawalForm = 'withdraw-form';
const ethAddressInput = 'eth-address-input';
const amountInput = 'amount-input';
const balanceAvailable = 'BALANCE_AVAILABLE_value';
@@ -17,6 +28,8 @@ const completeWithdrawalButton = 'complete-withdrawal';
const usdtName = 'USDC (local)';
const usdcEthAddress = '0x1b8a1B6CBE5c93609b46D1829Cc7f3Cb8eeE23a0';
const usdcSymbol = 'tUSDC';
const usdtSelectValue =
'993ed98f4f770d91a796faab1738551193ba45c62341d20597df70fea6704ede';
const truncatedWithdrawalEthAddress = '0xEe7D…22d94F';
const formValidationError = 'input-error-text';
const txTimeout = Cypress.env('txTimeout');
@@ -26,60 +39,72 @@ context(
{ tags: '@slow' },
function () {
before('visit withdrawals and connect vega wallet', function () {
cy.updateCapsuleMultiSig(); // When running tests locally, will fail if run without restarting capsule
cy.deposit_asset(usdcEthAddress);
cy.visit('/');
// When running tests locally, will fail if run without restarting capsule
cy.updateCapsuleMultiSig().then(() => {
depositAsset(usdcEthAddress, '100');
});
});
beforeEach('Navigate to withdrawal page', function () {
cy.reload();
cy.visit('/');
cy.wait_for_spinner();
cy.navigate_to('withdraw');
waitForSpinner();
navigateTo(navigation.withdraw);
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.vega_wallet_teardown();
ethereumWalletConnect();
vegaWalletTeardown();
});
it('Able to open withdrawal form with vega wallet connected', function () {
// needs to reload page for withdrawal form to be displayed in ci - not reproducible outside of ci
cy.reload();
waitForSpinner();
ethereumWalletConnect();
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(selectAsset)
.find('option')
.should('have.length.at.least', 2);
cy.getByTestId(ethAddressInput).should('be.visible');
cy.getByTestId(amountInput).should('be.visible');
cy.getByTestId(withdrawalForm).within(() => {
cy.get('select').find('option').should('have.length.at.least', 2);
cy.getByTestId(ethAddressInput).should('be.visible');
cy.getByTestId(amountInput).should('be.visible');
});
});
it('Unable to submit withdrawal with invalid fields', function () {
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(selectAsset).select(usdtName);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should('have.length', 1);
cy.getByTestId(amountInput).clear().click().type('0.0000001');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should(
'have.text',
'Value is below minimum'
);
cy.getByTestId(amountInput).clear().click().type('10');
cy.getByTestId(ethAddressInput).click().type('123');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should(
'have.text',
'Invalid Ethereum address'
);
cy.getByTestId(withdrawalForm).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should('have.length', 1);
cy.getByTestId(amountInput).clear().click().type('0.0000001');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should(
'have.text',
'Value is below minimum'
);
cy.getByTestId(amountInput).clear().click().type('10');
cy.getByTestId(ethAddressInput).click().type('123');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should(
'have.text',
'Invalid Ethereum address'
);
});
});
it('Able to withdraw asset: -eth wallet connected -withdraw funds button', function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(selectAsset).select(usdtName);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should('have.text', '100,000.00000T');
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('100');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(withdrawalForm).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('100');
cy.getByTestId(submitWithdrawalButton).click();
});
cy.contains('Awaiting network confirmation').should('be.visible');
// assert withdrawal request
@@ -136,15 +161,17 @@ context(
waitForAssetsDisplayed(usdtName);
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(selectAsset).select(usdtName);
cy.getByTestId(ethAddressInput).should('be.empty');
cy.getByTestId(amountInput).click().type('100');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(withdrawalForm).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(ethAddressInput).should('be.empty');
cy.getByTestId(amountInput).click().type('100');
cy.getByTestId(submitWithdrawalButton).click();
// Need eth address to submit withdrawal
cy.getByTestId(formValidationError).should('have.length', 1);
cy.getByTestId(ethAddressInput).click().type(ethWalletAddress);
cy.getByTestId(submitWithdrawalButton).click();
// Need eth address to submit withdrawal
cy.getByTestId(formValidationError).should('have.length', 1);
cy.getByTestId(ethAddressInput).click().type(ethWalletAddress);
cy.getByTestId(submitWithdrawalButton).click();
});
cy.contains('Awaiting network confirmation').should('be.visible');
// assert withdrawal request
@@ -180,10 +207,12 @@ context(
cy.connectPublicKey(vegaWalletPubKey);
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(selectAsset).select(usdtName);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(amountInput).click().type('100');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(withdrawalForm).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(amountInput).click().type('100');
cy.getByTestId(submitWithdrawalButton).click();
});
cy.getByTestId('dialog-content').within(() => {
cy.get('h1').should('have.text', 'Transaction failed');
@@ -191,7 +220,7 @@ context(
});
});
function waitForAssetsDisplayed(expectedAsset) {
function waitForAssetsDisplayed(expectedAsset: string) {
cy.contains(expectedAsset, txTimeout).should('be.visible');
}
}
@@ -1,60 +1,12 @@
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"]';
import { waitForSpinner } from '../../support/common.functions';
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(() => {
@@ -68,7 +20,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket();
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
}
});
cy.getByTestId('proposals-list-item')
@@ -157,5 +109,56 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
});
describe('Mobile view - navigation bar', function () {
before('Change to mobile resolution', function () {
cy.viewport('iphone-xr');
});
it('should have burger button', () => {
cy.getByTestId('button-menu-drawer').should('be.visible').click();
cy.getByTestId('menu-drawer').should('be.visible');
});
it('should have link for proposal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
.and('have.text', 'Proposals');
});
});
it('should have link for validator page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/validators"]')
.first()
.should('exist')
.and('have.text', 'Validators');
});
});
it('should have link for rewards page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/rewards"]')
.first()
.should('exist')
.and('have.text', 'Rewards');
});
});
it('should have link for withdrawal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
.should('exist')
.and('have.text', 'Withdraw');
});
});
after(function () {
cy.viewport(
Cypress.config('viewportWidth'),
Cypress.config('viewportHeight')
);
});
});
});
});
@@ -1,3 +1,11 @@
import {
navigateTo,
navigation,
verifyPageHeader,
verifyTabHighlighted,
waitForSpinner,
} from '../../support/common.functions';
const proposalDocumentationLink = '[data-testid="proposal-documentation-link"]';
const newProposalButton = '[data-testid="new-proposal-link"]';
const newProposalLink = '[data-testid="new-proposal-link"]';
@@ -9,16 +17,17 @@ context(
{ tags: '@smoke' },
function () {
before('navigate to governance page', function () {
cy.visit('/').navigate_to('proposals');
cy.visit('/');
navigateTo(navigation.proposals);
});
describe('with no network change proposals', function () {
it('should have governance tab highlighted', function () {
cy.verify_tab_highlighted('proposals');
verifyTabHighlighted(navigation.proposals);
});
it('should have GOVERNANCE header visible', function () {
cy.verify_page_header('Proposals');
verifyPageHeader('Proposals');
});
it('should be able to see a working link for - find out more about Vega governance', function () {
@@ -60,8 +69,8 @@ context(
cy.get(connectToVegaWalletButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet');
cy.navigate_to('proposals');
cy.wait_for_spinner();
navigateTo(navigation.proposals);
waitForSpinner();
});
});
}
@@ -1,29 +1,36 @@
/// <reference types="cypress" />
import {
navigateTo,
navigation,
waitForSpinner,
} from '../../support/common.functions';
import {
enterUniqueFreeFormProposalBody,
goToMakeNewProposal,
} from '../../support/governance.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey2');
const vegaPubkeyTruncated = Cypress.env('vegaWalletPublicKey2Short');
const banner = 'view-banner';
context('View functionality with public key', { tags: '@smoke' }, function () {
before('send asset to wallet', function () {
cy.vega_wallet_faucet_assets_without_check(
'fUSDC',
'1000000',
vegaWalletPubKey
);
vegaWalletFaucetAssetsWithoutCheck('fUSDC', '1000000', vegaWalletPubKey);
});
beforeEach('visit home page', function () {
cy.visit('/');
cy.wait_for_spinner();
waitForSpinner();
cy.connectPublicKey(vegaWalletPubKey);
});
it('Able to connect public key via wallet', function () {
verifyConnectedToPubKey();
cy.getByTestId('currency-title', Cypress.env('epochTimeout')).should(
'contain.text',
'USDC (fake)'
);
cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)');
});
it('Able to connect public key using url', function () {
@@ -35,9 +42,9 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
it('Unable to submit proposal with public key', function () {
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
cy.navigate_to('proposals');
cy.go_to_make_new_proposal('Freeform');
cy.enter_unique_freeform_proposal_body('50', 'pub key proposal test');
navigateTo(navigation.proposals);
goToMakeNewProposal('Freeform');
enterUniqueFreeFormProposalBody('50', 'pub key proposal test');
cy.getByTestId('dialog-content').within(() => {
cy.get('h1').should('have.text', 'Transaction failed');
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
@@ -50,7 +57,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,3 +1,9 @@
import {
navigateTo,
navigation,
verifyPageHeader,
} from '../../support/common.functions';
const viewToggle = '[data-testid="epoch-reward-view-toggle-total"]';
const warning = '[data-testid="callout"]';
@@ -6,16 +12,17 @@ context(
{ tags: '@regression' },
function () {
before('navigate to rewards page', function () {
cy.visit('/').navigate_to('rewards');
cy.visit('/');
navigateTo(navigation.rewards);
});
describe('with wallets disconnected', function () {
it('should have REWARDS tab highlighted', function () {
cy.verify_tab_highlighted('rewards');
verifyPageHeader('Rewards and fees');
});
it('should have rewards header visible', function () {
cy.verify_page_header('Rewards and fees');
verifyPageHeader('Rewards and fees');
});
it('should have epoch warning', function () {
@@ -23,7 +30,7 @@ context(
.should('be.visible')
.and(
'have.text',
'Rewards are credited 5 minutes after the epoch ends.This delay is set by a network parameter'
'Rewards are credited less than a minute after the epoch ends.This delay is set by a network parameter'
);
});
@@ -1,3 +1,5 @@
import { navigateTo, navigation } from '../../support/common.functions';
const tokenDetailsTable = '.token-details';
const address = '[data-testid="token-address"]';
const contract = '[data-testid="token-contract"]';
@@ -18,7 +20,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');
navigateTo(navigation.token);
});
describe('THE $VEGA TOKEN table', function () {
it('should have TOKEN ADDRESS', function () {
@@ -1,4 +1,5 @@
import { trancheData } from '../../fixtures/mocks/tranches';
import { navigateTo, navigation } from '../../support/common.functions';
const tranches = trancheData;
@@ -12,7 +13,7 @@ context(
});
it('Able to navigate to tranches page', function () {
cy.navigate_to('supply');
navigateTo(navigation.supply);
cy.url().should('include', '/token/tranches');
cy.get('h1').should('contain.text', 'Vesting tranches');
});
@@ -68,7 +69,7 @@ context(
});
it('Able to view tranches with less than 10 vega', function () {
cy.navigate_to('supply');
navigateTo(navigation.supply);
cy.getByTestId('show-all-tranches').click();
cy.getByTestId('tranche-item')
.should('have.length', 8)
@@ -1,234 +0,0 @@
/// <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"]';
const ethAddressLink = '[data-testid="link"]';
const validatorStatus = '[data-testid="validator-status"]';
const totalStake = '[data-testid="total-stake"]';
const pendingStake = '[data-testid="pending-stake"]';
const stakedByOperator = '[data-testid="staked-by-operator"]';
const stakedByDelegates = '[data-testid="staked-by-delegates"]';
const stakeShare = '[data-testid="stake-percentage"]';
const stakedByOperatorToolTip = "[data-testid='staked-operator-tooltip']";
const stakedByDelegatesToolTip = "[data-testid='staked-delegates-tooltip']";
const totalStakedToolTip = "[data-testid='total-staked-tooltip']";
const unnormalisedVotingPowerToolTip =
"[data-testid='unnormalised-voting-power-tooltip']";
const normalisedVotingPowerToolTip =
"[data-testid='normalised-voting-power-tooltip']";
const performancePenaltyToolTip = "[data-testid='performance-penalty-tooltip']";
const overstakedPenaltyToolTip = "[data-testid='overstaked-penalty-tooltip']";
const totalPenaltyToolTip = "[data-testid='total-penalty-tooltip']";
const epochCountDown = '[data-testid="epoch-countdown"]';
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'
);
});
});
});
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 2001-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.get('[col-id="stake"] > div > span > span')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should be able to see validator stake tooltip', function () {
cy.get('[col-id="stake"] > div > span > span').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 0.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 0.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator voting power tooltip', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.first()
.realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 0.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 0.10%');
});
it('Should be able to see validator total penalties', function () {
cy.get('[col-id="totalPenalties"] > div > span > span')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
cy.get('[col-id="totalPenalties"] > div > span > span').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 100.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty:'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 0.00%');
});
it('Should be able to see validator pending stake', function () {
cy.get('[col-id="pendingStake"] > div > span')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 2001-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
cy.click_on_validator_from_list(0);
});
// 2001-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 2001-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 2001-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 2001-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink).should('not.be.empty').and('have.attr', 'href');
});
// TODO validators missing url for more information about them 2001-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 2001-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 2001-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 2001-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
});
@@ -0,0 +1,246 @@
/// <reference types="cypress" />
import {
navigateTo,
navigation,
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
import { clickOnValidatorFromList } from '../../support/staking.functions';
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"]';
const ethAddressLink = '[data-testid="link"]';
const validatorStatus = '[data-testid="validator-status"]';
const totalStake = '[data-testid="total-stake"]';
const pendingStake = '[data-testid="pending-stake"]';
const stakedByOperator = '[data-testid="staked-by-operator"]';
const stakedByDelegates = '[data-testid="staked-by-delegates"]';
const stakeShare = '[data-testid="stake-percentage"]';
const stakedByOperatorToolTip = '[data-testid="staked-operator-tooltip"]';
const stakedByDelegatesToolTip = '[data-testid="staked-delegates-tooltip"]';
const totalStakedToolTip = '[data-testid="total-staked-tooltip"]';
const unnormalisedVotingPowerToolTip =
'[data-testid="unnormalised-voting-power-tooltip"]';
const normalisedVotingPowerToolTip =
'[data-testid="normalised-voting-power-tooltip"]';
const performancePenaltyToolTip = '[data-testid="performance-penalty-tooltip"]';
const overstakedPenaltyToolTip = '[data-testid="overstaked-penalty-tooltip"]';
const totalPenaltyToolTip = '[data-testid="total-penalty-tooltip"]';
const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d*\.?\d*$/;
context('Validators Page - verify elements on page', function () {
before('navigate to validators page', function () {
cy.visit('/');
navigateTo(navigation.validators);
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
describe('description section', function () {
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('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'
);
});
});
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 2001-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.get('[col-id="stake"] > div > span > span')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should be able to see validator stake tooltip', function () {
cy.get('[col-id="stake"] > div > span > span').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 0.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 0.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator voting power tooltip', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.first()
.realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 0.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 0.10%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.get('[col-id="totalPenalties"] > div > span > span')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
cy.get('[col-id="totalPenalties"] > div > span > span').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 100.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty:'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 0.00%');
});
it('Should be able to see validator pending stake', function () {
cy.get('[col-id="pendingStake"] > div > span')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 2001-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 2001-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 2001-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 2001-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 2001-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink)
.should('not.be.empty')
.and('have.attr', 'href');
});
// TODO validators missing url for more information about them 2001-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 2001-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 2001-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 2001-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
});
});
@@ -1,3 +1,11 @@
import {
navigateTo,
navigation,
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
const connectButton = '[data-testid="connect-to-eth-btn"]';
const lockedTokensInVestingContract = '6,499,972.30';
@@ -6,16 +14,17 @@ context(
{ tags: '@smoke' },
function () {
before('navigate to vesting page', function () {
cy.visit('/').navigate_to('vesting');
cy.visit('/');
navigateTo(navigation.vesting);
});
describe('with wallets disconnected', function () {
it('should have vesting tab highlighted', function () {
cy.verify_tab_highlighted('token');
verifyTabHighlighted(navigation.vesting);
});
it('should have VESTING header visible', function () {
cy.verify_page_header('Vesting');
verifyPageHeader('Vesting');
});
// 1005-VEST-018
@@ -28,7 +37,7 @@ context(
describe('With Eth wallet connected', function () {
before('connect eth wallet', function () {
cy.ethereum_wallet_connect();
ethereumWalletConnect();
cy.getByTestId('view-connected-eth-btn').click();
});
@@ -38,15 +47,18 @@ context(
cy.getByTestId('currency-title')
.should('contain.text', 'VEGA')
.and('contain.text', 'In vesting contract');
cy.getByTestId('currency-value').should(
cy.get('[data-testid="currency-value"]:visible').should(
'have.text',
lockedTokensInVestingContract
);
cy.getByTestId('currency-locked').should(
cy.get('[data-testid="currency-locked"]:visible').should(
'have.text',
lockedTokensInVestingContract
);
cy.getByTestId('currency-unlocked').should('have.text', '0.00');
cy.get('[data-testid="currency-unlocked"]:visible').should(
'have.text',
'0.00'
);
});
});
// 1005-VEST-022 1005-VEST-023
@@ -120,7 +132,6 @@ context(
'All the tokens in this tranche are locked and can not be redeemed yet.'
);
});
cy.connectVegaWallet();
});
});
}
@@ -1,18 +1,22 @@
const walletContainer = '[data-testid="ethereum-wallet"]';
import { convertTokenValueToNumber } from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
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"]';
@@ -86,7 +90,7 @@ context(
// 0004-EWAL-001 0004-EWAL-002
describe('when Ethereum wallet connected', function () {
before('connect to Ethereum wallet', function () {
cy.ethereum_wallet_connect();
ethereumWalletConnect();
});
it('should have ETHEREUM KEY header visible', function () {
@@ -177,22 +181,25 @@ context(
.within(() => {
cy.get(currencyValue)
.invoke('text')
.convert_token_value_to_number()
.as('value');
.then((currencyValueTxt) => {
convertTokenValueToNumber(currencyValueTxt).as('value');
});
cy.get(currencyLocked)
.invoke('text')
.convert_token_value_to_number()
.as('locked');
.then((currencyLockedTxt) => {
convertTokenValueToNumber(currencyLockedTxt).as('locked');
});
cy.get(currencyUnlocked)
.invoke('text')
.convert_token_value_to_number()
.as('unlocked');
.then((currencyUnlockedTxt) => {
convertTokenValueToNumber(currencyUnlockedTxt).as('unlocked');
});
})
.then(function () {
expect(parseFloat(this.value).toFixed(1)).to.equal(
parseFloat(
Math.round((this.locked + this.unlocked) * 100) / 100
).toFixed(1)
(Math.round((this.locked + this.unlocked) * 100) / 100).toFixed(
1
)
);
});
});
@@ -246,16 +253,19 @@ context(
.within(() => {
cy.get(currencyValue)
.invoke('text')
.convert_token_value_to_number()
.as('value');
.then((currencyValueTxt) => {
convertTokenValueToNumber(currencyValueTxt).as('value');
});
cy.get(currencyLocked)
.invoke('text')
.convert_token_value_to_number()
.as('locked');
.then((currencyLockedTxt) => {
convertTokenValueToNumber(currencyLockedTxt).as('locked');
});
cy.get(currencyUnlocked)
.invoke('text')
.convert_token_value_to_number()
.as('unlocked');
.then((currencyUnlockedTxt) => {
convertTokenValueToNumber(currencyUnlockedTxt).as('unlocked');
});
})
.then(function () {
expect(this.value).to.equal(this.locked + this.unlocked);
@@ -1,6 +1,9 @@
import { truncateByChars } from '@vegaprotocol/utils';
import { waitForSpinner } from '../../support/common.functions';
import { vegaWalletTeardown } from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
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 +17,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 +30,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 +56,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')
@@ -150,7 +132,7 @@ context(
before('connect vega wallet', function () {
cy.visit('/');
cy.connectVegaWallet();
cy.vega_wallet_teardown();
vegaWalletTeardown();
});
// 0002-WCON-007
@@ -175,19 +157,11 @@ 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)
.should('be.visible')
.and('have.text', `VEGAAssociated`);
.and('contain.text', `VEGAAssociated`);
});
});
@@ -196,14 +170,19 @@ context(
{ tags: '@smoke' },
function () {
cy.get(walletContainer).within(() => {
cy.get(currencyValue).should('be.visible').and('have.text', `0.00`);
cy.get(currencyValue)
.should('be.visible')
.and('contain.text', `0.00`);
});
}
);
it('should have Unstaked value visible', { tags: '@smoke' }, function () {
cy.get(walletContainer).within(() => {
cy.get(vegaUnstaked).should('be.visible').and('have.text', `0.00`);
cy.get(vegaUnstaked)
.should('be.visible')
.invoke('text')
.and('not.be.empty');
});
});
@@ -299,113 +278,73 @@ 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) {
vegaWalletFaucetAssetsWithoutCheck(id, amount, vegaWalletPublicKey);
}
cy.reload();
cy.wait_for_spinner();
waitForSpinner();
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');
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.getByTestId('currency-title', txTimeout).should(
'have.length.at.least',
5
);
});
});
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');
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()
.within(() => cy.contains_exactly('6.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(id)
.parent()
.siblings()
.invoke('text')
.then((el) => {
const value = parseFloat(el);
cy.wrap(value).should('be.gte', parseFloat(expectedAmount));
});
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
.contains(currency.name);
cy.get(vegaWalletCurrencyTitle)
.contains(id)
.parent()
.contains(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,3 +1,10 @@
import {
navigateTo,
navigation,
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
context(
@@ -5,16 +12,17 @@ context(
{ tags: '@smoke' },
function () {
before('navigate to withdrawals page', function () {
cy.visit('/').navigate_to('withdraw');
cy.visit('/');
navigateTo(navigation.withdraw);
});
describe('with wallets disconnected', function () {
it('should have withdraw tab highlighted', function () {
cy.verify_tab_highlighted('token');
verifyTabHighlighted(navigation.withdraw);
});
it('should have WITHDRAW header visible', function () {
cy.verify_page_header('Withdrawals');
verifyPageHeader('Withdrawals');
});
it('should have connect Vega wallet button', function () {

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