Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo

This commit is contained in:
madalinaraicu
2022-08-04 13:19:56 +01:00
43 changed files with 1509 additions and 426 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#65c89b",
"activityBar.activeBorder": "#945bc4",
"activityBar.background": "#65c89b",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#945bc4",
"activityBarBadge.foreground": "#e7e7e7",
"sash.hoverBorder": "#65c89b",
"statusBar.background": "#42b883",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#359268",
"statusBarItem.remoteBackground": "#42b883",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#42b883",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#42b88399",
"titleBar.inactiveForeground": "#15202b99"
},
"peacock.color": "#42b883",
"editor.formatOnSave": true
}
@@ -33,6 +33,8 @@ const BIG_NUMBER_PARAMS = [
'spam.protection.proposal.min.tokens',
'governance.proposal.updateMarket.minProposerBalance',
'governance.proposal.asset.minProposerBalance',
'governance.proposal.market.minProposerBalance',
'governance.proposal.market.minVoterBalance',
];
export const renderRow = ({
@@ -5,6 +5,8 @@ import { generateMarketTags } from '../support/mocks/generate-market-tags';
import { generateMarketPositions } from '../support/mocks/generate-market-positions';
import { generateEstimateOrder } from '../support/mocks/generate-estimate-order';
import { generatePartyBalance } from '../support/mocks/generate-party-balance';
import { generatePartyMarketData } from '../support/mocks/generate-party-market-data';
import { generateMarketMarkPrice } from '../support/mocks/generate-market-mark-price';
const connectVegaWallet = () => {
const form = 'rest-connector-form';
@@ -28,6 +30,8 @@ describe('Market trade', () => {
aliasQuery(req, 'MarketPositions', generateMarketPositions());
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
aliasQuery(req, 'PartyBalanceQuery', generatePartyBalance());
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
aliasQuery(req, 'MarketMarkPrice', generateMarketMarkPrice());
});
cy.visit('/markets');
cy.wait('@SimpleMarkets').then((response) => {
@@ -104,7 +108,7 @@ describe('Market trade', () => {
.find('dl')
.eq(3)
.find('dd div')
.should('have.text', '-785.81045');
.should('have.text', ' - ');
cy.getByTestId('place-order').click();
cy.getByTestId('dialog-title').should(
'have.text',
@@ -0,0 +1,9 @@
export const generateMarketMarkPrice = () => {
return {
market: {
decimalPlaces: 5,
data: { markPrice: '692748', __typename: 'MarketData' },
__typename: 'Market',
},
};
};
@@ -0,0 +1,19 @@
export const generatePartyMarketData = () => {
return {
party: {
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
accounts: [
{
type: 'General',
balance: '1200000',
asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' },
market: null,
__typename: 'Account',
},
],
marginsConnection: { edges: null, __typename: 'MarginConnection' },
positionsConnection: { edges: null, __typename: 'PositionConnection' },
__typename: 'Party',
},
};
};
@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import { act } from 'react-dom/test-utils';
import {
render,
@@ -60,6 +60,7 @@ describe('SimpleMarketList', () => {
};
await act(async () => {
render(
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[mocks, filterMock]}>
<SimpleMarketList />
</MockedProvider>,
@@ -129,6 +130,7 @@ describe('SimpleMarketList', () => {
};
await act(async () => {
render(
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[mocks, filterMock]}>
<SimpleMarketList />
</MockedProvider>,
@@ -59,6 +59,7 @@ describe('SimpleMarketToolbar', () => {
it('should be properly rendered', async () => {
render(
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[filterMock]} addTypename={false}>
<WrappedCompForTest />
</MockedProvider>,
@@ -83,6 +84,7 @@ describe('SimpleMarketToolbar', () => {
it('navigation should work well', async () => {
render(
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[filterMock]} addTypename={false}>
<WrappedCompForTest />
</MockedProvider>,
@@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: MarketMarkPrice
// ====================================================
export interface MarketMarkPrice_market_data {
__typename: "MarketData";
/**
* the mark price (actually an unsigned int)
*/
markPrice: string;
}
export interface MarketMarkPrice_market {
__typename: "Market";
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* marketData for the given market
*/
data: MarketMarkPrice_market_data | null;
}
export interface MarketMarkPrice {
/**
* An instrument that is trading on the VEGA network
*/
market: MarketMarkPrice_market | null;
}
export interface MarketMarkPriceVariables {
marketId: string;
}
@@ -0,0 +1,118 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: PartyMarketData
// ====================================================
export interface PartyMarketData_party_accounts_asset {
__typename: "Asset";
/**
* The id of the asset
*/
id: string;
/**
* The precision of the asset
*/
decimals: number;
}
export interface PartyMarketData_party_accounts_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface PartyMarketData_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
/**
* Asset, the 'currency'
*/
asset: PartyMarketData_party_accounts_asset;
/**
* Market (only relevant to margin accounts)
*/
market: PartyMarketData_party_accounts_market | null;
}
export interface PartyMarketData_party_marginsConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface PartyMarketData_party_marginsConnection_edges_node {
__typename: "MarginLevels";
/**
* market in which the margin is required for this party
*/
market: PartyMarketData_party_marginsConnection_edges_node_market;
/**
* this is the minimal margin required for a party to place a new order on the network (unsigned int actually)
*/
initialLevel: string;
/**
* minimal margin for the position to be maintained in the network (unsigned int actually)
*/
maintenanceLevel: string;
/**
* if the margin is between maintenance and search, the network will initiate a collateral search (unsigned int actually)
*/
searchLevel: string;
}
export interface PartyMarketData_party_marginsConnection_edges {
__typename: "MarginEdge";
node: PartyMarketData_party_marginsConnection_edges_node;
}
export interface PartyMarketData_party_marginsConnection {
__typename: "MarginConnection";
/**
* The margin levels in this connection
*/
edges: PartyMarketData_party_marginsConnection_edges[] | null;
}
export interface PartyMarketData_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Collateral accounts relating to a party
*/
accounts: PartyMarketData_party_accounts[] | null;
/**
* Margin level for a market
*/
marginsConnection: PartyMarketData_party_marginsConnection;
}
export interface PartyMarketData {
/**
* An entity that is trading on the VEGA network
*/
party: PartyMarketData_party | null;
}
export interface PartyMarketDataVariables {
partyId: string;
}
@@ -0,0 +1,38 @@
import { gql, useQuery } from '@apollo/client';
import { useMemo, useRef } from 'react';
import type {
MarketMarkPrice,
MarketMarkPriceVariables,
} from './__generated__/MarketMarkPrice';
const MARKET_MARK_PRICE = gql`
query MarketMarkPrice($marketId: ID!) {
market(id: $marketId) {
decimalPlaces
data {
markPrice
}
}
}
`;
export default (marketId: string) => {
const memoRef = useRef<MarketMarkPrice | null>(null);
const { data } = useQuery<MarketMarkPrice, MarketMarkPriceVariables>(
MARKET_MARK_PRICE,
{
pollInterval: 5000,
variables: { marketId },
skip: !marketId,
}
);
return useMemo(() => {
if (
data &&
data.market?.data?.markPrice !== memoRef.current?.market?.data?.markPrice
) {
memoRef.current = data;
}
return memoRef.current;
}, [data, memoRef]);
};
@@ -1,71 +0,0 @@
import { renderHook } from '@testing-library/react-hooks';
import useOrderCloseOut from './use-order-closeout';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
describe('useOrderCloseOut Hook', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
depth: {
lastTrade: {
price: '1000000',
},
},
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'assetId',
},
},
},
},
};
const partyData = {
party: {
accounts: [
{
balance: '200000',
asset: {
id: 'assetId',
decimals: 5,
},
},
],
},
};
it('return proper buy value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: order as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
})
);
expect(result.current).toEqual('9.00000');
});
it('return proper sell value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
})
);
expect(result.current).toEqual('11.00000');
});
it('return proper empty value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
})
);
expect(result.current).toEqual(' - ');
});
});
@@ -0,0 +1,99 @@
import * as React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { MockedProvider } from '@apollo/client/testing';
import useOrderCloseOut from './use-order-closeout';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
describe('useOrderCloseOut Hook', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
depth: {
lastTrade: {
price: '1000000',
},
},
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'assetId',
},
},
},
},
};
const partyData = {
party: {
accounts: [
{
balance: '200000',
asset: {
id: 'assetId',
decimals: 5,
},
},
],
},
};
it('return proper buy value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
order: order as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
}),
{
wrapper: ({ children }: { children: React.ReactNode }) => (
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[]}>{children}</MockedProvider>
),
}
);
expect(result.current).toEqual(' - ');
});
it('return proper sell value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
}),
{
wrapper: ({ children }: { children: React.ReactNode }) => (
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[]}>{children}</MockedProvider>
),
}
);
expect(result.current).toEqual('1.00000');
});
it('return proper empty value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
}),
{
wrapper: ({ children }: { children: React.ReactNode }) => (
// @ts-ignore different versions of react types in apollo and app
<MockedProvider mocks={[]}>{children}</MockedProvider>
),
}
);
expect(result.current).toEqual('0.00000');
});
});
@@ -3,8 +3,46 @@ import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { useSettlementAccount } from './use-settlement-account';
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { useVegaWallet, VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
import { gql, useQuery } from '@apollo/client';
import useMarketPositions from './use-market-positions';
import useMarketData from './use-market-data';
import type {
PartyMarketData,
PartyMarketDataVariables,
} from './__generated__/PartyMarketData';
const CLOSEOUT_PRICE_QUERY = gql`
query PartyMarketData($partyId: ID!) {
party(id: $partyId) {
id
accounts {
type
balance
asset {
id
decimals
}
market {
id
}
}
marginsConnection {
edges {
node {
market {
id
}
initialLevel
maintenanceLevel
searchLevel
}
}
}
}
}
`;
interface Props {
order: Order;
@@ -13,25 +51,65 @@ interface Props {
}
const useOrderCloseOut = ({ order, market, partyData }: Props): string => {
const { keypair } = useVegaWallet();
const account = useSettlementAccount(
market.tradableInstrument.instrument.product.settlementAsset.id,
partyData?.party?.accounts || []
);
if (account?.balance && market.depth.lastTrade) {
const price = new BigNumber(
addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
);
const balance = new BigNumber(
addDecimal(account.balance, account.asset.decimals)
);
const { size, side } = order;
const bigOne = new BigNumber(1);
return formatNumber(
side === VegaWalletOrderSide.Buy
? bigOne.minus(balance.div(price.times(size))).times(price)
: bigOne.plus(balance.div(price.times(size))).times(price),
const { data } = useQuery<PartyMarketData, PartyMarketDataVariables>(
CLOSEOUT_PRICE_QUERY,
{
pollInterval: 5000,
variables: { partyId: keypair?.pub || '' },
skip: !keypair?.pub,
}
);
const markPriceData = useMarketData(market.id);
const marketPositions = useMarketPositions({
marketId: market.id,
partyId: keypair?.pub || '',
});
const marginMaintenanceLevel = new BigNumber(
addDecimal(
data?.party?.marginsConnection.edges?.find(
(nodes) => nodes.node.market.id === market.id
)?.node.maintenanceLevel || 0,
market.decimalPlaces
);
)
);
const positionAccount = data?.party?.accounts?.find(
(account) => account.market?.id === market.id
);
const positionAccountBalance = new BigNumber(
addDecimal(
positionAccount?.balance || 0,
positionAccount?.asset?.decimals || 0
)
);
const generalAccountBalance = new BigNumber(
addDecimal(account?.balance || 0, account?.asset.decimals || 0)
);
const volume = new BigNumber(
addDecimal(
marketPositions?.openVolume.toNumber() || 0,
market.positionDecimalPlaces
)
)[order.side === VegaWalletOrderSide.Buy ? 'plus' : 'minus'](order.size);
const markPrice = new BigNumber(
addDecimal(
markPriceData?.market?.data?.markPrice || 0,
markPriceData?.market?.decimalPlaces || 0
)
);
// regarding formula (marginMaintenanceLevel - positionAccountBalance - generalAccountBalance) / volume + markPrice
const marginDifference = marginMaintenanceLevel
.minus(positionAccountBalance)
.minus(generalAccountBalance);
const closeOut = marginDifference.div(volume).plus(markPrice);
if (closeOut.isPositive()) {
return formatNumber(closeOut, market.decimalPlaces);
}
return ' - ';
};
@@ -58,7 +58,7 @@ describe('useOrderMargin Hook', () => {
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
.toString();
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual(
calledSize
);
});
@@ -74,7 +74,7 @@ describe('useOrderMargin Hook', () => {
);
expect(result.current).toEqual('200000');
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual(
order.size
);
});
@@ -99,7 +99,7 @@ describe('useOrderMargin Hook', () => {
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
.toString();
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
expect((useQuery as jest.Mock).mock.calls[1][1].variables.size).toEqual(
calledSize
);
});
@@ -12,8 +12,9 @@ import {
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { addDecimal, removeDecimal } from '@vegaprotocol/react-helpers';
import useMarketPositions from './use-market-positions';
import useMarketData from './use-market-data';
export const ESTIMATE_ORDER_QUERY = gql`
query EstimateOrder(
@@ -66,26 +67,35 @@ const types: Record<VegaWalletOrderType, OrderType> = {
const useOrderMargin = ({ order, market, partyId }: Props) => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId });
const markPriceData = useMarketData(market.id);
const { data } = useQuery<EstimateOrder, EstimateOrderVariables>(
ESTIMATE_ORDER_QUERY,
{
variables: {
marketId: market.id,
partyId,
price: market.depth.lastTrade?.price,
size: new BigNumber(marketPositions?.openVolume || 0)
[order.side === VegaWalletOrderSide.Buy ? 'plus' : 'minus'](
order.size
)
.toString(),
price: markPriceData?.market?.data?.markPrice || '',
size: removeDecimal(
BigNumber.maximum(
0,
new BigNumber(marketPositions?.openVolume || 0)[
order.side === VegaWalletOrderSide.Buy ? 'plus' : 'minus'
](order.size)
).toString(),
market.positionDecimalPlaces
),
side: order.side === VegaWalletOrderSide.Buy ? Side.Buy : Side.Sell,
timeInForce: times[order.timeInForce],
type: types[order.type],
},
skip:
!partyId || !market.id || !order.size || !market.depth.lastTrade?.price,
!partyId ||
!market.id ||
!order.size ||
!markPriceData?.market?.data?.markPrice,
}
);
if (data?.estimateOrder.marginLevels.initialLevel) {
return addDecimal(
BigNumber.maximum(
@@ -23,6 +23,9 @@ export function createClient(base?: string) {
const cache = new InMemoryCache({
typePolicies: {
Party: {
keyFields: false,
},
Query: {},
Account: {
keyFields: false,
+3 -1
View File
@@ -4,7 +4,9 @@
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node", "@testing-library/jest-dom"],
"jsx": "react"
"jsx": "react",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": [
"**/*.test.ts",
+126 -36
View File
@@ -71,7 +71,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "116061.65519824298138415",
"locked_amount": "115646.398397049886622025",
"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": "57067.87104692389040607",
"locked_amount": "56660.54550111376802106",
"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": "78001.92736422610737413692656",
"locked_amount": "77445.18366492547660676708772",
"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": "24007.467840021196513942",
"locked_amount": "23836.113017034806838684",
"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": "7814.197818973311608206",
"locked_amount": "7758.423486879085377994",
"deposits": [
{
"amount": "10833.29",
@@ -728,7 +728,7 @@
"tranche_end": "2022-11-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "0",
"locked_amount": "11035.58650362318825",
"locked_amount": "10821.53532608695575",
"deposits": [
{
"amount": "15000",
@@ -813,8 +813,8 @@
"tranche_start": "2022-06-02T00:00:00.000Z",
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "179856.049568108351",
"locked_amount": "1611716.80872347542469696",
"total_removed": "329229.1901984227119",
"locked_amount": "1602413.32165500775060365",
"deposits": [
{
"amount": "1852091.69",
@@ -828,6 +828,16 @@
}
],
"withdrawals": [
{
"amount": "142606.4205106701317",
"user": "0xEbaeCD22304445A471630600708A197A311daB7F",
"tx": "0x80afde6647a9d51f9514de7a3629ea4d4d683b773c1b641ecce64e6731061770"
},
{
"amount": "6766.7201196442292",
"user": "0x3469bbE61Ef6d5F31FE75a3f93C8835Aa2A62359",
"tx": "0xfa4011d0218b4d6a8f40cdcc7a98cab94239d16aac1effc7938c50dc710b9437"
},
{
"amount": "8141.4968979657483",
"user": "0x3469bbE61Ef6d5F31FE75a3f93C8835Aa2A62359",
@@ -851,6 +861,12 @@
}
],
"withdrawals": [
{
"amount": "142606.4205106701317",
"user": "0xEbaeCD22304445A471630600708A197A311daB7F",
"tranche_id": 15,
"tx": "0x80afde6647a9d51f9514de7a3629ea4d4d683b773c1b641ecce64e6731061770"
},
{
"amount": "171714.5526701426027",
"user": "0xEbaeCD22304445A471630600708A197A311daB7F",
@@ -859,8 +875,8 @@
}
],
"total_tokens": "1852091.69",
"withdrawn_tokens": "171714.5526701426027",
"remaining_tokens": "1680377.1373298573973"
"withdrawn_tokens": "314320.9731808127344",
"remaining_tokens": "1537770.7168191872656"
},
{
"address": "0x3469bbE61Ef6d5F31FE75a3f93C8835Aa2A62359",
@@ -873,6 +889,12 @@
}
],
"withdrawals": [
{
"amount": "6766.7201196442292",
"user": "0x3469bbE61Ef6d5F31FE75a3f93C8835Aa2A62359",
"tranche_id": 15,
"tx": "0xfa4011d0218b4d6a8f40cdcc7a98cab94239d16aac1effc7938c50dc710b9437"
},
{
"amount": "8141.4968979657483",
"user": "0x3469bbE61Ef6d5F31FE75a3f93C8835Aa2A62359",
@@ -881,8 +903,8 @@
}
],
"total_tokens": "87836.69",
"withdrawn_tokens": "8141.4968979657483",
"remaining_tokens": "79695.1931020342517"
"withdrawn_tokens": "14908.2170176099775",
"remaining_tokens": "72928.4729823900225"
}
]
},
@@ -1045,7 +1067,7 @@
"tranche_end": "2023-02-01T00:00:00.000Z",
"total_added": "42500",
"total_removed": "0",
"locked_amount": "42094.99672906602225",
"locked_amount": "41690.67783816424975",
"deposits": [
{
"amount": "12500",
@@ -1889,7 +1911,7 @@
"tranche_end": "2022-09-30T00:00:00.000Z",
"total_added": "60916.66666633337",
"total_removed": "19238.601152747179372649",
"locked_amount": "9097.9157367340121736957898191851",
"locked_amount": "8824.499406409460153812915466858",
"deposits": [
{
"amount": "2833.333333",
@@ -5535,8 +5557,8 @@
"tranche_start": "2021-09-03T00:00:00.000Z",
"tranche_end": "2022-09-03T00:00:00.000Z",
"total_added": "25026.000000000000000003",
"total_removed": "10713.64234595729",
"locked_amount": "2142.40208980213216824025682115677321172",
"total_removed": "10717.53683479529",
"locked_amount": "2022.38267579908617786024243378995433783",
"deposits": [
{
"amount": "2",
@@ -9365,6 +9387,11 @@
"user": "0xa0479A52EEe756B49cbdB3Fd07F8292E4Bd61c39",
"tx": "0xde6380a15a7f1084900a531bec6b0587fa283a24ed1cadde171509469dd22c4e"
},
{
"amount": "3.894488838",
"user": "0xc5467213593778E528f0eB8117cc7AFBC5b7491b",
"tx": "0xb66fc7655dbf1319f97206bf2359f4676375a2ad5cff28a3d8d5472e6529a6a1"
},
{
"amount": "842.29689345648",
"user": "0xab6dE081Af6C78433AFDaC5B756804bcE17e9eC1",
@@ -16502,6 +16529,12 @@
"tranche_id": 11,
"tx": "0xf1be3a0908eb819cafd136a09720acbde12b65cdd5d2034ba0da7eb13908ab66"
},
{
"amount": "3.894488838",
"user": "0xc5467213593778E528f0eB8117cc7AFBC5b7491b",
"tranche_id": 11,
"tx": "0xb66fc7655dbf1319f97206bf2359f4676375a2ad5cff28a3d8d5472e6529a6a1"
},
{
"amount": "5.695719178",
"user": "0xc5467213593778E528f0eB8117cc7AFBC5b7491b",
@@ -16582,8 +16615,8 @@
}
],
"total_tokens": "200",
"withdrawn_tokens": "179.914548452",
"remaining_tokens": "20.085451548"
"withdrawn_tokens": "183.80903729",
"remaining_tokens": "16.19096271"
},
{
"address": "0x1775cc97E5c05Fde8b571ef75CA52d0A9ff19025",
@@ -16608,7 +16641,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "148381.2401909658774",
"locked_amount": "2501148.91954371656585585758",
"locked_amount": "2486852.69904964821018624352",
"deposits": [
{
"amount": "1998.95815",
@@ -17364,8 +17397,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15788853.065470999700000001",
"total_removed": "86199.494349116919035",
"locked_amount": "14096062.5380303455040661406657035491132739",
"total_removed": "86584.688871825589835",
"locked_amount": "14045628.5918282085678143485290670936334145",
"deposits": [
{
"amount": "16249.93",
@@ -17914,6 +17947,11 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xa19e2f313b5a01721ecd982b38a21384f907157339a373adcf63c7b811d5f1db"
},
{
"amount": "385.1945227086708",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x5e3154931eb02fcabcef7432fab7a7ed98ed5fab9def3407a1fe2c8c9137e148"
},
{
"amount": "325.1056930428465",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -18229,6 +18267,12 @@
"tranche_id": 2,
"tx": "0xa19e2f313b5a01721ecd982b38a21384f907157339a373adcf63c7b811d5f1db"
},
{
"amount": "385.1945227086708",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0x5e3154931eb02fcabcef7432fab7a7ed98ed5fab9def3407a1fe2c8c9137e148"
},
{
"amount": "325.1056930428465",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -18453,8 +18497,8 @@
}
],
"total_tokens": "194999.1675",
"withdrawn_tokens": "20783.144809529895675",
"remaining_tokens": "174216.022690470104325"
"withdrawn_tokens": "21168.339332238566475",
"remaining_tokens": "173830.828167761433525"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -19907,8 +19951,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "2553288.680626734507145402",
"locked_amount": "7358916.80976112071634476419463288",
"total_removed": "2554510.751279612473423152",
"locked_amount": "7312116.91273662300150809192245716",
"deposits": [
{
"amount": "129284.449",
@@ -20172,6 +20216,16 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x4e19528b1c582b289b6f9e74a7bd3b832d303f4cc19478d847aacc7e2d03868a"
},
{
"amount": "716.81727939132601225",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xe7c0a1696e3770d4cf80bff7a6dd71e3a0b78a570fdbe5e30fb72a13d40628fc"
},
{
"amount": "505.2533734866402655",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x9b4a5987d5a0d250fa7f6eceeec0bfc8ab9928c42711e8814e7b32b305486c77"
},
{
"amount": "600.79246609513746875",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -22058,6 +22112,18 @@
"tranche_id": 3,
"tx": "0x4e19528b1c582b289b6f9e74a7bd3b832d303f4cc19478d847aacc7e2d03868a"
},
{
"amount": "716.81727939132601225",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0xe7c0a1696e3770d4cf80bff7a6dd71e3a0b78a570fdbe5e30fb72a13d40628fc"
},
{
"amount": "505.2533734866402655",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x9b4a5987d5a0d250fa7f6eceeec0bfc8ab9928c42711e8814e7b32b305486c77"
},
{
"amount": "600.79246609513746875",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -23524,8 +23590,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "177854.79751098610462275",
"remaining_tokens": "181268.67206401389537725"
"withdrawn_tokens": "179076.8681638640709005",
"remaining_tokens": "180046.6014111359290995"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -24595,7 +24661,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "1604355.105699796690650516",
"locked_amount": "2590649.13435475633919175491355577",
"locked_amount": "2572158.21199695668247849611613021",
"deposits": [
{
"amount": "552496.6455",
@@ -25872,7 +25938,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "923.07230685326",
"locked_amount": "396321.34347845219603071008320652",
"locked_amount": "394056.02562048672468433565905632",
"deposits": [
{
"amount": "3000",
@@ -51835,7 +51901,7 @@
"tranche_start": "2021-12-05T00:00:00.000Z",
"tranche_end": "2022-06-05T00:00:00.000Z",
"total_added": "171288.42",
"total_removed": "43101.6840385532989",
"total_removed": "43601.6840385532989",
"locked_amount": "0",
"deposits": [
{
@@ -56150,6 +56216,16 @@
"user": "0x099B26208FFe4CF7DE5d0c12bF5fcCf6aA90A7Ca",
"tx": "0xef953ed5449b70426b128c591e38931bb8d8f64bd0d0016607ad3aafe3454e85"
},
{
"amount": "250",
"user": "0x48e589D7B3088E571c8D17C03736f482747AD1Ff",
"tx": "0x9c8527743dc330b0b44b6c1ec5ea8da6b44f9120e1728a8b95b8241df2ba7ca7"
},
{
"amount": "250",
"user": "0xB04C9d6D966Bf6BA63318563b67201332945080A",
"tx": "0x752996e92fdf6a5b7fe6fd5a0556adb73e47b2c971567da4ac438ecb2cdcc77f"
},
{
"amount": "250",
"user": "0x1f7C20Bd98B2f346A58d2c5faaFc940E0C36450E",
@@ -66998,10 +67074,17 @@
"tx": "0x9f916cf09e8a3c4ade0ffce5190db464d0a2b1dadba78e1ee7ba5d6e751d6148"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "250",
"user": "0xB04C9d6D966Bf6BA63318563b67201332945080A",
"tranche_id": 6,
"tx": "0x752996e92fdf6a5b7fe6fd5a0556adb73e47b2c971567da4ac438ecb2cdcc77f"
}
],
"total_tokens": "250",
"withdrawn_tokens": "0",
"remaining_tokens": "250"
"withdrawn_tokens": "250",
"remaining_tokens": "0"
},
{
"address": "0x217254578EFd323CF93b1E0FC900A2f4C97A2e6e",
@@ -71784,10 +71867,17 @@
"tx": "0xe32a466fc780a0fb3fd84a804f622931ebfaf3f428bff0dc6d141270410e75f8"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "250",
"user": "0x48e589D7B3088E571c8D17C03736f482747AD1Ff",
"tranche_id": 6,
"tx": "0x9c8527743dc330b0b44b6c1ec5ea8da6b44f9120e1728a8b95b8241df2ba7ca7"
}
],
"total_tokens": "250",
"withdrawn_tokens": "0",
"remaining_tokens": "250"
"withdrawn_tokens": "250",
"remaining_tokens": "0"
},
{
"address": "0x5f3Bce4B242d00ED748d48172C1f2D47A0bcB19B",
@@ -38,7 +38,7 @@
"tranche_end": "2022-11-26T13:48:10.000Z",
"total_added": "100",
"total_removed": "0",
"locked_amount": "31.73197932521563",
"locked_amount": "31.25240994419076",
"deposits": [
{
"amount": "100",
@@ -242,7 +242,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1100",
"total_removed": "673.04388635",
"locked_amount": "211.813727168949774",
"locked_amount": "206.538463977676336",
"deposits": [
{
"amount": "1000",
+1 -1
View File
@@ -69,7 +69,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1010.000000000000000001",
"total_removed": "668.4622323651",
"locked_amount": "194.48351312785388340019255793378995434",
"locked_amount": "189.63986237950281760018776223997970576",
"deposits": [
{
"amount": "1000",
+4
View File
@@ -17,5 +17,9 @@ export const NetworkParams = {
'governance.proposal.updateNetParam.requiredMajority',
GOV_UPDATE_NET_PARAM_REQUIRED_PARTICIPATION:
'governance.proposal.updateNetParam.requiredParticipation',
GOV_FREEFORM_REQUIRED_PARTICIPATION:
'governance.proposal.freeform.requiredParticipation',
GOV_FREEFORM_REQUIRED_MAJORITY:
'governance.proposal.freeform.requiredMajority',
VALIDATOR_DELEGATION_MIN_AMOUNT: 'validators.delegation.minAmount',
};
+5
View File
@@ -5,3 +5,8 @@
*/
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// eslint-disable-next-line @typescript-eslint/no-var-requires
window.Buffer = require('buffer/').Buffer;
// eslint-disable-next-line no-undef, no-global-assign, no-native-reassign
window.global = globalThis;
@@ -25,6 +25,8 @@ const useProposalNetworkParams = ({
NetworkParams.GOV_ASSET_REQUIRED_PARTICIPATION,
NetworkParams.GOV_UPDATE_NET_PARAM_REQUIRED_MAJORITY,
NetworkParams.GOV_UPDATE_NET_PARAM_REQUIRED_PARTICIPATION,
NetworkParams.GOV_FREEFORM_REQUIRED_MAJORITY,
NetworkParams.GOV_FREEFORM_REQUIRED_PARTICIPATION,
]);
if (loading || !data) {
return {
@@ -42,6 +44,8 @@ const useProposalNetworkParams = ({
assetParticipation,
paramMajority,
paramParticipation,
freeformMajority,
freeformParticipation,
] = data;
switch (proposal.terms.change.__typename) {
@@ -65,6 +69,11 @@ const useProposalNetworkParams = ({
requiredMajority: newMarketMajority,
requiredParticipation: new BigNumber(newMarketParticipation),
};
case 'NewFreeform':
return {
requiredMajority: freeformMajority,
requiredParticipation: freeformParticipation,
};
default:
throw new Error('Unknown proposal type');
}
+1 -17
View File
@@ -1,6 +1,5 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { Navbar } from '../components/navbar';
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
import {
@@ -8,11 +7,7 @@ import {
VegaManageDialog,
VegaWalletProvider,
} from '@vegaprotocol/wallet';
import {
useEnvironment,
EnvironmentProvider,
NetworkSwitcherDialog,
} from '@vegaprotocol/environment';
import { EnvironmentProvider } from '@vegaprotocol/environment';
import { Connectors } from '../lib/vega-connectors';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { AppLoader } from '../components/app-loader';
@@ -21,9 +16,7 @@ import './styles.css';
import { useGlobalStore } from '../stores';
function AppBody({ Component, pageProps }: AppProps) {
const { push } = useRouter();
const store = useGlobalStore();
const { VEGA_NETWORKS } = useEnvironment();
const [theme, toggleTheme] = useThemeSwitcher();
return (
@@ -61,15 +54,6 @@ function AppBody({ Component, pageProps }: AppProps) {
dialogOpen={store.vegaWalletManageDialog}
setDialogOpen={(open) => store.setVegaWalletManageDialog(open)}
/>
<NetworkSwitcherDialog
dialogOpen={store.vegaNetworkSwitcherDialog}
setDialogOpen={(open) => store.setVegaNetworkSwitcherDialog(open)}
onConnect={({ network }) => {
if (VEGA_NETWORKS[network]) {
push(VEGA_NETWORKS[network] ?? '');
}
}}
/>
</AppLoader>
</div>
</ThemeContext.Provider>
+1
View File
@@ -6,4 +6,5 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/environment',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};
-2
View File
@@ -1,5 +1,3 @@
export * from './network-loader';
export * from './network-switcher';
export * from './network-switcher-dialog';
export * from './node-switcher';
export * from './node-switcher-dialog';
@@ -0,0 +1,61 @@
import type { ReactNode } from 'react';
import { ApolloProvider } from '@apollo/client';
import { useEnvironment } from '../../hooks';
import { render, screen } from '@testing-library/react';
import { NetworkLoader } from './network-loader';
jest.mock('@apollo/client');
jest.mock('../../hooks');
// @ts-ignore Typescript doesn't recognise mocked instances
ApolloProvider.mockImplementation(({ children }: { children: ReactNode }) => {
return children;
});
const SKELETON_TEXT = 'LOADING';
const SUCCESS_TEXT = 'LOADED';
const createClient = jest.fn();
beforeEach(() => {
createClient.mockReset();
createClient.mockImplementation(() => {
return jest.fn();
});
});
describe('Network loader', () => {
it('renders a skeleton when there is no vega url in the environment', () => {
// @ts-ignore Typescript doesn't recognise mocked instances
useEnvironment.mockImplementation(() => ({
VEGA_URL: undefined,
}));
render(
<NetworkLoader skeleton={SKELETON_TEXT} createClient={createClient}>
{SUCCESS_TEXT}
</NetworkLoader>
);
expect(screen.getByText(SKELETON_TEXT)).toBeInTheDocument();
expect(() => screen.getByText(SUCCESS_TEXT)).toThrow();
expect(createClient).not.toHaveBeenCalled();
});
it('renders the child components wrapped in an apollo provider when the environment has a vega url', () => {
// @ts-ignore Typescript doesn't recognise mocked instances
useEnvironment.mockImplementation(() => ({
VEGA_URL: 'http://vega.node',
}));
render(
<NetworkLoader skeleton={SKELETON_TEXT} createClient={createClient}>
{SUCCESS_TEXT}
</NetworkLoader>
);
expect(() => screen.getByText(SKELETON_TEXT)).toThrow();
expect(screen.getByText(SUCCESS_TEXT)).toBeInTheDocument();
expect(createClient).toHaveBeenCalledWith('http://vega.node');
});
});
@@ -1 +0,0 @@
export { NetworkSwitcherDialog } from './network-switcher-dialog';
@@ -1,37 +0,0 @@
import type { ComponentProps } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { NetworkSwitcher } from '../network-switcher';
type NetworkSwitcherDialogProps = Pick<
ComponentProps<typeof NetworkSwitcher>,
'onConnect' | 'onError'
> & {
dialogOpen: boolean;
setDialogOpen: (dialogOpen: boolean) => void;
intent?: Intent;
};
export const NetworkSwitcherDialog = ({
dialogOpen,
setDialogOpen,
intent,
onConnect,
onError,
}: NetworkSwitcherDialogProps) => {
return (
<Dialog
open={dialogOpen}
onChange={setDialogOpen}
title={t('Choose a network')}
intent={intent}
>
<NetworkSwitcher
onConnect={onConnect}
onError={onError}
onClose={() => setDialogOpen(false)}
/>
</Dialog>
);
};
@@ -1 +0,0 @@
export { NetworkSwitcher } from './network-switcher';
@@ -1,49 +0,0 @@
import { useForm, Controller } from 'react-hook-form';
import { Button, Select } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '../../hooks';
import type { Networks } from '../../types';
type NetworkState = {
network: Networks;
};
type NetworkSwitcherProps = {
onConnect: (network: NetworkState) => void;
onError?: () => void;
onClose: () => void;
};
export const NetworkSwitcher = ({ onConnect }: NetworkSwitcherProps) => {
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const { control, handleSubmit } = useForm<NetworkState>({
defaultValues: {
network: VEGA_ENV,
},
});
return (
<form onSubmit={handleSubmit(onConnect)}>
<div className="py-16">
<Controller
name="network"
control={control}
render={({ field }) => (
<Select value={field.value} onChange={field.onChange}>
{Object.keys(VEGA_NETWORKS).map((network, index) => (
<option key={index} value={network}>
{network}
</option>
))}
</Select>
)}
/>
</div>
<div className="py-16">
<Button data-testid="connect-network" type="submit">
{t('Connect')}
</Button>
</div>
</form>
);
};
@@ -0,0 +1,668 @@
import { useState } from 'react';
import { MockedProvider } from '@apollo/react-testing';
import { render, screen, fireEvent } from '@testing-library/react';
import { useEnvironment } from '../../hooks/use-environment';
import { useNodes } from '../../hooks/use-nodes';
import createMockClient from '../../hooks/mocks/apollo-client';
import { STATS_QUERY } from '../../utils/request-node';
import { NodeSwitcher } from './node-switcher';
import { getErrorByType } from '../../utils/validate-node';
import type { Configuration, NodeData } from '../../';
import { Networks, ErrorType, CUSTOM_NODE_KEY } from '../../';
type NodeDataProp = 'responseTime' | 'block' | 'chain' | 'ssl';
jest.mock('../../hooks/use-environment');
jest.mock('../../hooks/use-nodes');
const mockNodesImplementation =
(
updateNodeUrlMock: jest.Mock,
getNodeState: typeof getValidNodeState = getValidNodeState
) =>
(config: Configuration) => {
const [{ state, clients }, setImplementation] = useState<{
state: Record<string, NodeData>;
clients: Record<string, ReturnType<typeof createMockClients>>;
}>({
state: createMockState(Networks.TESTNET, config.hosts),
clients: createMockClients(config.hosts),
});
return {
state,
clients,
updateNodeUrl: updateNodeUrlMock.mockImplementation(
(node: string, url: string) => {
setImplementation((prev) => ({
state: {
...prev.state,
[node]: getNodeState(Networks.TESTNET, url),
},
clients: {
...prev.clients,
[node]: createMockClient({ network: Networks.TESTNET }),
},
}));
}
),
updateNodeBlock: (node: string, value: number) => {
setImplementation((prev) => ({
state: {
...prev.state,
[node]: {
...prev.state[node],
block: {
...prev.state[node].block,
value,
},
},
},
clients: prev.clients,
}));
},
};
};
const statsQueryMock = {
request: {
query: STATS_QUERY,
},
result: {
data: {
statistics: {
blockHeight: 1234,
},
},
},
};
const onConnect = jest.fn();
const HOSTS = ['https://host1.com', 'https://host2.com'];
const enum STATES {
LOADING = 'is loading',
HAS_ERROR = 'has an error',
}
const getValidNodeState = (env: Networks, url: string) => ({
url,
initialized: true,
responseTime: {
isLoading: false,
hasError: false,
value: 10,
},
block: {
isLoading: false,
hasError: false,
value: 123,
},
ssl: {
isLoading: false,
hasError: false,
value: true,
},
chain: {
isLoading: false,
hasError: false,
value: `${env.toLowerCase()}-1234`,
},
});
const createMockState = (env: Networks, nodes: string[]) =>
nodes.reduce(
(acc, node) => ({
...acc,
[node]: getValidNodeState(env, node),
}),
{}
);
const createMockClients = (nodes: string[]) =>
nodes.reduce(
(acc, node) => ({
...acc,
[node]: createMockClient({ network: Networks.TESTNET }),
}),
{}
);
beforeEach(() => {
onConnect.mockReset();
// @ts-ignore Typescript doesn't recognise mocked instances
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.TESTNET,
VEGA_URL: undefined,
}));
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation((config: Configuration) => ({
state: createMockState(Networks.TESTNET, config.hosts),
clients: createMockClients(config.hosts),
updateNodeUrl: jest.fn(),
updateNodeBlock: jest.fn(),
}));
});
describe('Node switcher', () => {
it('renders with empty config', () => {
render(<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />);
expect(() => screen.getAllByTestId('node')).toThrow();
expect(screen.getByRole('radio', { checked: false })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
});
it('renders with the provided config nodes', () => {
render(<NodeSwitcher config={{ hosts: HOSTS }} onConnect={onConnect} />);
HOSTS.forEach((host) => {
expect(
screen.getByRole('radio', { checked: false, name: host })
).toBeInTheDocument();
});
expect(
screen.getByRole('radio', { checked: false, name: 'Other' })
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
});
it('marks the node in the environment as selected', () => {
// @ts-ignore Typescript doesn't recognise mocked instances
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.TESTNET,
VEGA_URL: HOSTS[0],
}));
render(<NodeSwitcher config={{ hosts: HOSTS }} onConnect={onConnect} />);
HOSTS.forEach((host) => {
expect(
screen.getByRole('radio', { checked: host === HOSTS[0], name: host })
).toBeInTheDocument();
});
expect(
screen.getByRole('radio', { checked: false, name: 'Other' })
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Connect' })).not.toHaveAttribute(
'disabled'
);
});
it.each`
dataProp | state
${'responseTime'} | ${STATES.LOADING}
${'responseTime'} | ${STATES.HAS_ERROR}
${'block'} | ${STATES.LOADING}
${'block'} | ${STATES.HAS_ERROR}
${'chain'} | ${STATES.LOADING}
${'chain'} | ${STATES.HAS_ERROR}
${'ssl'} | ${STATES.LOADING}
${'ssl'} | ${STATES.HAS_ERROR}
`(
'disables selecting a node when the $dataProp $state',
({ dataProp, state }: { dataProp: NodeDataProp; state: STATES }) => {
const mockUrl = 'https://host.url';
const mockConfig = {
hosts: [mockUrl],
};
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation((config: Configuration) => {
const nodeState = getValidNodeState(Networks.TESTNET, mockUrl);
return {
state: {
[mockUrl]: {
...nodeState,
[dataProp]: {
...nodeState[dataProp],
isLoading:
state === STATES.LOADING
? true
: nodeState[dataProp].isLoading,
hasError:
state === STATES.HAS_ERROR
? true
: nodeState[dataProp].hasError,
value: undefined,
},
},
},
clients: createMockClients(config.hosts),
updateNodeUrl: jest.fn(),
updateNodeBlock: jest.fn(),
};
});
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={mockConfig} onConnect={onConnect} />
</MockedProvider>
);
expect(screen.getByRole('radio', { name: mockUrl })).toHaveAttribute(
'disabled'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
}
);
it('disables selecting a node when it has an invalid url', () => {
const mockUrl = 'not-valid-url';
const mockConfig = {
hosts: [mockUrl],
};
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation((config: Configuration) => ({
state: {
[mockUrl]: getValidNodeState(Networks.TESTNET, mockUrl),
},
clients: createMockClients(config.hosts),
updateNodeUrl: jest.fn(),
updateNodeBlock: jest.fn(),
}));
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={mockConfig} onConnect={onConnect} />
</MockedProvider>
);
expect(screen.getByRole('radio', { name: mockUrl })).toHaveAttribute(
'disabled'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
});
it('disables selecting a node when it is on an incorrect network', () => {
const mockUrl = 'https://mock.url';
const mockConfig = {
hosts: [mockUrl],
};
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation((config: Configuration) => {
const nodeState = getValidNodeState(Networks.TESTNET, mockUrl);
return {
state: {
[mockUrl]: {
...nodeState,
chain: {
...nodeState.chain,
value: `some-network-id`,
},
},
},
clients: createMockClients(config.hosts),
updateNodeUrl: jest.fn(),
updateNodeBlock: jest.fn(),
};
});
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={mockConfig} onConnect={onConnect} />
</MockedProvider>
);
expect(screen.getByRole('radio', { name: mockUrl })).toHaveAttribute(
'disabled'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
});
it('allows connecting to a valid node', () => {
render(<NodeSwitcher config={{ hosts: HOSTS }} onConnect={onConnect} />);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('radio', { name: HOSTS[0] }));
expect(screen.getByRole('button', { name: 'Connect' })).not.toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
expect(onConnect).toHaveBeenCalledWith(HOSTS[0]);
});
it('allows checking a custom node', () => {
const updateNodeUrlMock = jest.fn();
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation((config: Configuration) => ({
state: createMockState(Networks.TESTNET, config.hosts),
clients: createMockClients(config.hosts),
updateNodeUrl: updateNodeUrlMock,
updateNodeBlock: jest.fn(),
}));
const mockUrl = 'https://custom.url';
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
</MockedProvider>
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'true'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.change(screen.getByRole('textbox'), {
target: {
value: mockUrl,
},
});
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'false'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
expect(updateNodeUrlMock).toHaveBeenCalledWith(CUSTOM_NODE_KEY, mockUrl);
});
it('allows connecting to a custom node', () => {
const mockUrl = 'https://custom.url';
const updateNodeUrlMock = jest.fn();
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation(mockNodesImplementation(updateNodeUrlMock));
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
</MockedProvider>
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'true'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.change(screen.getByRole('textbox'), {
target: {
value: mockUrl,
},
});
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'false'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
expect(screen.getByRole('button', { name: 'Connect' })).not.toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
expect(onConnect).toHaveBeenCalledWith(mockUrl);
});
it.each`
dataProp | state
${'responseTime'} | ${STATES.LOADING}
${'responseTime'} | ${STATES.HAS_ERROR}
${'block'} | ${STATES.LOADING}
${'block'} | ${STATES.HAS_ERROR}
${'chain'} | ${STATES.LOADING}
${'chain'} | ${STATES.HAS_ERROR}
${'ssl'} | ${STATES.LOADING}
${'ssl'} | ${STATES.HAS_ERROR}
`(
'disables selecting a custom node when the $dataProp $state',
({ dataProp, state }: { dataProp: NodeDataProp; state: STATES }) => {
const mockUrl = 'https://custom.url';
const updateNodeUrlMock = jest.fn();
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation(
mockNodesImplementation(updateNodeUrlMock, (env) => {
const nodeState = getValidNodeState(env, mockUrl);
return {
...nodeState,
[dataProp]: {
...nodeState[dataProp],
isLoading:
state === STATES.LOADING ? true : nodeState[dataProp].isLoading,
hasError:
state === STATES.HAS_ERROR
? true
: nodeState[dataProp].hasError,
value: undefined,
},
};
})
);
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
</MockedProvider>
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'true'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.change(screen.getByRole('textbox'), {
target: {
value: mockUrl,
},
});
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'false'
);
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
if (state === STATES.LOADING) {
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.getByRole('link', { name: 'Checking' })).toHaveAttribute(
'aria-disabled',
'true'
);
}
if (state === STATES.HAS_ERROR) {
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
'aria-disabled',
'false'
);
}
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
if (state === STATES.HAS_ERROR) {
const expectedErrorType =
dataProp === 'ssl' ? ErrorType.SSL_ERROR : ErrorType.CONNECTION_ERROR;
const error = getErrorByType(
expectedErrorType,
Networks.TESTNET,
mockUrl
);
// eslint-disable-next-line jest/no-conditional-expect
expect(error?.headline).not.toBeNull();
// eslint-disable-next-line jest/no-conditional-expect
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
}
}
);
it('disables selecting a custom node when it has an invalid url', () => {
const mockUrl = 'not-valid-url';
const updateNodeUrlMock = jest.fn();
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation(mockNodesImplementation(updateNodeUrlMock));
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
</MockedProvider>
);
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
fireEvent.change(screen.getByRole('textbox'), {
target: {
value: mockUrl,
},
});
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
const error = getErrorByType(
ErrorType.INVALID_URL,
Networks.TESTNET,
mockUrl
);
expect(error?.headline).not.toBeNull();
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
});
it('disables selecting a custom node when it is on an incorrect network', () => {
const mockUrl = 'https://mock.url';
const updateNodeUrlMock = jest.fn();
// @ts-ignore Typescript doesn't recognise mocked instances
useNodes.mockImplementation(
mockNodesImplementation(updateNodeUrlMock, (env, url) => {
const nodeState = getValidNodeState(env, url);
return {
...nodeState,
chain: {
...nodeState.chain,
value: 'network-chain-id',
},
};
})
);
render(
<MockedProvider mocks={[statsQueryMock]}>
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
</MockedProvider>
);
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
fireEvent.change(screen.getByRole('textbox'), {
target: {
value: mockUrl,
},
});
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
'disabled'
);
const error = getErrorByType(
ErrorType.INVALID_NETWORK,
Networks.TESTNET,
mockUrl
);
expect(error?.headline).not.toBeNull();
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
});
it.each`
description | errorType
${'the node has an invalid url'} | ${ErrorType.INVALID_URL}
${'the node is on an invalid network'} | ${ErrorType.INVALID_NETWORK}
${'the node has an ssl issue'} | ${ErrorType.SSL_ERROR}
${'the node cannot be reached'} | ${ErrorType.CONNECTION_ERROR}
${'none of the config nodes can be connected to'} | ${ErrorType.CONNECTION_ERROR_ALL}
${'the config cannot be loaded'} | ${ErrorType.CONFIG_LOAD_ERROR}
${'the config is invalid'} | ${ErrorType.CONFIG_VALIDATION_ERROR}
`(
'displays initial error when $description',
({ errorType }: { errorType: ErrorType }) => {
const mockEnvUrl = 'https://mock.url';
// @ts-ignore Typescript doesn't recognise mocked instances
useEnvironment.mockImplementation(() => ({
VEGA_ENV: Networks.TESTNET,
VEGA_URL: mockEnvUrl,
}));
render(
<NodeSwitcher
config={{ hosts: HOSTS }}
initialErrorType={errorType}
onConnect={onConnect}
/>
);
const error = getErrorByType(errorType, Networks.TESTNET, mockEnvUrl);
expect(error?.headline).not.toBeNull();
expect(error?.message).not.toBeNull();
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
expect(screen.getByText(error?.message ?? '')).toBeInTheDocument();
}
);
});
@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useState } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import {
RadioGroup,
@@ -55,14 +55,9 @@ export const NodeSwitcher = ({
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 { state, clients, updateNodeUrl, updateNodeBlock } = useNodes(config);
const highestBlock = getHighestBlock(VEGA_ENV, state);
const customUrl = state[CUSTOM_NODE_KEY]?.url;
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
@@ -71,12 +66,7 @@ export const NodeSwitcher = ({
}
};
const isSubmitDisabled = getIsFormDisabled(
nodeRadio,
customNodeText,
VEGA_ENV,
state
);
const isSubmitDisabled = getIsFormDisabled(nodeRadio, VEGA_ENV, state);
const customNodeData =
nodeRadio &&
@@ -94,7 +84,12 @@ export const NodeSwitcher = ({
return (
<div className="text-black dark:text-white w-full lg:min-w-[800px]">
<NodeError {...(customNodeError || networkError)} />
<form onSubmit={() => onSubmit(nodeRadio)}>
<form
onSubmit={(event) => {
event.preventDefault();
onSubmit(nodeRadio);
}}
>
<p className="text-body-large font-bold mt-16 mb-32">
{t('Select a GraphQL node to connect to:')}
</p>
@@ -124,7 +119,7 @@ export const NodeSwitcher = ({
highestBlock={highestBlock}
setBlock={(block) => updateNodeBlock(node, block)}
>
<div className="mb-8 break-all">
<div className="mb-8 break-all" data-testid="node">
<Radio
id={`node-url-${index}`}
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
@@ -158,6 +153,7 @@ export const NodeSwitcher = ({
>
<Input
placeholder="https://"
role="textbox"
value={customNodeText}
hasError={
!!customNodeText &&
@@ -121,17 +121,6 @@ beforeEach(() => {
});
afterAll(() => {
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_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();
});
+17 -9
View File
@@ -12,7 +12,7 @@ import {
getIsNodeLoading,
} from '../utils/validate-node';
import { ErrorType } from '../types';
import type { Environment, RawEnvironment, NodeData } from '../types';
import type { Environment, Networks, RawEnvironment, NodeData } from '../types';
type EnvironmentProviderProps = {
definitions?: Partial<RawEnvironment>;
@@ -26,8 +26,15 @@ export type EnvironmentState = Environment & {
const EnvironmentContext = createContext({} as EnvironmentState);
const hasFinishedLoading = (node: NodeData) =>
node.initialized && !getIsNodeLoading(node) && !node.verified;
const hasLoaded = (env: Networks, node: NodeData) =>
node.initialized &&
!getIsNodeLoading(node) &&
getErrorType(env, node) === null;
const hasFailedLoading = (env: Networks, node: NodeData) =>
node.initialized &&
!getIsNodeLoading(node) &&
getErrorType(env, node) !== null;
export const EnvironmentProvider = ({
definitions,
@@ -47,14 +54,14 @@ export const EnvironmentProvider = ({
error && console.warn(error.headline);
}
});
const { state: nodes, clients } = useNodes(environment.VEGA_ENV, config);
const { state: nodes, clients } = useNodes(config);
const nodeKeys = Object.keys(nodes);
useEffect(() => {
if (!environment.VEGA_URL) {
const successfulNodeKey = nodeKeys.find(
(key) => nodes[key].verified
) as keyof typeof nodes;
const successfulNodeKey = nodeKeys.find((key) =>
hasLoaded(environment.VEGA_ENV, nodes[key])
);
if (successfulNodeKey && nodes[successfulNodeKey]) {
Object.keys(clients).forEach((node) => clients[node]?.stop());
updateEnvironment((prevEnvironment) => ({
@@ -81,8 +88,9 @@ export const EnvironmentProvider = ({
// 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
nodeKeys.filter((key) =>
hasFailedLoading(environment.VEGA_ENV, nodes[key])
).length === nodeKeys.length
) {
Object.keys(clients).forEach((node) => clients[node]?.stop());
setNetworkError(ErrorType.CONNECTION_ERROR_ALL);
+16 -46
View File
@@ -2,19 +2,16 @@ 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,
@@ -61,7 +58,7 @@ afterAll(() => {
describe('useNodes hook', () => {
it('returns the default state when empty config provided', () => {
const { result } = renderHook(() => useNodes(MOCK_ENV, { hosts: [] }));
const { result } = renderHook(() => useNodes({ hosts: [] }));
expect(result.current.state).toEqual({});
});
@@ -69,13 +66,12 @@ describe('useNodes hook', () => {
it('sets loading state while waiting for the results', async () => {
const node = 'https://some.url';
const { result, waitForNextUpdate } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
useNodes({ hosts: [node] })
);
expect(result.current.state[node]).toEqual({
...initialState,
url: node,
verified: false,
initialized: true,
responseTime: {
...initialState.responseTime,
@@ -101,9 +97,7 @@ describe('useNodes hook', () => {
it('sets statistics results', async () => {
const mockResult = getMockStatisticsResult();
const node = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].block).toEqual({
@@ -128,9 +122,7 @@ describe('useNodes hook', () => {
it('sets subscription result', async () => {
const node = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].ssl).toEqual({
@@ -143,9 +135,7 @@ describe('useNodes hook', () => {
it('sets error when host in not a valid url', async () => {
const node = 'not-url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].block.hasError).toBe(true);
@@ -162,9 +152,7 @@ describe('useNodes hook', () => {
);
const node = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].block).toEqual({
@@ -194,9 +182,7 @@ describe('useNodes hook', () => {
);
const node = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].ssl).toEqual({
@@ -210,9 +196,7 @@ describe('useNodes hook', () => {
it('allows updating block values', async () => {
const mockResult = getMockStatisticsResult();
const node = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [node] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].block.value).toEqual(
@@ -232,9 +216,7 @@ describe('useNodes hook', () => {
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] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
await waitFor(() => {
expect(result.current.state[node].block.value).toEqual(
@@ -251,9 +233,7 @@ describe('useNodes hook', () => {
it('adds new node', async () => {
const node = 'custom-node-key';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
expect(result.current.state[node]).toEqual(undefined);
@@ -269,9 +249,7 @@ describe('useNodes hook', () => {
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] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
act(() => {
result.current.updateNodeUrl(node, newUrl);
@@ -285,9 +263,7 @@ describe('useNodes hook', () => {
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: [] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
expect(result.current.state[node]).toBe(undefined);
@@ -312,9 +288,7 @@ describe('useNodes hook', () => {
const node = 'node-key';
const url = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
expect(result.current.state[node]).toBe(undefined);
@@ -351,9 +325,7 @@ describe('useNodes hook', () => {
const node = 'node-key';
const url = 'https://some.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
expect(result.current.state[node]).toBe(undefined);
@@ -374,7 +346,7 @@ describe('useNodes hook', () => {
const url1 = 'https://some.url';
const url2 = 'https://some-other.url';
const { result, waitFor } = renderHook(() =>
useNodes(MOCK_ENV, { hosts: [url1, url2] })
useNodes({ hosts: [url1, url2] })
);
await waitFor(() => {
@@ -386,9 +358,7 @@ describe('useNodes hook', () => {
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: [] })
);
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
act(() => {
result.current.updateNodeUrl(node, url);
+74 -83
View File
@@ -3,8 +3,7 @@ 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';
import type { NodeData, Configuration } from '../types';
type StatisticsPayload = {
block: NodeData['block']['value'];
@@ -59,7 +58,6 @@ function withError<T>(value?: T) {
const getNodeData = (url?: string): NodeData => ({
url: url ?? '',
verified: false,
initialized: false,
responseTime: withData(),
block: withData(),
@@ -108,88 +106,81 @@ const initializeNodes = (
);
};
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;
}
};
const reducer = (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
);
});
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);
});
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) => {
export const useNodes = (config?: Configuration) => {
const [clients, setClients] = useState<ClientCollection>({});
const [state, dispatch] = useReducer(reducer(env), getInitialState(config));
const [state, dispatch] = useReducer(reducer, getInitialState(config));
const configCacheKey = config?.hosts.join(';');
const allUrls = Object.keys(state).map((node) => state[node].url);
+8
View File
@@ -0,0 +1,8 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;
-1
View File
@@ -37,7 +37,6 @@ type NodeCheck<T> = {
export type NodeData = {
url: string;
verified: boolean;
initialized: boolean;
ssl: NodeCheck<boolean>;
block: NodeCheck<number>;
+2 -11
View File
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/react-helpers';
import { CUSTOM_NODE_KEY, ErrorType } from '../types';
import { ErrorType } from '../types';
import type { Networks, NodeData } from '../types';
export const getIsNodeLoading = (node?: NodeData): boolean => {
@@ -40,7 +40,6 @@ export const getIsNodeDisabled = (env: Networks, data?: NodeData) => {
export const getIsFormDisabled = (
currentNode: string | undefined,
inputText: string,
env: Networks,
state: Record<string, NodeData>
) => {
@@ -48,16 +47,8 @@ export const getIsFormDisabled = (
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);
return data ? getIsNodeDisabled(env, data) : true;
};
export const getErrorByType = (
+1 -1
View File
@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
"types": ["jest", "node", "@testing-library/jest-dom"]
},
"include": [
"**/*.test.ts",
+1
View File
@@ -41,6 +41,7 @@
"apollo": "^2.33.9",
"autoprefixer": "^10.4.2",
"bignumber.js": "^9.0.2",
"buffer": "^6.0.3",
"classnames": "^2.3.1",
"core-js": "^3.6.5",
"date-fns": "^2.28.0",
+9 -1
View File
@@ -8954,6 +8954,14 @@ buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
buffer@~5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
@@ -13774,7 +13782,7 @@ identity-obj-proxy@3.0.0:
dependencies:
harmony-reflect "^1.4.6"
ieee754@^1.1.13, ieee754@^1.1.4:
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==