Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo into fix/603-656-609-filter-rejected-markets-order-when-suspended-remove-trading-continuous
This commit is contained in:
commit
3c02d9f086
@ -71,7 +71,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "120450.26235752800414383",
|
||||
"locked_amount": "120390.925836805547370405",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@ -521,7 +521,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "61372.6885461730976664",
|
||||
"locked_amount": "61314.48494679130129641",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@ -554,7 +554,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "83885.86968996291364071230136",
|
||||
"locked_amount": "83806.31541804023550991778112",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@ -587,7 +587,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "25818.43022173346529338",
|
||||
"locked_amount": "25793.944972595938010492",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@ -620,7 +620,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "8403.648501057666105483",
|
||||
"locked_amount": "8395.67879006263668482",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@ -708,7 +708,7 @@
|
||||
"tranche_end": "2022-11-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "13297.7850430253625",
|
||||
"locked_amount": "13267.19882246376825",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "15000",
|
||||
@ -794,7 +794,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "179856.049568108351",
|
||||
"locked_amount": "1710040.653982735209492448",
|
||||
"locked_amount": "1708711.259252262998351268",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@ -1846,7 +1846,7 @@
|
||||
"tranche_end": "2022-09-30T00:00:00.000Z",
|
||||
"total_added": "60916.66666633337",
|
||||
"total_removed": "19238.601152747179372649",
|
||||
"locked_amount": "11987.514524408138935750336335075",
|
||||
"locked_amount": "11948.4454929703980087638493118875",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2833.333333",
|
||||
@ -5415,9 +5415,9 @@
|
||||
"tranche_id": 11,
|
||||
"tranche_start": "2021-09-03T00:00:00.000Z",
|
||||
"tranche_end": "2022-09-03T00:00:00.000Z",
|
||||
"total_added": "24344.000000000000000003",
|
||||
"total_added": "24799.000000000000000003",
|
||||
"total_removed": "7424.31284562443",
|
||||
"locked_amount": "3317.87584195839735528040887395357686461",
|
||||
"locked_amount": "3362.8941250634193663004068181126331811",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "25",
|
||||
@ -5889,6 +5889,31 @@
|
||||
"user": "0x44d145E145B7811ad309032D4c5CdeB1bf044719",
|
||||
"tx": "0x23fdff0f268bba36817cb496454cbc4be34306eaaaf330d5d26134849c480153"
|
||||
},
|
||||
{
|
||||
"amount": "200",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tx": "0xf88b8ed5ce899f00f466830936599112ea678a4e5911801fb3e445c22eb0295f"
|
||||
},
|
||||
{
|
||||
"amount": "100",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tx": "0x0c0ea0139cc752d22c0f8a6b3ffa5c4bcdca1c5604089ebd0f34898f1049db61"
|
||||
},
|
||||
{
|
||||
"amount": "90",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tx": "0x29371816157718f7d5136d0ed8a0f1e09045ee3cfcd67199d45e3f4157f7e011"
|
||||
},
|
||||
{
|
||||
"amount": "35",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tx": "0x6a081b2c5dfa2d5c916a598569519605322721c5511d26cf4aed8e08e49cf700"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tx": "0xc37be614ef14bbbedc808ffc7651fbb7a3c04e4d1612f00f19299da7916003e6"
|
||||
},
|
||||
{
|
||||
"amount": "75",
|
||||
"user": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
||||
@ -10138,6 +10163,45 @@
|
||||
"withdrawn_tokens": "548.42303763865",
|
||||
"remaining_tokens": "86.57696236135"
|
||||
},
|
||||
{
|
||||
"address": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "200",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xf88b8ed5ce899f00f466830936599112ea678a4e5911801fb3e445c22eb0295f"
|
||||
},
|
||||
{
|
||||
"amount": "100",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x0c0ea0139cc752d22c0f8a6b3ffa5c4bcdca1c5604089ebd0f34898f1049db61"
|
||||
},
|
||||
{
|
||||
"amount": "90",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x29371816157718f7d5136d0ed8a0f1e09045ee3cfcd67199d45e3f4157f7e011"
|
||||
},
|
||||
{
|
||||
"amount": "35",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x6a081b2c5dfa2d5c916a598569519605322721c5511d26cf4aed8e08e49cf700"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xc37be614ef14bbbedc808ffc7651fbb7a3c04e4d1612f00f19299da7916003e6"
|
||||
}
|
||||
],
|
||||
"withdrawals": [],
|
||||
"total_tokens": "455",
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "455"
|
||||
},
|
||||
{
|
||||
"address": "0x3a380f7CFdEeb723228cA57d2795EA215094000d",
|
||||
"deposits": [
|
||||
@ -15970,7 +16034,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "106452.6400159857469",
|
||||
"locked_amount": "2652238.43698016889396750676",
|
||||
"locked_amount": "2650195.62012551847485631374",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@ -16705,7 +16769,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15788853.065470999700000001",
|
||||
"total_removed": "63288.646809993954255",
|
||||
"locked_amount": "14629073.3850061841914328106732955796688294",
|
||||
"locked_amount": "14621866.7729235704178754560630990929629629",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@ -19047,8 +19111,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "2414153.780525405655617797",
|
||||
"locked_amount": "7853521.23058973302728403033753401",
|
||||
"total_removed": "2414740.542831207365597297",
|
||||
"locked_amount": "7846833.585955419427221742736577",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@ -19337,6 +19401,11 @@
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x63919ba0a519b04dfe5a5b222e5952117f1cdbb1e6194ae85b22a4ed8cef6fa5"
|
||||
},
|
||||
{
|
||||
"amount": "586.7623058017099795",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xd2d49b3e73cd00446605bc1838132e4a985c89452600447ec1886cd64f54c200"
|
||||
},
|
||||
{
|
||||
"amount": "509.31853983395369725",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@ -21071,6 +21140,12 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0x63919ba0a519b04dfe5a5b222e5952117f1cdbb1e6194ae85b22a4ed8cef6fa5"
|
||||
},
|
||||
{
|
||||
"amount": "586.7623058017099795",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xd2d49b3e73cd00446605bc1838132e4a985c89452600447ec1886cd64f54c200"
|
||||
},
|
||||
{
|
||||
"amount": "509.31853983395369725",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@ -22417,8 +22492,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "165410.8035768271129215",
|
||||
"remaining_tokens": "193712.6659981728870785"
|
||||
"withdrawn_tokens": "165997.565882628822901",
|
||||
"remaining_tokens": "193125.903692371177099"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@ -23482,7 +23557,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "1572374.955637071973431516",
|
||||
"locked_amount": "2786070.33236677382072939102631165",
|
||||
"locked_amount": "2783428.003525218209183951863562051",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@ -24704,7 +24779,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "279.2296426169",
|
||||
"locked_amount": "420262.3411807600824310398427194",
|
||||
"locked_amount": "419938.63019460327182018065246068",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@ -50502,7 +50577,7 @@
|
||||
"tranche_start": "2021-12-05T00:00:00.000Z",
|
||||
"tranche_end": "2022-06-05T00:00:00.000Z",
|
||||
"total_added": "171288.42",
|
||||
"total_removed": "36852.7133578316377",
|
||||
"total_removed": "37437.7065343307989",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@ -54782,6 +54857,31 @@
|
||||
"user": "0xe627552C6338855983a147fa5235E5D92a3C2891",
|
||||
"tx": "0x3c75d0f2db07e4b2c54411e3d1a6124e63bc0f99ca4d171bd8679158aee3e698"
|
||||
},
|
||||
{
|
||||
"amount": "117.3375178075",
|
||||
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
||||
"tx": "0x6d40af7a502c2fb843a6bdd7313a5c66811e62cadc5a75521aa49d886635634d"
|
||||
},
|
||||
{
|
||||
"amount": "116.827485434478",
|
||||
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||
"tx": "0x04b50ec6c4ff0f0cc3d9f2b500e6e91e920c901c98241d4ea595c03dfef12dfb"
|
||||
},
|
||||
{
|
||||
"amount": "117.344131565",
|
||||
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||
"tx": "0xe297545c0eb18382b44e8bc9fdd1fb78db661757843ef2f5513d980a470ed873"
|
||||
},
|
||||
{
|
||||
"amount": "116.1440755221832",
|
||||
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||
"tx": "0xa789793e6cad59188752a73b219c335d19aaf5a2c7199edf45db321a92a6c0c9"
|
||||
},
|
||||
{
|
||||
"amount": "117.33996617",
|
||||
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||
"tx": "0x69b4d26b8ecf677bc94a8b715fd4c7f56cfb535f50cb8cbf01f06520917cf97f"
|
||||
},
|
||||
{
|
||||
"amount": "60.4448387275",
|
||||
"user": "0xEe3183EcE9ee7d73Fb7bA7F4eB262A2dE68C42B0",
|
||||
@ -66307,6 +66407,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "117.3375178075",
|
||||
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x6d40af7a502c2fb843a6bdd7313a5c66811e62cadc5a75521aa49d886635634d"
|
||||
},
|
||||
{
|
||||
"amount": "132.6624821925",
|
||||
"user": "0xE8E8Ee384f5751Df12b5FA53203Dae5BC739493A",
|
||||
@ -66315,8 +66421,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "132.6624821925",
|
||||
"remaining_tokens": "117.3375178075"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||
@ -66329,6 +66435,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "117.33996617",
|
||||
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x69b4d26b8ecf677bc94a8b715fd4c7f56cfb535f50cb8cbf01f06520917cf97f"
|
||||
},
|
||||
{
|
||||
"amount": "132.66003383",
|
||||
"user": "0x23A9974c47827A596FF04930308ed49afAE3B32e",
|
||||
@ -66337,8 +66449,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "132.66003383",
|
||||
"remaining_tokens": "117.33996617"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||
@ -66351,6 +66463,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "116.1440755221832",
|
||||
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||
"tranche_id": 6,
|
||||
"tx": "0xa789793e6cad59188752a73b219c335d19aaf5a2c7199edf45db321a92a6c0c9"
|
||||
},
|
||||
{
|
||||
"amount": "131.2759244778168",
|
||||
"user": "0x3752a11Ecf3b01fca00dDe8feB3DC421eBa8a279",
|
||||
@ -66359,8 +66477,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "247.42",
|
||||
"withdrawn_tokens": "131.2759244778168",
|
||||
"remaining_tokens": "116.1440755221832"
|
||||
"withdrawn_tokens": "247.42",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||
@ -66373,6 +66491,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "116.827485434478",
|
||||
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||
"tranche_id": 6,
|
||||
"tx": "0x04b50ec6c4ff0f0cc3d9f2b500e6e91e920c901c98241d4ea595c03dfef12dfb"
|
||||
},
|
||||
{
|
||||
"amount": "132.092514565522",
|
||||
"user": "0x204A4128c015Ca88E7279E07B35F62dca12b3BFc",
|
||||
@ -66381,8 +66505,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "248.92",
|
||||
"withdrawn_tokens": "132.092514565522",
|
||||
"remaining_tokens": "116.827485434478"
|
||||
"withdrawn_tokens": "248.92",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||
@ -66395,6 +66519,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "117.344131565",
|
||||
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||
"tranche_id": 6,
|
||||
"tx": "0xe297545c0eb18382b44e8bc9fdd1fb78db661757843ef2f5513d980a470ed873"
|
||||
},
|
||||
{
|
||||
"amount": "132.655868435",
|
||||
"user": "0xEa530644eD9fbb022bBDBe54c0CF73Be49C68D64",
|
||||
@ -66403,8 +66533,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "250",
|
||||
"withdrawn_tokens": "132.655868435",
|
||||
"remaining_tokens": "117.344131565"
|
||||
"withdrawn_tokens": "250",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xB93EB9807aC74B3004B38E5fB66605578C86A145",
|
||||
|
@ -38,7 +38,7 @@
|
||||
"tranche_end": "2022-11-26T13:48:10.000Z",
|
||||
"total_added": "100",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "36.800409056316585",
|
||||
"locked_amount": "36.73188419583967",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "100",
|
||||
@ -242,7 +242,7 @@
|
||||
"tranche_end": "2022-10-12T00:53:20.000Z",
|
||||
"total_added": "1100",
|
||||
"total_removed": "673.04388635",
|
||||
"locked_amount": "267.5664542110604",
|
||||
"locked_amount": "266.812680745814324",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1000",
|
||||
|
@ -69,7 +69,7 @@
|
||||
"tranche_end": "2022-10-12T00:53:20.000Z",
|
||||
"total_added": "1010.000000000000000001",
|
||||
"total_removed": "668.4622323651",
|
||||
"locked_amount": "245.674653411973640000243242231100964",
|
||||
"locked_amount": "244.98258434804671850024255701420598685",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1000",
|
||||
|
@ -46,7 +46,7 @@ describe('fills', () => {
|
||||
];
|
||||
const result = generateFills({
|
||||
party: {
|
||||
tradesPaged: {
|
||||
tradesConnection: {
|
||||
edges: fills.map((f, i) => {
|
||||
return {
|
||||
__typename: 'TradeEdge',
|
||||
|
@ -1,13 +1,13 @@
|
||||
import type {
|
||||
Fills,
|
||||
Fills_party_tradesPaged_edges_node,
|
||||
Fills_party_tradesConnection_edges_node,
|
||||
} from '@vegaprotocol/fills';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||
const fills: Fills_party_tradesPaged_edges_node[] = [
|
||||
const fills: Fills_party_tradesConnection_edges_node[] = [
|
||||
generateFill({
|
||||
buyer: {
|
||||
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||
@ -49,7 +49,7 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||
const defaultResult: Fills = {
|
||||
party: {
|
||||
id: 'buyer-id',
|
||||
tradesPaged: {
|
||||
tradesConnection: {
|
||||
__typename: 'TradeConnection',
|
||||
totalCount: 1,
|
||||
edges: fills.map((f) => {
|
||||
@ -73,9 +73,9 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||
};
|
||||
|
||||
export const generateFill = (
|
||||
override?: PartialDeep<Fills_party_tradesPaged_edges_node>
|
||||
override?: PartialDeep<Fills_party_tradesConnection_edges_node>
|
||||
) => {
|
||||
const defaultFill: Fills_party_tradesPaged_edges_node = {
|
||||
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
||||
__typename: 'Trade',
|
||||
id: '0',
|
||||
createdAt: new Date().toISOString(),
|
||||
|
@ -1,6 +1,5 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||
|
@ -1,6 +1,5 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
|
@ -43,6 +43,7 @@ const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
|
||||
|
||||
const mockEnvironment = {
|
||||
VEGA_ENV: 'TESTNET',
|
||||
VEGA_URL: 'https://vega-node.url',
|
||||
VEGA_NETWORKS: JSON.stringify({}),
|
||||
GIT_BRANCH: 'test',
|
||||
GIT_COMMIT_HASH: 'abcdef',
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './network-loader';
|
1
libs/environment/src/components/network-loader/index.tsx
Normal file
1
libs/environment/src/components/network-loader/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NetworkLoader } from './network-loader';
|
@ -1,128 +1,8 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ApolloClient } from '@apollo/client';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import {
|
||||
Callout,
|
||||
Intent,
|
||||
Button,
|
||||
Icon,
|
||||
Loader,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useEnvironment } from '../../hooks';
|
||||
import type { ConfigStatus } from '../../types';
|
||||
|
||||
type MessageComponentProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StatusMessage = ({ children }: MessageComponentProps) => (
|
||||
<div className="flex items-center fixed bottom-0 right-0 px-16 bg-intent-highlight text-black">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
type ErrorComponentProps = MessageComponentProps & {
|
||||
children?: ReactNode;
|
||||
showTryAgain?: boolean;
|
||||
};
|
||||
|
||||
const Error = ({ children, showTryAgain }: ErrorComponentProps) => (
|
||||
<div>
|
||||
<div className="mb-16">{children}</div>
|
||||
{showTryAgain && (
|
||||
<Button
|
||||
className="mt-8"
|
||||
variant="secondary"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
{t('Try again')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
type StatusComponentProps = {
|
||||
status: ConfigStatus;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const StatusComponent = ({ status, children }: StatusComponentProps) => {
|
||||
switch (status) {
|
||||
case 'error-loading-config':
|
||||
return (
|
||||
<Callout
|
||||
title={t('Error')}
|
||||
intent={Intent.Danger}
|
||||
iconName="error"
|
||||
iconDescription={t('Error')}
|
||||
children={
|
||||
<Error>
|
||||
{t('There was an error fetching the network configuration.')}
|
||||
</Error>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'error-validating-config':
|
||||
return (
|
||||
<Callout
|
||||
title={t('Error')}
|
||||
intent={Intent.Danger}
|
||||
iconName="error"
|
||||
iconDescription={t('Error')}
|
||||
children={
|
||||
<Error>
|
||||
{t('The network configuration for the app is invalid.')}
|
||||
</Error>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'error-loading-node':
|
||||
return (
|
||||
<Callout
|
||||
title={t('Error')}
|
||||
intent={Intent.Danger}
|
||||
iconName="error"
|
||||
iconDescription={t('Error')}
|
||||
children={
|
||||
<Error showTryAgain>{t('Failed to connect to a data node.')}</Error>
|
||||
}
|
||||
/>
|
||||
);
|
||||
case 'idle':
|
||||
case 'loading-config':
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<StatusMessage>
|
||||
<Loader size="small" forceTheme="light" />
|
||||
<span className="ml-8">{t('Loading configuration...')}</span>
|
||||
</StatusMessage>
|
||||
</>
|
||||
);
|
||||
case 'loading-node':
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<StatusMessage>
|
||||
<Loader size="small" forceTheme="light" />
|
||||
<span className="ml-8">{t('Finding a node...')}</span>
|
||||
</StatusMessage>
|
||||
</>
|
||||
);
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<StatusMessage>
|
||||
<Icon name="antenna" />
|
||||
<span className="ml-8">{t("You're connected!")}</span>
|
||||
</StatusMessage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type NetworkLoaderProps<T> = {
|
||||
children?: ReactNode;
|
||||
@ -135,9 +15,7 @@ export function NetworkLoader<T>({
|
||||
children,
|
||||
createClient,
|
||||
}: NetworkLoaderProps<T>) {
|
||||
// this is to prevent an error rendering callouts on the server side
|
||||
const [canShowCallout, setShowCallout] = useState(false);
|
||||
const { configStatus, VEGA_URL } = useEnvironment();
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
|
||||
const client = useMemo(() => {
|
||||
if (VEGA_URL) {
|
||||
@ -146,16 +24,12 @@ export function NetworkLoader<T>({
|
||||
return undefined;
|
||||
}, [VEGA_URL, createClient]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCallout(true);
|
||||
}, []);
|
||||
|
||||
if (!client) {
|
||||
return canShowCallout ? (
|
||||
return (
|
||||
<div className="h-full min-h-screen flex items-center justify-center">
|
||||
<StatusComponent status={configStatus}>{skeleton}</StatusComponent>
|
||||
{skeleton}
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
return <ApolloProvider client={client}>{children}</ApolloProvider>;
|
||||
|
@ -30,8 +30,10 @@ export const NetworkSwitcher = ({ onConnect }: NetworkSwitcherProps) => {
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onChange={field.onChange}>
|
||||
{Object.keys(VEGA_NETWORKS).map((network) => (
|
||||
<option value={network}>{network}</option>
|
||||
{Object.keys(VEGA_NETWORKS).map((network, index) => (
|
||||
<option key={index} value={network}>
|
||||
{network}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
|
@ -1,27 +1,59 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { NodeSwitcher } from '../node-switcher';
|
||||
import { useEnvironment } from '../../hooks/use-environment';
|
||||
import type { Configuration } from '../../types';
|
||||
|
||||
type NodeSwitcherDialogProps = ComponentProps<typeof NodeSwitcher> & {
|
||||
type NodeSwitcherDialogProps = Pick<
|
||||
ComponentProps<typeof NodeSwitcher>,
|
||||
'initialErrorType' | 'onConnect'
|
||||
> & {
|
||||
loading: boolean;
|
||||
config?: Configuration;
|
||||
dialogOpen: boolean;
|
||||
toggleDialogOpen: (dialogOpen: boolean) => void;
|
||||
setDialogOpen: (dialogOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const NodeSwitcherDialog = ({
|
||||
config,
|
||||
loading,
|
||||
initialErrorType,
|
||||
dialogOpen,
|
||||
toggleDialogOpen,
|
||||
setDialogOpen,
|
||||
onConnect,
|
||||
}: NodeSwitcherDialogProps) => {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
return (
|
||||
<Dialog open={dialogOpen} onChange={toggleDialogOpen}>
|
||||
<NodeSwitcher
|
||||
config={config}
|
||||
onConnect={(url) => {
|
||||
onConnect(url);
|
||||
toggleDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
<Dialog open={dialogOpen} onChange={setDialogOpen}>
|
||||
<div className="uppercase text-h3 text-center mb-8">
|
||||
{t('Connected node')}
|
||||
</div>
|
||||
{!config && loading && (
|
||||
<div className="py-16">
|
||||
<p className="mb-32 text-center">{t('Loading configuration...')}</p>
|
||||
<Loader size="large" />
|
||||
</div>
|
||||
)}
|
||||
{config && dialogOpen && (
|
||||
<>
|
||||
<p className="mb-32 text-center">
|
||||
{t(`This app will only work on a `)}
|
||||
<span className="font-mono capitalize">
|
||||
{VEGA_ENV.toLowerCase()}
|
||||
</span>
|
||||
{t(' chain ID')}
|
||||
</p>
|
||||
<NodeSwitcher
|
||||
config={config}
|
||||
initialErrorType={initialErrorType}
|
||||
onConnect={(url) => {
|
||||
onConnect(url);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: BlockHeightStats
|
||||
// ====================================================
|
||||
|
||||
export interface BlockHeightStats_statistics {
|
||||
__typename: "Statistics";
|
||||
/**
|
||||
* Current block number
|
||||
*/
|
||||
blockHeight: string;
|
||||
}
|
||||
|
||||
export interface BlockHeightStats {
|
||||
/**
|
||||
* get statistics about the vega node
|
||||
*/
|
||||
statistics: BlockHeightStats_statistics;
|
||||
}
|
@ -3,6 +3,7 @@ import classnames from 'classnames';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
type LayoutCellProps = {
|
||||
label?: string;
|
||||
hasError?: boolean;
|
||||
isLoading?: boolean;
|
||||
children?: ReactNode;
|
||||
@ -10,17 +11,27 @@ type LayoutCellProps = {
|
||||
|
||||
export const LayoutCell = ({
|
||||
hasError,
|
||||
label,
|
||||
isLoading,
|
||||
children,
|
||||
}: LayoutCellProps) => {
|
||||
const classes = [
|
||||
'px-8 lg:text-right flex justify-between lg:block',
|
||||
'bg-white-60 dark:bg-black-60 lg:bg-transparent lg:dark:bg-transparent',
|
||||
'm-2 lg:m-0',
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('px-8 text-right', {
|
||||
'text-danger': !isLoading && hasError,
|
||||
'text-white-60 dark:text-black-60': isLoading,
|
||||
})}
|
||||
>
|
||||
{isLoading ? t('Checking') : children || '-'}
|
||||
<div className={classnames(classes)}>
|
||||
{label && <span className="lg:hidden">{label}</span>}
|
||||
<span
|
||||
className={classnames('font-mono', {
|
||||
'text-danger': !isLoading && hasError,
|
||||
'text-white-60 dark:text-black-60': isLoading,
|
||||
})}
|
||||
>
|
||||
{isLoading ? t('Checking') : children || '-'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ type LayoutRowProps = {
|
||||
|
||||
export const LayoutRow = ({ children }: LayoutRowProps) => {
|
||||
return (
|
||||
<div className="grid gap-4 py-8 w-full grid-cols-[minmax(200px,_1fr),_150px_125px_100px]">
|
||||
<div className="lg:grid lg:gap-4 py-8 w-full lg:h-[42px] lg:grid-cols-[minmax(200px,_1fr),_150px_125px_100px]">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type { BlockHeightStats } from './__generated__/BlockHeightStats';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import type { Statistics } from '../../utils/__generated__/Statistics';
|
||||
import { STATS_QUERY } from '../../utils/request-node';
|
||||
|
||||
type NodeBlockHeightProps = {
|
||||
value?: number;
|
||||
@ -9,17 +10,9 @@ type NodeBlockHeightProps = {
|
||||
|
||||
const POLL_INTERVAL = 3000;
|
||||
|
||||
const BLOCK_HEIGHT_QUERY = gql`
|
||||
query BlockHeightStats {
|
||||
statistics {
|
||||
blockHeight
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NodeBlockHeight = ({ value, setValue }: NodeBlockHeightProps) => {
|
||||
const { data, startPolling, stopPolling } = useQuery<BlockHeightStats>(
|
||||
BLOCK_HEIGHT_QUERY,
|
||||
const { data, startPolling, stopPolling } = useQuery<Statistics>(
|
||||
STATS_QUERY,
|
||||
{
|
||||
pollInterval: POLL_INTERVAL,
|
||||
}
|
||||
|
@ -1,3 +1,16 @@
|
||||
export const NodeError = () => {
|
||||
return <div />;
|
||||
type NodeErrorProps = {
|
||||
headline?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export const NodeError = ({ headline, message }: NodeErrorProps) => {
|
||||
if (!headline && !message) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="p-16 my-16 border border-danger">
|
||||
<p className="font-bold">{headline}</p>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,55 +1,57 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { NodeData } from '../../types';
|
||||
import { createClient } from '../../utils/apollo-client';
|
||||
import { useNode } from '../../hooks/use-node';
|
||||
import type createClient from '../../utils/apollo-client';
|
||||
import { LayoutRow } from './layout-row';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { NodeBlockHeight } from './node-block-height';
|
||||
|
||||
type NodeStatsContentProps = {
|
||||
data: NodeData;
|
||||
data?: NodeData;
|
||||
highestBlock: number;
|
||||
setBlock: (value: number) => void;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const getResponseTimeDisplayValue = (
|
||||
responseTime: NodeData['responseTime']
|
||||
responseTime?: NodeData['responseTime']
|
||||
) => {
|
||||
if (typeof responseTime.value === 'number') {
|
||||
if (typeof responseTime?.value === 'number') {
|
||||
return `${Number(responseTime.value).toFixed(2)}ms`;
|
||||
}
|
||||
if (responseTime.hasError) {
|
||||
if (responseTime?.hasError) {
|
||||
return t('n/a');
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
const getBlockDisplayValue = (block: NodeData['block']) => {
|
||||
if (block.value) {
|
||||
return '';
|
||||
const getBlockDisplayValue = (
|
||||
block: NodeData['block'] | undefined,
|
||||
setBlock: (block: number) => void
|
||||
) => {
|
||||
if (block?.value) {
|
||||
return <NodeBlockHeight value={block?.value} setValue={setBlock} />;
|
||||
}
|
||||
if (block.hasError) {
|
||||
if (block?.hasError) {
|
||||
return t('n/a');
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
const getSslDisplayValue = (ssl: NodeData['ssl']) => {
|
||||
if (ssl.value) {
|
||||
const getSslDisplayValue = (ssl?: NodeData['ssl']) => {
|
||||
if (ssl?.value) {
|
||||
return t('Yes');
|
||||
}
|
||||
if (ssl.hasError) {
|
||||
if (ssl?.hasError) {
|
||||
return t('No');
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
const NodeStatsContent = ({
|
||||
data: { url, responseTime, block, ssl },
|
||||
// @ts-ignore Allow defaulting to an empty object
|
||||
data = {},
|
||||
highestBlock,
|
||||
setBlock,
|
||||
children,
|
||||
@ -58,24 +60,28 @@ const NodeStatsContent = ({
|
||||
<LayoutRow>
|
||||
{children}
|
||||
<LayoutCell
|
||||
isLoading={responseTime.isLoading}
|
||||
hasError={responseTime.hasError}
|
||||
label={t('Response time')}
|
||||
isLoading={data.responseTime?.isLoading}
|
||||
hasError={data.responseTime?.hasError}
|
||||
>
|
||||
{getResponseTimeDisplayValue(responseTime)}
|
||||
{getResponseTimeDisplayValue(data.responseTime)}
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
isLoading={block.isLoading}
|
||||
label={t('Block')}
|
||||
isLoading={data.block?.isLoading}
|
||||
hasError={
|
||||
block.hasError || (!!block.value && highestBlock > block.value)
|
||||
data.block?.hasError ||
|
||||
(!!data.block?.value && highestBlock > data.block.value)
|
||||
}
|
||||
>
|
||||
{url && block.value && (
|
||||
<NodeBlockHeight value={block.value} setValue={setBlock} />
|
||||
)}
|
||||
{getBlockDisplayValue(block)}
|
||||
{getBlockDisplayValue(data.block, setBlock)}
|
||||
</LayoutCell>
|
||||
<LayoutCell isLoading={ssl.isLoading} hasError={ssl.hasError}>
|
||||
{getSslDisplayValue(ssl)}
|
||||
<LayoutCell
|
||||
label={t('SSL')}
|
||||
isLoading={data.ssl?.isLoading}
|
||||
hasError={data.ssl?.hasError}
|
||||
>
|
||||
{getSslDisplayValue(data.ssl)}
|
||||
</LayoutCell>
|
||||
</LayoutRow>
|
||||
);
|
||||
@ -95,47 +101,28 @@ const Wrapper = ({ client, children }: WrapperProps) => {
|
||||
};
|
||||
|
||||
export type NodeStatsProps = {
|
||||
url?: string;
|
||||
data?: NodeData;
|
||||
client?: ReturnType<typeof createClient>;
|
||||
highestBlock: number;
|
||||
setBlock: (value: number) => void;
|
||||
render: (data: NodeData) => ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const NodeStats = ({
|
||||
url,
|
||||
data,
|
||||
client,
|
||||
highestBlock,
|
||||
render,
|
||||
children,
|
||||
setBlock,
|
||||
}: NodeStatsProps) => {
|
||||
const [client, setClient] = useState<
|
||||
undefined | ReturnType<typeof createClient>
|
||||
>();
|
||||
const { state, reset, updateBlockState } = useNode(url, client);
|
||||
|
||||
useEffect(() => {
|
||||
client?.stop();
|
||||
reset();
|
||||
setClient(url ? createClient(url) : undefined);
|
||||
return () => client?.stop();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
|
||||
const onHandleBlockChange = useCallback(
|
||||
(value: number) => {
|
||||
updateBlockState(value);
|
||||
setBlock(value);
|
||||
},
|
||||
[updateBlockState, setBlock]
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper client={client}>
|
||||
<NodeStatsContent
|
||||
data={state}
|
||||
data={data}
|
||||
highestBlock={highestBlock}
|
||||
setBlock={onHandleBlockChange}
|
||||
setBlock={setBlock}
|
||||
>
|
||||
{render(state)}
|
||||
{children}
|
||||
</NodeStatsContent>
|
||||
</Wrapper>
|
||||
);
|
||||
|
@ -1,16 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { RadioGroup, Button, Radio } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
RadioGroup,
|
||||
Button,
|
||||
Input,
|
||||
Link,
|
||||
Radio,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '../../hooks/use-environment';
|
||||
import type { Configuration, NodeData, Networks } from '../../types';
|
||||
import { useNodes } from '../../hooks/use-nodes';
|
||||
import {
|
||||
getIsNodeLoading,
|
||||
getIsNodeDisabled,
|
||||
getIsFormDisabled,
|
||||
getErrorType,
|
||||
getErrorByType,
|
||||
getHasInvalidChain,
|
||||
} from '../../utils/validate-node';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
import type { Configuration, NodeData, ErrorType, Networks } from '../../types';
|
||||
import { LayoutRow } from './layout-row';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { NodeError } from './node-error';
|
||||
import { NodeStats } from './node-stats';
|
||||
|
||||
type NodeSwitcherProps = {
|
||||
error?: string;
|
||||
config: Configuration;
|
||||
initialErrorType?: ErrorType;
|
||||
onConnect: (url: string) => void;
|
||||
};
|
||||
|
||||
@ -18,85 +34,153 @@ const getDefaultNode = (urls: string[], currentUrl?: string) => {
|
||||
return currentUrl && urls.includes(currentUrl) ? currentUrl : undefined;
|
||||
};
|
||||
|
||||
const getIsLoading = ({ chain, responseTime, block, ssl }: NodeData) => {
|
||||
return (
|
||||
chain.isLoading ||
|
||||
responseTime.isLoading ||
|
||||
block.isLoading ||
|
||||
ssl.isLoading
|
||||
);
|
||||
const getHighestBlock = (env: Networks, state: Record<string, NodeData>) => {
|
||||
return Object.keys(state).reduce((acc, node) => {
|
||||
if (getHasInvalidChain(env, state[node].chain.value)) return acc;
|
||||
const value = Number(state[node].block.value);
|
||||
return value ? Math.max(acc, value) : acc;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getHasMatchingChain = (env: Networks, chain?: string) => {
|
||||
return chain?.includes(env.toLowerCase()) ?? false;
|
||||
};
|
||||
|
||||
const getIsDisabled = (env: Networks, data: NodeData) => {
|
||||
const { chain, responseTime, block, ssl } = data;
|
||||
return (
|
||||
!getHasMatchingChain(env, data.chain.value) ||
|
||||
getIsLoading(data) ||
|
||||
chain.hasError ||
|
||||
responseTime.hasError ||
|
||||
block.hasError ||
|
||||
ssl.hasError
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => {
|
||||
export const NodeSwitcher = ({
|
||||
config,
|
||||
initialErrorType,
|
||||
onConnect,
|
||||
}: NodeSwitcherProps) => {
|
||||
const { VEGA_ENV, VEGA_URL } = useEnvironment();
|
||||
const [node, setNode] = useState(getDefaultNode(config.hosts, VEGA_URL));
|
||||
const [highestBlock, setHighestBlock] = useState(0);
|
||||
const [networkError, setNetworkError] = useState(
|
||||
getErrorByType(initialErrorType, VEGA_ENV, VEGA_URL)
|
||||
);
|
||||
const [customNodeText, setCustomNodeText] = useState('');
|
||||
const [nodeRadio, setNodeRadio] = useState(
|
||||
getDefaultNode(config.hosts, VEGA_URL)
|
||||
);
|
||||
const { state, clients, updateNodeUrl, updateNodeBlock } = useNodes(
|
||||
VEGA_ENV,
|
||||
config
|
||||
);
|
||||
const highestBlock = useMemo(
|
||||
() => getHighestBlock(VEGA_ENV, state),
|
||||
[VEGA_ENV, state]
|
||||
);
|
||||
const customUrl = state[CUSTOM_NODE_KEY]?.url;
|
||||
|
||||
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
|
||||
if (node) {
|
||||
onConnect(node);
|
||||
if (node && state[node]) {
|
||||
onConnect(state[node].url);
|
||||
}
|
||||
};
|
||||
|
||||
const isSubmitDisabled = !node;
|
||||
const isSubmitDisabled = getIsFormDisabled(
|
||||
nodeRadio,
|
||||
customNodeText,
|
||||
VEGA_ENV,
|
||||
state
|
||||
);
|
||||
|
||||
const customNodeData =
|
||||
nodeRadio &&
|
||||
state[CUSTOM_NODE_KEY] &&
|
||||
state[CUSTOM_NODE_KEY].url === customNodeText
|
||||
? state[nodeRadio]
|
||||
: undefined;
|
||||
|
||||
const customNodeError = getErrorByType(
|
||||
getErrorType(VEGA_ENV, customNodeData),
|
||||
VEGA_ENV,
|
||||
customUrl
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="text-black dark:text-white w-full">
|
||||
<NodeError />
|
||||
<form onSubmit={() => onSubmit(node)}>
|
||||
<p className="text-body-large font-bold mb-32">
|
||||
<div className="text-black dark:text-white w-full lg:min-w-[800px]">
|
||||
<NodeError {...(customNodeError || networkError)} />
|
||||
<form onSubmit={() => onSubmit(nodeRadio)}>
|
||||
<p className="text-body-large font-bold mt-16 mb-32">
|
||||
{t('Select a GraphQL node to connect to:')}
|
||||
</p>
|
||||
<div>
|
||||
<LayoutRow>
|
||||
<div />
|
||||
<LayoutCell>{t('Response time')}</LayoutCell>
|
||||
<LayoutCell>{t('Block')}</LayoutCell>
|
||||
<LayoutCell>{t('SSL')}</LayoutCell>
|
||||
</LayoutRow>
|
||||
<div className="hidden lg:block">
|
||||
<LayoutRow>
|
||||
<div />
|
||||
<span className="px-8 text-right">{t('Response time')}</span>
|
||||
<span className="px-8 text-right">{t('Block')}</span>
|
||||
<span className="px-8 text-right">{t('SSL')}</span>
|
||||
</LayoutRow>
|
||||
</div>
|
||||
<RadioGroup
|
||||
className="block"
|
||||
value={node}
|
||||
onChange={(value) => setNode(value)}
|
||||
value={nodeRadio}
|
||||
onChange={(value) => {
|
||||
setNodeRadio(value);
|
||||
setNetworkError(null);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{config.hosts.map((url, index) => (
|
||||
<div className="w-full">
|
||||
{config.hosts.map((node, index) => (
|
||||
<NodeStats
|
||||
key={index}
|
||||
url={url}
|
||||
data={state[node]}
|
||||
client={clients[node]}
|
||||
highestBlock={highestBlock}
|
||||
setBlock={(block) =>
|
||||
setHighestBlock(Math.max(block, highestBlock))
|
||||
}
|
||||
render={(data) => (
|
||||
<div>
|
||||
<Radio
|
||||
id={`node-url-${index}`}
|
||||
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
value={url}
|
||||
label={url}
|
||||
disabled={getIsDisabled(VEGA_ENV, data)}
|
||||
setBlock={(block) => updateNodeBlock(node, block)}
|
||||
>
|
||||
<div className="mb-8 break-all">
|
||||
<Radio
|
||||
id={`node-url-${index}`}
|
||||
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
value={node}
|
||||
label={node}
|
||||
disabled={getIsNodeDisabled(VEGA_ENV, state[node])}
|
||||
/>
|
||||
</div>
|
||||
</NodeStats>
|
||||
))}
|
||||
<NodeStats
|
||||
data={state[CUSTOM_NODE_KEY]}
|
||||
client={customUrl ? clients[customUrl] : undefined}
|
||||
highestBlock={highestBlock}
|
||||
setBlock={(block) => updateNodeBlock(CUSTOM_NODE_KEY, block)}
|
||||
>
|
||||
<div className="flex w-full mb-8">
|
||||
<Radio
|
||||
id={`node-url-custom`}
|
||||
value={CUSTOM_NODE_KEY}
|
||||
label={
|
||||
nodeRadio === CUSTOM_NODE_KEY || !!state[CUSTOM_NODE_KEY]
|
||||
? ''
|
||||
: t('Other')
|
||||
}
|
||||
/>
|
||||
{(customNodeText || nodeRadio === CUSTOM_NODE_KEY) && (
|
||||
<div className="flex w-full gap-8">
|
||||
<Input
|
||||
placeholder="https://"
|
||||
value={customNodeText}
|
||||
hasError={
|
||||
!!customNodeText &&
|
||||
!!(
|
||||
customNodeError?.headline ||
|
||||
customNodeError?.message
|
||||
)
|
||||
}
|
||||
onChange={(e) => setCustomNodeText(e.target.value)}
|
||||
/>
|
||||
<Link
|
||||
aria-disabled={!customNodeText}
|
||||
onClick={() => {
|
||||
setNetworkError(null);
|
||||
updateNodeUrl(CUSTOM_NODE_KEY, customNodeText);
|
||||
}}
|
||||
>
|
||||
{state[CUSTOM_NODE_KEY] &&
|
||||
getIsNodeLoading(state[CUSTOM_NODE_KEY])
|
||||
? t('Checking')
|
||||
: t('Check')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NodeStats>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
82
libs/environment/src/hooks/mocks/apollo-client.tsx
Normal file
82
libs/environment/src/hooks/mocks/apollo-client.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import {
|
||||
STATS_QUERY,
|
||||
TIME_UPDATE_SUBSCRIPTION,
|
||||
} from '../../utils/request-node';
|
||||
import type { Statistics } from '../../utils/__generated__/Statistics';
|
||||
import type { BlockTime } from '../../utils/__generated__/BlockTime';
|
||||
import { Networks } from '../../types';
|
||||
import type { RequestHandlerResponse } from 'mock-apollo-client';
|
||||
import { createMockClient } from 'mock-apollo-client';
|
||||
|
||||
export type MockRequestConfig = {
|
||||
hasError?: boolean;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
type MockClientProps = {
|
||||
network?: Networks;
|
||||
statistics?: MockRequestConfig;
|
||||
busEvents?: MockRequestConfig;
|
||||
};
|
||||
|
||||
export const getMockBusEventsResult = (): BlockTime => ({
|
||||
busEvents: [
|
||||
{
|
||||
__typename: 'BusEvent',
|
||||
eventId: '0',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const getMockStatisticsResult = (
|
||||
env: Networks = Networks.TESTNET
|
||||
): Statistics => ({
|
||||
statistics: {
|
||||
__typename: 'Statistics',
|
||||
chainId: `${env.toLowerCase()}-0123`,
|
||||
blockHeight: '11',
|
||||
},
|
||||
});
|
||||
|
||||
export const getMockQueryResult = (env: Networks): Statistics => ({
|
||||
statistics: {
|
||||
__typename: 'Statistics',
|
||||
chainId: `${env.toLowerCase()}-0123`,
|
||||
blockHeight: '11',
|
||||
},
|
||||
});
|
||||
|
||||
function getHandler<T>(
|
||||
{ hasError, delay = 0 }: MockRequestConfig = {},
|
||||
result: T
|
||||
) {
|
||||
return () =>
|
||||
new Promise<RequestHandlerResponse<T>>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (hasError) {
|
||||
reject(new Error('Failed to execute query.'));
|
||||
return;
|
||||
}
|
||||
resolve({ data: result });
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
export default function ({
|
||||
network,
|
||||
statistics,
|
||||
busEvents,
|
||||
}: MockClientProps = {}) {
|
||||
const mockClient = createMockClient();
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
STATS_QUERY,
|
||||
getHandler(statistics, getMockStatisticsResult(network))
|
||||
);
|
||||
mockClient.setRequestHandler(
|
||||
TIME_UPDATE_SUBSCRIPTION,
|
||||
getHandler(busEvents, getMockBusEventsResult())
|
||||
);
|
||||
|
||||
return mockClient;
|
||||
}
|
@ -1,19 +1,17 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import type { EnvironmentWithOptionalUrl } from './use-config';
|
||||
import { useConfig, LOCAL_STORAGE_NETWORK_KEY } from './use-config';
|
||||
import { Networks } from '../types';
|
||||
import { useConfig } from './use-config';
|
||||
import { Networks, ErrorType } from '../types';
|
||||
|
||||
type HostMapping = Record<string, number | Error>;
|
||||
|
||||
const mockHostsMap: HostMapping = {
|
||||
'https://host1.com': 300,
|
||||
'https://host2.com': 500,
|
||||
'https://host3.com': 100,
|
||||
'https://host4.com': 650,
|
||||
const mockConfig = {
|
||||
hosts: [
|
||||
'https://vega-host-1.com',
|
||||
'https://vega-host-2.com',
|
||||
'https://vega-host-3.com',
|
||||
'https://vega-host-4.com',
|
||||
],
|
||||
};
|
||||
|
||||
const hostList = Object.keys(mockHostsMap);
|
||||
|
||||
const mockEnvironment: EnvironmentWithOptionalUrl = {
|
||||
VEGA_ENV: Networks.TESTNET,
|
||||
VEGA_CONFIG_URL: 'https://vega.url/config.json',
|
||||
@ -26,281 +24,110 @@ const mockEnvironment: EnvironmentWithOptionalUrl = {
|
||||
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||
};
|
||||
|
||||
function setupFetch(configUrl: string, hostMap: HostMapping) {
|
||||
const hostUrls = Object.keys(hostMap);
|
||||
function setupFetch(configUrl: string) {
|
||||
return (url: RequestInfo) => {
|
||||
if (url === configUrl) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ hosts: hostUrls }),
|
||||
json: () => Promise.resolve(mockConfig),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
if (hostUrls.includes(url as string)) {
|
||||
const value = hostMap[url as string];
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
if (typeof value === 'number') {
|
||||
setTimeout(() => {
|
||||
resolve({ ok: true } as Response);
|
||||
}, value);
|
||||
} else {
|
||||
reject(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
} as Response);
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
const mockUpdate = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
mockUpdate.mockClear();
|
||||
onError.mockClear();
|
||||
window.localStorage.clear();
|
||||
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockReset();
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMap)
|
||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '')
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore: typescript doesn't recognise the mocked fetch instance
|
||||
fetch.mockRestore();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useConfig hook', () => {
|
||||
it('updates the environment with a host url from the network configuration', async () => {
|
||||
const allowedStatuses = [
|
||||
'idle',
|
||||
'loading-config',
|
||||
'loading-node',
|
||||
'success',
|
||||
];
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, mockUpdate)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('success');
|
||||
result.all.forEach((state) => {
|
||||
expect(allowedStatuses).toContain('status' in state && state.status);
|
||||
});
|
||||
|
||||
// fetches config
|
||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
// calls each node
|
||||
hostList.forEach((url) => {
|
||||
expect(fetch).toHaveBeenCalledWith(url);
|
||||
});
|
||||
|
||||
// updates the environment
|
||||
expect(hostList).toContain(mockUpdate.mock.calls[0][0]({}).VEGA_URL);
|
||||
});
|
||||
|
||||
it('uses the host from the configuration which responds first', async () => {
|
||||
const shortestResponseTime = Object.values(mockHostsMap).sort()[0];
|
||||
const expectedHost = hostList.find((url: keyof typeof mockHostsMap) => {
|
||||
return mockHostsMap[url] === shortestResponseTime;
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, mockUpdate)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('success');
|
||||
expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe(expectedHost);
|
||||
});
|
||||
|
||||
it('ignores failing hosts and uses one which returns a success response', async () => {
|
||||
const mockHostsMapScoped = {
|
||||
'https://host1.com': 350,
|
||||
'https://host2.com': new Error('Server error'),
|
||||
'https://host3.com': 230,
|
||||
'https://host4.com': new Error('Server error'),
|
||||
it("doesn't update when there is no VEGA_CONFIG_URL in the environment", async () => {
|
||||
const mockEnvWithoutUrl = {
|
||||
...mockEnvironment,
|
||||
VEGA_CONFIG_URL: undefined,
|
||||
};
|
||||
const { result } = renderHook(() => useConfig(mockEnvWithoutUrl, onError));
|
||||
|
||||
expect(result.current.config).toBe(undefined);
|
||||
});
|
||||
|
||||
it('fetches configuration from the provided url', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, onError)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
expect(result.current.config).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('caches the configuration', async () => {
|
||||
const { result: firstResult, waitForNextUpdate: waitForFirstUpdate } =
|
||||
renderHook(() => useConfig(mockEnvironment, onError));
|
||||
|
||||
await waitForFirstUpdate();
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(firstResult.current.config).toEqual(mockConfig);
|
||||
|
||||
const { result: secondResult } = renderHook(() =>
|
||||
useConfig(mockEnvironment, onError)
|
||||
);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(secondResult.current.config).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('executes the error callback when the config endpoint fails', async () => {
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
|
||||
global.fetch.mockImplementation(() => Promise.reject());
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, onError)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
expect(result.current.config).toEqual({ hosts: [] });
|
||||
expect(onError).toHaveBeenCalledWith(ErrorType.CONFIG_LOAD_ERROR);
|
||||
});
|
||||
|
||||
it('executes the error callback when the config validation fails', async () => {
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: 'not-valid-config' }),
|
||||
})
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, mockUpdate)
|
||||
useConfig(mockEnvironment, onError)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('success');
|
||||
expect(mockUpdate.mock.calls[0][0]({}).VEGA_URL).toBe('https://host3.com');
|
||||
});
|
||||
|
||||
it('returns the correct error status for when the config cannot be accessed', async () => {
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockImplementation((url: RequestInfo) => {
|
||||
if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
||||
return Promise.reject(new Error('Server error'));
|
||||
}
|
||||
return Promise.resolve({ ok: true } as Response);
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, mockUpdate)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('error-loading-config');
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the correct error status for when the config is not valid', async () => {
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockImplementation((url: RequestInfo) => {
|
||||
if (url === mockEnvironment.VEGA_CONFIG_URL) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ some: 'data' }),
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ ok: true } as Response);
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, mockUpdate)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('error-validating-config');
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the correct error status for when no hosts can be accessed', async () => {
|
||||
const mockHostsMapScoped = {
|
||||
'https://host1.com': new Error('Server error'),
|
||||
'https://host2.com': new Error('Server error'),
|
||||
'https://host3.com': new Error('Server error'),
|
||||
'https://host4.com': new Error('Server error'),
|
||||
};
|
||||
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironment.VEGA_CONFIG_URL ?? '', mockHostsMapScoped)
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useConfig(mockEnvironment, mockUpdate)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('error-loading-node');
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('caches the list of networks', async () => {
|
||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||
|
||||
await run1.waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await run1.waitForNextUpdate();
|
||||
|
||||
expect(run1.result.current.status).toBe('success');
|
||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
fetch.mockClear();
|
||||
|
||||
const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||
|
||||
jest.runAllTimers();
|
||||
await run2.waitForNextUpdate();
|
||||
|
||||
expect(run2.result.current.status).toBe('success');
|
||||
expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
});
|
||||
|
||||
it('caches the list of networks between runs', async () => {
|
||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||
|
||||
await run1.waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await run1.waitForNextUpdate();
|
||||
|
||||
expect(run1.result.current.status).toBe('success');
|
||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
|
||||
// @ts-ignore typescript doesn't recognise the mocked instance
|
||||
fetch.mockClear();
|
||||
|
||||
const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||
|
||||
jest.runAllTimers();
|
||||
await run2.waitForNextUpdate();
|
||||
|
||||
expect(run2.result.current.status).toBe('success');
|
||||
expect(fetch).not.toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
});
|
||||
|
||||
it('refetches the network configuration and resets the cache when malformed data found in the storage', async () => {
|
||||
window.localStorage.setItem(
|
||||
`${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
|
||||
'{not:{valid:{json'
|
||||
);
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||
|
||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||
|
||||
await run1.waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await run1.waitForNextUpdate();
|
||||
|
||||
expect(run1.result.current.status).toBe('success');
|
||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('refetches the network configuration and resets the cache when invalid data found in the storage', async () => {
|
||||
window.localStorage.setItem(
|
||||
`${LOCAL_STORAGE_NETWORK_KEY}-${mockEnvironment.VEGA_ENV}`,
|
||||
JSON.stringify({ invalid: 'data' })
|
||||
);
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop);
|
||||
|
||||
const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate));
|
||||
|
||||
await run1.waitForNextUpdate();
|
||||
jest.runAllTimers();
|
||||
await run1.waitForNextUpdate();
|
||||
|
||||
expect(run1.result.current.status).toBe('success');
|
||||
expect(fetch).toHaveBeenCalledWith(mockEnvironment.VEGA_CONFIG_URL);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
expect(result.current.config).toBe(undefined);
|
||||
expect(onError).toHaveBeenCalledWith(ErrorType.CONFIG_VALIDATION_ERROR);
|
||||
});
|
||||
});
|
||||
|
@ -1,33 +1,26 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
Environment,
|
||||
Configuration,
|
||||
ConfigStatus,
|
||||
Networks,
|
||||
} from '../types';
|
||||
import { ErrorType } from '../types';
|
||||
import type { Environment, Configuration, Networks } from '../types';
|
||||
import { validateConfiguration } from '../utils/validate-configuration';
|
||||
import { promiseRaceToSuccess } from '../utils/promise-race-success';
|
||||
|
||||
export const LOCAL_STORAGE_NETWORK_KEY = 'vegaNetworkConfig';
|
||||
|
||||
export type EnvironmentWithOptionalUrl = Partial<Environment> &
|
||||
Omit<Environment, 'VEGA_URL'>;
|
||||
|
||||
const requestToNode = async (url: string, index: number): Promise<number> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed connecting to node: ${url}.`);
|
||||
const compileHosts = (hosts: string[], envUrl?: string) => {
|
||||
if (envUrl && !hosts.includes(envUrl)) {
|
||||
return [...hosts, envUrl];
|
||||
}
|
||||
return index;
|
||||
return hosts;
|
||||
};
|
||||
|
||||
const getCacheKey = (env: Networks) => `${LOCAL_STORAGE_NETWORK_KEY}-${env}`;
|
||||
|
||||
const getCachedConfig = (env: Networks) => {
|
||||
const key = getCacheKey(env);
|
||||
const value = LocalStorage.getItem(key);
|
||||
const cacheKey = getCacheKey(env);
|
||||
const value = LocalStorage.getItem(cacheKey);
|
||||
|
||||
if (value) {
|
||||
try {
|
||||
@ -40,9 +33,9 @@ const getCachedConfig = (env: Networks) => {
|
||||
|
||||
return config;
|
||||
} catch (err) {
|
||||
LocalStorage.removeItem(key);
|
||||
LocalStorage.removeItem(cacheKey);
|
||||
console.warn(
|
||||
'Malformed data found for network configuration. Removed and continuing...'
|
||||
'Malformed data found for network configuration. Removed cached configuration, continuing...'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -52,80 +45,54 @@ const getCachedConfig = (env: Networks) => {
|
||||
|
||||
export const useConfig = (
|
||||
environment: EnvironmentWithOptionalUrl,
|
||||
updateEnvironment: Dispatch<SetStateAction<Environment>>
|
||||
onError: (errorType: ErrorType) => void
|
||||
) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState<Configuration | undefined>(
|
||||
getCachedConfig(environment.VEGA_ENV)
|
||||
);
|
||||
const [status, setStatus] = useState<ConfigStatus>(
|
||||
environment.VEGA_CONFIG_URL ? 'idle' : 'success'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config && status === 'idle') {
|
||||
(async () => {
|
||||
setStatus('loading-config');
|
||||
let isMounted = true;
|
||||
(async () => {
|
||||
if (!config && environment.VEGA_CONFIG_URL) {
|
||||
isMounted && setLoading(true);
|
||||
try {
|
||||
const response = await fetch(environment.VEGA_CONFIG_URL ?? '');
|
||||
const response = await fetch(environment.VEGA_CONFIG_URL);
|
||||
const configData: Configuration = await response.json();
|
||||
|
||||
if (validateConfiguration(configData)) {
|
||||
setStatus('error-validating-config');
|
||||
onError(ErrorType.CONFIG_VALIDATION_ERROR);
|
||||
isMounted && setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setConfig({ hosts: configData.hosts });
|
||||
const hosts = compileHosts(configData.hosts, environment.VEGA_URL);
|
||||
|
||||
isMounted && setConfig({ hosts });
|
||||
LocalStorage.setItem(
|
||||
getCacheKey(environment.VEGA_ENV),
|
||||
JSON.stringify({ hosts: configData.hosts })
|
||||
JSON.stringify({ hosts })
|
||||
);
|
||||
isMounted && setLoading(false);
|
||||
} catch (err) {
|
||||
setStatus('error-loading-config');
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
setConfig({ hosts: [] });
|
||||
}
|
||||
onError(ErrorType.CONFIG_LOAD_ERROR);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
// load config only once per runtime
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [environment.VEGA_CONFIG_URL, !!config, status, setStatus, setConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
config &&
|
||||
!['loading-node', 'success', 'error-loading-node'].includes(status)
|
||||
) {
|
||||
(async () => {
|
||||
setStatus('loading-node');
|
||||
|
||||
// if there's only one configured node to choose from, set is as the env url
|
||||
if (config.hosts.length === 1) {
|
||||
setStatus('success');
|
||||
updateEnvironment((prevEnvironment) => ({
|
||||
...prevEnvironment,
|
||||
VEGA_URL: prevEnvironment.VEGA_URL || config.hosts[0],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// when there are multiple possible hosts, set the env url to the node which responds first
|
||||
try {
|
||||
const requests = config.hosts.map(requestToNode);
|
||||
const index = await promiseRaceToSuccess(requests);
|
||||
setStatus('success');
|
||||
updateEnvironment((prevEnvironment) => ({
|
||||
...prevEnvironment,
|
||||
VEGA_URL: prevEnvironment.VEGA_URL || config.hosts[index],
|
||||
}));
|
||||
} catch (err) {
|
||||
setStatus('error-loading-node');
|
||||
}
|
||||
})();
|
||||
}
|
||||
// load config only once per runtime
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status, !!config, setStatus, updateEnvironment]);
|
||||
}, [environment.VEGA_CONFIG_URL, !!config, onError, setLoading]);
|
||||
|
||||
return {
|
||||
status,
|
||||
loading,
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
@ -1,8 +1,22 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
// having the node switcher dialog in the environment provider breaks the test renderer
|
||||
// workaround based on: https://github.com/facebook/react/issues/11565
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import type { EnvironmentState } from './use-environment';
|
||||
import createClient from '../utils/apollo-client';
|
||||
import { useEnvironment, EnvironmentProvider } from './use-environment';
|
||||
import { Networks } from '../types';
|
||||
import { Networks, ErrorType } from '../types';
|
||||
import type { MockRequestConfig } from './mocks/apollo-client';
|
||||
import createMockClient from './mocks/apollo-client';
|
||||
import { getErrorByType } from '../utils/validate-node';
|
||||
|
||||
jest.mock('../utils/apollo-client');
|
||||
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: (node: ReactNode) => node,
|
||||
}));
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
||||
return <EnvironmentProvider {...props} />;
|
||||
@ -10,26 +24,10 @@ const MockWrapper = (props: ComponentProps<typeof EnvironmentProvider>) => {
|
||||
|
||||
const MOCK_HOST = 'https://vega.host/query';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
const mockFetch = (url: RequestInfo) => {
|
||||
if (url === mockEnvironmentState.VEGA_CONFIG_URL) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
hosts: [MOCK_HOST],
|
||||
}),
|
||||
} as Response);
|
||||
}
|
||||
return Promise.resolve({ ok: true } as Response);
|
||||
};
|
||||
|
||||
const mockEnvironmentState: EnvironmentState = {
|
||||
configStatus: 'success',
|
||||
const mockEnvironmentState = {
|
||||
VEGA_URL: 'https://vega.xyz',
|
||||
VEGA_ENV: Networks.TESTNET,
|
||||
VEGA_CONFIG_URL: 'https://vega.xyz/testnet-config.json',
|
||||
@ -45,25 +43,76 @@ const mockEnvironmentState: EnvironmentState = {
|
||||
GIT_COMMIT_HASH: 'abcde01234',
|
||||
GITHUB_FEEDBACK_URL: 'https://github.com/test/feedback',
|
||||
setNodeSwitcherOpen: noop,
|
||||
networkError: undefined,
|
||||
};
|
||||
|
||||
const MOCK_DURATION = 76;
|
||||
|
||||
window.performance.getEntriesByName = jest
|
||||
.fn()
|
||||
.mockImplementation((url: string) => [
|
||||
{
|
||||
entryType: 'resource',
|
||||
name: url,
|
||||
startTime: 0,
|
||||
toJSON: () => ({}),
|
||||
duration: MOCK_DURATION,
|
||||
},
|
||||
]);
|
||||
|
||||
function setupFetch(
|
||||
configUrl: string = mockEnvironmentState.VEGA_CONFIG_URL,
|
||||
hosts?: string[]
|
||||
) {
|
||||
return (url: RequestInfo) => {
|
||||
if (url === configUrl) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ hosts: hosts || [MOCK_HOST] }),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
} as Response);
|
||||
};
|
||||
}
|
||||
|
||||
const getQuickestNode = (mockNodes: Record<string, MockRequestConfig>) => {
|
||||
const { nodeUrl } = Object.keys(mockNodes).reduce<{
|
||||
nodeUrl?: string;
|
||||
delay: number;
|
||||
}>(
|
||||
(acc, url) => {
|
||||
const { delay = 0, hasError = false } = mockNodes[url];
|
||||
if (!hasError && delay < acc.delay) {
|
||||
return { nodeUrl: url, delay };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ nodeUrl: undefined, delay: Infinity }
|
||||
);
|
||||
return nodeUrl;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockReset();
|
||||
// @ts-ignore typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(mockFetch);
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch());
|
||||
|
||||
window.localStorage.clear();
|
||||
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => createMockClient());
|
||||
|
||||
process.env['NX_VEGA_URL'] = mockEnvironmentState.VEGA_URL;
|
||||
process.env['NX_VEGA_ENV'] = mockEnvironmentState.VEGA_ENV;
|
||||
process.env['NX_VEGA_CONFIG_URL'] = mockEnvironmentState.VEGA_CONFIG_URL;
|
||||
process.env['NX_ETHEREUM_PROVIDER_URL'] =
|
||||
mockEnvironmentState.ETHEREUM_PROVIDER_URL;
|
||||
process.env['NX_ETHERSCAN_URL'] = mockEnvironmentState.ETHERSCAN_URL;
|
||||
process.env['NX_VEGA_NETWORKS'] = JSON.stringify(
|
||||
mockEnvironmentState.VEGA_NETWORKS
|
||||
);
|
||||
process.env['NX_ETHEREUM_PROVIDER_URL'] =
|
||||
mockEnvironmentState.ETHEREUM_PROVIDER_URL;
|
||||
process.env['NX_ETHERSCAN_URL'] = mockEnvironmentState.ETHERSCAN_URL;
|
||||
process.env['NX_GIT_BRANCH'] = mockEnvironmentState.GIT_BRANCH;
|
||||
process.env['NX_GIT_ORIGIN_URL'] = mockEnvironmentState.GIT_ORIGIN_URL;
|
||||
process.env['NX_GIT_COMMIT_HASH'] = mockEnvironmentState.GIT_COMMIT_HASH;
|
||||
@ -72,20 +121,18 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore: typescript doesn't recognise the mocked fetch instance
|
||||
fetch.mockRestore();
|
||||
window.localStorage.clear();
|
||||
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
delete process.env['NX_VEGA_ENV'];
|
||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||
delete process.env['NX_VEGA_NETWORKS'];
|
||||
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
||||
delete process.env['NX_ETHERSCAN_URL'];
|
||||
delete process.env['NX_VEGA_NETWORKS'];
|
||||
delete process.env['NX_GIT_BRANCH'];
|
||||
delete process.env['NX_GIT_ORIGIN_URL'];
|
||||
delete process.env['NX_GIT_COMMIT_HASH'];
|
||||
delete process.env['NX_GITHUB_FEEDBACK_URL'];
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useEnvironment hook', () => {
|
||||
@ -114,21 +161,6 @@ describe('useEnvironment hook', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows for the VEGA_URL to be missing when there is a VEGA_CONFIG_URL present', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: MOCK_HOST,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows for the VEGA_NETWORKS to be missing from the environment', async () => {
|
||||
delete process.env['NX_VEGA_NETWORKS'];
|
||||
const { result, waitForNextUpdate } = renderHook(() => useEnvironment(), {
|
||||
@ -214,6 +246,9 @@ describe('useEnvironment hook', () => {
|
||||
`(
|
||||
'uses correct default ethereum connection variables in $env',
|
||||
async ({ env, etherscanUrl, providerUrl }) => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => createMockClient({ network: env }));
|
||||
|
||||
process.env['NX_VEGA_ENV'] = env;
|
||||
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
||||
delete process.env['NX_ETHERSCAN_URL'];
|
||||
@ -251,4 +286,319 @@ describe('useEnvironment hook', () => {
|
||||
`The NX_ETHEREUM_PROVIDER_URL environment variable must be a valid url`
|
||||
);
|
||||
});
|
||||
|
||||
describe('node selection', () => {
|
||||
it('updates the VEGA_URL from the config when it is missing from the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: MOCK_HOST,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the VEGA_URL with the quickest node to respond from the config urls', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
const mockNodes: Record<string, MockRequestConfig> = {
|
||||
'https://mock-node-1.com': { hasError: false, delay: 4 },
|
||||
'https://mock-node-2.com': { hasError: false, delay: 5 },
|
||||
'https://mock-node-3.com': { hasError: false, delay: 8 },
|
||||
'https://mock-node-4.com': { hasError: false, delay: 0 },
|
||||
};
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||
);
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||
return createMockClient({ statistics: mockNodes[url] });
|
||||
});
|
||||
|
||||
const nodeUrl = getQuickestNode(mockNodes);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: nodeUrl,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores failing nodes and selects the first successful one to use', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
const mockNodes: Record<string, MockRequestConfig> = {
|
||||
'https://mock-node-1.com': { hasError: true, delay: 4 },
|
||||
'https://mock-node-2.com': { hasError: false, delay: 5 },
|
||||
'https://mock-node-3.com': { hasError: false, delay: 8 },
|
||||
'https://mock-node-4.com': { hasError: true, delay: 0 },
|
||||
};
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||
);
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||
return createMockClient({ statistics: mockNodes[url] });
|
||||
});
|
||||
|
||||
const nodeUrl = getQuickestNode(mockNodes);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: nodeUrl,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when cannot connect to any nodes', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
const mockNodes: Record<string, MockRequestConfig> = {
|
||||
'https://mock-node-1.com': { hasError: true, delay: 4 },
|
||||
'https://mock-node-2.com': { hasError: true, delay: 5 },
|
||||
'https://mock-node-3.com': { hasError: true, delay: 8 },
|
||||
'https://mock-node-4.com': { hasError: true, delay: 0 },
|
||||
};
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(
|
||||
setupFetch(mockEnvironmentState.VEGA_CONFIG_URL, Object.keys(mockNodes))
|
||||
);
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation((url: keyof typeof mockNodes) => {
|
||||
return createMockClient({ statistics: mockNodes[url] });
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: undefined,
|
||||
networkError: ErrorType.CONNECTION_ERROR_ALL,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when it cannot fetch the network config and there is no VEGA_URL in the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() => {
|
||||
throw new Error('Cannot fetch');
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: undefined,
|
||||
networkError: ErrorType.CONFIG_LOAD_ERROR,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs an error when it cannot fetch the network config and there is a VEGA_URL in the environment', async () => {
|
||||
const consoleWarnSpy = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(noop);
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() => {
|
||||
throw new Error('Cannot fetch');
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
getErrorByType(
|
||||
ErrorType.CONFIG_LOAD_ERROR,
|
||||
mockEnvironmentState.VEGA_ENV
|
||||
)?.headline
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('has a network error when the config is invalid and there is no VEGA_URL in the environment', async () => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ some: 'invalid-object' }),
|
||||
})
|
||||
);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
VEGA_URL: undefined,
|
||||
networkError: ErrorType.CONFIG_VALIDATION_ERROR,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('logs an error when the network config in invalid and there is a VEGA_URL in the environment', async () => {
|
||||
const consoleWarnSpy = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(noop);
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ some: 'invalid-object' }),
|
||||
})
|
||||
);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
getErrorByType(
|
||||
ErrorType.CONFIG_VALIDATION_ERROR,
|
||||
mockEnvironmentState.VEGA_ENV
|
||||
)?.headline
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// SKIP due to https://github.com/facebook/jest/issues/12670
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('has a network error when the selected node is not a valid url', async () => {
|
||||
process.env['NX_VEGA_URL'] = 'not-url';
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
networkError: ErrorType.INVALID_URL,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when cannot connect to the selected node', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => {
|
||||
return createMockClient({ statistics: { hasError: true } });
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
networkError: ErrorType.CONNECTION_ERROR,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when the selected node is not on the correct network', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => {
|
||||
return createMockClient({ network: Networks.MAINNET });
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
networkError: ErrorType.INVALID_NETWORK,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has a network error when the selected node has not ssl available', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => {
|
||||
return createMockClient({ busEvents: { hasError: true } });
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook(() => useEnvironment(), {
|
||||
wrapper: MockWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.error).toBe(undefined);
|
||||
expect(result.current).toEqual({
|
||||
...mockEnvironmentState,
|
||||
networkError: ErrorType.SSL_ERROR,
|
||||
setNodeSwitcherOpen: result.current.setNodeSwitcherOpen,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,18 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, createContext, useContext } from 'react';
|
||||
import { useEffect, useState, createContext, useContext } from 'react';
|
||||
|
||||
import { NodeSwitcherDialog } from '../components/node-switcher-dialog';
|
||||
import { useConfig } from './use-config';
|
||||
import { useNodes } from './use-nodes';
|
||||
import { compileEnvironment } from '../utils/compile-environment';
|
||||
import { validateEnvironment } from '../utils/validate-environment';
|
||||
import type { Environment, RawEnvironment, ConfigStatus } from '../types';
|
||||
import {
|
||||
getErrorType,
|
||||
getErrorByType,
|
||||
getIsNodeLoading,
|
||||
} from '../utils/validate-node';
|
||||
import { ErrorType } from '../types';
|
||||
import type { Environment, RawEnvironment, NodeData } from '../types';
|
||||
|
||||
type EnvironmentProviderProps = {
|
||||
definitions?: Partial<RawEnvironment>;
|
||||
@ -13,24 +20,77 @@ type EnvironmentProviderProps = {
|
||||
};
|
||||
|
||||
export type EnvironmentState = Environment & {
|
||||
configStatus: ConfigStatus;
|
||||
networkError?: ErrorType;
|
||||
setNodeSwitcherOpen: () => void;
|
||||
};
|
||||
|
||||
const EnvironmentContext = createContext({} as EnvironmentState);
|
||||
|
||||
const hasFinishedLoading = (node: NodeData) =>
|
||||
node.initialized && !getIsNodeLoading(node) && !node.verified;
|
||||
|
||||
export const EnvironmentProvider = ({
|
||||
definitions,
|
||||
children,
|
||||
}: EnvironmentProviderProps) => {
|
||||
const [networkError, setNetworkError] = useState<undefined | ErrorType>();
|
||||
const [isNodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const [environment, updateEnvironment] = useState<Environment>(
|
||||
compileEnvironment(definitions)
|
||||
);
|
||||
const { status: configStatus, config } = useConfig(
|
||||
environment,
|
||||
updateEnvironment
|
||||
);
|
||||
const { loading, config } = useConfig(environment, (errorType) => {
|
||||
if (!environment.VEGA_URL) {
|
||||
setNetworkError(errorType);
|
||||
setNodeSwitcherOpen(true);
|
||||
} else {
|
||||
const error = getErrorByType(errorType, environment.VEGA_ENV);
|
||||
error && console.warn(error.headline);
|
||||
}
|
||||
});
|
||||
const { state: nodes, clients } = useNodes(environment.VEGA_ENV, config);
|
||||
const nodeKeys = Object.keys(nodes);
|
||||
|
||||
useEffect(() => {
|
||||
if (!environment.VEGA_URL) {
|
||||
const successfulNodeKey = nodeKeys.find(
|
||||
(key) => nodes[key].verified
|
||||
) as keyof typeof nodes;
|
||||
if (successfulNodeKey && nodes[successfulNodeKey]) {
|
||||
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||
updateEnvironment((prevEnvironment) => ({
|
||||
...prevEnvironment,
|
||||
VEGA_URL: nodes[successfulNodeKey].url,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// if the selected node has errors
|
||||
if (environment.VEGA_URL && nodes[environment.VEGA_URL]) {
|
||||
const errorType = getErrorType(
|
||||
environment.VEGA_ENV,
|
||||
nodes[environment.VEGA_URL]
|
||||
);
|
||||
if (errorType !== null) {
|
||||
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||
setNetworkError(errorType);
|
||||
setNodeSwitcherOpen(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// if the config doesn't contain nodes the app can connect to
|
||||
if (
|
||||
nodeKeys.length > 0 &&
|
||||
nodeKeys.filter((key) => hasFinishedLoading(nodes[key])).length ===
|
||||
nodeKeys.length
|
||||
) {
|
||||
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||
setNetworkError(ErrorType.CONNECTION_ERROR_ALL);
|
||||
setNodeSwitcherOpen(true);
|
||||
}
|
||||
// prevent infinite render loop by skipping deps which will change as a result
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [environment.VEGA_URL, nodes]);
|
||||
|
||||
const errorMessage = validateEnvironment(environment);
|
||||
|
||||
@ -42,20 +102,20 @@ export const EnvironmentProvider = ({
|
||||
<EnvironmentContext.Provider
|
||||
value={{
|
||||
...environment,
|
||||
configStatus,
|
||||
networkError,
|
||||
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
||||
}}
|
||||
>
|
||||
{config && (
|
||||
<NodeSwitcherDialog
|
||||
dialogOpen={isNodeSwitcherOpen}
|
||||
toggleDialogOpen={setNodeSwitcherOpen}
|
||||
config={config}
|
||||
onConnect={(url) =>
|
||||
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<NodeSwitcherDialog
|
||||
dialogOpen={isNodeSwitcherOpen}
|
||||
initialErrorType={networkError}
|
||||
setDialogOpen={setNodeSwitcherOpen}
|
||||
loading={loading}
|
||||
config={config}
|
||||
onConnect={(url) =>
|
||||
updateEnvironment((env) => ({ ...env, VEGA_URL: url }))
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</EnvironmentContext.Provider>
|
||||
);
|
||||
|
@ -1,267 +0,0 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
import { useNode, STATS_QUERY, TIME_UPDATE_SUBSCRIPTION } from './use-node';
|
||||
|
||||
const MOCK_DURATION = 1073;
|
||||
|
||||
const MOCK_STATISTICS_QUERY_RESULT = {
|
||||
blockHeight: '11',
|
||||
chainId: 'testnet_01234',
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
url: '',
|
||||
responseTime: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
block: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
ssl: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
chain: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const createMockClient = ({
|
||||
failStats = false,
|
||||
failSubscription = false,
|
||||
}: { failStats?: boolean; failSubscription?: boolean } = {}) => {
|
||||
const provider = new MockedProvider({
|
||||
mocks: [
|
||||
{
|
||||
request: {
|
||||
query: STATS_QUERY,
|
||||
},
|
||||
result: failStats
|
||||
? undefined
|
||||
: {
|
||||
data: {
|
||||
statistics: {
|
||||
__typename: 'Statistics',
|
||||
...MOCK_STATISTICS_QUERY_RESULT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: TIME_UPDATE_SUBSCRIPTION,
|
||||
},
|
||||
result: failSubscription
|
||||
? undefined
|
||||
: {
|
||||
data: {
|
||||
busEvents: {
|
||||
eventId: 'time-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return provider.state.client;
|
||||
};
|
||||
|
||||
window.performance.getEntriesByName = jest
|
||||
.fn()
|
||||
.mockImplementation((url: string) => [
|
||||
{
|
||||
entryType: 'resource',
|
||||
name: url,
|
||||
startTime: 0,
|
||||
toJSON: () => ({}),
|
||||
duration: MOCK_DURATION,
|
||||
},
|
||||
]);
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore allow deleting the spy function after we're done with the tests
|
||||
delete window.performance.getEntriesByName;
|
||||
});
|
||||
|
||||
describe('useNode hook', () => {
|
||||
it('returns the default state when no arguments provided', () => {
|
||||
const { result } = renderHook(() => useNode());
|
||||
expect(result.current.state).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('returns the default state when no url provided', () => {
|
||||
const client = createMockClient();
|
||||
const { result } = renderHook(() => useNode(undefined, client));
|
||||
expect(result.current.state).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('returns the default state when no client provided', () => {
|
||||
const url = 'https://some.url';
|
||||
const { result } = renderHook(() => useNode(url, undefined));
|
||||
expect(result.current.state).toEqual({ ...initialState, url });
|
||||
});
|
||||
|
||||
it('sets loading state while waiting for the results', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient();
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useNode(url, client)
|
||||
);
|
||||
|
||||
expect(result.current.state).toEqual({
|
||||
url,
|
||||
responseTime: {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
block: {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
ssl: {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
chain: {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
it('sets statistics results', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient();
|
||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.block).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight),
|
||||
});
|
||||
|
||||
expect(result.current.state.chain).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: MOCK_STATISTICS_QUERY_RESULT.chainId,
|
||||
});
|
||||
|
||||
expect(result.current.state.responseTime).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: MOCK_DURATION,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets subscription result', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient();
|
||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.ssl).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when statistics request fails', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient({ failStats: true });
|
||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.block).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
expect(result.current.state.chain).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
expect(result.current.state.responseTime).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when subscription request fails', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient({ failSubscription: true });
|
||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.ssl).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('allows updating block values', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient({ failSubscription: true });
|
||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.block.value).toEqual(11);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.updateBlockState(12);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.block.value).toEqual(12);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows resetting the state to defaults', async () => {
|
||||
const url = 'https://some.url';
|
||||
const client = createMockClient();
|
||||
const { result, waitFor } = renderHook(() => useNode(url, client));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.block.value).toBe(
|
||||
Number(MOCK_STATISTICS_QUERY_RESULT.blockHeight)
|
||||
);
|
||||
expect(result.current.state.chain.value).toBe(
|
||||
MOCK_STATISTICS_QUERY_RESULT.chainId
|
||||
);
|
||||
expect(result.current.state.responseTime.value).toBe(MOCK_DURATION);
|
||||
expect(result.current.state.ssl.value).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.state).toEqual({ ...initialState, url });
|
||||
});
|
||||
});
|
@ -1,188 +0,0 @@
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import { gql } from '@apollo/client';
|
||||
import type { createClient } from '../utils/apollo-client';
|
||||
import type { NodeData } from '../types';
|
||||
import type { Statistics } from './__generated__/Statistics';
|
||||
|
||||
type StatisticsPayload = {
|
||||
block: NodeData['block']['value'];
|
||||
chain: NodeData['chain']['value'];
|
||||
responseTime: NodeData['responseTime']['value'];
|
||||
};
|
||||
|
||||
export const STATS_QUERY = gql`
|
||||
query Statistics {
|
||||
statistics {
|
||||
chainId
|
||||
blockHeight
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TIME_UPDATE_SUBSCRIPTION = gql`
|
||||
subscription BlockTime {
|
||||
busEvents(types: TimeUpdate, batchSize: 1) {
|
||||
eventId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
enum ACTION {
|
||||
GET_STATISTICS,
|
||||
GET_STATISTICS_SUCCESS,
|
||||
GET_STATISTICS_FAILURE,
|
||||
CHECK_SUBSCRIPTION,
|
||||
CHECK_SUBSCRIPTION_SUCCESS,
|
||||
CHECK_SUBSCRIPTION_FAILURE,
|
||||
UPDATE_BLOCK,
|
||||
RESET_STATE,
|
||||
}
|
||||
|
||||
function withData<T>(value?: T) {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function withError<T>(value?: T) {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
const getInitialState = (url?: string): NodeData => ({
|
||||
url: url ?? '',
|
||||
responseTime: withData(),
|
||||
block: withData(),
|
||||
ssl: withData(),
|
||||
chain: withData(),
|
||||
});
|
||||
|
||||
const getResponseTime = (url: string) => {
|
||||
const requests = window.performance.getEntriesByName(url);
|
||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||
return duration;
|
||||
};
|
||||
|
||||
type ActionType<T extends ACTION, P = undefined> = {
|
||||
type: T;
|
||||
payload?: P;
|
||||
};
|
||||
|
||||
type Action =
|
||||
| ActionType<ACTION.GET_STATISTICS>
|
||||
| ActionType<ACTION.GET_STATISTICS_SUCCESS, StatisticsPayload>
|
||||
| ActionType<ACTION.GET_STATISTICS_FAILURE>
|
||||
| ActionType<ACTION.CHECK_SUBSCRIPTION>
|
||||
| ActionType<ACTION.CHECK_SUBSCRIPTION_SUCCESS>
|
||||
| ActionType<ACTION.CHECK_SUBSCRIPTION_FAILURE>
|
||||
| ActionType<ACTION.UPDATE_BLOCK, number>
|
||||
| ActionType<ACTION.RESET_STATE>;
|
||||
|
||||
const reducer = (state: NodeData, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ACTION.GET_STATISTICS:
|
||||
return produce(state, (state) => {
|
||||
state.block.isLoading = true;
|
||||
state.chain.isLoading = true;
|
||||
state.responseTime.isLoading = true;
|
||||
});
|
||||
case ACTION.GET_STATISTICS_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
state.block = withData(action.payload?.block);
|
||||
state.chain = withData(action.payload?.chain);
|
||||
state.responseTime = withData(action.payload?.responseTime);
|
||||
});
|
||||
case ACTION.GET_STATISTICS_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
state.block = withError();
|
||||
state.chain = withError();
|
||||
state.responseTime = withError();
|
||||
});
|
||||
case ACTION.CHECK_SUBSCRIPTION:
|
||||
return produce(state, (state) => {
|
||||
state.ssl.isLoading = true;
|
||||
});
|
||||
case ACTION.CHECK_SUBSCRIPTION_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
state.ssl = withData(true);
|
||||
});
|
||||
case ACTION.CHECK_SUBSCRIPTION_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
state.ssl = withError();
|
||||
});
|
||||
case ACTION.UPDATE_BLOCK:
|
||||
return produce(state, (state) => {
|
||||
state.block.value = action.payload;
|
||||
});
|
||||
case ACTION.RESET_STATE:
|
||||
return produce(state, (state) => {
|
||||
state.responseTime = withData();
|
||||
state.block = withData();
|
||||
state.ssl = withData();
|
||||
state.chain = withData();
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const useNode = (
|
||||
url?: string,
|
||||
client?: ReturnType<typeof createClient>
|
||||
) => {
|
||||
const [state, dispatch] = useReducer(reducer, getInitialState(url));
|
||||
|
||||
useEffect(() => {
|
||||
if (client && url) {
|
||||
dispatch({ type: ACTION.GET_STATISTICS });
|
||||
dispatch({ type: ACTION.CHECK_SUBSCRIPTION });
|
||||
|
||||
client
|
||||
.query<Statistics>({
|
||||
query: STATS_QUERY,
|
||||
})
|
||||
.then((res) => {
|
||||
dispatch({
|
||||
type: ACTION.GET_STATISTICS_SUCCESS,
|
||||
payload: {
|
||||
chain: res.data.statistics.chainId,
|
||||
block: Number(res.data.statistics.blockHeight),
|
||||
responseTime: getResponseTime(url),
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch({ type: ACTION.GET_STATISTICS_FAILURE });
|
||||
});
|
||||
|
||||
const subscription = client
|
||||
.subscribe({
|
||||
query: TIME_UPDATE_SUBSCRIPTION,
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
.subscribe({
|
||||
next() {
|
||||
dispatch({ type: ACTION.CHECK_SUBSCRIPTION_SUCCESS });
|
||||
subscription.unsubscribe();
|
||||
},
|
||||
error() {
|
||||
dispatch({ type: ACTION.CHECK_SUBSCRIPTION_FAILURE });
|
||||
subscription.unsubscribe();
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [client, url, dispatch]);
|
||||
|
||||
return {
|
||||
state,
|
||||
updateBlockState: (value: number) =>
|
||||
dispatch({ type: ACTION.UPDATE_BLOCK, payload: value }),
|
||||
reset: () => dispatch({ type: ACTION.RESET_STATE }),
|
||||
};
|
||||
};
|
401
libs/environment/src/hooks/use-nodes.spec.tsx
Normal file
401
libs/environment/src/hooks/use-nodes.spec.tsx
Normal file
@ -0,0 +1,401 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { ApolloClient } from '@apollo/client';
|
||||
import createClient from '../utils/apollo-client';
|
||||
import { useNodes } from './use-nodes';
|
||||
import { Networks } from '../types';
|
||||
import createMockClient, {
|
||||
getMockStatisticsResult,
|
||||
} from './mocks/apollo-client';
|
||||
|
||||
jest.mock('../utils/apollo-client');
|
||||
|
||||
const MOCK_ENV = Networks.DEVNET;
|
||||
const MOCK_DURATION = 1073;
|
||||
|
||||
const initialState = {
|
||||
url: '',
|
||||
verified: false,
|
||||
initialized: false,
|
||||
responseTime: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
block: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
ssl: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
chain: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
window.performance.getEntriesByName = jest
|
||||
.fn()
|
||||
.mockImplementation((url: string) => [
|
||||
{
|
||||
entryType: 'resource',
|
||||
name: url,
|
||||
startTime: 0,
|
||||
toJSON: () => ({}),
|
||||
duration: MOCK_DURATION,
|
||||
},
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() => createMockClient());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useNodes hook', () => {
|
||||
it('returns the default state when empty config provided', () => {
|
||||
const { result } = renderHook(() => useNodes(MOCK_ENV, { hosts: [] }));
|
||||
|
||||
expect(result.current.state).toEqual({});
|
||||
});
|
||||
|
||||
it('sets loading state while waiting for the results', async () => {
|
||||
const node = 'https://some.url';
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
expect(result.current.state[node]).toEqual({
|
||||
...initialState,
|
||||
url: node,
|
||||
verified: false,
|
||||
initialized: true,
|
||||
responseTime: {
|
||||
...initialState.responseTime,
|
||||
isLoading: true,
|
||||
},
|
||||
block: {
|
||||
...initialState.block,
|
||||
isLoading: true,
|
||||
},
|
||||
chain: {
|
||||
...initialState.chain,
|
||||
isLoading: true,
|
||||
},
|
||||
ssl: {
|
||||
...initialState.ssl,
|
||||
isLoading: true,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
it('sets statistics results', async () => {
|
||||
const mockResult = getMockStatisticsResult();
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: Number(mockResult.statistics.blockHeight),
|
||||
});
|
||||
|
||||
expect(result.current.state[node].chain).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: mockResult.statistics.chainId,
|
||||
});
|
||||
|
||||
expect(result.current.state[node].responseTime).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: MOCK_DURATION,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets subscription result', async () => {
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].ssl).toEqual({
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when host in not a valid url', async () => {
|
||||
const node = 'not-url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.hasError).toBe(true);
|
||||
expect(result.current.state[node].chain.hasError).toBe(true);
|
||||
expect(result.current.state[node].responseTime.hasError).toBe(true);
|
||||
expect(result.current.state[node].responseTime.hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when statistics request fails', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() =>
|
||||
createMockClient({ statistics: { hasError: true } })
|
||||
);
|
||||
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
expect(result.current.state[node].chain).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
expect(result.current.state[node].responseTime).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when subscription request fails', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() =>
|
||||
createMockClient({ busEvents: { hasError: true } })
|
||||
);
|
||||
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].ssl).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('allows updating block values', async () => {
|
||||
const mockResult = getMockStatisticsResult();
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.value).toEqual(
|
||||
Number(mockResult.statistics.blockHeight)
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeBlock(node, 12);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.value).toEqual(12);
|
||||
});
|
||||
});
|
||||
|
||||
it('does nothing when calling the block update on a non-existing node', async () => {
|
||||
const mockResult = getMockStatisticsResult();
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.value).toEqual(
|
||||
Number(mockResult.statistics.blockHeight)
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeBlock('https://non-existing.url', 12);
|
||||
});
|
||||
|
||||
expect(result.current.state['https://non-existing.url']).toBe(undefined);
|
||||
});
|
||||
|
||||
it('adds new node', async () => {
|
||||
const node = 'custom-node-key';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
|
||||
expect(result.current.state[node]).toEqual(undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.addNode(node);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node]).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets new url for node', async () => {
|
||||
const node = 'https://some.url';
|
||||
const newUrl = 'https://some-other.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, newUrl);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].url).toBe(newUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when custom node has an invalid url', async () => {
|
||||
const node = 'node-key';
|
||||
const url = 'not-url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
|
||||
expect(result.current.state[node]).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, url);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].url).toBe(url);
|
||||
expect(result.current.state[node].block.hasError).toBe(true);
|
||||
expect(result.current.state[node].chain.hasError).toBe(true);
|
||||
expect(result.current.state[node].responseTime.hasError).toBe(true);
|
||||
expect(result.current.state[node].ssl.hasError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when custom node statistics request fails', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() =>
|
||||
createMockClient({ statistics: { hasError: true } })
|
||||
);
|
||||
|
||||
const node = 'node-key';
|
||||
const url = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
|
||||
expect(result.current.state[node]).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, url);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
expect(result.current.state[node].chain).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
expect(result.current.state[node].responseTime).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets error when custom node subscription fails', async () => {
|
||||
// @ts-ignore allow adding a mock return value to mocked module
|
||||
createClient.mockImplementation(() =>
|
||||
createMockClient({ busEvents: { hasError: true } })
|
||||
);
|
||||
|
||||
const node = 'node-key';
|
||||
const url = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
|
||||
expect(result.current.state[node]).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, url);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].ssl).toEqual({
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes a collection of clients', async () => {
|
||||
const url1 = 'https://some.url';
|
||||
const url2 = 'https://some-other.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [url1, url2] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.clients[url1]).toBeInstanceOf(ApolloClient);
|
||||
expect(result.current.clients[url2]).toBeInstanceOf(ApolloClient);
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes a client for the custom node', async () => {
|
||||
const node = 'node-key';
|
||||
const url = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, url);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.clients[url]).toBeInstanceOf(ApolloClient);
|
||||
});
|
||||
});
|
||||
});
|
260
libs/environment/src/hooks/use-nodes.tsx
Normal file
260
libs/environment/src/hooks/use-nodes.tsx
Normal file
@ -0,0 +1,260 @@
|
||||
import type { Dispatch } from 'react';
|
||||
import { useState, useEffect, useReducer } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import type createClient from '../utils/apollo-client';
|
||||
import { initializeNode } from '../utils/initialize-node';
|
||||
import { getErrorType, getIsNodeLoading } from '../utils/validate-node';
|
||||
import type { NodeData, Configuration, Networks } from '../types';
|
||||
|
||||
type StatisticsPayload = {
|
||||
block: NodeData['block']['value'];
|
||||
chain: NodeData['chain']['value'];
|
||||
responseTime: NodeData['responseTime']['value'];
|
||||
};
|
||||
|
||||
export enum ACTIONS {
|
||||
GET_STATISTICS,
|
||||
GET_STATISTICS_SUCCESS,
|
||||
GET_STATISTICS_FAILURE,
|
||||
CHECK_SUBSCRIPTION,
|
||||
CHECK_SUBSCRIPTION_SUCCESS,
|
||||
CHECK_SUBSCRIPTION_FAILURE,
|
||||
ADD_NODE,
|
||||
UPDATE_NODE_URL,
|
||||
UPDATE_NODE_BLOCK,
|
||||
}
|
||||
|
||||
type ActionType<T extends ACTIONS, P = undefined> = {
|
||||
type: T;
|
||||
node: string;
|
||||
payload?: P;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| ActionType<ACTIONS.GET_STATISTICS, { url: string }>
|
||||
| ActionType<ACTIONS.GET_STATISTICS_SUCCESS, StatisticsPayload>
|
||||
| ActionType<ACTIONS.GET_STATISTICS_FAILURE>
|
||||
| ActionType<ACTIONS.CHECK_SUBSCRIPTION, { url: string }>
|
||||
| ActionType<ACTIONS.CHECK_SUBSCRIPTION_SUCCESS>
|
||||
| ActionType<ACTIONS.CHECK_SUBSCRIPTION_FAILURE>
|
||||
| ActionType<ACTIONS.ADD_NODE>
|
||||
| ActionType<ACTIONS.UPDATE_NODE_URL, { url: string }>
|
||||
| ActionType<ACTIONS.UPDATE_NODE_BLOCK, number>;
|
||||
|
||||
function withData<T>(value?: T) {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function withError<T>(value?: T) {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasError: true,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
const getNodeData = (url?: string): NodeData => ({
|
||||
url: url ?? '',
|
||||
verified: false,
|
||||
initialized: false,
|
||||
responseTime: withData(),
|
||||
block: withData(),
|
||||
ssl: withData(),
|
||||
chain: withData(),
|
||||
});
|
||||
|
||||
const getInitialState = (config?: Configuration) =>
|
||||
(config?.hosts ?? []).reduce<Record<string, NodeData>>(
|
||||
(acc, url) => ({
|
||||
...acc,
|
||||
[url]: getNodeData(url),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
type ClientCollection = Record<
|
||||
string,
|
||||
undefined | ReturnType<typeof createClient>
|
||||
>;
|
||||
|
||||
type ClientData = {
|
||||
clients: ClientCollection;
|
||||
subscriptions: ReturnType<typeof initializeNode>['unsubscribe'][];
|
||||
};
|
||||
|
||||
const initializeNodes = (
|
||||
dispatch: Dispatch<Action>,
|
||||
nodes: Record<string, string>
|
||||
) => {
|
||||
return Object.keys(nodes).reduce<ClientData>(
|
||||
(acc, node) => {
|
||||
const { client, unsubscribe } = initializeNode(
|
||||
dispatch,
|
||||
node,
|
||||
nodes[node]
|
||||
);
|
||||
Object.assign(acc.clients, { [nodes[node]]: client });
|
||||
acc.subscriptions.push(unsubscribe);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
clients: {},
|
||||
subscriptions: [],
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const reducer =
|
||||
(env: Networks) => (state: Record<string, NodeData>, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.GET_STATISTICS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) {
|
||||
state[action.node] = getNodeData(action.payload?.url);
|
||||
}
|
||||
state[action.node].url = action.payload?.url ?? '';
|
||||
state[action.node].initialized = true;
|
||||
state[action.node].block.isLoading = true;
|
||||
state[action.node].chain.isLoading = true;
|
||||
state[action.node].responseTime.isLoading = true;
|
||||
});
|
||||
case ACTIONS.GET_STATISTICS_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block = withData(action.payload?.block);
|
||||
state[action.node].chain = withData(action.payload?.chain);
|
||||
state[action.node].responseTime = withData(
|
||||
action.payload?.responseTime
|
||||
);
|
||||
state[action.node].verified =
|
||||
!getIsNodeLoading(state[action.node]) &&
|
||||
getErrorType(env, state[action.node]) === null;
|
||||
});
|
||||
case ACTIONS.GET_STATISTICS_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block = withError();
|
||||
state[action.node].chain = withError();
|
||||
state[action.node].responseTime = withError();
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) {
|
||||
state[action.node] = getNodeData(action.payload?.url);
|
||||
}
|
||||
state[action.node].url = action.payload?.url ?? '';
|
||||
state[action.node].ssl.isLoading = true;
|
||||
state[action.node].initialized = true;
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].ssl = withData(true);
|
||||
state[action.node].verified =
|
||||
!getIsNodeLoading(state[action.node]) &&
|
||||
getErrorType(env, state[action.node]) === null;
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].ssl = withError();
|
||||
});
|
||||
case ACTIONS.ADD_NODE:
|
||||
return produce(state, (state) => {
|
||||
state[action.node] = getNodeData();
|
||||
});
|
||||
case ACTIONS.UPDATE_NODE_URL:
|
||||
return produce(state, (state) => {
|
||||
const existingNode = Object.keys(state).find(
|
||||
(node) =>
|
||||
action.node !== node && state[node].url === action.payload?.url
|
||||
);
|
||||
state[action.node] = existingNode
|
||||
? state[existingNode]
|
||||
: getNodeData(action.payload?.url);
|
||||
});
|
||||
case ACTIONS.UPDATE_NODE_BLOCK:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block.value = action.payload;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const useNodes = (env: Networks, config?: Configuration) => {
|
||||
const [clients, setClients] = useState<ClientCollection>({});
|
||||
const [state, dispatch] = useReducer(reducer(env), getInitialState(config));
|
||||
const configCacheKey = config?.hosts.join(';');
|
||||
const allUrls = Object.keys(state).map((node) => state[node].url);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Object.keys(clients).forEach((url) => clients[url]?.stop());
|
||||
};
|
||||
// stop all created clients on unmount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nodeUrlMap = (config?.hosts || []).reduce(
|
||||
(acc, url) => ({ ...acc, [url]: url }),
|
||||
{}
|
||||
);
|
||||
const { clients: newClients, subscriptions } = initializeNodes(
|
||||
dispatch,
|
||||
nodeUrlMap
|
||||
);
|
||||
setClients(newClients);
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
// use primitive cache key to prevent infinite rerender loop
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [configCacheKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const allNodes = Object.keys(state);
|
||||
const initializedUrls = Object.keys(clients);
|
||||
const nodeUrlMap = allUrls
|
||||
.filter((node) => !initializedUrls.includes(node))
|
||||
.reduce<Record<string, string>>((acc, url) => {
|
||||
const node = allNodes.find((key) => state[key].url === url);
|
||||
if (node) {
|
||||
acc[node] = url;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const { clients: newClients, subscriptions } = initializeNodes(
|
||||
dispatch,
|
||||
nodeUrlMap
|
||||
);
|
||||
setClients((prevClients) => ({
|
||||
...prevClients,
|
||||
...newClients,
|
||||
}));
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
// use primitive cache key to prevent infinite rerender loop
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allUrls.join(';')]);
|
||||
|
||||
return {
|
||||
state,
|
||||
clients,
|
||||
addNode: (node: string) => dispatch({ type: ACTIONS.ADD_NODE, node }),
|
||||
updateNodeUrl: (node: string, url: string) =>
|
||||
dispatch({ type: ACTIONS.UPDATE_NODE_URL, node, payload: { url } }),
|
||||
updateNodeBlock: (node: string, value: number) =>
|
||||
dispatch({ type: ACTIONS.UPDATE_NODE_BLOCK, node, payload: value }),
|
||||
};
|
||||
};
|
@ -6,6 +6,18 @@ import { Networks, ENV_KEYS } from './utils/validate-environment';
|
||||
|
||||
export { ENV_KEYS, Networks };
|
||||
|
||||
export const CUSTOM_NODE_KEY = 'custom';
|
||||
|
||||
export enum ErrorType {
|
||||
INVALID_URL,
|
||||
INVALID_NETWORK,
|
||||
SSL_ERROR,
|
||||
CONNECTION_ERROR,
|
||||
CONNECTION_ERROR_ALL,
|
||||
CONFIG_LOAD_ERROR,
|
||||
CONFIG_VALIDATION_ERROR,
|
||||
}
|
||||
|
||||
export type Environment = z.infer<typeof envSchema> & {
|
||||
// provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS
|
||||
VEGA_NETWORKS: Partial<Record<Networks, string>>;
|
||||
@ -17,15 +29,6 @@ export type RawEnvironment = Record<EnvKey, string>;
|
||||
|
||||
export type Configuration = z.infer<typeof configSchema>;
|
||||
|
||||
export type ConfigStatus =
|
||||
| 'idle'
|
||||
| 'success'
|
||||
| 'loading-config'
|
||||
| 'loading-node'
|
||||
| 'error-loading-config'
|
||||
| 'error-validating-config'
|
||||
| 'error-loading-node';
|
||||
|
||||
type NodeCheck<T> = {
|
||||
isLoading: boolean;
|
||||
hasError: boolean;
|
||||
@ -34,6 +37,8 @@ type NodeCheck<T> = {
|
||||
|
||||
export type NodeData = {
|
||||
url: string;
|
||||
verified: boolean;
|
||||
initialized: boolean;
|
||||
ssl: NodeCheck<boolean>;
|
||||
block: NodeCheck<number>;
|
||||
responseTime: NodeCheck<number>;
|
||||
|
@ -14,13 +14,14 @@ import { RetryLink } from '@apollo/client/link/retry';
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
export function createClient(base?: string) {
|
||||
export const GQL_PATH = 'query';
|
||||
|
||||
export default function createClient(base?: string) {
|
||||
if (!base) {
|
||||
throw new Error('Base must be passed into createClient!');
|
||||
}
|
||||
const gqlPath = 'query';
|
||||
const urlHTTP = new URL(gqlPath, base);
|
||||
const urlWS = new URL(gqlPath, base);
|
||||
const urlHTTP = new URL(GQL_PATH, base);
|
||||
const urlWS = new URL(GQL_PATH, base);
|
||||
// Replace http with ws, preserving if its a secure connection eg. https => wss
|
||||
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
|
||||
|
||||
|
56
libs/environment/src/utils/initialize-node.tsx
Normal file
56
libs/environment/src/utils/initialize-node.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import type { Dispatch } from 'react';
|
||||
import { ACTIONS } from '../hooks/use-nodes';
|
||||
import type { Action } from '../hooks/use-nodes';
|
||||
import { requestNode } from './request-node';
|
||||
import { GQL_PATH } from './apollo-client';
|
||||
|
||||
const getResponseTime = (url: string) => {
|
||||
const requestUrl = new URL(GQL_PATH, url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||
return duration;
|
||||
};
|
||||
|
||||
export const initializeNode = (
|
||||
dispatch: Dispatch<Action>,
|
||||
node: string,
|
||||
nodeUrl?: string
|
||||
) => {
|
||||
let isMounted = true;
|
||||
const url = nodeUrl ?? node;
|
||||
|
||||
dispatch({ type: ACTIONS.GET_STATISTICS, node, payload: { url } });
|
||||
dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION, node, payload: { url } });
|
||||
|
||||
const client = requestNode(url, {
|
||||
onStatsSuccess: (data) => {
|
||||
isMounted &&
|
||||
dispatch({
|
||||
type: ACTIONS.GET_STATISTICS_SUCCESS,
|
||||
node,
|
||||
payload: {
|
||||
chain: data.statistics.chainId,
|
||||
block: Number(data.statistics.blockHeight),
|
||||
responseTime: getResponseTime(url),
|
||||
},
|
||||
});
|
||||
},
|
||||
onStatsFailure: () => {
|
||||
isMounted && dispatch({ type: ACTIONS.GET_STATISTICS_FAILURE, node });
|
||||
},
|
||||
onSubscriptionSuccess: () => {
|
||||
isMounted && dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_SUCCESS, node });
|
||||
},
|
||||
onSubscriptionFailure: () => {
|
||||
isMounted && dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_FAILURE, node });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
client,
|
||||
unsubscribe: () => {
|
||||
client?.stop();
|
||||
isMounted = false;
|
||||
},
|
||||
};
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
export function promiseRaceToSuccess<T>(requests: Array<Promise<T>>) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let hasResolved = false;
|
||||
const failures = [];
|
||||
|
||||
requests.forEach((req) => {
|
||||
req
|
||||
.then((res) => {
|
||||
if (!hasResolved) {
|
||||
resolve(res);
|
||||
hasResolved = true;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
failures.push(err);
|
||||
if (failures.length === requests.length) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
76
libs/environment/src/utils/request-node.ts
Normal file
76
libs/environment/src/utils/request-node.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import createClient from './apollo-client';
|
||||
import type { Statistics } from './__generated__/Statistics';
|
||||
|
||||
export const STATS_QUERY = gql`
|
||||
query Statistics {
|
||||
statistics {
|
||||
chainId
|
||||
blockHeight
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TIME_UPDATE_SUBSCRIPTION = gql`
|
||||
subscription BlockTime {
|
||||
busEvents(types: TimeUpdate, batchSize: 1) {
|
||||
eventId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Callbacks = {
|
||||
onStatsSuccess: (data: Statistics) => void;
|
||||
onStatsFailure: () => void;
|
||||
onSubscriptionSuccess: () => void;
|
||||
onSubscriptionFailure: () => void;
|
||||
};
|
||||
|
||||
export const requestNode = (
|
||||
url: string,
|
||||
{
|
||||
onStatsSuccess,
|
||||
onStatsFailure,
|
||||
onSubscriptionSuccess,
|
||||
onSubscriptionFailure,
|
||||
}: Callbacks
|
||||
) => {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (err) {
|
||||
onStatsFailure();
|
||||
onSubscriptionFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient(url);
|
||||
|
||||
client
|
||||
.query<Statistics>({
|
||||
query: STATS_QUERY,
|
||||
})
|
||||
.then((res) => {
|
||||
onStatsSuccess(res.data);
|
||||
})
|
||||
.catch(() => {
|
||||
onStatsFailure();
|
||||
});
|
||||
|
||||
const subscription = client
|
||||
.subscribe({
|
||||
query: TIME_UPDATE_SUBSCRIPTION,
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
.subscribe({
|
||||
next() {
|
||||
onSubscriptionSuccess();
|
||||
subscription.unsubscribe();
|
||||
},
|
||||
error() {
|
||||
onSubscriptionFailure();
|
||||
subscription.unsubscribe();
|
||||
},
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
149
libs/environment/src/utils/validate-node.tsx
Normal file
149
libs/environment/src/utils/validate-node.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { CUSTOM_NODE_KEY, ErrorType } from '../types';
|
||||
import type { Networks, NodeData } from '../types';
|
||||
|
||||
export const getIsNodeLoading = ({
|
||||
chain,
|
||||
responseTime,
|
||||
block,
|
||||
ssl,
|
||||
}: NodeData) => {
|
||||
return (
|
||||
chain.isLoading ||
|
||||
responseTime.isLoading ||
|
||||
block.isLoading ||
|
||||
ssl.isLoading
|
||||
);
|
||||
};
|
||||
|
||||
export const getHasInvalidChain = (env: Networks, chain = '') => {
|
||||
return !(chain.split('-')[0] === env.toLowerCase() ?? false);
|
||||
};
|
||||
|
||||
export const getIsInvalidUrl = (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return false;
|
||||
} catch (err) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsNodeDisabled = (env: Networks, data?: NodeData) => {
|
||||
return (
|
||||
!!data &&
|
||||
(getIsNodeLoading(data) ||
|
||||
getHasInvalidChain(env, data.chain.value) ||
|
||||
getIsInvalidUrl(data.url) ||
|
||||
data.chain.hasError ||
|
||||
data.responseTime.hasError ||
|
||||
data.block.hasError ||
|
||||
data.ssl.hasError)
|
||||
);
|
||||
};
|
||||
|
||||
export const getIsFormDisabled = (
|
||||
currentNode: string | undefined,
|
||||
inputText: string,
|
||||
env: Networks,
|
||||
state: Record<string, NodeData>
|
||||
) => {
|
||||
if (!currentNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentNode === CUSTOM_NODE_KEY &&
|
||||
state[CUSTOM_NODE_KEY] &&
|
||||
inputText !== state[CUSTOM_NODE_KEY].url
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const data = state[currentNode];
|
||||
return getIsNodeDisabled(env, data);
|
||||
};
|
||||
|
||||
export const getErrorByType = (
|
||||
errorType: ErrorType | undefined | null,
|
||||
env: Networks,
|
||||
url?: string
|
||||
) => {
|
||||
switch (errorType) {
|
||||
case ErrorType.INVALID_URL:
|
||||
return {
|
||||
headline: t('Error: invalid url'),
|
||||
message: t(url ? `${url} is not a valid url.` : ''),
|
||||
};
|
||||
case ErrorType.INVALID_NETWORK:
|
||||
return {
|
||||
headline: t(`Error: incorrect network`),
|
||||
message: t(`This node is not on the ${env} network.`),
|
||||
};
|
||||
case ErrorType.SSL_ERROR:
|
||||
return {
|
||||
headline: t(`Error: the node you are reading from does not have SSL`),
|
||||
message: t(
|
||||
url
|
||||
? `${url} does not have SSL. SSL is required to subscribe to data.`
|
||||
: ''
|
||||
),
|
||||
};
|
||||
case ErrorType.CONNECTION_ERROR:
|
||||
return {
|
||||
headline: t(`Error: can't connect to node`),
|
||||
message: t(url ? `There was an error connecting to ${url}.` : ''),
|
||||
};
|
||||
case ErrorType.CONNECTION_ERROR_ALL:
|
||||
return {
|
||||
headline: t(`Error: can't connect to any of the nodes on the network`),
|
||||
message: t(
|
||||
`Please try entering a custom node address, or try again later.`
|
||||
),
|
||||
};
|
||||
case ErrorType.CONFIG_VALIDATION_ERROR:
|
||||
return {
|
||||
headline: t(
|
||||
`Error: the configuration found for the network ${env} is invalid`
|
||||
),
|
||||
message: t(
|
||||
`Please try entering a custom node address, or try again later.`
|
||||
),
|
||||
};
|
||||
case ErrorType.CONFIG_LOAD_ERROR:
|
||||
return {
|
||||
headline: t(`Error: can't load network configuration`),
|
||||
message: t(
|
||||
`You can try entering a custom node address, or try again later.`
|
||||
),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getErrorType = (env: Networks, data?: NodeData) => {
|
||||
if (data && !getIsNodeLoading(data) && data.initialized) {
|
||||
if (getIsInvalidUrl(data.url)) {
|
||||
return ErrorType.INVALID_URL;
|
||||
}
|
||||
|
||||
if (
|
||||
data.chain.hasError ||
|
||||
data.responseTime.hasError ||
|
||||
data.block.hasError
|
||||
) {
|
||||
return ErrorType.CONNECTION_ERROR;
|
||||
}
|
||||
|
||||
if (getHasInvalidChain(env, data.chain.value)) {
|
||||
return ErrorType.INVALID_NETWORK;
|
||||
}
|
||||
|
||||
if (data.ssl.hasError) {
|
||||
return ErrorType.SSL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
@ -14,6 +14,7 @@
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts"
|
||||
"**/*.d.ts",
|
||||
"**/__mocks__/*.tsx"
|
||||
]
|
||||
}
|
||||
|
52
libs/fills/src/lib/__generated__/Fills.ts
generated
52
libs/fills/src/lib/__generated__/Fills.ts
generated
@ -9,7 +9,7 @@ import { Pagination, Side } from "@vegaprotocol/types";
|
||||
// GraphQL query operation: Fills
|
||||
// ====================================================
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_buyer {
|
||||
export interface Fills_party_tradesConnection_edges_node_buyer {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
@ -17,7 +17,7 @@ export interface Fills_party_tradesPaged_edges_node_buyer {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_seller {
|
||||
export interface Fills_party_tradesConnection_edges_node_seller {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
@ -25,7 +25,7 @@ export interface Fills_party_tradesPaged_edges_node_seller {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_buyerFee {
|
||||
export interface Fills_party_tradesConnection_edges_node_buyerFee {
|
||||
__typename: "TradeFee";
|
||||
/**
|
||||
* The maker fee, aggressive party to the other party (the one who had an order in the book)
|
||||
@ -41,7 +41,7 @@ export interface Fills_party_tradesPaged_edges_node_buyerFee {
|
||||
liquidityFee: string;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_sellerFee {
|
||||
export interface Fills_party_tradesConnection_edges_node_sellerFee {
|
||||
__typename: "TradeFee";
|
||||
/**
|
||||
* The maker fee, aggressive party to the other party (the one who had an order in the book)
|
||||
@ -57,7 +57,7 @@ export interface Fills_party_tradesPaged_edges_node_sellerFee {
|
||||
liquidityFee: string;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product_settlementAsset {
|
||||
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
@ -73,15 +73,15 @@ export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_in
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product {
|
||||
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product {
|
||||
__typename: "Future";
|
||||
/**
|
||||
* The name of the asset (string)
|
||||
*/
|
||||
settlementAsset: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product_settlementAsset;
|
||||
settlementAsset: Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product_settlementAsset;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument {
|
||||
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Uniquely identify an instrument across all instruments available on Vega (string)
|
||||
@ -94,18 +94,18 @@ export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_in
|
||||
/**
|
||||
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
||||
*/
|
||||
product: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product;
|
||||
product: Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument_product;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument {
|
||||
export interface Fills_party_tradesConnection_edges_node_market_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of or reference to a fully specified instrument.
|
||||
*/
|
||||
instrument: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument;
|
||||
instrument: Fills_party_tradesConnection_edges_node_market_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node_market {
|
||||
export interface Fills_party_tradesConnection_edges_node_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
@ -141,10 +141,10 @@ export interface Fills_party_tradesPaged_edges_node_market {
|
||||
/**
|
||||
* An instance of or reference to a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: Fills_party_tradesPaged_edges_node_market_tradableInstrument;
|
||||
tradableInstrument: Fills_party_tradesConnection_edges_node_market_tradableInstrument;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges_node {
|
||||
export interface Fills_party_tradesConnection_edges_node {
|
||||
__typename: "Trade";
|
||||
/**
|
||||
* The hash of the trade data
|
||||
@ -177,38 +177,38 @@ export interface Fills_party_tradesPaged_edges_node {
|
||||
/**
|
||||
* The party that bought
|
||||
*/
|
||||
buyer: Fills_party_tradesPaged_edges_node_buyer;
|
||||
buyer: Fills_party_tradesConnection_edges_node_buyer;
|
||||
/**
|
||||
* The party that sold
|
||||
*/
|
||||
seller: Fills_party_tradesPaged_edges_node_seller;
|
||||
seller: Fills_party_tradesConnection_edges_node_seller;
|
||||
/**
|
||||
* The fee paid by the buyer side of the trade
|
||||
*/
|
||||
buyerFee: Fills_party_tradesPaged_edges_node_buyerFee;
|
||||
buyerFee: Fills_party_tradesConnection_edges_node_buyerFee;
|
||||
/**
|
||||
* The fee paid by the seller side of the trade
|
||||
*/
|
||||
sellerFee: Fills_party_tradesPaged_edges_node_sellerFee;
|
||||
sellerFee: Fills_party_tradesConnection_edges_node_sellerFee;
|
||||
/**
|
||||
* The market the trade occurred on
|
||||
*/
|
||||
market: Fills_party_tradesPaged_edges_node_market;
|
||||
market: Fills_party_tradesConnection_edges_node_market;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_edges {
|
||||
export interface Fills_party_tradesConnection_edges {
|
||||
__typename: "TradeEdge";
|
||||
node: Fills_party_tradesPaged_edges_node;
|
||||
node: Fills_party_tradesConnection_edges_node;
|
||||
cursor: string;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged_pageInfo {
|
||||
export interface Fills_party_tradesConnection_pageInfo {
|
||||
__typename: "PageInfo";
|
||||
startCursor: string;
|
||||
endCursor: string;
|
||||
}
|
||||
|
||||
export interface Fills_party_tradesPaged {
|
||||
export interface Fills_party_tradesConnection {
|
||||
__typename: "TradeConnection";
|
||||
/**
|
||||
* The total number of trades in this connection
|
||||
@ -217,11 +217,11 @@ export interface Fills_party_tradesPaged {
|
||||
/**
|
||||
* The trade in this connection
|
||||
*/
|
||||
edges: Fills_party_tradesPaged_edges[];
|
||||
edges: Fills_party_tradesConnection_edges[];
|
||||
/**
|
||||
* The pagination information
|
||||
*/
|
||||
pageInfo: Fills_party_tradesPaged_pageInfo;
|
||||
pageInfo: Fills_party_tradesConnection_pageInfo;
|
||||
}
|
||||
|
||||
export interface Fills_party {
|
||||
@ -230,7 +230,7 @@ export interface Fills_party {
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
tradesPaged: Fills_party_tradesPaged;
|
||||
tradesConnection: Fills_party_tradesConnection;
|
||||
}
|
||||
|
||||
export interface Fills {
|
||||
|
@ -5,7 +5,7 @@ import type { PageInfo, Pagination } from '@vegaprotocol/react-helpers';
|
||||
import type { FillFields } from './__generated__/FillFields';
|
||||
import type {
|
||||
Fills,
|
||||
Fills_party_tradesPaged_edges,
|
||||
Fills_party_tradesConnection_edges,
|
||||
} from './__generated__/Fills';
|
||||
import type { FillsSub } from './__generated__/FillsSub';
|
||||
|
||||
@ -17,19 +17,41 @@ const FILL_FRAGMENT = gql`
|
||||
size
|
||||
buyOrder
|
||||
sellOrder
|
||||
aggressor
|
||||
buyer {
|
||||
id
|
||||
}
|
||||
seller {
|
||||
id
|
||||
}
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
market {
|
||||
id
|
||||
name
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
id
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,7 +63,7 @@ export const FILLS_QUERY = gql`
|
||||
query Fills($partyId: ID!, $marketId: ID, $pagination: Pagination) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
tradesPaged(marketId: $marketId, pagination: $pagination) {
|
||||
tradesConnection(marketId: $marketId, pagination: $pagination) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
@ -67,7 +89,10 @@ export const FILLS_SUB = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const update = (data: Fills_party_tradesPaged_edges[], delta: FillFields[]) => {
|
||||
const update = (
|
||||
data: Fills_party_tradesConnection_edges[],
|
||||
delta: FillFields[]
|
||||
) => {
|
||||
return produce(data, (draft) => {
|
||||
delta.forEach((node) => {
|
||||
const index = draft.findIndex((edge) => edge.node.id === node.id);
|
||||
@ -80,21 +105,23 @@ const update = (data: Fills_party_tradesPaged_edges[], delta: FillFields[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getData = (responseData: Fills): Fills_party_tradesPaged_edges[] | null =>
|
||||
responseData.party?.tradesPaged.edges || null;
|
||||
const getData = (
|
||||
responseData: Fills
|
||||
): Fills_party_tradesConnection_edges[] | null =>
|
||||
responseData.party?.tradesConnection.edges || null;
|
||||
|
||||
const getPageInfo = (responseData: Fills): PageInfo | null =>
|
||||
responseData.party?.tradesPaged.pageInfo || null;
|
||||
responseData.party?.tradesConnection.pageInfo || null;
|
||||
|
||||
const getTotalCount = (responseData: Fills): number | undefined =>
|
||||
responseData.party?.tradesPaged.totalCount;
|
||||
responseData.party?.tradesConnection.totalCount;
|
||||
|
||||
const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || [];
|
||||
|
||||
const append = (
|
||||
data: Fills_party_tradesPaged_edges[] | null,
|
||||
data: Fills_party_tradesConnection_edges[] | null,
|
||||
pageInfo: PageInfo,
|
||||
insertionData: Fills_party_tradesPaged_edges[] | null,
|
||||
insertionData: Fills_party_tradesConnection_edges[] | null,
|
||||
insertionPageInfo: PageInfo | null,
|
||||
pagination?: Pagination
|
||||
) => {
|
||||
|
@ -6,7 +6,7 @@ import { FillsTable } from './fills-table';
|
||||
import type { IGetRowsParams } from 'ag-grid-community';
|
||||
|
||||
import { fillsDataProvider as dataProvider } from './fills-data-provider';
|
||||
import type { Fills_party_tradesPaged_edges } from './__generated__/Fills';
|
||||
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
|
||||
import type { FillsSub_trades } from './__generated__/FillsSub';
|
||||
|
||||
interface FillsManagerProps {
|
||||
@ -15,11 +15,11 @@ interface FillsManagerProps {
|
||||
|
||||
export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const dataRef = useRef<Fills_party_tradesPaged_edges[] | null>(null);
|
||||
const dataRef = useRef<Fills_party_tradesConnection_edges[] | null>(null);
|
||||
const totalCountRef = useRef<number | undefined>(undefined);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: Fills_party_tradesPaged_edges[] }) => {
|
||||
({ data }: { data: Fills_party_tradesConnection_edges[] }) => {
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
@ -35,7 +35,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
data: Fills_party_tradesPaged_edges[];
|
||||
data: Fills_party_tradesConnection_edges[];
|
||||
totalCount?: number;
|
||||
}) => {
|
||||
dataRef.current = data;
|
||||
@ -48,7 +48,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
|
||||
const { data, error, loading, load, totalCount } = useDataProvider<
|
||||
Fills_party_tradesPaged_edges[],
|
||||
Fills_party_tradesConnection_edges[],
|
||||
FillsSub_trades[]
|
||||
>({ dataProvider, update, insert, variables });
|
||||
totalCountRef.current = totalCount;
|
||||
|
@ -16,7 +16,7 @@ Default.args = {
|
||||
partyId: 'party-id',
|
||||
datasource: {
|
||||
getRows: makeGetRows(
|
||||
fills.party?.tradesPaged.edges.map((e) => e.node) || []
|
||||
fills.party?.tradesConnection.edges.map((e) => e.node) || []
|
||||
),
|
||||
},
|
||||
};
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import merge from 'lodash/merge';
|
||||
import type { IGetRowsParams } from 'ag-grid-community';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type {
|
||||
Fills,
|
||||
Fills_party_tradesPaged_edges_node,
|
||||
Fills_party_tradesConnection_edges_node,
|
||||
} from './__generated__/Fills';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
|
||||
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||
const fills: Fills_party_tradesPaged_edges_node[] = [
|
||||
const fills: Fills_party_tradesConnection_edges_node[] = [
|
||||
generateFill({
|
||||
buyer: {
|
||||
id: 'party-id',
|
||||
@ -50,7 +50,7 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||
const defaultResult: Fills = {
|
||||
party: {
|
||||
id: 'buyer-id',
|
||||
tradesPaged: {
|
||||
tradesConnection: {
|
||||
__typename: 'TradeConnection',
|
||||
totalCount: 1,
|
||||
edges: fills.map((f) => {
|
||||
@ -74,9 +74,9 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||
};
|
||||
|
||||
export const generateFill = (
|
||||
override?: PartialDeep<Fills_party_tradesPaged_edges_node>
|
||||
override?: PartialDeep<Fills_party_tradesConnection_edges_node>
|
||||
) => {
|
||||
const defaultFill: Fills_party_tradesPaged_edges_node = {
|
||||
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
||||
__typename: 'Trade',
|
||||
id: '0',
|
||||
createdAt: new Date().toISOString(),
|
||||
@ -135,7 +135,7 @@ export const generateFill = (
|
||||
};
|
||||
|
||||
export const makeGetRows =
|
||||
(data: Fills_party_tradesPaged_edges_node[]) =>
|
||||
(data: Fills_party_tradesConnection_edges_node[]) =>
|
||||
({ successCallback }: IGetRowsParams) => {
|
||||
successCallback(data, data.length);
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Fragment } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Link, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
@ -56,7 +57,7 @@ export const NetworkInfo = () => {
|
||||
<p className="mb-16">
|
||||
{t('Known issues and feedback on')}{' '}
|
||||
{feedbackLinks.map(({ name, url }, index) => (
|
||||
<>
|
||||
<Fragment key={index}>
|
||||
<Link key={index} href={url}>
|
||||
{name}
|
||||
</Link>
|
||||
@ -66,7 +67,7 @@ export const NetworkInfo = () => {
|
||||
{feedbackLinks.length > 1 &&
|
||||
index === feedbackLinks.length - 1 &&
|
||||
`, ${t('and')} `}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
|
5
libs/types/src/__generated__/globalTypes.ts
generated
5
libs/types/src/__generated__/globalTypes.ts
generated
@ -110,6 +110,7 @@ export enum MarketTradingMode {
|
||||
BatchAuction = "BatchAuction",
|
||||
Continuous = "Continuous",
|
||||
MonitoringAuction = "MonitoringAuction",
|
||||
NoTrading = "NoTrading",
|
||||
OpeningAuction = "OpeningAuction",
|
||||
}
|
||||
|
||||
@ -211,6 +212,7 @@ export enum ProposalRejectionReason {
|
||||
EnactTimeTooLate = "EnactTimeTooLate",
|
||||
EnactTimeTooSoon = "EnactTimeTooSoon",
|
||||
IncompatibleTimestamps = "IncompatibleTimestamps",
|
||||
InsufficientEquityLikeShare = "InsufficientEquityLikeShare",
|
||||
InsufficientTokens = "InsufficientTokens",
|
||||
InvalidAsset = "InvalidAsset",
|
||||
InvalidAssetDetails = "InvalidAssetDetails",
|
||||
@ -218,6 +220,7 @@ export enum ProposalRejectionReason {
|
||||
InvalidFutureMaturityTimestamp = "InvalidFutureMaturityTimestamp",
|
||||
InvalidFutureProduct = "InvalidFutureProduct",
|
||||
InvalidInstrumentSecurity = "InvalidInstrumentSecurity",
|
||||
InvalidMarket = "InvalidMarket",
|
||||
InvalidRiskParameter = "InvalidRiskParameter",
|
||||
InvalidShape = "InvalidShape",
|
||||
MajorityThresholdNotReached = "MajorityThresholdNotReached",
|
||||
@ -236,6 +239,8 @@ export enum ProposalRejectionReason {
|
||||
OpeningAuctionDurationTooSmall = "OpeningAuctionDurationTooSmall",
|
||||
ParticipationThresholdNotReached = "ParticipationThresholdNotReached",
|
||||
ProductMaturityIsPassed = "ProductMaturityIsPassed",
|
||||
TooManyMarketDecimalPlaces = "TooManyMarketDecimalPlaces",
|
||||
TooManyPriceMonitoringTriggers = "TooManyPriceMonitoringTriggers",
|
||||
UnsupportedProduct = "UnsupportedProduct",
|
||||
UnsupportedTradingMode = "UnsupportedTradingMode",
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export function Dialog({
|
||||
}: DialogProps) {
|
||||
const contentClasses = classNames(
|
||||
// Positions the modal in the center of screen
|
||||
'z-20 fixed w-full md:w-[720px] lg:w-[960px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
'z-20 fixed w-full md:w-[720px] lg:w-[940px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
// Need to apply background and text colors again as content is rendered in a portal
|
||||
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
||||
getIntentShadow(intent),
|
||||
|
@ -9,8 +9,10 @@ type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
* Form an HTML link tag
|
||||
*/
|
||||
export const Link = ({ className, children, ...props }: LinkProps) => {
|
||||
const anchorClassName = classNames(className, 'cursor-pointer', {
|
||||
const anchorClassName = classNames(className, {
|
||||
underline: typeof children === 'string',
|
||||
'cursor-pointer': props['aria-disabled'] !== true,
|
||||
'opacity-50 pointer-events-none': props['aria-disabled'] === true,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -146,6 +146,7 @@
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-websocket-mock": "^2.3.0",
|
||||
"lint-staged": "^12.3.3",
|
||||
"mock-apollo-client": "^1.2.0",
|
||||
"npmlog": "^6.0.2",
|
||||
"nx": "13.10.1",
|
||||
"prettier": "^2.5.1",
|
||||
|
@ -16451,6 +16451,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mock-apollo-client@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mock-apollo-client/-/mock-apollo-client-1.2.0.tgz#72543df0d74577d29be1b34cecba8898c7e71451"
|
||||
integrity sha512-zCVHv3p7zvUmen9zce9l965ZrI6rMbrm2/oqGaTerVYOaYskl/cVgTG/L7iIToTIpI7onk/f6tu8hxPXZdyy/g==
|
||||
|
||||
mock-socket@^9.1.0:
|
||||
version "9.1.3"
|
||||
resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.3.tgz#bcb106c6b345001fa7619466fcf2f8f5a156b10f"
|
||||
|
Loading…
Reference in New Issue
Block a user