Compare commits
7 Commits
develop
...
fix/pick-b
Author | SHA1 | Date | |
---|---|---|---|
|
bb98dcfae8 | ||
|
ecd2865bd0 | ||
|
adf0e26df6 | ||
|
a7f95aa953 | ||
|
3583f22067 | ||
|
2ac5471710 | ||
|
98ce1d0a25 |
10
README.md
10
README.md
@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
|
||||
|
||||
This repository is managed using [Nx](https://nx.dev).
|
||||
|
||||
# 🔎 Applications in this repo
|
||||
## 🔎 Applications in this repo
|
||||
|
||||
### [Block explorer](./apps/explorer)
|
||||
|
||||
@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
|
||||
|
||||
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
|
||||
|
||||
# 🧱 Libraries in this repo
|
||||
## 🧱 Libraries in this repo
|
||||
|
||||
### [UI toolkit](./libs/ui-toolkit)
|
||||
|
||||
@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
|
||||
|
||||
Generic react helpers that can be used across multiple applications, along with other utilities.
|
||||
|
||||
# 💻 Develop
|
||||
## 💻 Develop
|
||||
|
||||
### Set up
|
||||
|
||||
@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
|
||||
|
||||
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
|
||||
|
||||
# 🐋 Hosting a console
|
||||
## 🐋 Hosting a console
|
||||
|
||||
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
|
||||
|
||||
@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
|
||||
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
|
||||
```
|
||||
|
||||
# 📑 License
|
||||
## 📑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
@ -29,5 +29,5 @@ NX_REFERRALS=true
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||
# NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||
# NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||
|
@ -29,5 +29,5 @@ NX_REFERRALS=true
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||
# NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||
# NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||
|
@ -309,49 +309,29 @@ export const ActiveRewardCard = ({
|
||||
].includes(m.state)
|
||||
);
|
||||
|
||||
const assetInSettledMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return (
|
||||
m?.state &&
|
||||
[
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (marketSettled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gray out the cards that are related to suspended markets
|
||||
const suspended = transferNode.markets?.some(
|
||||
const assetInActiveMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const marketSuspended = transferNode.markets?.some(
|
||||
(m) =>
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
);
|
||||
|
||||
const assetInSuspendedMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return (
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Gray out the cards that are related to suspended markets
|
||||
// Or settlement assets in markets that are not active and eligible for rewards
|
||||
const { gradientClassName, mainClassName } =
|
||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
||||
marketSuspended || !assetInActiveMarket
|
||||
? {
|
||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||
@ -437,12 +417,12 @@ export const ActiveRewardCard = ({
|
||||
<span>
|
||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||
<Tooltip
|
||||
underline={suspended}
|
||||
underline={marketSuspended}
|
||||
description={
|
||||
(suspended || assetInSuspendedMarket) &&
|
||||
(marketSuspended || !assetInActiveMarket) &&
|
||||
(specificMarkets
|
||||
? t('Eligible market(s) currently suspended')
|
||||
: assetInSuspendedMarket
|
||||
: !assetInActiveMarket
|
||||
? t('Currently no markets eligible for reward')
|
||||
: '')
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ export const Settings = () => {
|
||||
intent={Intent.Primary}
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
|
@ -96,11 +96,6 @@ describe('TransferForm', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
targetText: 'Include transfer fee',
|
||||
tooltipText:
|
||||
'The fee will be taken from the amount you are transferring.',
|
||||
},
|
||||
{
|
||||
targetText: 'Transfer fee',
|
||||
tooltipText: /transfer\.fee\.factor/,
|
||||
@ -276,9 +271,6 @@ describe('TransferForm', () => {
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '50');
|
||||
|
||||
@ -288,10 +280,7 @@ describe('TransferForm', () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100.00');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
// If transfering from a vested account fees should be 0
|
||||
const expectedFee = '0';
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
|
||||
@ -396,78 +385,7 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({ ...props, submitTransfer: mockSubmit });
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
|
||||
// 1003-TRAN-022
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(checkbox).toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
||||
|
||||
// 1003-TRAN-020
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
expectedAmount
|
||||
);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
|
||||
amount
|
||||
);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(expectedAmount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('validates fields when checkbox is not checked', async () => {
|
||||
renderComponent(props);
|
||||
|
||||
@ -497,11 +415,8 @@ describe('TransferForm', () => {
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(checkbox).not.toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
TradingRichSelect,
|
||||
TradingSelect,
|
||||
Tooltip,
|
||||
TradingCheckbox,
|
||||
TradingButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
@ -135,32 +134,17 @@ export const TransferForm = ({
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Max amount given selected asset and from account
|
||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||
|
||||
const transferAmount = useMemo(() => {
|
||||
if (!amount) return undefined;
|
||||
if (includeFee && feeFactor) {
|
||||
return new BigNumber(1).minus(feeFactor).times(amount).toString();
|
||||
}
|
||||
return amount;
|
||||
}, [amount, includeFee, feeFactor]);
|
||||
|
||||
const fee = useMemo(() => {
|
||||
if (!transferAmount) return undefined;
|
||||
if (includeFee) {
|
||||
return new BigNumber(amount).minus(transferAmount).toString();
|
||||
}
|
||||
return (
|
||||
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
|
||||
);
|
||||
}, [amount, includeFee, transferAmount, feeFactor]);
|
||||
const fee = useMemo(
|
||||
() => feeFactor && new BigNumber(feeFactor).times(amount).toString(),
|
||||
[amount, feeFactor]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!transferAmount) {
|
||||
if (!amount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
|
||||
@ -173,7 +157,7 @@ export const TransferForm = ({
|
||||
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
transferAmount,
|
||||
amount,
|
||||
type,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
@ -183,7 +167,7 @@ export const TransferForm = ({
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[submitTransfer, transferAmount, assets]
|
||||
[submitTransfer, amount, assets]
|
||||
);
|
||||
|
||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||
@ -279,7 +263,6 @@ export const TransferForm = ({
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -449,27 +432,9 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<div className="mb-4">
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The fee will be taken from the amount you are transferring.`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<TradingCheckbox
|
||||
name="include-transfer-fee"
|
||||
disabled={!transferAmount || fromVested}
|
||||
label={t('Include transfer fee')}
|
||||
checked={includeFee}
|
||||
onCheckedChange={() => setIncludeFee((x) => !x)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{transferAmount && fee && (
|
||||
{amount && fee && (
|
||||
<TransferFee
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
amount={amount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fromVested ? '0' : fee}
|
||||
decimals={asset?.decimals}
|
||||
@ -484,29 +449,22 @@ export const TransferForm = ({
|
||||
|
||||
export const TransferFee = ({
|
||||
amount,
|
||||
transferAmount,
|
||||
feeFactor,
|
||||
fee,
|
||||
decimals,
|
||||
}: {
|
||||
amount: string;
|
||||
transferAmount: string;
|
||||
feeFactor: string | null;
|
||||
fee?: string;
|
||||
decimals?: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!feeFactor || !amount || !transferAmount || !fee) return null;
|
||||
if (
|
||||
isNaN(Number(feeFactor)) ||
|
||||
isNaN(Number(amount)) ||
|
||||
isNaN(Number(transferAmount)) ||
|
||||
isNaN(Number(fee))
|
||||
) {
|
||||
if (!feeFactor || !amount || !fee) return null;
|
||||
if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
|
||||
const totalValue = new BigNumber(amount).plus(fee).toString();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-2 text-xs">
|
||||
|
@ -23,7 +23,6 @@ import {
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@ -162,19 +161,6 @@ describe('useNodeBasicStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RowData', () => {
|
||||
const props = {
|
||||
id: '0',
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@ -8,6 +7,7 @@ import {
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { useT } from '../../use-t';
|
||||
import { useResponseTime } from '../../utils/time';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
@ -108,20 +108,6 @@ export const useNodeBasicStatus = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } =
|
||||
(requests.length && requests[requests.length - 1]) || {};
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const RowData = ({
|
||||
id,
|
||||
url,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
getUserEnabledFeatureFlags,
|
||||
setUserEnabledFeatureFlag,
|
||||
} from './use-environment';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
const noop = () => {
|
||||
/* no op*/
|
||||
@ -17,6 +18,10 @@ const noop = () => {
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client');
|
||||
jest.mock('zustand');
|
||||
jest.mock('../utils/time');
|
||||
|
||||
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
|
||||
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
|
||||
|
||||
const mockCreateClient = createClient as jest.Mock;
|
||||
const createDefaultMockClient = () => {
|
||||
@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||
const fastWait = 1000;
|
||||
const nodes = [slowNode, fastNode];
|
||||
|
||||
mockCanMeasureResponseTime.mockImplementation(() => true);
|
||||
mockMeasureResponseTime.mockImplementation((url: string) => {
|
||||
if (url === slowNode) return slowWait;
|
||||
if (url === fastNode) return fastWait;
|
||||
return Infinity;
|
||||
});
|
||||
|
||||
// @ts-ignore: typscript doesn't recognise the mock implementation
|
||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||
|
||||
@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
||||
statistics: {
|
||||
chainId: 'chain-id',
|
||||
blockHeight: '100',
|
||||
vegaTime: new Date().toISOString(),
|
||||
vegaTime: new Date(1).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
||||
expect(result.current.nodes).toEqual(nodes);
|
||||
});
|
||||
|
||||
jest.runAllTimers();
|
||||
jest.advanceTimersByTime(2000);
|
||||
// jest.runAllTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toEqual('success');
|
||||
|
@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
||||
import { envSchema } from '../utils/validate-environment';
|
||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||
import uniq from 'lodash/uniq';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import first from 'lodash/first';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
type Client = ReturnType<typeof createClient>;
|
||||
type ClientCollection = {
|
||||
@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
||||
|
||||
const VERSION = 1;
|
||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||
|
||||
const QUERY_TIMEOUT = 3000;
|
||||
const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
|
||||
const raceAgainst = (timeout: number): Promise<false> =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch and validate a vega node configuration
|
||||
*/
|
||||
@ -64,53 +76,88 @@ const fetchConfig = async (url?: string) => {
|
||||
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||
try {
|
||||
const url = await Promise.any(tests);
|
||||
return url;
|
||||
} catch {
|
||||
const nodes = await Promise.all(tests);
|
||||
const responsiveNodes = nodes
|
||||
.filter(([, q, s]) => q && s)
|
||||
.map(([url, q]) => {
|
||||
return {
|
||||
url,
|
||||
...q,
|
||||
};
|
||||
});
|
||||
|
||||
// more recent and faster at the top
|
||||
const ordered = orderBy(
|
||||
responsiveNodes,
|
||||
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
|
||||
['desc', 'desc', 'asc']
|
||||
);
|
||||
|
||||
const best = first(ordered);
|
||||
return best ? best.url : null;
|
||||
} catch (err) {
|
||||
// All tests rejected, no suitable node found
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type Maybe<T> = T | false;
|
||||
type QueryTestResult = {
|
||||
blockHeight: number;
|
||||
vegaTime: Date;
|
||||
responseTime: number;
|
||||
};
|
||||
type SubscriptionTestResult = true;
|
||||
type NodeTestResult = [
|
||||
/** url */
|
||||
string,
|
||||
Maybe<QueryTestResult>,
|
||||
Maybe<SubscriptionTestResult>
|
||||
];
|
||||
/**
|
||||
* Test a node for suitability for connection
|
||||
*/
|
||||
const testNode = async (
|
||||
url: string,
|
||||
client: Client
|
||||
): Promise<string | null> => {
|
||||
): Promise<NodeTestResult> => {
|
||||
const results = await Promise.all([
|
||||
// these promises will only resolve with true/false
|
||||
testQuery(client),
|
||||
testQuery(client, url),
|
||||
testSubscription(client),
|
||||
]);
|
||||
if (results[0] && results[1]) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const message = `Tests failed for node: ${url}`;
|
||||
console.warn(message);
|
||||
|
||||
// throwing here will mean this tests is ignored and a different
|
||||
// node that hopefully does resolve will fulfill the Promise.any
|
||||
throw new Error(message);
|
||||
return [url, ...results];
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a test query on a client
|
||||
*/
|
||||
const testQuery = async (client: Client) => {
|
||||
try {
|
||||
const result = await client.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
});
|
||||
if (!result || result.error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
const testQuery = (
|
||||
client: Client,
|
||||
url: string
|
||||
): Promise<Maybe<QueryTestResult>> => {
|
||||
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||
client
|
||||
.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result && !result.error) {
|
||||
const res = {
|
||||
blockHeight: Number(result.data.statistics.blockHeight),
|
||||
vegaTime: new Date(result.data.statistics.vegaTime),
|
||||
// only after a request has been sent we can retrieve the response time
|
||||
responseTime: canMeasureResponseTime(url)
|
||||
? measureResponseTime(url) || Infinity
|
||||
: Infinity,
|
||||
} as QueryTestResult;
|
||||
resolve(res);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => resolve(false))
|
||||
);
|
||||
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -118,7 +165,9 @@ const testQuery = async (client: Client) => {
|
||||
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||
* is deemed a failure
|
||||
*/
|
||||
const testSubscription = (client: Client) => {
|
||||
const testSubscription = (
|
||||
client: Client
|
||||
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||
return new Promise((resolve) => {
|
||||
const sub = client
|
||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
||||
|
22
libs/environment/src/utils/time.spec.ts
Normal file
22
libs/environment/src/utils/time.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useResponseTime } from './time';
|
||||
|
||||
const mockResponseTime = 50;
|
||||
global.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||
{
|
||||
duration: mockResponseTime,
|
||||
},
|
||||
]);
|
||||
|
||||
describe('useResponseTime', () => {
|
||||
it('returns response time when url is valid', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useResponseTime('https://localhost:1234')
|
||||
);
|
||||
expect(result.current.responseTime).toBe(50);
|
||||
});
|
||||
it('does not return response time when url is invalid', () => {
|
||||
const { result } = renderHook(() => useResponseTime('nope'));
|
||||
expect(result.current.responseTime).toBeUndefined();
|
||||
});
|
||||
});
|
25
libs/environment/src/utils/time.ts
Normal file
25
libs/environment/src/utils/time.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!canMeasureResponseTime(url)) return;
|
||||
const duration = measureResponseTime(url);
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const canMeasureResponseTime = (url: string) => {
|
||||
if (!isValidUrl(url)) return false;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const measureResponseTime = (url: string) => {
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||
return duration;
|
||||
};
|
@ -21,7 +21,6 @@
|
||||
"Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.": "Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.",
|
||||
"Enter manually": "Enter manually",
|
||||
"From account": "From account",
|
||||
"Include transfer fee": "Include transfer fee",
|
||||
"initial level": "initial level",
|
||||
"maintenance level": "maintenance level",
|
||||
"Margin health": "Margin health",
|
||||
@ -33,7 +32,6 @@
|
||||
"release level": "release level",
|
||||
"search level": "search level",
|
||||
"Select from wallet": "Select from wallet",
|
||||
"The fee will be taken from the amount you are transferring.": "The fee will be taken from the amount you are transferring.",
|
||||
"The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.",
|
||||
"The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.",
|
||||
"The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)",
|
||||
|
@ -300,7 +300,7 @@
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
"Vega Reward pot": "Vega Reward pot",
|
||||
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
|
||||
"Vega Wallet <0>full featured</0>": "Vega Wallet <0>full featured</0>",
|
||||
"Vega chart": "Vega chart",
|
||||
"Vesting": "Vesting",
|
||||
"Vesting multiplier": "Vesting multiplier",
|
||||
|
@ -113,6 +113,8 @@ export const filterAndSortClosedMarkets = (markets: MarketMaybeWithData[]) => {
|
||||
return [
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_CLOSED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
].includes(m.data?.marketState || m.state);
|
||||
});
|
||||
};
|
||||
|
4
nx.json
4
nx.json
@ -11,7 +11,9 @@
|
||||
"echo $NX_VEGA_URL",
|
||||
"echo $NX_TENDERMINT_URL",
|
||||
"echo $NX_TENDERMINT_WEBSOCKET_URL",
|
||||
"echo $NX_ETHEREUM_PROVIDER_URL"
|
||||
"echo $NX_ETHEREUM_PROVIDER_URL",
|
||||
"echo $NX_CHARTING_LIBRARY_PATH",
|
||||
"echo $NX_CHARTING_LIBRARY_HASH"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -40,22 +40,12 @@
|
||||
|
||||
## Transfer
|
||||
|
||||
- **Must** can select include transfer fee (<a name="1003-TRAN-015" href="#1003-TRAN-015">1003-TRAN-015</a>)
|
||||
|
||||
- **Must** display tooltip for "Include transfer fee" when hovered over.(<a name="1003-TRAN-016" href="#1003-TRAN-016">1003-TRAN-016</a>)
|
||||
|
||||
- **Must** display tooltip for "Transfer fee when hovered over.(<a name="1003-TRAN-017" href="#1003-TRAN-017">1003-TRAN-017</a>)
|
||||
|
||||
- **Must** display tooltip for "Amount to be transferred" when hovered over.(<a name="1003-TRAN-018" href="#1003-TRAN-018">1003-TRAN-018</a>)
|
||||
|
||||
- **Must** display tooltip for "Total amount (with fee)" when hovered over.(<a name="1003-TRAN-019" href="#1003-TRAN-019">1003-TRAN-019</a>)
|
||||
|
||||
- **Must** amount to be transferred and transfer fee update correctly when include transfer fee is selected (<a name="1003-TRAN-020" href="#1003-TRAN-020">1003-TRAN-020</a>)
|
||||
|
||||
- **Must** total amount with fee is correct with and without "Include transfer fee" selected (<a name="1003-TRAN-021" href="#1003-TRAN-021">1003-TRAN-021</a>)
|
||||
|
||||
- **Must** i cannot select include transfer fee unless amount is entered (<a name="1003-TRAN-022" href="#1003-TRAN-022">1003-TRAN-022</a>)
|
||||
|
||||
- **Must** With all fields entered correctly, clicking "confirm transfer" button will start transaction(<a name="1003-TRAN-023" href="#1003-TRAN-023">1003-TRAN-023</a>)
|
||||
|
||||
### Transfer page
|
||||
|
Loading…
Reference in New Issue
Block a user