Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo
This commit is contained in:
Vendored
+23
@@ -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',
|
||||
},
|
||||
};
|
||||
};
|
||||
+3
-1
@@ -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>,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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,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>
|
||||
|
||||
@@ -6,4 +6,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/environment',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -37,7 +37,6 @@ type NodeCheck<T> = {
|
||||
|
||||
export type NodeData = {
|
||||
url: string;
|
||||
verified: boolean;
|
||||
initialized: boolean;
|
||||
ssl: NodeCheck<boolean>;
|
||||
block: NodeCheck<number>;
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user