Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba8668290 | ||
|
|
35f47b75ac | ||
|
|
0697acdac9 | ||
|
|
92ec7166bc | ||
|
|
56b5214dbf | ||
|
|
94509a29c5 | ||
|
|
4e2b6b2d04 | ||
|
|
c29087cc96 | ||
|
|
5bde096977 | ||
|
|
923dc09eb6 | ||
|
|
4fbdb337dd | ||
|
|
6f4a5b9097 | ||
|
|
0c26d99ce2 | ||
|
|
5aedeba4ff | ||
|
|
5789d3496b | ||
|
|
d95fdca0d4 | ||
|
|
e4bf61c2e2 | ||
|
|
0ba54cd8a4 | ||
|
|
eee85c231b | ||
|
|
afdb387742 | ||
|
|
63698fbb80 | ||
|
|
58d8f7857e | ||
|
|
0931730582 | ||
|
|
3aefa10a4f |
@@ -48,7 +48,7 @@ const MarketLink = ({
|
||||
<Link
|
||||
className="underline"
|
||||
{...props}
|
||||
to={`/${Routes.MARKETS}#${id}`}
|
||||
to={`/${Routes.MARKETS}/${id}`}
|
||||
title={id}
|
||||
>
|
||||
{label}
|
||||
@@ -56,7 +56,7 @@ const MarketLink = ({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -22,8 +22,7 @@ export const MarketDetails = ({
|
||||
}: {
|
||||
market: MarketInfoNoCandlesQuery['market'];
|
||||
}) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
|
||||
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
@@ -39,6 +38,10 @@ export const MarketDetails = ({
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const panels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
@@ -179,32 +182,41 @@ export const MarketDetails = ({
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
),
|
||||
LPVolumeMin:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
LPVolumeMax:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders count towards a commitment they have to be
|
||||
within either the liquidity or price monitoring bounds (whichever is
|
||||
tighter).`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -12,6 +12,7 @@ NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit suppl
|
||||
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const connectPrompt = '[data-testid="eth-connect-prompt"]';
|
||||
const connectButton = '[data-testid="connect-to-eth-btn"]';
|
||||
|
||||
context(
|
||||
@@ -18,7 +17,7 @@ context(
|
||||
});
|
||||
|
||||
it('should have connect Eth wallet info', function () {
|
||||
cy.get(connectPrompt).should('be.visible');
|
||||
cy.get(connectButton).should('be.visible');
|
||||
});
|
||||
|
||||
it('should have connect Eth wallet button', function () {
|
||||
|
||||
@@ -13,4 +13,4 @@ last 2 Edge major versions
|
||||
last 2 Safari major version
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
|
||||
@@ -12,6 +12,7 @@ NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
@@ -15,6 +15,7 @@ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
@@ -9,3 +9,4 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
|
||||
@@ -10,3 +10,4 @@ NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
|
||||
@@ -6,3 +6,4 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
@@ -7,3 +7,4 @@ NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
@@ -10,3 +10,4 @@ NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
|
||||
@@ -1,17 +1,17 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import { useContracts } from '../../contexts/contracts/contracts-context';
|
||||
import { useGetAssociationBreakdown } from '../../hooks/use-get-association-breakdown';
|
||||
import { useGetUserTrancheBalances } from '../../hooks/use-get-user-tranche-balances';
|
||||
import { useGetUserBalances } from '../../hooks/use-get-user-balances';
|
||||
import { useBalances } from '../../lib/balances/balances-store';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useListenForStakingEvents as useListenForAssociationEvents } from '../../hooks/use-listen-for-staking-events';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import { useUserTrancheBalances } from '../../routes/redemption/hooks';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
|
||||
interface BalanceManagerProps {
|
||||
children: ReactElement;
|
||||
@@ -24,7 +24,13 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { updateBalances: updateStoreBalances } = useBalances();
|
||||
const updateStoreBalances = useBalances((state) => state.updateBalances);
|
||||
const setTranchesBalances = useBalances((state) => state.setTranchesBalances);
|
||||
const getUserBalances = useGetUserBalances(account);
|
||||
const userTrancheBalances = useUserTrancheBalances(account);
|
||||
useEffect(() => {
|
||||
setTranchesBalances(userTrancheBalances);
|
||||
}, [setTranchesBalances, userTrancheBalances]);
|
||||
const { config } = useEthereumConfig();
|
||||
|
||||
const numberOfConfirmations = config?.confirmations || 0;
|
||||
@@ -41,10 +47,10 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
|
||||
numberOfConfirmations
|
||||
);
|
||||
|
||||
const getUserTrancheBalances = useGetUserTrancheBalances(
|
||||
account || '',
|
||||
contracts?.vesting
|
||||
);
|
||||
const getTranches = useTranches((state) => state.getTranches);
|
||||
useEffect(() => {
|
||||
getTranches(decimals);
|
||||
}, [decimals, getTranches]);
|
||||
const getAssociationBreakdown = useGetAssociationBreakdown(
|
||||
account || '',
|
||||
contracts?.staking,
|
||||
@@ -54,50 +60,14 @@ export const BalanceManager = ({ children }: BalanceManagerProps) => {
|
||||
// update balances on connect to Ethereum
|
||||
useEffect(() => {
|
||||
const updateBalances = async () => {
|
||||
if (!account || !config) return;
|
||||
try {
|
||||
const [b, w, stats, a] = await Promise.all([
|
||||
contracts.vesting.user_total_all_tranches(account),
|
||||
contracts.token.balanceOf(account),
|
||||
contracts.vesting.user_stats(account),
|
||||
contracts.token.allowance(
|
||||
account,
|
||||
config.staking_bridge_contract.address
|
||||
),
|
||||
]);
|
||||
|
||||
const balance = toBigNum(b, decimals);
|
||||
const walletBalance = toBigNum(w, decimals);
|
||||
const lien = toBigNum(stats.lien, decimals);
|
||||
const allowance = toBigNum(a, decimals);
|
||||
|
||||
updateStoreBalances({
|
||||
balanceFormatted: balance,
|
||||
walletBalance,
|
||||
lien,
|
||||
allowance,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
const balances = await getUserBalances();
|
||||
if (balances) {
|
||||
updateStoreBalances(balances);
|
||||
}
|
||||
};
|
||||
|
||||
updateBalances();
|
||||
}, [
|
||||
decimals,
|
||||
contracts.token,
|
||||
contracts.vesting,
|
||||
account,
|
||||
config,
|
||||
updateStoreBalances,
|
||||
]);
|
||||
|
||||
// This use effect hook is very expensive and is kept separate to prevent expensive reloading of data.
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
getUserTrancheBalances();
|
||||
}
|
||||
}, [account, getUserTrancheBalances]);
|
||||
}, [getUserBalances, updateStoreBalances]);
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
|
||||
@@ -6,27 +6,22 @@ import {
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
|
||||
interface EthConnectPrompProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const EthConnectPrompt = ({ children }: EthConnectPrompProps) => {
|
||||
export const EthConnectPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Button
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
data-testid="connect-to-eth-btn"
|
||||
>
|
||||
{t('connectEthWallet')}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
fill={true}
|
||||
data-testid="connect-to-eth-btn"
|
||||
>
|
||||
{t('connectEthWallet')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ const envName = windowOrDefault('NX_VEGA_ENV') ?? 'local';
|
||||
|
||||
export const ENV = {
|
||||
// Environment
|
||||
tranchesServiceUrl: windowOrDefault('NX_TRANCHES_SERVICE_URL'),
|
||||
dsn: windowOrDefault('NX_SENTRY_DSN'),
|
||||
urlConnect: TRUTHY.includes(windowOrDefault('NX_ETH_URL_CONNECT')),
|
||||
explorerUrl: windowOrDefault('NX_VEGA_EXPLORER'),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import React from 'react';
|
||||
|
||||
import type { BigNumber } from '../../lib/bignumber';
|
||||
@@ -19,21 +18,7 @@ export interface VegaKey {
|
||||
meta: Array<{ key: string; value: string }> | null;
|
||||
}
|
||||
|
||||
export interface UserTrancheBalance {
|
||||
/** ID of tranche */
|
||||
id: number;
|
||||
|
||||
/** Users vesting tokens on tranche */
|
||||
locked: BigNumber;
|
||||
|
||||
/** Users vested tokens on tranche */
|
||||
vested: BigNumber;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
/** Array of tranche objects */
|
||||
tranches: Tranche[] | null;
|
||||
|
||||
/** Number of decimal places of the VEGA token (18 on Mainnet, 5 on Testnet) */
|
||||
decimals: number;
|
||||
|
||||
@@ -52,9 +37,6 @@ export interface AppState {
|
||||
/** Whether or not the connect to Ethereum wallet overlay is open */
|
||||
ethConnectOverlay: boolean;
|
||||
|
||||
/** The error if one was thrown during retrieval of tranche data */
|
||||
trancheError: Error | null;
|
||||
|
||||
/** Whether or not the mobile drawer is open. Only relevant on screens smaller than 960 */
|
||||
drawerOpen: boolean;
|
||||
|
||||
@@ -71,12 +53,10 @@ export enum AppStateActionType {
|
||||
SET_TOKEN,
|
||||
SET_ALLOWANCE,
|
||||
REFRESH_BALANCES,
|
||||
SET_TRANCHE_DATA,
|
||||
SET_VEGA_WALLET_OVERLAY,
|
||||
SET_VEGA_WALLET_MANAGE_OVERLAY,
|
||||
SET_ETH_WALLET_OVERLAY,
|
||||
SET_DRAWER,
|
||||
SET_TRANCHE_ERROR,
|
||||
REFRESH_ASSOCIATED_BALANCES,
|
||||
SET_ASSOCIATION_BREAKDOWN,
|
||||
SET_TRANSACTION_OVERLAY,
|
||||
@@ -90,14 +70,6 @@ export type AppStateAction =
|
||||
totalSupply: BigNumber;
|
||||
totalAssociated: BigNumber;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_TRANCHE_DATA;
|
||||
tranches: Tranche[];
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_TRANCHE_ERROR;
|
||||
error: Error | null;
|
||||
}
|
||||
| {
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY;
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -14,11 +14,9 @@ const initialAppState: AppState = {
|
||||
totalAssociated: new BigNumber(0),
|
||||
decimals: 0,
|
||||
totalSupply: new BigNumber(0),
|
||||
tranches: null,
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
trancheError: null,
|
||||
drawerOpen: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
@@ -34,17 +32,6 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
|
||||
totalAssociated: action.totalAssociated,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_TRANCHE_DATA:
|
||||
return {
|
||||
...state,
|
||||
tranches: action.tranches,
|
||||
};
|
||||
case AppStateActionType.SET_TRANCHE_ERROR: {
|
||||
return {
|
||||
...state,
|
||||
trancheError: action.error,
|
||||
};
|
||||
}
|
||||
case AppStateActionType.SET_VEGA_WALLET_OVERLAY: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,666 +0,0 @@
|
||||
import parseJSON from 'date-fns/parseJSON';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
const json: Tranche[] = [
|
||||
{
|
||||
tranche_id: 1,
|
||||
tranche_start: parseJSON('2022-03-05T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-06-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('3505372.53445'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('3505372.53445'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('187637.95'),
|
||||
user: '0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0',
|
||||
tx: '0xc4491908e3347b05f2394ac7e1006f573fe8cbc490a57cd1dadad70785e95024',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('200000'),
|
||||
user: '0x93b478148FF792B00076B7EdC89Db1FdE7772079',
|
||||
tx: '0xcc8fba855a0d965044d4cc587701fe9ee9f5863852cede017382ffe02963d9b8',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('21666.5743'),
|
||||
user: '0xB523235B6c7C74DDB26b10E78bFb2d0Cb63Ae289',
|
||||
tx: '0x8cc5159f3d665dd33a2bdac6e990cf1b65dc18b6b5e37a07b37dd54495a8d33c',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('200000'),
|
||||
user: '0x7227e17101E6C70F4dAfC7DDB77BB7D83DdfC1C8',
|
||||
tx: '0x0cdef6868bd6b977362396fd06525e1e757e827659c5d75cd512db121947e6f7',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('137998.56'),
|
||||
user: '0x69eFc5642CfcCB1777bc663433640531F044D1F5',
|
||||
tx: '0xaa9b3b39836a69f760f5d1d2fdba44ad348b27c8f31eb094ad6f779b39d7949c',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('16249.93'),
|
||||
user: '0x006B59DD3bC838A74476c0F4a33C1565831dA0DD',
|
||||
tx: '0xda9bdc846300600c0d360edcaf4a5ed40fa2586ba7872f2bf68251a669adff0e',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('187637.95'),
|
||||
user: '0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0',
|
||||
tranche_id: 1,
|
||||
tx: '0xc4491908e3347b05f2394ac7e1006f573fe8cbc490a57cd1dadad70785e95024',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('187637.95'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('187637.95'),
|
||||
},
|
||||
{
|
||||
address: '0x1A71e3ED1996CAbB91bB043f880CE963D601707e',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('112323.67'),
|
||||
user: '0x1A71e3ED1996CAbB91bB043f880CE963D601707e',
|
||||
tranche_id: 1,
|
||||
tx: '0xaa378d2d0d4d7b964a675bb19f19c4f7401deec6ce66b1bd98ad5b812026e53e',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('112323.67'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('112323.67'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 2,
|
||||
tranche_start: parseJSON('2022-06-05T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-12-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('15464357.550320999700000001'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('15464357.550320999700000001'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('1'),
|
||||
user: '0x7fff551249D223f723557a96a0e1a469C79cC934',
|
||||
tx: '0x55fa59d71aac37428a5804442c48da677a4426a4e92447919a8199520ce20f53',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('100000'),
|
||||
user: '0xe20D4d4fFb165e4b9926467d82d03c0e9ab66D89',
|
||||
tx: '0xabf2cc96c41c8b4f0cff8d8ff7448e241ff83e296a7b7dea79d9d9868f33b7ad',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('500000'),
|
||||
user: '0x5f01A497e4033E4812ba5D494bCBc2220cd510Ed',
|
||||
tx: '0xe3a1a74378a42eace12c21c24889d8b0da6a3736ca7987720e70f59886caa5a3',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('59228.95'),
|
||||
user: '0x1d20f66eF3889aa48Bb4Badbbf993dE965BDb029',
|
||||
tx: '0xf31eab593b86aac54f5fa9fe24bb26105ff52386be60bc24617dd362df7c6f15',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('206374.1211'),
|
||||
user: '0x1b979e8AE3BbbaF96Dd1bbbC0060b360A23f2EBE',
|
||||
tx: '0x2983daa9acf6da5fba0debc2aaaaf47389b92aea0de0c27b20809fb69a63ce69',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('200000'),
|
||||
user: '0xe45993F39183E148bC35BA02ba8C289111181c0f',
|
||||
tx: '0xbda7b70ab8ac629617e74daaaa5451b04d7830bc0da516e10d533efc2eef1530',
|
||||
},
|
||||
],
|
||||
withdrawals: [
|
||||
{
|
||||
amount: new BigNumber('0'),
|
||||
user: '0x4527F5A12bbbbb7c88c5863F8AB9a708928Fe702',
|
||||
tx: '0xbaa8632cd162265ca46baaaad746ec3d283a474e344727854d962e057380a51',
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
address: '0x7fff551249D223f723557a96a0e1aCCCC79cC934',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('1'),
|
||||
user: '0x7fff551249D223f723557a96a0e1aCCCC79cC934',
|
||||
tranche_id: 2,
|
||||
tx: '0x55fa59d71aac37428a0000042c48da677a4426a4e92447919a8199520ce20f53',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('1'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('1'),
|
||||
},
|
||||
{
|
||||
address: '0xCc5CAFD3daA3bb2c1168521F35d1eBEB6cf7c051',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('200000'),
|
||||
user: '0xCc5CAFD3daA3bb2c1168521F35d1eBEB6cf7c051',
|
||||
tranche_id: 2,
|
||||
tx: '0x8168230eb08320a7a874ebeeea20f0def842b8059a2b05a036870e00ca624c88',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('200000'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('200000'),
|
||||
},
|
||||
{
|
||||
address: '0x9cd59376F896a5F2084232E386A65c17EEA4Fe9f',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('200000'),
|
||||
user: '0x9cd59376F896a5F2084232E386A65c17EEA4Fe9f',
|
||||
tranche_id: 2,
|
||||
tx: '0x2eb27449e08a245314faaaaf0d93fafc70733586495f18cf2fb69ef979459efd',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('200000'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('200000'),
|
||||
},
|
||||
{
|
||||
address: '0xe45993F39183E148bC35BA02ba8aaaa807981caa',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('200000'),
|
||||
user: '0xe45993F39183E148bC35BA02ba8aaaa07981caa',
|
||||
tranche_id: 2,
|
||||
tx: '0xbda7b70ab8ac629617e74d33575451b04d7830bc0da516e10d533efc2eef1530',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('200000'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('200000'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 3,
|
||||
tranche_start: parseJSON('2021-11-05T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-05-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('14597706.0446472999'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('14445316.74229298796336861823365303'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('129284.449'),
|
||||
user: '0x26100B2C8168Cb0A6c869a5698265086A3Dfeaaa',
|
||||
tx: '0xcc67a776a2e3b48864470aaa9e0940c1814663b4fde6df60c1a099a636dcae79',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('1151405.093'),
|
||||
user: '0x777Ec2e2beaB6a63c1E763D0dc4120AF60BEe39F',
|
||||
tx: '0x1237d45a40aacc5dff7f8f69e8a4c0536fd0460b915de576bd3f0fdf5892c0a4',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('1151073.595'),
|
||||
user: '0x5CD0Ec63687588817044794bF15d4e37991efAB3',
|
||||
tx: '0xb4eaca7d8abeaf7e1d9d28b1f1fa09fc1933af0e11bff4b0120dcef7d2dfc64d',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('54034.5'),
|
||||
user: '0xc01F2E57554Bb392384feCA6a54c8E3A3Ca94E42',
|
||||
tx: '0xf33289a2a73a65b132accdddd600a8d2c7556c2d80784d5f8d4eea1ecacf93cc',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('115049.22'),
|
||||
user: '0x01a8055A97b461b58ba8e37cd349721FeAe77A8D',
|
||||
tx: '0xf650c3599b24d01dfa1f3e19b22f870ff8ac8dfac529893f0b22d548c3535eda',
|
||||
},
|
||||
],
|
||||
withdrawals: [
|
||||
{
|
||||
amount: new BigNumber('0'),
|
||||
user: '0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b',
|
||||
tx: '0x1af894d1f9ce5ea79aa52d180fbff5f30b8b456e43b76ca5d7d73366e422ea37',
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
address: '0x26100B2C8168Cb0A6c869a5698265086A3DfeF98',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('129284.449'),
|
||||
user: '0x26100B2C8168Cb0A6c869a5698265086A3DfeF98',
|
||||
tranche_id: 3,
|
||||
tx: '0xcc67a776a2e3b48864470eff9e0940c1814663b4fde6df60c1a099a636dcae79',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('129284.449'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('129284.449'),
|
||||
},
|
||||
{
|
||||
address: '0xd4632B682228Db5f38E2283869AEe8c29ee6Eec8',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('44499.2'),
|
||||
user: '0xd4632B682228Db5f38E2283869AEe8c29ee6Eec8',
|
||||
tranche_id: 3,
|
||||
tx: '0x57019840d1ce05e0ef65c45801d6699e5eb1032bd8ad56493594d4866996ea82',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('44499.2'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('44499.2'),
|
||||
},
|
||||
{
|
||||
address: '0xe2E6F37cb1f1980418012BF69f43910d6Bc73e73',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('66748.8'),
|
||||
user: '0xe2E6F37cb1f1980418012BF69f43910d6Bc73e73',
|
||||
tranche_id: 3,
|
||||
tx: '0xd257a3aacd5bdf86cbea0ed3db3697f55dff9092129db6baedacaf00b50af936',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('66748.8'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('66748.8'),
|
||||
},
|
||||
{
|
||||
address: '0xc01F2E57554Bb392384feCA6a54c8E3A3Ca94E42',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('54034.5'),
|
||||
user: '0xc01F2E57554Bb392384feCA6a54c8E3A3Ca94E42',
|
||||
tranche_id: 3,
|
||||
tx: '0xf33289a2a73a65b132accdddd600a8d2c7556c2d80784d5f8d4eea1ecacf93cc',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('54034.5'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('54034.5'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 4,
|
||||
tranche_start: parseJSON('2021-10-05T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('5198082.8647159303'),
|
||||
total_removed: new BigNumber('12706.1452878164044708'),
|
||||
locked_amount: new BigNumber('4849328.20502099651595959714823613'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('110499.5291'),
|
||||
user: '0xa8679b60612Fb2e19d68964326CA02dCe6a08D08',
|
||||
tx: '0x9d3432b818054796489848c415af5c523acb16c1540e5865010baf71964c03a7',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('331498.5873'),
|
||||
user: '0x39fEc2e2beaB6a63c1E763D0dc4120AF60BEe39F',
|
||||
tx: '0xdf5c6e44ef0763e785721802704e5fd186fa3964302a566899027447d0dda57b',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('27624.032275'),
|
||||
user: '0xF5Fb27b912D987B5b6e02A1B1BE0C1F0740E2c6f',
|
||||
tx: '0x1860d39ae3e9ad710da9e2a9bf9606eedc1b176fca673473d6854ea91ed8beb5',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('73666.3527333333'),
|
||||
user: '0x2895059cB5a492BEd58D1fB22713006EfaD465eA',
|
||||
tx: '0x260619e2129cc58ae03f6b4ecfc5a6eeab29b5998de69875accb3cb945beed04',
|
||||
},
|
||||
],
|
||||
withdrawals: [
|
||||
{
|
||||
amount: new BigNumber('1290.014571009862016'),
|
||||
user: '0xBc934494675a6ceB639B9EfEe5b9C0f017D35a75',
|
||||
tx: '0x637c3648ce941a77e08e741f981806a5d56275db6d280f041902386ae8567d06',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('5197.621879605058623'),
|
||||
user: '0xafa64cCa337eFEE0AD827F6C2684e69275226e90',
|
||||
tx: '0xe8b8c1a38d3ae809dcaae54a11adeeba0f46be03924e91d8f251f3f2d48c3553',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('376.2625308599198808'),
|
||||
user: '0x1dD2718fd01d05C9F50Fce8Bb723A4C7483A1E15',
|
||||
tx: '0x78af737235f1123bb1c6a607a0b4b7a3c5ed6859a4e8912f45bb7c812724b1de',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('4162.301372742020875'),
|
||||
user: '0xBc934494675a6ceB639B9EfEe5b9C0f017D35a75',
|
||||
tx: '0x85c7d9979f0e3a3660dc5952732eaf9414f7c28a1c00a9a10f449843f91d85e0',
|
||||
},
|
||||
{
|
||||
amount: new BigNumber('1679.944933599543076'),
|
||||
user: '0x9058e12e2F32cB1cD4D3123359963D77786477FC',
|
||||
tx: '0xd92083f2e90e84cb70ef27ac410ed1144200c41b1714fb54d29f5f23ca086ecb',
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
address: '0xa8679b60612Fb2e19d68964326CA02dCe6a08D08',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('110499.5291'),
|
||||
user: '0xa8679b60612Fb2e19d68964326CA02dCe6a08D08',
|
||||
tranche_id: 4,
|
||||
tx: '0x9d3432b818054796489848c415af5c523acb16c1540e5865010baf71964c03a7',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('110499.5291'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('110499.5291'),
|
||||
},
|
||||
{
|
||||
address: '0x8767d65677Cabaa2050b764AEf40610f2f9796F5',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('1104995.291'),
|
||||
user: '0x8767d65677Cabaa2050b764AEf40610f2f9796F5',
|
||||
tranche_id: 4,
|
||||
tx: '0xc6298f52c173a837abd051ed810b01eb5731be307376b73df6e31b4de39d0122',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('1104995.291'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('1104995.291'),
|
||||
},
|
||||
{
|
||||
address: '0x91715128a71c9C734CDC20E5EdaaeA02E72e422E',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('165749.29365'),
|
||||
user: '0x91715128a71c9C734CDC20E5EdEEeA02E72e422E',
|
||||
tranche_id: 4,
|
||||
tx: '0xf828cea685a0689f27446f02d3376c3afa4398182d3b5bf1c81e949f5965d1c1',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('165749.29365'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('165749.29365'),
|
||||
},
|
||||
{
|
||||
address: '0x2895059cB5a492BEd58D1fB22713006EfaD465eA',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('73666.3527333333'),
|
||||
user: '0x2895059cB5a492BEd58D1fB22713006EfaD465eA',
|
||||
tranche_id: 4,
|
||||
tx: '0x260619e2129cc58ae03f6b4ecfc5a6eeab29b5998de69875accb3cb945beed04',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('73666.3527333333'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('73666.3527333333'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 23,
|
||||
tranche_start: parseJSON('2022-04-30T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2022-04-30T00:00:00.000Z'),
|
||||
total_added: new BigNumber('10833.29'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('10833.29'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('10833.29'),
|
||||
user: '0xF3359E5B89f7804c8c9283781Aaaa33BBd979c9D',
|
||||
tx: '0x65f904ef34d6992b52f449a709d7a24411a501fd07f90aaa9255eacc994bc229',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0xF3359E5B89f7804c8c9283781Aaaa33BBd979c9D',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('10833.29'),
|
||||
user: '0xF3359E5B89f7804c8c9283781Aaaa33BBd979c9D',
|
||||
tranche_id: 23,
|
||||
tx: '0x65f904ef34d6992b52f449a709d7a24411a501fd07f90aaa9255eacc994bc229',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('10833.29'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('10833.29'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 24,
|
||||
tranche_start: parseJSON('2022-09-26T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2022-09-26T00:00:00.000Z'),
|
||||
total_added: new BigNumber('16249.93'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('16249.93'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('16249.93'),
|
||||
user: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
|
||||
tx: '0x3dbd991b7914986505d89a7c1562278dffffa2f5f444bdbf5d6bbd838e3d8d5d',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('16249.93'),
|
||||
user: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
|
||||
tranche_id: 24,
|
||||
tx: '0x3dbd991b7914986505d89a7c1562278dffffa2f5f444bdbf5d6bbd838e3d8d5d',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('16249.93'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('16249.93'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 25,
|
||||
tranche_start: parseJSON('2022-02-10T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('0'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('0'),
|
||||
deposits: [],
|
||||
withdrawals: [],
|
||||
users: [],
|
||||
},
|
||||
{
|
||||
tranche_id: 26,
|
||||
tranche_start: parseJSON('2022-02-04T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('135173.4239508'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('135173.4239508'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('135173.4239508'),
|
||||
user: '0xc90eA4d8D214D548221EE3622a8BE1D61f7077A2',
|
||||
tx: '0x123fb1e293a8246b92d85a47c8d33def9fb6468c7cbb70f1754d78914b12dbf8',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0x222eA4d8D214D548221EE3622a8BE1D61f7077A2',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('135173.4239508'),
|
||||
user: '0x222eA4d8D214D548221EE3622a8BE1D61f7077A2',
|
||||
tranche_id: 26,
|
||||
tx: '0x9fafb1e293a8246b92d85a47c8d33def9fb6468c7cbb70f1754d78914b12dbf8',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('135173.4239508'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('135173.4239508'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 27,
|
||||
tranche_start: parseJSON('2022-05-09T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('32499.86'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('32499.86'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('32499.86'),
|
||||
user: '0x1E7c4E57A1dc4dD4bBE81b833e3E437f69619DaB',
|
||||
tx: '0x25a3dd4852ce8ac1c15fe42123cfab14e4bf8a5c1cb97167cb4d6fe20bd319ae',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0x3E7c4E57A1dc4dD4bBE81bbEFBe3Eaaaf69619Da9',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('32499.86'),
|
||||
user: '0x3E7c4E57A1dc4dD4bBE81bbEFBe3Eaaaf69619Da9',
|
||||
tranche_id: 27,
|
||||
tx: '0x75a3dd4852ce8ac1c15fe42ff6cfab1455bf8a5c1cb97167cb4d6fe20bd319ae',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('32499.86'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('32499.86'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 28,
|
||||
tranche_start: parseJSON('2022-04-30T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('10833.29'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('10833.29'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('10833.29'),
|
||||
user: '0xF3359E5B89f7804c8c9283781Aaa133BBd979c9D',
|
||||
tx: '0x166d235eff44e7bcc63597c0d6e698552e4440fe90ec7cf07a1d510c275cf3e0',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0x12349E5B89f7804c8c9283781A23133BBd979c22',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('10833.29'),
|
||||
user: '0x12349E5B89f7804c8c9283781A23133BBd979c22',
|
||||
tranche_id: 28,
|
||||
tx: '0x166d235eff44e7baa6359712345698552e6150fe90ec7cf07a1d510c275cf3e0',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('10833.29'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('10833.29'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 29,
|
||||
tranche_start: parseJSON('2022-09-26T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-04-05T00:00:00.000Z'),
|
||||
total_added: new BigNumber('16249.93'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('16249.93'),
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('16249.93'),
|
||||
user: '0xcE96670971ec2E1E79D0d12388adbA2FfD6F6C7f',
|
||||
tx: '0xa770ab2d05e6c81be46b73ac2326e81a111da4ce55a2d8544bd95b48ad530674',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
users: [
|
||||
{
|
||||
address: '0xcE96670971ec2E1E79D0d96688adbA2FfD6F6C7f',
|
||||
deposits: [
|
||||
{
|
||||
amount: new BigNumber('16249.93'),
|
||||
user: '0xcE96670971ec212379D0d12388adbA2Ffc6F6C7f',
|
||||
tranche_id: 29,
|
||||
tx: '0xa220ab2d05e6c81be12373ac2326e81a912da4ce55a2d8544bd95b48ad530674',
|
||||
},
|
||||
],
|
||||
withdrawals: [],
|
||||
total_tokens: new BigNumber('16249.93'),
|
||||
withdrawn_tokens: new BigNumber('0'),
|
||||
remaining_tokens: new BigNumber('16249.93'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tranche_id: 30,
|
||||
tranche_start: parseJSON('2021-11-01T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2022-05-01T00:00:00.000Z'),
|
||||
total_added: new BigNumber('0'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('0'),
|
||||
deposits: [],
|
||||
withdrawals: [],
|
||||
users: [],
|
||||
},
|
||||
{
|
||||
tranche_id: 31,
|
||||
tranche_start: parseJSON('2022-02-01T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2022-08-01T00:00:00.000Z'),
|
||||
total_added: new BigNumber('0'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('0'),
|
||||
deposits: [],
|
||||
withdrawals: [],
|
||||
users: [],
|
||||
},
|
||||
{
|
||||
tranche_id: 32,
|
||||
tranche_start: parseJSON('2022-05-01T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2022-11-01T00:00:00.000Z'),
|
||||
total_added: new BigNumber('0'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('0'),
|
||||
deposits: [],
|
||||
withdrawals: [],
|
||||
users: [],
|
||||
},
|
||||
{
|
||||
tranche_id: 33,
|
||||
tranche_start: parseJSON('2022-11-01T00:00:00.000Z'),
|
||||
tranche_end: parseJSON('2023-05-01T00:00:00.000Z'),
|
||||
total_added: new BigNumber('0'),
|
||||
total_removed: new BigNumber('0'),
|
||||
locked_amount: new BigNumber('0'),
|
||||
deposits: [],
|
||||
withdrawals: [],
|
||||
users: [],
|
||||
},
|
||||
];
|
||||
|
||||
export default json;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import mock from './tranches-mock';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
export function useTranches() {
|
||||
const [tranches, setTranches] = React.useState<Tranche[] | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const run = async () => {
|
||||
try {
|
||||
setTranches(mock);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
};
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return { tranches, error };
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useContracts } from '../contexts/contracts/contracts-context';
|
||||
import { useAppState } from '../contexts/app-state/app-state-context';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
|
||||
export const useGetUserBalances = (account: string | undefined) => {
|
||||
const { token, vesting } = useContracts();
|
||||
const { config } = useEthereumConfig();
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
return useCallback(async () => {
|
||||
if (!account || !config) return;
|
||||
try {
|
||||
const [b, w, stats, a] = await Promise.all([
|
||||
vesting.user_total_all_tranches(account),
|
||||
token.balanceOf(account),
|
||||
vesting.user_stats(account),
|
||||
token.allowance(account, config.staking_bridge_contract.address),
|
||||
]);
|
||||
|
||||
const balance = toBigNum(b, decimals);
|
||||
const walletBalance = toBigNum(w, decimals);
|
||||
const lien = toBigNum(stats.lien, decimals);
|
||||
const allowance = toBigNum(a, decimals);
|
||||
|
||||
return {
|
||||
balanceFormatted: balance,
|
||||
walletBalance,
|
||||
lien,
|
||||
allowance,
|
||||
balance,
|
||||
};
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
return null;
|
||||
}
|
||||
}, [account, config, decimals, token, vesting]);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { TokenVesting } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../lib/bignumber';
|
||||
import { useTranches } from './use-tranches';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useBalances } from '../lib/balances/balances-store';
|
||||
|
||||
export const useGetUserTrancheBalances = (
|
||||
address: string,
|
||||
vesting: TokenVesting
|
||||
) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
appDispatch,
|
||||
} = useAppState();
|
||||
const { setTranchesBalances } = useBalances();
|
||||
const { tranches } = useTranches();
|
||||
return React.useCallback(async () => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_TRANCHE_ERROR,
|
||||
error: null,
|
||||
});
|
||||
try {
|
||||
if (!tranches) {
|
||||
return;
|
||||
}
|
||||
const userTranches = tranches?.filter((t) =>
|
||||
t.users.some(
|
||||
({ address: a }) =>
|
||||
a && address && a.toLowerCase() === address.toLowerCase()
|
||||
)
|
||||
);
|
||||
const trancheIds = [0, ...userTranches.map((t) => t.tranche_id)];
|
||||
const promises = trancheIds.map(async (tId) => {
|
||||
const [t, v] = await Promise.all([
|
||||
vesting.get_tranche_balance(address, tId),
|
||||
vesting.get_vested_for_tranche(address, tId),
|
||||
]);
|
||||
|
||||
const total = toBigNum(t, decimals);
|
||||
const vested = toBigNum(v, decimals);
|
||||
|
||||
return {
|
||||
id: tId,
|
||||
locked: tId === 0 ? total : total.minus(vested),
|
||||
vested: tId === 0 ? new BigNumber(0) : vested,
|
||||
};
|
||||
});
|
||||
|
||||
const trancheBalances = await Promise.all(promises);
|
||||
setTranchesBalances(trancheBalances);
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_TRANCHE_DATA,
|
||||
tranches,
|
||||
});
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_TRANCHE_ERROR,
|
||||
error: e as Error,
|
||||
});
|
||||
}
|
||||
}, [appDispatch, tranches, setTranchesBalances, address, vesting, decimals]);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import React, { useEffect } from 'react';
|
||||
import type { Networks } from '@vegaprotocol/environment';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
import { BigNumber } from '../lib/bignumber';
|
||||
|
||||
const TRANCHES_URLS: { [N in Networks]: string } = {
|
||||
MAINNET: 'https://static.vega.xyz/assets/mainnet-tranches.json',
|
||||
MIRROR: 'https://static.vega.xyz/assets/mirror-tranches.json',
|
||||
TESTNET: 'https://static.vega.xyz/assets/testnet-tranches.json',
|
||||
SANDBOX: 'https://static.vega.xyz/assets/sandbox-tranches.json',
|
||||
STAGNET1: 'https://static.vega.xyz/assets/stagnet1-tranches.json',
|
||||
STAGNET3: 'https://static.vega.xyz/assets/stagnet3-tranches.json',
|
||||
DEVNET: 'https://static.vega.xyz/assets/devnet-tranches.json',
|
||||
CUSTOM: 'https://static.vega.xyz/assets/testnet-tranches.json',
|
||||
};
|
||||
|
||||
export function useTranches() {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const [tranches, setTranches] = React.useState<Tranche[] | null>(null);
|
||||
const url = React.useMemo(() => TRANCHES_URLS[VEGA_ENV], [VEGA_ENV]);
|
||||
const {
|
||||
state: { data, loading, error },
|
||||
} = useFetch<Tranche[] | null>(url);
|
||||
useEffect(() => {
|
||||
const processedTrances = data
|
||||
?.map((t) => ({
|
||||
...t,
|
||||
tranche_start: new Date(t.tranche_start),
|
||||
tranche_end: new Date(t.tranche_end),
|
||||
total_added: new BigNumber(t.total_added),
|
||||
total_removed: new BigNumber(t.total_removed),
|
||||
locked_amount: new BigNumber(t.locked_amount),
|
||||
deposits: t.deposits.map((d) => ({
|
||||
...d,
|
||||
amount: new BigNumber(d.amount),
|
||||
})),
|
||||
withdrawals: t.withdrawals.map((w) => ({
|
||||
...w,
|
||||
amount: new BigNumber(w.amount),
|
||||
})),
|
||||
users: t.users.map((u) => ({
|
||||
...u,
|
||||
// @ts-ignore - types are incorrect in the SDK lib
|
||||
deposits: u.deposits.map((d) => ({
|
||||
...d,
|
||||
amount: new BigNumber(d.amount),
|
||||
})),
|
||||
// @ts-ignore - types are incorrect in the SDK lib
|
||||
withdrawals: u.withdrawals.map((w) => ({
|
||||
...w,
|
||||
amount: new BigNumber(w.amount),
|
||||
})),
|
||||
total_tokens: new BigNumber(u.total_tokens),
|
||||
withdrawn_tokens: new BigNumber(u.withdrawn_tokens),
|
||||
remaining_tokens: new BigNumber(u.remaining_tokens),
|
||||
})),
|
||||
}))
|
||||
.sort((a: Tranche, b: Tranche) => a.tranche_id - b.tranche_id);
|
||||
setTranches(processedTrances ? processedTrances : null);
|
||||
}, [data]);
|
||||
|
||||
return {
|
||||
tranches,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
import { BigNumber } from '../bignumber';
|
||||
|
||||
export function generateTranche(id: number): Tranche {
|
||||
return {
|
||||
tranche_id: id,
|
||||
tranche_start: new Date(),
|
||||
tranche_end: new Date(),
|
||||
total_added: new BigNumber(0),
|
||||
total_removed: new BigNumber(0),
|
||||
locked_amount: new BigNumber(0),
|
||||
deposits: [],
|
||||
withdrawals: [],
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function generateTranches(count = 1) {
|
||||
return new Array(count).fill(null).map((_, i) => generateTranche(i));
|
||||
}
|
||||
+8
-3
@@ -1,6 +1,11 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { create } from 'zustand';
|
||||
import type { UserTrancheBalance } from '../../contexts/app-state/app-state-context';
|
||||
|
||||
interface UserTrancheBalance {
|
||||
id: number;
|
||||
locked: BigNumber;
|
||||
vested: BigNumber;
|
||||
}
|
||||
|
||||
export interface AssociationBreakdown {
|
||||
stakingAssociations: { [vegaKey: string]: BigNumber };
|
||||
@@ -13,8 +18,8 @@ export type BalancesStore = {
|
||||
balanceFormatted: BigNumber;
|
||||
totalVestedBalance: BigNumber;
|
||||
totalLockedBalance: BigNumber;
|
||||
walletBalance: BigNumber;
|
||||
trancheBalances: UserTrancheBalance[];
|
||||
walletBalance: BigNumber;
|
||||
lien: BigNumber;
|
||||
walletAssociatedBalance: BigNumber;
|
||||
vestingAssociatedBalance: BigNumber;
|
||||
@@ -38,8 +43,8 @@ export const useBalances = create<BalancesStore>((set) => ({
|
||||
stakingAssociations: {},
|
||||
vestingAssociations: {},
|
||||
},
|
||||
trancheBalances: [],
|
||||
allowance: new BigNumber(0),
|
||||
trancheBalances: [],
|
||||
totalVestedBalance: new BigNumber(0),
|
||||
totalLockedBalance: new BigNumber(0),
|
||||
balanceFormatted: new BigNumber(0),
|
||||
@@ -0,0 +1,80 @@
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import type { TrancheServiceResponse } from '@vegaprotocol/smart-contracts';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import create from 'zustand';
|
||||
import { ENV } from '../../config';
|
||||
|
||||
export interface Tranche {
|
||||
tranche_id: number;
|
||||
tranche_start: Date;
|
||||
tranche_end: Date;
|
||||
total_added: BigNumber;
|
||||
total_removed: BigNumber;
|
||||
locked_amount: BigNumber;
|
||||
users: string[];
|
||||
}
|
||||
|
||||
const URL = `${ENV.tranchesServiceUrl}/tranches/stats`;
|
||||
|
||||
export interface UserTrancheBalance {
|
||||
/** ID of tranche */
|
||||
id: number;
|
||||
|
||||
/** Users vesting tokens on tranche */
|
||||
locked: BigNumber;
|
||||
|
||||
/** Users vested tokens on tranche */
|
||||
vested: BigNumber;
|
||||
}
|
||||
|
||||
export type TranchesStore = {
|
||||
tranches: Tranche[] | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
getTranches: (decimals: number) => void;
|
||||
};
|
||||
|
||||
const secondsToDate = (seconds: number) => new Date(seconds * 1000);
|
||||
|
||||
export const useTranches = create<TranchesStore>((set) => ({
|
||||
tranches: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
getTranches: async (decimals: number) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const res = await fetch(URL);
|
||||
const data = (await res.json()) as TrancheServiceResponse;
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
const tranches = Object.values(data.tranches)
|
||||
?.map((t) => {
|
||||
const tranche_progress =
|
||||
t.duration !== 0 ? (now - t.cliff_start) / t.duration : 0;
|
||||
const lockedDecimal = tranche_progress < 0 ? 1 : 1 - tranche_progress;
|
||||
return {
|
||||
tranche_id: t.tranche_id,
|
||||
tranche_start: secondsToDate(t.cliff_start),
|
||||
tranche_end: secondsToDate(t.cliff_start + t.duration),
|
||||
total_added: toBigNum(t.initial_balance, decimals),
|
||||
total_removed: toBigNum(t.initial_balance, decimals).minus(
|
||||
toBigNum(t.current_balance, decimals)
|
||||
),
|
||||
locked_amount: toBigNum(t.initial_balance, decimals).times(
|
||||
lockedDecimal
|
||||
),
|
||||
users: t.users,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.tranche_id - b.tranche_id);
|
||||
set({
|
||||
tranches,
|
||||
});
|
||||
} catch (e) {
|
||||
set({ error: e as unknown as Error });
|
||||
} finally {
|
||||
set({
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -4,7 +4,6 @@ import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import { useContracts } from '../../contexts/contracts/contracts-context';
|
||||
@@ -22,6 +21,7 @@ import { TrancheNotFound } from './tranche-not-found';
|
||||
import { UntargetedClaim } from './untargeted-claim';
|
||||
import { Verifying } from './verifying';
|
||||
import type { ClaimAction, ClaimState } from './claim-reducer';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
|
||||
interface ClaimFlowProps {
|
||||
state: ClaimState;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { format } from 'date-fns';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
import { DATE_FORMAT_LONG } from '../../lib/date-formats';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
|
||||
interface ClaimInfoProps {
|
||||
tranche: Tranche;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
import { useContracts } from '../../contexts/contracts/contracts-context';
|
||||
import { useGetUserTrancheBalances } from '../../hooks/use-get-user-tranche-balances';
|
||||
import { useRefreshBalances } from '../../hooks/use-refresh-balances';
|
||||
import { useSearchParams } from '../../hooks/use-search-params';
|
||||
import { ClaimError } from './claim-error';
|
||||
@@ -13,7 +11,7 @@ import {
|
||||
initialClaimState,
|
||||
} from './claim-reducer';
|
||||
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const Claim = ({
|
||||
address,
|
||||
@@ -23,10 +21,8 @@ const Claim = ({
|
||||
tranches: Tranche[];
|
||||
}) => {
|
||||
const params = useSearchParams();
|
||||
const { vesting } = useContracts();
|
||||
const { appState } = useAppState();
|
||||
const [state, dispatch] = React.useReducer(claimReducer, initialClaimState);
|
||||
const getUserTrancheBalances = useGetUserTrancheBalances(address, vesting);
|
||||
const refreshBalances = useRefreshBalances(address);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -48,10 +44,9 @@ const Claim = ({
|
||||
// If the claim has been committed refetch the new VEGA balance
|
||||
React.useEffect(() => {
|
||||
if (state.claimStatus === ClaimStatus.Finished && address) {
|
||||
getUserTrancheBalances();
|
||||
refreshBalances();
|
||||
}
|
||||
}, [address, getUserTrancheBalances, refreshBalances, state.claimStatus]);
|
||||
}, [address, refreshBalances, state.claimStatus]);
|
||||
|
||||
if (state.error) {
|
||||
return <ClaimError />;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EthConnectPrompt } from '../../components/eth-connect-prompt';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { useTranches } from '../../hooks/use-tranches';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import type { RouteChildProps } from '..';
|
||||
import Claim from './claim';
|
||||
import { ClaimRestricted } from './claim-restricted';
|
||||
@@ -16,7 +16,11 @@ const ClaimIndex = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { account } = useWeb3React();
|
||||
const { tranches, loading, error } = useTranches();
|
||||
const { tranches, loading, error } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
|
||||
if (loading || !tranches) {
|
||||
return (
|
||||
@@ -38,13 +42,14 @@ const ClaimIndex = ({ name }: RouteChildProps) => {
|
||||
|
||||
if (!account) {
|
||||
content = (
|
||||
<EthConnectPrompt>
|
||||
<>
|
||||
<p data-testid="eth-connect-prompt">
|
||||
{t(
|
||||
"Use the Ethereum wallet you want to send your tokens to. You'll also need enough Ethereum to pay gas."
|
||||
)}
|
||||
</p>
|
||||
</EthConnectPrompt>
|
||||
<EthConnectPrompt />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
content = isRestricted() ? (
|
||||
|
||||
@@ -51,11 +51,9 @@ const mockAppState: AppState = {
|
||||
totalAssociated: new BigNumber('50063005'),
|
||||
decimals: 18,
|
||||
totalSupply: new BigNumber(65000000),
|
||||
tranches: null,
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
trancheError: null,
|
||||
drawerOpen: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
|
||||
@@ -14,11 +14,9 @@ const mockAppState: AppState = {
|
||||
totalAssociated: new BigNumber('50063005'),
|
||||
decimals: 18,
|
||||
totalSupply: mockTotalSupply,
|
||||
tranches: null,
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
trancheError: null,
|
||||
drawerOpen: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
|
||||
@@ -1,56 +1,81 @@
|
||||
import { Callout, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useBalances } from '../../../lib/balances/balances-store';
|
||||
import React from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { AddLockedTokenAddress } from '../../../components/add-locked-token';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import { truncateMiddle } from '../../../lib/truncate-middle';
|
||||
import Routes from '../../routes';
|
||||
import type { RedemptionState } from '../redemption-reducer';
|
||||
import { Tranche0Table, TrancheTable } from '../tranche-table';
|
||||
import { VestingTable } from './vesting-table';
|
||||
import { useTranches } from '../../../lib/tranches/tranches-store';
|
||||
import { useGetUserBalances } from '../../../hooks/use-get-user-balances';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useUserTrancheBalances } from '../hooks';
|
||||
|
||||
interface UserBalances {
|
||||
balanceFormatted: BigNumber;
|
||||
walletBalance: BigNumber;
|
||||
lien: BigNumber;
|
||||
allowance: BigNumber;
|
||||
balance: BigNumber;
|
||||
}
|
||||
|
||||
export const RedemptionInformation = () => {
|
||||
const { state, account } = useOutletContext<{
|
||||
state: RedemptionState;
|
||||
account: string;
|
||||
}>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
balanceFormatted,
|
||||
lien,
|
||||
totalVestedBalance,
|
||||
totalLockedBalance,
|
||||
trancheBalances,
|
||||
} = useBalances();
|
||||
|
||||
const { userTranches } = state;
|
||||
|
||||
const filteredTranches = React.useMemo(
|
||||
const navigate = useNavigate();
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const { address } = useParams<{ address: string }>();
|
||||
const [userBalances, setUserBalances] = useState<null | UserBalances>();
|
||||
const getUsersBalances = useGetUserBalances(address);
|
||||
useEffect(() => {
|
||||
getUsersBalances().then(setUserBalances);
|
||||
}, [getUsersBalances]);
|
||||
const userTrancheBalances = useUserTrancheBalances(address);
|
||||
const filteredTranches = useMemo(
|
||||
() =>
|
||||
userTranches.filter((tr) => {
|
||||
const balance = trancheBalances.find(
|
||||
tranches?.filter((tr) => {
|
||||
const balance = userTrancheBalances.find(
|
||||
({ id }) => id.toString() === tr.tranche_id.toString()
|
||||
);
|
||||
return (
|
||||
balance?.locked.isGreaterThan(0) || balance?.vested.isGreaterThan(0)
|
||||
);
|
||||
}),
|
||||
[trancheBalances, userTranches]
|
||||
}) || [],
|
||||
[userTrancheBalances, tranches]
|
||||
);
|
||||
const { totalLocked, totalVested } = useMemo(() => {
|
||||
return {
|
||||
totalLocked: BigNumber.sum.apply(null, [
|
||||
new BigNumber(0),
|
||||
...userTrancheBalances.map(({ locked }) => locked),
|
||||
]),
|
||||
totalVested: BigNumber.sum.apply(null, [
|
||||
new BigNumber(0),
|
||||
...userTrancheBalances.map(({ vested }) => vested),
|
||||
]),
|
||||
};
|
||||
}, [userTrancheBalances]);
|
||||
|
||||
const zeroTranche = React.useMemo(() => {
|
||||
const zeroTranche = trancheBalances.find((t) => t.id === 0);
|
||||
const zeroTranche = useMemo(() => {
|
||||
const zeroTranche = userTrancheBalances.find((t) => t.id === 0);
|
||||
if (zeroTranche && zeroTranche.locked.isGreaterThan(0)) {
|
||||
return zeroTranche;
|
||||
}
|
||||
return null;
|
||||
}, [trancheBalances]);
|
||||
}, [userTrancheBalances]);
|
||||
|
||||
if (!filteredTranches.length) {
|
||||
const isAccountValid = useMemo(
|
||||
() => address && address.length === 42 && address.startsWith('0x'),
|
||||
[address]
|
||||
);
|
||||
|
||||
if (!isAccountValid || !address) {
|
||||
return <div>The address {address} is not a valid Ethereum address</div>;
|
||||
}
|
||||
|
||||
if (!filteredTranches.length || !userBalances) {
|
||||
return (
|
||||
<section data-testid="redemption-page">
|
||||
<div className="mb-8">
|
||||
@@ -79,17 +104,17 @@ export const RedemptionInformation = () => {
|
||||
{t(
|
||||
'{{address}} has {{balance}} VEGA tokens in {{tranches}} tranches of the vesting contract.',
|
||||
{
|
||||
address: truncateMiddle(account),
|
||||
balance: formatNumber(balanceFormatted),
|
||||
address: truncateMiddle(address),
|
||||
balance: formatNumber(userBalances.balanceFormatted),
|
||||
tranches: filteredTranches.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<div className="mb-24">
|
||||
<VestingTable
|
||||
associated={lien}
|
||||
locked={totalLockedBalance}
|
||||
vested={totalVestedBalance}
|
||||
associated={userBalances.lien}
|
||||
locked={totalLocked}
|
||||
vested={totalVested}
|
||||
/>
|
||||
</div>
|
||||
{filteredTranches.length ? <h2>{t('Tranche breakdown')}</h2> : null}
|
||||
@@ -98,7 +123,7 @@ export const RedemptionInformation = () => {
|
||||
trancheId={0}
|
||||
total={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
trancheBalances.find(
|
||||
userTrancheBalances.find(
|
||||
({ id }) => id.toString() === zeroTranche.id.toString()
|
||||
)!.locked
|
||||
}
|
||||
@@ -108,22 +133,25 @@ export const RedemptionInformation = () => {
|
||||
<TrancheTable
|
||||
key={tr.tranche_id}
|
||||
tranche={tr}
|
||||
lien={lien}
|
||||
lien={userBalances.lien}
|
||||
locked={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
trancheBalances.find(
|
||||
userTrancheBalances.find(
|
||||
({ id }) => id.toString() === tr.tranche_id.toString()
|
||||
)!.locked
|
||||
}
|
||||
vested={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
trancheBalances.find(
|
||||
userTrancheBalances.find(
|
||||
({ id }) => id.toString() === tr.tranche_id.toString()
|
||||
)!.vested
|
||||
}
|
||||
totalVested={totalVestedBalance}
|
||||
totalLocked={totalLockedBalance}
|
||||
onClick={() => navigate(`/vesting/${tr.tranche_id}`)}
|
||||
totalVested={totalVested}
|
||||
totalLocked={totalLocked}
|
||||
onClick={() =>
|
||||
navigate(`${Routes.REDEEM}/${address}/${tr.tranche_id}`)
|
||||
}
|
||||
address={address}
|
||||
/>
|
||||
))}
|
||||
<Callout
|
||||
@@ -132,7 +160,7 @@ export const RedemptionInformation = () => {
|
||||
intent={Intent.Warning}
|
||||
>
|
||||
<p>{t('Find out more about Staking.')}</p>
|
||||
<Link to="/staking" className="underline text-white">
|
||||
<Link to={Routes.VALIDATORS} className="underline text-white">
|
||||
{t('Stake VEGA tokens')}
|
||||
</Link>
|
||||
</Callout>
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface VestingTableProps {
|
||||
}
|
||||
|
||||
const VestingTableIndicatorSquare = ({ colour }: { colour: string }) => (
|
||||
<span className={`bg-${colour} inline-block h-12 w-12 mr-4`} />
|
||||
<span className={`bg-${colour} inline-block h-4 w-4 mr-1`} />
|
||||
);
|
||||
|
||||
export const VestingTable = ({
|
||||
@@ -65,17 +65,17 @@ export const VestingTable = ({
|
||||
</KeyValueTable>
|
||||
<div className="flex border-white border">
|
||||
<div
|
||||
className="bg-vega-pink h-16"
|
||||
className="bg-vega-pink h-4"
|
||||
style={{ flex: lockedPercentage.toNumber() }}
|
||||
/>
|
||||
<div
|
||||
className="bg-vega-green h-16"
|
||||
className="bg-vega-green h-4"
|
||||
style={{ flex: vestedPercentage.toNumber() }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-4 mt-4">
|
||||
<div className="flex h-1 mt-1">
|
||||
<div
|
||||
className="bg-vega-yellow h-4"
|
||||
className="bg-vega-yellow h-1"
|
||||
style={{ flex: stakedPercentage.toNumber() }}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import { useContracts } from '../../contexts/contracts/contracts-context';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
||||
|
||||
export const useUserTrancheBalances = (address: string | undefined) => {
|
||||
const [userTrancheBalances, setUserTrancheBalances] = useState<
|
||||
{
|
||||
id: number;
|
||||
locked: BigNumber;
|
||||
vested: BigNumber;
|
||||
}[]
|
||||
>([]);
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { vesting } = useContracts();
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const loadUserTrancheBalances = useCallback(async () => {
|
||||
if (!address) return;
|
||||
const userTranches =
|
||||
tranches?.filter((t) =>
|
||||
t.users.some(
|
||||
(a) => a && address && a.toLowerCase() === address.toLowerCase()
|
||||
)
|
||||
) || [];
|
||||
const trancheIds = [0, ...userTranches.map((t) => t.tranche_id)];
|
||||
const promises = trancheIds.map(async (tId) => {
|
||||
const [t, v] = await Promise.all([
|
||||
vesting.get_tranche_balance(address, tId),
|
||||
vesting.get_vested_for_tranche(address, tId),
|
||||
]);
|
||||
|
||||
const total = toBigNum(t, decimals);
|
||||
const vested = toBigNum(v, decimals);
|
||||
|
||||
return {
|
||||
id: tId,
|
||||
locked: tId === 0 ? total : total.minus(vested),
|
||||
vested: tId === 0 ? new BigNumber(0) : vested,
|
||||
};
|
||||
});
|
||||
|
||||
const trancheBalances = await Promise.all(promises);
|
||||
setUserTrancheBalances(trancheBalances);
|
||||
}, [address, decimals, tranches, vesting]);
|
||||
useEffect(() => {
|
||||
loadUserTrancheBalances();
|
||||
}, [loadUserTrancheBalances]);
|
||||
return userTrancheBalances;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import type { BigNumber } from '../../lib/bignumber';
|
||||
|
||||
export interface TrancheBalance {
|
||||
id: number;
|
||||
locked: BigNumber;
|
||||
vested: BigNumber;
|
||||
}
|
||||
|
||||
export interface RedemptionState {
|
||||
userTranches: Tranche[];
|
||||
}
|
||||
|
||||
export const initialRedemptionState: RedemptionState = {
|
||||
userTranches: [],
|
||||
};
|
||||
|
||||
export enum RedemptionActionType {
|
||||
SET_USER_TRANCHES,
|
||||
}
|
||||
|
||||
export type RedemptionAction = {
|
||||
type: RedemptionActionType.SET_USER_TRANCHES;
|
||||
userTranches: Tranche[];
|
||||
};
|
||||
|
||||
export function redemptionReducer(
|
||||
state: RedemptionState,
|
||||
action: RedemptionAction
|
||||
): RedemptionState {
|
||||
switch (action.type) {
|
||||
case RedemptionActionType.SET_USER_TRANCHES:
|
||||
return {
|
||||
...state,
|
||||
userTranches: action.userTranches,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,62 @@
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Intent,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { EthConnectPrompt } from '../../components/eth-connect-prompt';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { useTranches } from '../../hooks/use-tranches';
|
||||
import { useBalances } from '../../lib/balances/balances-store';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import RoutesConfig from '../routes';
|
||||
import {
|
||||
initialRedemptionState,
|
||||
RedemptionActionType,
|
||||
redemptionReducer,
|
||||
} from './redemption-reducer';
|
||||
|
||||
interface FormFields {
|
||||
address: string;
|
||||
}
|
||||
|
||||
const RedemptionRouter = () => {
|
||||
const { address } = useParams<{ address: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [state, dispatch] = React.useReducer(
|
||||
redemptionReducer,
|
||||
initialRedemptionState
|
||||
);
|
||||
const { trancheBalances } = useBalances();
|
||||
const { account } = useWeb3React();
|
||||
const { tranches, error, loading } = useTranches();
|
||||
|
||||
React.useEffect(() => {
|
||||
const run = (address: string) => {
|
||||
const userTranches = tranches?.filter((t) =>
|
||||
t.users.some(
|
||||
({ address: a }) => a.toLowerCase() === address.toLowerCase()
|
||||
)
|
||||
);
|
||||
|
||||
if (userTranches) {
|
||||
dispatch({
|
||||
type: RedemptionActionType.SET_USER_TRANCHES,
|
||||
userTranches,
|
||||
});
|
||||
const validatePubkey = useCallback(
|
||||
(value: string) => {
|
||||
if (!value.startsWith('0x')) {
|
||||
return t('Address must begin with 0x');
|
||||
} else if (value.length !== 42) {
|
||||
return t('Pubkey must be 42 characters in length');
|
||||
} else if (Number.isNaN(+value)) {
|
||||
return t('Pubkey must be be valid hex');
|
||||
}
|
||||
};
|
||||
return true;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
const { account } = useWeb3React();
|
||||
const { tranches, error, loading } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>();
|
||||
|
||||
if (account) {
|
||||
run(account);
|
||||
}
|
||||
}, [account, tranches]);
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
navigate(`${RoutesConfig.REDEEM}/${fields.address}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -63,30 +74,48 @@ const RedemptionRouter = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
if (!address) {
|
||||
return (
|
||||
<EthConnectPrompt>
|
||||
<p data-testid="eth-connect-prompt">
|
||||
{t(
|
||||
"Use the Ethereum wallet you want to send your tokens to. You'll also need enough Ethereum to pay gas."
|
||||
)}
|
||||
</p>
|
||||
</EthConnectPrompt>
|
||||
<div className="max-w-md">
|
||||
{!account ? (
|
||||
<EthConnectPrompt />
|
||||
) : (
|
||||
<Button
|
||||
fill={true}
|
||||
variant="primary"
|
||||
onClick={() => navigate(`${RoutesConfig.REDEEM}/${account}`)}
|
||||
>
|
||||
{t('View connected Eth Wallet')}
|
||||
</Button>
|
||||
)}
|
||||
<p className="py-4 flex justify-center">{t('OR')}</p>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
data-testid="view-connector-form"
|
||||
>
|
||||
<FormGroup label={'View Ethereum as user:'} labelFor="address">
|
||||
<Input
|
||||
{...register('address', {
|
||||
required: t('Required'),
|
||||
validate: validatePubkey,
|
||||
})}
|
||||
id="address"
|
||||
data-testid="address"
|
||||
type="text"
|
||||
/>
|
||||
{errors.address?.message && (
|
||||
<InputError intent="danger">{errors.address.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button data-testid="connect" type="submit" fill={true}>
|
||||
{t('View Ethereum user')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!trancheBalances.length) {
|
||||
return (
|
||||
<>
|
||||
<Callout>
|
||||
<p>{t('You have no VEGA tokens currently vesting.')}</p>
|
||||
</Callout>
|
||||
<Link to={RoutesConfig.SUPPLY}>{t('viewAllTranches')}</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <Outlet context={{ state, account }} />;
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default RedemptionRouter;
|
||||
|
||||
@@ -33,10 +33,10 @@ export const TrancheItem = ({
|
||||
}: TrancheItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const labelClasses =
|
||||
'inline-block uppercase bg-white text-black py-4 px-8 font-mono';
|
||||
'inline-block uppercase bg-white text-black py-1 px-2 font-mono';
|
||||
|
||||
return (
|
||||
<section data-testid="tranche-item" className="mb-40">
|
||||
<section data-testid="tranche-item" className="mb-8">
|
||||
<div className="flex border-b">
|
||||
{link ? (
|
||||
<Link to={link}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatNumber } from '../../lib/format-number';
|
||||
import Routes from '../routes';
|
||||
import { TrancheItem } from './tranche-item';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
|
||||
export interface TrancheTableProps {
|
||||
tranche: {
|
||||
@@ -22,6 +23,7 @@ export interface TrancheTableProps {
|
||||
totalLocked: BigNumber;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
address: string | null;
|
||||
}
|
||||
|
||||
export const Tranche0Table = ({
|
||||
@@ -65,9 +67,11 @@ export const TrancheTable = ({
|
||||
totalVested,
|
||||
totalLocked,
|
||||
disabled = false,
|
||||
address,
|
||||
}: TrancheTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const total = vested.plus(locked);
|
||||
const { account: connectedAddress } = useWeb3React();
|
||||
const trancheFullyLocked =
|
||||
tranche.tranche_start.getTime() > new Date().getTime();
|
||||
const totalAllTranches = totalVested.plus(totalLocked);
|
||||
@@ -93,13 +97,20 @@ export const TrancheTable = ({
|
||||
amount: reduceAmount,
|
||||
}}
|
||||
components={{
|
||||
stakeLink: <Link to={`/staking`} />,
|
||||
disassociateLink: <Link to={`/staking/disassociate`} />,
|
||||
stakeLink: <Link className="underline" to={Routes.VALIDATORS} />,
|
||||
disassociateLink: (
|
||||
<Link className="underline" to={Routes.DISASSOCIATE} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (!trancheFullyLocked && redeemable) {
|
||||
} else if (
|
||||
!trancheFullyLocked &&
|
||||
redeemable &&
|
||||
connectedAddress &&
|
||||
address === connectedAddress
|
||||
) {
|
||||
message = (
|
||||
<Button onClick={onClick} disabled={disabled}>
|
||||
{t('Redeem unlocked VEGA from tranche {{id}}', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useBalances } from '../../../lib/balances/balances-store';
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useParams, useOutletContext } from 'react-router-dom';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { TransactionCallout } from '../../../components/transaction-callout';
|
||||
import { useContracts } from '../../../contexts/contracts/contracts-context';
|
||||
@@ -9,32 +9,36 @@ import {
|
||||
TransactionActionType,
|
||||
TxState,
|
||||
} from '../../../hooks/transaction-reducer';
|
||||
import { useGetUserTrancheBalances } from '../../../hooks/use-get-user-tranche-balances';
|
||||
import { useRefreshBalances } from '../../../hooks/use-refresh-balances';
|
||||
import { useTransaction } from '../../../hooks/use-transaction';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import Routes from '../../routes';
|
||||
import type { RedemptionState } from '../redemption-reducer';
|
||||
import { TrancheTable } from '../tranche-table';
|
||||
import { useTranches } from '../../../lib/tranches/tranches-store';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { EthConnectPrompt } from '../../../components/eth-connect-prompt';
|
||||
import { useUserTrancheBalances } from '../hooks';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
|
||||
export const RedeemFromTranche = () => {
|
||||
const { state, address } = useOutletContext<{
|
||||
state: RedemptionState;
|
||||
address: string;
|
||||
}>();
|
||||
const { account: address } = useWeb3React();
|
||||
const { vesting } = useContracts();
|
||||
const { t } = useTranslation();
|
||||
const { lien, totalVestedBalance, trancheBalances, totalLockedBalance } =
|
||||
useBalances();
|
||||
const refreshBalances = useRefreshBalances(address);
|
||||
const getUserTrancheBalances = useGetUserTrancheBalances(address, vesting);
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { lien, totalVestedBalance, totalLockedBalance } = useBalances();
|
||||
const refreshBalances = useRefreshBalances(address || '');
|
||||
const { tranches, getTranches } = useTranches((state) => ({
|
||||
tranches: state.tranches,
|
||||
getTranches: state.getTranches,
|
||||
}));
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const numberId = Number(id);
|
||||
const { userTranches } = state;
|
||||
const tranche = React.useMemo(
|
||||
() => userTranches.find(({ tranche_id }) => tranche_id === numberId),
|
||||
[numberId, userTranches]
|
||||
() => tranches?.find(({ tranche_id }) => tranche_id === numberId) || null,
|
||||
[numberId, tranches]
|
||||
);
|
||||
const {
|
||||
state: txState,
|
||||
@@ -42,7 +46,7 @@ export const RedeemFromTranche = () => {
|
||||
dispatch: txDispatch,
|
||||
} = useTransaction(() => vesting.withdraw_from_tranche(numberId));
|
||||
const { token } = useContracts();
|
||||
|
||||
const trancheBalances = useUserTrancheBalances(address || '');
|
||||
const redeemedAmount = React.useMemo(() => {
|
||||
return (
|
||||
trancheBalances.find(({ id: bId }) => bId.toString() === id?.toString())
|
||||
@@ -55,10 +59,10 @@ export const RedeemFromTranche = () => {
|
||||
// If the claim has been committed refetch the new VEGA balance
|
||||
React.useEffect(() => {
|
||||
if (txState.txState === TxState.Complete && address) {
|
||||
getUserTrancheBalances();
|
||||
refreshBalances();
|
||||
getTranches(decimals);
|
||||
}
|
||||
}, [address, getUserTrancheBalances, refreshBalances, txState.txState]);
|
||||
}, [address, decimals, getTranches, refreshBalances, txState.txState]);
|
||||
|
||||
const trancheBalance = React.useMemo(() => {
|
||||
return trancheBalances.find(
|
||||
@@ -66,6 +70,10 @@ export const RedeemFromTranche = () => {
|
||||
);
|
||||
}, [id, trancheBalances]);
|
||||
|
||||
if (!address) {
|
||||
return <EthConnectPrompt />;
|
||||
}
|
||||
|
||||
if (
|
||||
!tranche ||
|
||||
tranche.total_removed.isEqualTo(tranche.total_added) ||
|
||||
@@ -147,6 +155,7 @@ export const RedeemFromTranche = () => {
|
||||
locked={trancheBalance.locked}
|
||||
vested={trancheBalance.vested}
|
||||
onClick={perform}
|
||||
address={address || ''}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -300,12 +300,17 @@ const routerConfig = [
|
||||
element: <LazyRedemption name="Vesting" />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <LazyRedemptionIndex />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <LazyRedemptionTranche />,
|
||||
path: ':address',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <LazyRedemptionIndex />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <LazyRedemptionTranche />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+3
-2
@@ -16,10 +16,11 @@ export const StakingWalletsContainer = ({
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<EthConnectPrompt>
|
||||
<>
|
||||
<p>{t('associateInfo1')}</p>
|
||||
<p>{t('associateInfo2')}</p>
|
||||
</EthConnectPrompt>
|
||||
<EthConnectPrompt />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { sumCirculatingTokens } from './token-details-circulating';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
it('It sums some easy tranches correctly', () => {
|
||||
const tranches: Partial<Tranche>[] = [
|
||||
{ total_added: new BigNumber('100'), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber('100'), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber('100'), locked_amount: new BigNumber(0) },
|
||||
const tranches = [
|
||||
{ total_added: new BigNumber(100), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber(100), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber(100), locked_amount: new BigNumber(0) },
|
||||
];
|
||||
|
||||
const result = sumCirculatingTokens(tranches as Tranche[]);
|
||||
const result = sumCirculatingTokens(tranches);
|
||||
expect(result.toString()).toEqual('300');
|
||||
});
|
||||
|
||||
it('It sums some longer tranches correctly', () => {
|
||||
const tranches: Partial<Tranche>[] = [
|
||||
const tranches = [
|
||||
{
|
||||
total_added: new BigNumber('10000000000'),
|
||||
total_added: new BigNumber(10000000000),
|
||||
locked_amount: new BigNumber(0),
|
||||
},
|
||||
{ total_added: new BigNumber('20'), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber('3000'), locked_amount: new BigNumber(3020) },
|
||||
{ total_added: new BigNumber(20), locked_amount: new BigNumber(0) },
|
||||
{ total_added: new BigNumber(3000), locked_amount: new BigNumber(3020) },
|
||||
];
|
||||
|
||||
const result = sumCirculatingTokens(tranches as Tranche[]);
|
||||
const result = sumCirculatingTokens(tranches);
|
||||
expect(result.toString()).toEqual('10000000000');
|
||||
});
|
||||
|
||||
it('Handles null tranche array', () => {
|
||||
const tranches = null;
|
||||
|
||||
const result = sumCirculatingTokens(tranches as unknown as Tranche[]);
|
||||
const result = sumCirculatingTokens(tranches);
|
||||
expect(result.toString()).toEqual('0');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import type { Tranche } from '../../../lib/tranches/tranches-store';
|
||||
|
||||
/**
|
||||
* Add together the circulating tokens from all tranches
|
||||
@@ -9,7 +9,9 @@ import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
* @param decimals decimal places for the formatted result
|
||||
* @return The total circulating tokens from all tranches
|
||||
*/
|
||||
export function sumCirculatingTokens(tranches: Tranche[] | null): BigNumber {
|
||||
export function sumCirculatingTokens(
|
||||
tranches: { total_added: BigNumber; locked_amount: BigNumber }[] | null
|
||||
): BigNumber {
|
||||
let totalCirculating: BigNumber = new BigNumber(0);
|
||||
|
||||
tranches?.forEach(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranches } from '../../../hooks/use-tranches';
|
||||
import { useTranches } from '../../../lib/tranches/tranches-store';
|
||||
import type { BigNumber } from '../../../lib/bignumber';
|
||||
import { formatNumber } from '../../../lib/format-number';
|
||||
import { TokenDetailsCirculating } from './token-details-circulating';
|
||||
@@ -25,7 +25,11 @@ export const TokenDetails = ({
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { tranches, loading, error } = useTranches();
|
||||
const { tranches, loading, error } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
const { config } = useEthereumConfig();
|
||||
const { token } = useContracts();
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ import { Outlet } from 'react-router-dom';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { useTranches } from '../../hooks/use-tranches';
|
||||
import type { RouteChildProps } from '..';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const TrancheRouter = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { tranches, error, loading } = useTranches();
|
||||
const { tranches, error, loading } = useTranches((state) => ({
|
||||
loading: state.loading,
|
||||
error: state.error,
|
||||
tranches: state.tranches,
|
||||
}));
|
||||
|
||||
if (!tranches || loading) {
|
||||
return (
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import type { Tranche as ITranche } from '@vegaprotocol/smart-contracts';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Link,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { formatNumber } from '@vegaprotocol/react-helpers';
|
||||
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import { formatNumber } from '../../lib/format-number';
|
||||
import { TrancheItem } from '../redemption/tranche-item';
|
||||
import Routes from '../routes';
|
||||
import { TrancheLabel } from './tranche-label';
|
||||
|
||||
const TrancheProgressContents = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="flex justify-between gap-4 font-mono py-2 px-4">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
export const Tranche = () => {
|
||||
const tranches = useOutletContext<ITranche[]>();
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const { trancheId } = useParams<{ trancheId: string }>();
|
||||
const { trancheId } = useParams<{ trancheId: string; address: string }>();
|
||||
const { chainId } = useWeb3React();
|
||||
const tranche = tranches.find(
|
||||
const tranche = tranches?.find(
|
||||
(tranche) => trancheId && parseInt(trancheId) === tranche.tranche_id
|
||||
);
|
||||
|
||||
const lockedData = React.useMemo(() => {
|
||||
if (!tranche) return null;
|
||||
const locked = tranche.locked_amount.div(tranche.total_added);
|
||||
return {
|
||||
locked,
|
||||
unlocked: new BigNumber(1).minus(locked),
|
||||
};
|
||||
}, [tranche]);
|
||||
|
||||
if (!tranche) {
|
||||
return <Navigate to={Routes.NOT_FOUND} />;
|
||||
}
|
||||
@@ -67,33 +51,36 @@ export const Tranche = () => {
|
||||
</div>
|
||||
<h2>{t('Holders')}</h2>
|
||||
{tranche.users.length ? (
|
||||
<ul role="list">
|
||||
{tranche.users.map((user, i) => {
|
||||
const unlocked = user.remaining_tokens.times(
|
||||
lockedData?.unlocked || 0
|
||||
);
|
||||
const locked = user.remaining_tokens.times(lockedData?.locked || 0);
|
||||
return (
|
||||
<li className="pb-4" key={i}>
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/tx/${user.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user.address}
|
||||
</Link>
|
||||
<TrancheProgressContents>
|
||||
<span>{t('Locked')}</span>
|
||||
<span>{t('Unlocked')}</span>
|
||||
</TrancheProgressContents>
|
||||
<TrancheProgressContents>
|
||||
<span>{formatNumber(locked)}</span>
|
||||
<span>{formatNumber(unlocked)}</span>
|
||||
</TrancheProgressContents>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<RoundedWrapper>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
<h1>{t('Ethereum Address')}</h1>
|
||||
<h1>{t('View tranche data')}</h1>
|
||||
</KeyValueTableRow>
|
||||
{tranche.users.map((user) => (
|
||||
<KeyValueTableRow key={user}>
|
||||
{
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/address/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
<RouterLink
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
}
|
||||
</KeyValueTableRow>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
</RoundedWrapper>
|
||||
) : (
|
||||
<p>{t('No users')}</p>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import type { Tranche } from '@vegaprotocol/smart-contracts';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -10,6 +8,8 @@ import { TrancheLabel } from './tranche-label';
|
||||
import { VestingChart } from './vesting-chart';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const trancheMinimum = 10;
|
||||
|
||||
@@ -17,7 +17,7 @@ const shouldShowTranche = (t: Tranche) =>
|
||||
!t.total_added.isLessThanOrEqualTo(trancheMinimum);
|
||||
|
||||
export const Tranches = () => {
|
||||
const tranches = useOutletContext<Tranche[]>();
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const [showAll, setShowAll] = React.useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const { chainId } = useWeb3React();
|
||||
@@ -38,25 +38,24 @@ export const Tranches = () => {
|
||||
<ul role="list">
|
||||
{(showAll ? tranches : filteredTranches).map((tranche) => {
|
||||
return (
|
||||
<React.Fragment key={tranche.tranche_id}>
|
||||
<TrancheItem
|
||||
link={`${tranche.tranche_id}`}
|
||||
tranche={tranche}
|
||||
locked={tranche.locked_amount}
|
||||
unlocked={tranche.total_added.minus(tranche.locked_amount)}
|
||||
total={tranche.total_added}
|
||||
secondaryHeader={
|
||||
<TrancheLabel chainId={chainId} id={tranche.tranche_id} />
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<TrancheItem
|
||||
key={tranche.tranche_id}
|
||||
link={`${tranche.tranche_id}`}
|
||||
tranche={tranche}
|
||||
locked={tranche.locked_amount}
|
||||
unlocked={tranche.total_added.minus(tranche.locked_amount)}
|
||||
total={tranche.total_added}
|
||||
secondaryHeader={
|
||||
<TrancheLabel chainId={chainId} id={tranche.tranche_id} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p>{t('No tranches')}</p>
|
||||
)}
|
||||
<section className="text-center mt-32">
|
||||
<section className="text-center mt-4">
|
||||
<ButtonLink onClick={() => setShowAll(!showAll)}>
|
||||
{showAll
|
||||
? t(
|
||||
|
||||
@@ -22,6 +22,7 @@ module.exports = defineConfig({
|
||||
viewportHeight: 900,
|
||||
responseTimeout: 50000,
|
||||
requestTimeout: 20000,
|
||||
retries: 2,
|
||||
},
|
||||
env: {
|
||||
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
|
||||
|
||||
@@ -72,7 +72,11 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('approve-warning').should(
|
||||
'contain.text',
|
||||
`Deposits of ${btcSymbol} not approved`
|
||||
);
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10');
|
||||
@@ -407,7 +411,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10000');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { removeDecimal } from '@vegaprotocol/cypress';
|
||||
import { ethers } from 'ethers';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
@@ -9,7 +11,7 @@ const formFieldError = 'input-error-text';
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
function openDepositForm() {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockSubscription();
|
||||
cy.mockTradingPage();
|
||||
@@ -20,10 +22,14 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.wait('@Assets');
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
}
|
||||
|
||||
before(() => {
|
||||
openDepositForm();
|
||||
});
|
||||
|
||||
it('handles empty fields', () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
});
|
||||
@@ -44,6 +50,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('invalid amount', () => {
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
// Deposit amount smaller than minimum viable for selected asset
|
||||
// Select an amount so that we have a known decimal places value to work with
|
||||
selectAsset(ASSET_EURO);
|
||||
@@ -56,12 +69,16 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('insufficient funds', () => {
|
||||
// 1001-DEPO-005
|
||||
// Deposit amount is valid, but less than approved. This will always be the case because our
|
||||
// CI wallet wont have approved any assets
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
cy.get(amountField)
|
||||
.clear()
|
||||
.type('100')
|
||||
.type('850')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Insufficient amount in Ethereum wallet');
|
||||
});
|
||||
@@ -88,3 +105,90 @@ describe('deposit actions', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-submit').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
function mockWeb3DepositCalls({
|
||||
allowance,
|
||||
depositLifetimeLimit,
|
||||
balance,
|
||||
deposited,
|
||||
dps,
|
||||
}: {
|
||||
allowance: string;
|
||||
depositLifetimeLimit: string;
|
||||
balance: string;
|
||||
deposited: string;
|
||||
dps: number;
|
||||
}) {
|
||||
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
|
||||
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
|
||||
const toResult = (value: string, dps: number) => {
|
||||
const rawValue = removeDecimal(value, dps);
|
||||
return ethers.utils.hexZeroPad(
|
||||
ethers.utils.hexlify(parseInt(rawValue)),
|
||||
32
|
||||
);
|
||||
};
|
||||
cy.intercept('POST', 'http://localhost:8545', (req) => {
|
||||
// Mock chainId call
|
||||
if (req.body.method === 'eth_chainId') {
|
||||
req.alias = 'eth_chainId';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: '0xaa36a7', // 11155111 for sepolia chain id
|
||||
});
|
||||
}
|
||||
|
||||
// Mock deposited amount
|
||||
if (req.body.method === 'eth_getStorageAt') {
|
||||
req.alias = 'eth_getStorageAt';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(deposited, dps),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.method === 'eth_call') {
|
||||
// Mock approved amount for asset on collateral bridge
|
||||
if (
|
||||
req.body.params[0].to === assetContractAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
|
||||
) {
|
||||
req.alias = 'eth_call_allowance';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(allowance, dps),
|
||||
});
|
||||
}
|
||||
// Mock balance of asset in Ethereum wallet
|
||||
else if (
|
||||
req.body.params[0].to === assetContractAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
|
||||
) {
|
||||
req.alias = 'eth_call_balanceOf';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(balance, dps),
|
||||
});
|
||||
}
|
||||
// Mock deposit lifetime limit
|
||||
else if (
|
||||
req.body.params[0].to === collateralBridgeAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
|
||||
) {
|
||||
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(depositLifetimeLimit, dps),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
validateMarketDataRow(0, 'Mark Price', '0.05749');
|
||||
validateMarketDataRow(1, 'Best Bid Price', '6.81765 ');
|
||||
validateMarketDataRow(2, 'Best Offer Price', '6.81769 ');
|
||||
validateMarketDataRow(3, 'Quote Unit', 'BTC');
|
||||
});
|
||||
|
||||
it('market volume displayed', () => {
|
||||
@@ -168,9 +169,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
it('liquidity price range displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
|
||||
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00%');
|
||||
validateMarketDataRow(1, 'LP Volume Min', '0.05634 tBTC');
|
||||
validateMarketDataRow(2, 'LP Volume Max', '0.05864 tBTC');
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price');
|
||||
validateMarketDataRow(1, 'Lowest Price', '0.05634 BTC');
|
||||
validateMarketDataRow(2, 'Highest Price', '0.05864 BTC');
|
||||
});
|
||||
|
||||
it('oracle displayed', () => {
|
||||
|
||||
@@ -178,9 +178,9 @@ function openMarketDropDown() {
|
||||
if (button.is(':visible')) {
|
||||
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
|
||||
cy.getByTestId(popoverTrigger).click({ force: true });
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
}
|
||||
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
|
||||
cy.getByTestId(popoverTrigger).click({ force: true });
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
|
||||
@@ -17,19 +17,50 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// Mock authentication
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
token: 'test-token',
|
||||
},
|
||||
});
|
||||
// Mock getting keys from wallet
|
||||
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
|
||||
body: {
|
||||
keys: [
|
||||
{
|
||||
algorithm: {
|
||||
name: 'algo',
|
||||
version: 1,
|
||||
},
|
||||
index: 0,
|
||||
meta: [],
|
||||
pub: 'HOSTED_PUBKEY',
|
||||
tainted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
mockConnectWallet();
|
||||
cy.contains('Connect Vega wallet');
|
||||
cy.contains('Hosted Fairground wallet');
|
||||
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(form).find('#wallet').click().type('user');
|
||||
cy.getByTestId(form).find('#passphrase').click().type('pass');
|
||||
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid credentials', () => {
|
||||
// Mock incorrect username/password
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
error: 'No wallet',
|
||||
},
|
||||
statusCode: 403, // 403 forbidden invalid crednetials
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
@@ -40,7 +71,7 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid fields', () => {
|
||||
it('doesnt connect with empty fields', () => {
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
|
||||
@@ -3,12 +3,14 @@ query AccountHistory(
|
||||
$assetId: ID!
|
||||
$accountTypes: [AccountType!]
|
||||
$dateRange: DateRange
|
||||
$marketIds: [ID!]
|
||||
) {
|
||||
balanceChanges(
|
||||
filter: {
|
||||
partyIds: [$partyId]
|
||||
accountTypes: $accountTypes
|
||||
assetId: $assetId
|
||||
marketIds: $marketIds
|
||||
}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AccountHistoryQueryVariables = Types.Exact<{
|
||||
assetId: Types.Scalars['ID'];
|
||||
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -23,9 +24,9 @@ export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: {
|
||||
|
||||
|
||||
export const AccountHistoryDocument = gql`
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange) {
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
|
||||
balanceChanges(
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId}
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
edges {
|
||||
@@ -58,6 +59,7 @@ export const AccountHistoryDocument = gql`
|
||||
* assetId: // value for 'assetId'
|
||||
* accountTypes: // value for 'accountTypes'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* marketIds: // value for 'marketIds'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import compact from 'lodash/compact';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -25,8 +26,10 @@ import {
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { PriceChart } from 'pennant';
|
||||
import 'pennant/dist/style.css';
|
||||
import { accountsOnlyDataProvider } from '@vegaprotocol/accounts';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { accountsDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
|
||||
const DateRange = {
|
||||
RANGE_1D: '1D',
|
||||
@@ -97,7 +100,7 @@ const AccountHistoryManager = ({
|
||||
);
|
||||
|
||||
const { data: accounts } = useDataProvider({
|
||||
dataProvider: accountsOnlyDataProvider,
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: variablesForOneTimeQuery,
|
||||
skip: !pubKey,
|
||||
});
|
||||
@@ -118,6 +121,39 @@ const AccountHistoryManager = ({
|
||||
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
|
||||
DateRange.RANGE_1M
|
||||
);
|
||||
const [market, setMarket] = useState<Market | null>(null);
|
||||
const marketFilterCb = useCallback(
|
||||
(item: Market) =>
|
||||
!asset?.id ||
|
||||
item.tradableInstrument.instrument.product.settlementAsset.id ===
|
||||
asset?.id,
|
||||
[asset?.id]
|
||||
);
|
||||
const markets = useMemo<Market[] | null>(() => {
|
||||
const arr =
|
||||
accounts
|
||||
?.filter((item: Account) => Boolean(item && item.market))
|
||||
.map<Market>((item) => item.market as Market) ?? null;
|
||||
return arr
|
||||
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
|
||||
a.tradableInstrument.instrument.code.localeCompare(
|
||||
b.tradableInstrument.instrument.code
|
||||
)
|
||||
)
|
||||
: null;
|
||||
}, [accounts, marketFilterCb]);
|
||||
const resolveMarket = useCallback(
|
||||
(m: Market) => {
|
||||
setMarket(m);
|
||||
const newAssetId =
|
||||
m.tradableInstrument.instrument.product.settlementAsset.id;
|
||||
const newAsset = assets.find((item) => item.id === newAssetId);
|
||||
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
|
||||
setAsset(newAsset);
|
||||
}
|
||||
},
|
||||
[asset, assets]
|
||||
);
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
@@ -126,62 +162,113 @@ const AccountHistoryManager = ({
|
||||
accountTypes: accountType ? [accountType] : undefined,
|
||||
dateRange:
|
||||
range === 'All' ? undefined : { start: calculateStartDate(range) },
|
||||
marketIds: market?.id ? [market.id] : undefined,
|
||||
}),
|
||||
[pubKey, asset, accountType, range]
|
||||
[pubKey, asset, accountType, range, market?.id]
|
||||
);
|
||||
|
||||
const { data } = useAccountHistoryQuery({
|
||||
variables,
|
||||
skip: !asset || !pubKey,
|
||||
});
|
||||
|
||||
const accountTypeMenu = useMemo(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
onClick={() => setAccountType(type as Schema.AccountType)}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [accountType]);
|
||||
const assetsMenu = useMemo(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
{a.symbol}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [assets, asset]);
|
||||
const marketsMenu = useMemo(() => {
|
||||
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
|
||||
markets?.length ? (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{market
|
||||
? market.tradableInstrument.instrument.code
|
||||
: t('Select market')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{market && (
|
||||
<DropdownMenuItem key="0" onClick={() => setMarket(null)}>
|
||||
{t('All markets')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{markets?.map((m) => (
|
||||
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
|
||||
{m.tradableInstrument.instrument.code}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null;
|
||||
}, [markets, market, accountType, resolveMarket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
|
||||
asset?.id
|
||||
) {
|
||||
setMarket(null);
|
||||
}
|
||||
}, [accountType, asset?.id, market]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col gap-8">
|
||||
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
onClick={() => setAccountType(type as Schema.AccountType)}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
{a.symbol}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<>
|
||||
{accountTypeMenu}
|
||||
{assetsMenu}
|
||||
{marketsMenu}
|
||||
</>
|
||||
</div>
|
||||
<div className="pt-1 justify-items-end">
|
||||
<Toggle
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ReactNode } from 'react';
|
||||
import { AppFailure } from './app-failure';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
|
||||
const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
loading: () => <>Loading...</>,
|
||||
});
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export const MarketLiquiditySupplied = ({
|
||||
{showMessage && (
|
||||
<p className="mt-4">
|
||||
{t(
|
||||
'The market is in an auction because there are no priced limit orders, which are required to deploy liquidity commitment pegged orders. This means the order book is empty on one or both sides.'
|
||||
'The market has sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { Banner } from '../components/banner';
|
||||
import classNames from 'classnames';
|
||||
import { AppLoader } from '../components/app-loader';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -115,7 +115,7 @@ function VegaTradingApp(props: AppProps) {
|
||||
// Prevent HashRouter from being server side rendered as it
|
||||
// relies on presence of document object
|
||||
if (status === 'default') {
|
||||
return null;
|
||||
return <DynamicLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,8 @@ export default function Document() {
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/preloader.css" media="all" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(0) {
|
||||
animation-delay: 0ms;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:first-child {
|
||||
animation-delay: -0.2s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(4) {
|
||||
animation-delay: 1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(5) {
|
||||
animation-delay: -0.25s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(6) {
|
||||
animation-delay: 0.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(7) {
|
||||
animation-delay: -1.05s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(8) {
|
||||
animation-delay: 0.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(9) {
|
||||
animation-delay: -0.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(10) {
|
||||
animation-delay: 0.5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(11) {
|
||||
animation-delay: -2.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(12) {
|
||||
animation-delay: 2.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(13) {
|
||||
animation-delay: -0.65s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(14) {
|
||||
animation-delay: 0.7s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(15) {
|
||||
animation-delay: -3s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(16) {
|
||||
animation-delay: 2.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader .pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #000;
|
||||
animation: flickering 0.4s steps(2, jump-none) infinite alternate;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ export const AssetDetailsDialog = ({
|
||||
const { data } = useAssetsDataProvider();
|
||||
|
||||
const asset = data?.find((a) => a.id === assetId);
|
||||
const assetSymbol = asset?.symbol || '';
|
||||
|
||||
const content = asset ? (
|
||||
<div className="my-2">
|
||||
@@ -97,6 +98,12 @@ export const AssetDetailsDialog = ({
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
<p className="text-sm mb-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
|
||||
[assetSymbol]
|
||||
)}
|
||||
</p>
|
||||
<div className="w-1/4">
|
||||
<Button
|
||||
data-testid="close-asset-details-dialog"
|
||||
|
||||
@@ -48,7 +48,7 @@ export const DealTicketEstimates = ({
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Position Size')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={notionalSize}
|
||||
description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT}
|
||||
description={constants.NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName || '')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -66,7 +66,7 @@ export const DealTicketEstimates = ({
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Margin')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={estMargin}
|
||||
description={constants.EST_MARGIN_TOOLTIP_TEXT}
|
||||
description={constants.EST_MARGIN_TOOLTIP_TEXT(quoteName || '')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -75,7 +75,7 @@ export const DealTicketEstimates = ({
|
||||
<DataTitle quoteName={quoteName}>{t('Est. Close out')}</DataTitle>
|
||||
<ValueTooltipRow
|
||||
value={estCloseOut}
|
||||
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT}
|
||||
description={constants.EST_CLOSEOUT_TOOLTIP_TEXT(quoteName || '')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -318,33 +318,37 @@ const SummaryMessage = memo(
|
||||
}
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
}}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||
return (
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -363,7 +367,11 @@ const SummaryMessage = memo(
|
||||
// If there is no blocking error but user doesn't have enough
|
||||
// balance render the margin warning, but still allow submission
|
||||
if (balanceError) {
|
||||
return <MarginWarning balance={balance} margin={margin} asset={asset} />;
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<MarginWarning balance={balance} margin={margin} asset={asset} />;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auction mode warning
|
||||
@@ -375,13 +383,15 @@ const SummaryMessage = memo(
|
||||
].includes(marketData.marketTradingMode)
|
||||
) {
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { createDocsLinks } from '@vegaprotocol/react-helpers';
|
||||
import { compileGridData } from './compile-grid-data';
|
||||
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
type TradingModeTooltipProps = {
|
||||
marketId?: string;
|
||||
@@ -115,13 +116,21 @@ export const TradingModeTooltip = ({
|
||||
case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: {
|
||||
switch (trigger) {
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: {
|
||||
const notEnoughLiquidity = new BigNumber(
|
||||
marketData.suppliedStake || 0
|
||||
).isLessThan(marketData.targetStake || 0);
|
||||
return (
|
||||
<section data-testid="trading-mode-tooltip">
|
||||
<p className={classNames({ 'mb-4': Boolean(compiledGrid) })}>
|
||||
<span>
|
||||
{t(
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)}
|
||||
<span className="mb-2">
|
||||
{notEnoughLiquidity &&
|
||||
t(
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)}
|
||||
{!notEnoughLiquidity &&
|
||||
t(
|
||||
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
|
||||
)}
|
||||
</span>{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const EST_MARGIN_TOOLTIP_TEXT = t(
|
||||
'When opening a position on a futures market, you must post margin to cover any potential losses that you may incur. The margin is typically a fraction of the notional position size. For example, for a notional position size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.'
|
||||
);
|
||||
export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
`A fraction of the notional position size, in the market's settlement asset %s, to cover any potential losses that you may incur.
|
||||
|
||||
For example, for a notional size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.`,
|
||||
[settlementAsset]
|
||||
);
|
||||
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
|
||||
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'
|
||||
);
|
||||
export const EST_CLOSEOUT_TOOLTIP_TEXT = t(
|
||||
'Because you only need to post a fraction of your position size as margin when trading futures, it is possible to obtain leverage meaning your notional position size exceeds your account balance. In this scenario, if the market moves against your position, it will sometimes be necessary to force close your position due to insufficient funds. The estimated close out tells you the price at which that would happen based on current position and account balance.'
|
||||
);
|
||||
export const NOTIONAL_SIZE_TOOLTIP_TEXT = t(
|
||||
'The notional size represents the position size in the settlement asset of the futures contract. The notional size is calculated by multiplying the number of contracts by the price of the contract. For example, ten contracts traded at a price of $50 has a notional size of $500.'
|
||||
);
|
||||
export const EST_CLOSEOUT_TOOLTIP_TEXT = (quote: string) =>
|
||||
t(
|
||||
`If the price drops below this number, measured in the market price quote unit %s, you will be closed out, based on your current position and account balance.`,
|
||||
[quote]
|
||||
);
|
||||
export const NOTIONAL_SIZE_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
`The notional size represents the position size in the settlement asset %s of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract.
|
||||
|
||||
For example 10 contracts traded at a price of $50 has a notional size of $500.`,
|
||||
[settlementAsset]
|
||||
);
|
||||
export const EST_FEES_TOOLTIP_TEXT = t(
|
||||
'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.'
|
||||
);
|
||||
|
||||
@@ -72,13 +72,13 @@ export const useFeeDealTicketDetails = (
|
||||
return null;
|
||||
}, [derivedPrice, order.size, market.decimalPlaces]);
|
||||
|
||||
const symbol =
|
||||
const assetSymbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
market,
|
||||
symbol,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
estMargin,
|
||||
estCloseOut,
|
||||
@@ -87,7 +87,7 @@ export const useFeeDealTicketDetails = (
|
||||
};
|
||||
}, [
|
||||
market,
|
||||
symbol,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
estMargin,
|
||||
estCloseOut,
|
||||
@@ -98,7 +98,7 @@ export const useFeeDealTicketDetails = (
|
||||
|
||||
export interface FeeDetails {
|
||||
market: Market;
|
||||
symbol: string;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
estMargin: OrderMargin | null;
|
||||
estCloseOut: string | null;
|
||||
@@ -106,7 +106,7 @@ export interface FeeDetails {
|
||||
}
|
||||
|
||||
export const getFeeDetailsValues = ({
|
||||
symbol,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
estMargin,
|
||||
estCloseOut,
|
||||
@@ -114,6 +114,7 @@ export const getFeeDetailsValues = ({
|
||||
}: FeeDetails) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const formatValueWithMarketDp = (
|
||||
value: string | number | null | undefined
|
||||
): string => {
|
||||
@@ -132,8 +133,8 @@ export const getFeeDetailsValues = ({
|
||||
{
|
||||
label: t('Notional'),
|
||||
value: formatValueWithMarketDp(notionalSize),
|
||||
symbol,
|
||||
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
{
|
||||
label: t('Fees'),
|
||||
@@ -144,31 +145,31 @@ export const getFeeDetailsValues = ({
|
||||
<>
|
||||
<span>
|
||||
{t(
|
||||
'The most you would be expected to pay in fees, the actual amount may vary.'
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||
)}
|
||||
</span>
|
||||
<FeesBreakdown
|
||||
fees={estMargin?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={symbol}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
symbol,
|
||||
symbol: assetSymbol,
|
||||
},
|
||||
{
|
||||
label: t('Margin'),
|
||||
value:
|
||||
estMargin?.margin && `~${formatValueWithAssetDp(estMargin?.margin)}`,
|
||||
symbol,
|
||||
labelDescription: EST_MARGIN_TOOLTIP_TEXT,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_MARGIN_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
{
|
||||
label: t('Liquidation'),
|
||||
value: estCloseOut && `~${formatValueWithMarketDp(estCloseOut)}`,
|
||||
symbol: market.tradableInstrument.instrument.product.quoteName,
|
||||
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT,
|
||||
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT(quoteName),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -4,10 +4,12 @@ import type { DepositFormProps } from './deposit-form';
|
||||
import { DepositForm } from './deposit-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@vegaprotocol/web3');
|
||||
jest.mock('@web3-react/core');
|
||||
|
||||
const mockConnector = { deactivate: jest.fn() };
|
||||
@@ -37,6 +39,8 @@ function generateAsset(): AssetFieldsFragment {
|
||||
let asset: AssetFieldsFragment;
|
||||
let props: DepositFormProps;
|
||||
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
const MOCK_VEGA_KEY =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
|
||||
beforeEach(() => {
|
||||
asset = generateAsset();
|
||||
@@ -89,14 +93,17 @@ describe('Deposit form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when submitted with invalid ethereum address', async () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' });
|
||||
it('fails when Ethereum wallet not connected', async () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: false,
|
||||
account: '',
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Invalid Ethereum address')
|
||||
await screen.findByText('Connect Ethereum wallet')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -138,7 +145,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in Ethereum wallet')
|
||||
await screen.findByText('Amount is above deposit limit')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -159,7 +166,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above approved amount')
|
||||
await screen.findByText('Amount is above approved amount.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -193,9 +200,9 @@ describe('Deposit form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles deposit approvals', () => {
|
||||
it('handles deposit approvals', async () => {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ pubKey: null });
|
||||
mockUseVegaWallet.mockReturnValue({ pubKey: MOCK_VEGA_KEY });
|
||||
|
||||
const mockUseWeb3React = useWeb3React as jest.Mock;
|
||||
mockUseWeb3React.mockReturnValue({
|
||||
@@ -212,13 +219,18 @@ describe('Deposit form', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByText(`Approve ${asset.symbol}`, {
|
||||
selector: '[type="button"]',
|
||||
})
|
||||
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
|
||||
`Deposits of ${asset.symbol} not approved`
|
||||
);
|
||||
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: `Approve ${asset.symbol}` })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles submitting a deposit', async () => {
|
||||
@@ -284,4 +296,55 @@ describe('Deposit form', () => {
|
||||
render(<DepositForm {...props} />);
|
||||
expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders a connect button if Ethereum wallet is not connected', () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: false,
|
||||
account: '',
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('From (Ethereum address)')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a disabled input if Ethereum wallet is connected', () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: true,
|
||||
account: MOCK_ETH_ADDRESS,
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Connect' })
|
||||
).not.toBeInTheDocument();
|
||||
const fromInput = screen.getByLabelText('From (Ethereum address)');
|
||||
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
|
||||
expect(fromInput).toBeDisabled();
|
||||
expect(fromInput).toHaveAttribute('readonly');
|
||||
});
|
||||
|
||||
it('prevents submission if you are on the wrong chain', () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: true,
|
||||
account: MOCK_ETH_ADDRESS,
|
||||
chainId: 1,
|
||||
});
|
||||
(useWeb3ConnectStore as unknown as jest.Mock).mockImplementation(
|
||||
// eslint-disable-next-line
|
||||
(selector: (result: ReturnType<typeof useWeb3ConnectStore>) => any) => {
|
||||
return selector({
|
||||
desiredChainId: 11155111,
|
||||
open: jest.fn(),
|
||||
foo: 'asdf',
|
||||
});
|
||||
}
|
||||
);
|
||||
render(<DepositForm {...props} />);
|
||||
expect(screen.getByTestId('chain-error')).toHaveTextContent(
|
||||
/this app only works on/i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { AssetOption } from '@vegaprotocol/assets';
|
||||
import {
|
||||
ethereumAddress,
|
||||
t,
|
||||
ethereumAddress,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
minSafe,
|
||||
@@ -14,17 +14,19 @@ import {
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
InputError,
|
||||
RichSelect,
|
||||
Notification,
|
||||
Intent,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import type { FieldError } from 'react-hook-form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { DepositLimits } from './deposit-limits';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import {
|
||||
@@ -72,7 +74,8 @@ export const DepositForm = ({
|
||||
isFaucetable,
|
||||
}: DepositFormProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const { account } = useWeb3React();
|
||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||
const { isActive, account } = useWeb3React();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
@@ -83,26 +86,27 @@ export const DepositForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
from: account,
|
||||
to: pubKey ? pubKey : undefined,
|
||||
asset: selectedAsset?.id || '',
|
||||
},
|
||||
});
|
||||
|
||||
const onDeposit = async (fields: FormFields) => {
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
||||
throw new Error('Invalid asset');
|
||||
}
|
||||
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
if (approved) {
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
} else {
|
||||
submitApprove();
|
||||
}
|
||||
};
|
||||
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
const maxApproved = allowance ? allowance : new BigNumber(0);
|
||||
const maxAvailable = balance ? balance : new BigNumber(0);
|
||||
@@ -133,9 +137,12 @@ export const DepositForm = ({
|
||||
return minViableAmount;
|
||||
}, [selectedAsset]);
|
||||
|
||||
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
|
||||
const formState = getFormState(selectedAsset, isActive, approved);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onDeposit)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
noValidate={true}
|
||||
data-testid="deposit-form"
|
||||
>
|
||||
@@ -143,19 +150,54 @@ export const DepositForm = ({
|
||||
label={t('From (Ethereum address)')}
|
||||
labelFor="ethereum-address"
|
||||
>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
{...register('from', {
|
||||
<Controller
|
||||
name="from"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
required: (value) => {
|
||||
if (!value) return t('Connect Ethereum wallet');
|
||||
return true;
|
||||
},
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<EthereumButton
|
||||
clearAddress={() => {
|
||||
setValue('from', '');
|
||||
clearErrors('from');
|
||||
}}
|
||||
defaultValue={account}
|
||||
render={() => {
|
||||
if (isActive && account) {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
value={account}
|
||||
readOnly={true}
|
||||
disabled={true}
|
||||
{...register('from', {
|
||||
validate: {
|
||||
required,
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<DisconnectEthereumButton
|
||||
onDisconnect={() => {
|
||||
setValue('from', ''); // clear from value so required ethereum connection validation works
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={openDialog}
|
||||
variant="primary"
|
||||
fill={true}
|
||||
type="button"
|
||||
data-testid="connect-eth-wallet-btn"
|
||||
>
|
||||
{t('Connect')}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.from?.message && (
|
||||
@@ -241,148 +283,136 @@ export const DepositForm = ({
|
||||
deposited={deposited}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
allowance={allowance}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
} else if (value.isGreaterThan(maxAmount.limit)) {
|
||||
return t('Amount is above temporary deposit limit');
|
||||
} else if (value.isGreaterThan(maxAmount.approved)) {
|
||||
return t('Amount is above approved amount');
|
||||
}
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
{formState === 'deposit' && (
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
approved: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.approved)) {
|
||||
return t('Amount is above approved amount');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
limit: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.limit)) {
|
||||
return t('Amount is above deposit limit');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
balance: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
},
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormButton
|
||||
selectedAsset={selectedAsset}
|
||||
amount={new BigNumber(amount || 0)}
|
||||
allowance={allowance}
|
||||
onApproveClick={submitApprove}
|
||||
/>
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<AmountError error={errors.amount} submitApprove={submitApprove} />
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormButton selectedAsset={selectedAsset} formState={formState} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const AmountError = ({
|
||||
error,
|
||||
submitApprove,
|
||||
}: {
|
||||
error: FieldError;
|
||||
submitApprove: () => void;
|
||||
}) => {
|
||||
if (error.type === 'approved') {
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}.
|
||||
<button onClick={submitApprove} className="underline ml-2">
|
||||
{t('Update approve amount')}
|
||||
</button>
|
||||
</InputError>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}
|
||||
</InputError>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormButtonProps {
|
||||
selectedAsset?: Asset;
|
||||
amount: BigNumber;
|
||||
allowance: BigNumber | undefined;
|
||||
onApproveClick: () => void;
|
||||
formState: ReturnType<typeof getFormState>;
|
||||
}
|
||||
|
||||
const FormButton = ({
|
||||
selectedAsset,
|
||||
amount,
|
||||
allowance,
|
||||
onApproveClick,
|
||||
}: FormButtonProps) => {
|
||||
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
|
||||
open: store.open,
|
||||
desiredChainId: store.desiredChainId,
|
||||
}));
|
||||
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
||||
const { isActive, chainId } = useWeb3React();
|
||||
const approved =
|
||||
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
|
||||
let button = null;
|
||||
let message: ReactNode = '';
|
||||
|
||||
if (!isActive) {
|
||||
button = (
|
||||
<Button onClick={open} data-testid="connect-eth-wallet-btn">
|
||||
{t('Connect Ethereum wallet')}
|
||||
</Button>
|
||||
);
|
||||
} else if (chainId !== desiredChainId) {
|
||||
const chainName = getChainName(desiredChainId);
|
||||
message = t(`This app only works on ${chainName}.`);
|
||||
button = (
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
disabled={true}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else if (!selectedAsset) {
|
||||
button = (
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else if (approved) {
|
||||
message = (
|
||||
<>
|
||||
<Icon name="tick" className="mr-2" />
|
||||
<span>{t('Approved')}</span>
|
||||
</>
|
||||
);
|
||||
button = (
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
message = t(`Deposits of ${selectedAsset.symbol} not approved`);
|
||||
button = (
|
||||
<Button
|
||||
onClick={onApproveClick}
|
||||
data-testid="deposit-approve-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
>
|
||||
{t(`Approve ${selectedAsset.symbol}`)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||
const submitText =
|
||||
formState === 'approve'
|
||||
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
|
||||
: t('Deposit');
|
||||
const invalidChain = isActive && chainId !== desiredChainId;
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{message && <p className="text-center">{message}</p>}
|
||||
{button}
|
||||
</div>
|
||||
<>
|
||||
{formState === 'approve' && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-warning"
|
||||
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{invalidChain && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="chain-error"
|
||||
message={t(
|
||||
`This app only works on ${getChainName(desiredChainId)}.`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant={isActive ? 'primary' : 'default'}
|
||||
fill={true}
|
||||
disabled={invalidChain}
|
||||
>
|
||||
{submitText}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -398,21 +428,20 @@ const UseButton = (props: UseButtonProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
|
||||
const openDialog = useWeb3ConnectStore((state) => state.open);
|
||||
const { isActive, connector } = useWeb3React();
|
||||
const DisconnectEthereumButton = ({
|
||||
onDisconnect,
|
||||
}: {
|
||||
onDisconnect: () => void;
|
||||
}) => {
|
||||
const { connector } = useWeb3React();
|
||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||
|
||||
if (!isActive) {
|
||||
return <UseButton onClick={openDialog}>{t('Connect')}</UseButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
connector.deactivate();
|
||||
clearAddress();
|
||||
removeEagerConnector();
|
||||
onDisconnect();
|
||||
}}
|
||||
data-testid="disconnect-ethereum-wallet"
|
||||
>
|
||||
@@ -420,3 +449,14 @@ const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
|
||||
</UseButton>
|
||||
);
|
||||
};
|
||||
|
||||
const getFormState = (
|
||||
selectedAsset: Asset | undefined,
|
||||
isActive: boolean,
|
||||
approved: boolean
|
||||
) => {
|
||||
if (!selectedAsset) return 'deposit';
|
||||
if (!isActive) return 'deposit';
|
||||
if (approved) return 'deposit';
|
||||
return 'approve';
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ interface DepositLimitsProps {
|
||||
deposited: BigNumber;
|
||||
asset: Asset;
|
||||
balance?: BigNumber;
|
||||
allowance?: BigNumber;
|
||||
}
|
||||
|
||||
export const DepositLimits = ({
|
||||
@@ -18,6 +19,7 @@ export const DepositLimits = ({
|
||||
deposited,
|
||||
asset,
|
||||
balance,
|
||||
allowance,
|
||||
}: DepositLimitsProps) => {
|
||||
const limits = [
|
||||
{
|
||||
@@ -44,6 +46,12 @@ export const DepositLimits = ({
|
||||
rawValue: max.minus(deposited),
|
||||
value: compactNumber(max.minus(deposited), asset.decimals),
|
||||
},
|
||||
{
|
||||
key: 'ALLOWANCE',
|
||||
label: t('Approved'),
|
||||
rawValue: allowance,
|
||||
value: allowance ? compactNumber(allowance, asset.decimals) : '-',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -45,8 +45,9 @@ export const FillsManager = ({
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
noRowsOverlayComponent={() => null}
|
||||
onMarketClick={onMarketClick}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -27,12 +27,13 @@ export const AssetProposalNotification = ({
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="asset-proposal-notification"
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="asset-proposal-notification"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ export const MarketProposalNotification = ({
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="market-proposal-notification"
|
||||
className="px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,8 @@ import type { Asset } from '@vegaprotocol/assets';
|
||||
import { assetsProvider } from '@vegaprotocol/assets';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
import type { PageInfo } from '@vegaprotocol/react-helpers';
|
||||
import { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
defaultAppend as append,
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
useDataProvider,
|
||||
@@ -27,13 +25,15 @@ import type {
|
||||
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
|
||||
|
||||
export type LedgerEntry = LedgerEntryFragment & {
|
||||
id: number;
|
||||
asset: Asset | null | undefined;
|
||||
marketSender: Market | null | undefined;
|
||||
marketReceiver: Market | null | undefined;
|
||||
};
|
||||
|
||||
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
||||
export type AggregatedLedgerEntriesNode = AggregatedLedgerEntriesEdge & {
|
||||
node: LedgerEntry;
|
||||
};
|
||||
|
||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||
return responseData?.ledgerEntries?.edges || [];
|
||||
@@ -94,33 +94,29 @@ export const update = (
|
||||
});
|
||||
};
|
||||
|
||||
const getPageInfo = (responseData: LedgerEntriesQuery): PageInfo | null =>
|
||||
responseData.ledgerEntries?.pageInfo || null;
|
||||
|
||||
const ledgerEntriesOnlyProvider = makeDataProvider({
|
||||
query: LedgerEntriesDocument,
|
||||
getData,
|
||||
getDelta: getData,
|
||||
update,
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 100,
|
||||
},
|
||||
additionalContext: {
|
||||
isEnlargedTimeout: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const ledgerEntriesProvider = makeDerivedDataProvider<
|
||||
(AggregatedLedgerEntriesEdge | null)[],
|
||||
AggregatedLedgerEntriesEdge[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
LedgerEntriesQueryVariables
|
||||
>(
|
||||
[ledgerEntriesOnlyProvider, assetsProvider, marketsProvider],
|
||||
[
|
||||
ledgerEntriesOnlyProvider,
|
||||
(callback, client) => assetsProvider(callback, client),
|
||||
marketsProvider,
|
||||
],
|
||||
([entries, assets, markets]) => {
|
||||
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
|
||||
const entry = edge?.node;
|
||||
const entry = edge.node;
|
||||
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
|
||||
const marketSender = markets.find(
|
||||
(market: Market) => market.id === entry.fromAccountMarketId
|
||||
@@ -148,21 +144,22 @@ export const useLedgerEntriesDataProvider = ({
|
||||
filter,
|
||||
gridRef,
|
||||
}: Props) => {
|
||||
const dataRef = useRef<(AggregatedLedgerEntriesEdge | null)[] | null>(null);
|
||||
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
|
||||
const totalCountRef = useRef<number>();
|
||||
|
||||
const variables = useMemo<LedgerEntriesQueryVariables>(
|
||||
() => ({
|
||||
partyId,
|
||||
dateRange: filter?.vegaTime?.value,
|
||||
fromAccountType: filter?.fromAccountType?.value ?? null,
|
||||
toAccountType: filter?.toAccountType?.value ?? null,
|
||||
pagination: {
|
||||
first: 5000,
|
||||
},
|
||||
}),
|
||||
[partyId, filter]
|
||||
[partyId, filter?.vegaTime?.value]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: (AggregatedLedgerEntriesEdge | null)[] | null }) => {
|
||||
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
},
|
||||
[gridRef]
|
||||
@@ -173,7 +170,7 @@ export const useLedgerEntriesDataProvider = ({
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
data: (AggregatedLedgerEntriesEdge | null)[] | null;
|
||||
data: AggregatedLedgerEntriesEdge[] | null;
|
||||
totalCount?: number;
|
||||
}) => {
|
||||
totalCountRef.current = totalCount;
|
||||
|
||||
@@ -3,7 +3,9 @@ import type * as Schema from '@vegaprotocol/types';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { subDays, formatRFC3339 } from 'date-fns';
|
||||
import type { AggregatedLedgerEntriesNode } from './ledger-entries-data-provider';
|
||||
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
|
||||
import { LedgerTable } from './ledger-table';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
@@ -15,43 +17,43 @@ export interface Filter {
|
||||
fromAccountType?: { value: Types.AccountType[] };
|
||||
toAccountType?: { value: Types.AccountType[] };
|
||||
}
|
||||
|
||||
type LedgerManagerProps = { partyId: string };
|
||||
export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
const defaultFilter = {
|
||||
vegaTime: {
|
||||
value: { start: formatRFC3339(subDays(Date.now(), 7)) },
|
||||
},
|
||||
};
|
||||
export const LedgerManager = ({ partyId }: { partyId: string }) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [filter, setFilter] = useState<Filter | undefined>();
|
||||
const [filter, setFilter] = useState<Filter>(defaultFilter);
|
||||
const [dataCount, setDataCount] = useState(0);
|
||||
|
||||
const { data, error, loading, getRows, reload } =
|
||||
useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
const { data, error, loading, reload } = useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
const updatedFilter = event.api.getFilterModel();
|
||||
if (Object.keys(updatedFilter).length) {
|
||||
setFilter(updatedFilter);
|
||||
} else if (filter) {
|
||||
setFilter(undefined);
|
||||
}
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
const getRowId = useCallback(
|
||||
({ data }: { data: Types.AggregatedLedgerEntry }) =>
|
||||
`${data.vegaTime}-${data.fromAccountPartyId}-${data.toAccountPartyId}`,
|
||||
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
|
||||
const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() };
|
||||
setFilter(updatedFilter);
|
||||
}, []);
|
||||
const extractNodesDecorator = useCallback(
|
||||
(data: AggregatedLedgerEntriesNode[] | null, loading: boolean) =>
|
||||
data && !loading ? data.map((item) => item.node) : null,
|
||||
[]
|
||||
);
|
||||
|
||||
const extractedData = extractNodesDecorator(data, loading);
|
||||
useEffect(() => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, [extractedData]);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<LedgerTable
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
rowData={extractedData}
|
||||
onFilterChanged={onFilterChanged}
|
||||
getRowId={getRowId}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -59,7 +61,7 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No entries')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataCondition={() => !dataCount}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@vegaprotocol/types';
|
||||
import type { LedgerEntry } from './ledger-entries-data-provider';
|
||||
import { forwardRef } from 'react';
|
||||
import { formatRFC3339, subDays } from 'date-fns';
|
||||
|
||||
export const TransferTooltipCellComponent = ({
|
||||
value,
|
||||
@@ -35,6 +36,11 @@ export const TransferTooltipCellComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) };
|
||||
const dateRangeFilterParams = {
|
||||
maxNextDays: 0,
|
||||
defaultRangeFilter,
|
||||
};
|
||||
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
|
||||
|
||||
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
@@ -42,17 +48,20 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No entries')}
|
||||
ref={ref}
|
||||
getRowId={({ data }) => data.id}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
tooltipComponent: TransferTooltipCellComponent,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
filterParams: {
|
||||
...dateRangeFilterParams,
|
||||
buttons: ['reset'],
|
||||
},
|
||||
}}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
@@ -201,6 +210,7 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
|
||||
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-'
|
||||
}
|
||||
filterParams={dateRangeFilterParams}
|
||||
filter={DateRangeFilter}
|
||||
/>
|
||||
</AgGrid>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
PriceCell,
|
||||
Vol,
|
||||
VolCell,
|
||||
CumulativeVol,
|
||||
addDecimalsFormatNumber,
|
||||
VolumeType,
|
||||
addDecimal,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface OrderbookRowProps {
|
||||
@@ -42,17 +41,17 @@ export const OrderbookRow = React.memo(
|
||||
}: OrderbookRowProps) => {
|
||||
return (
|
||||
<>
|
||||
<Vol
|
||||
<VolCell
|
||||
testId={`bid-vol-${price}`}
|
||||
value={bid}
|
||||
valueFormatted={addDecimal(bid, positionDecimalPlaces)}
|
||||
valueFormatted={addDecimalsFormatNumber(bid, positionDecimalPlaces)}
|
||||
relativeValue={relativeBid}
|
||||
type={VolumeType.bid}
|
||||
/>
|
||||
<Vol
|
||||
<VolCell
|
||||
testId={`ask-vol-${price}`}
|
||||
value={ask}
|
||||
valueFormatted={addDecimal(ask, positionDecimalPlaces)}
|
||||
valueFormatted={addDecimalsFormatNumber(ask, positionDecimalPlaces)}
|
||||
relativeValue={relativeAsk}
|
||||
type={VolumeType.ask}
|
||||
/>
|
||||
@@ -70,7 +69,6 @@ export const OrderbookRow = React.memo(
|
||||
relativeAsk={cumulativeRelativeAsk}
|
||||
relativeBid={cumulativeRelativeBid}
|
||||
indicativeVolume={indicativeVolume}
|
||||
className="pr-4 text-black dark:text-white"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -201,26 +201,8 @@ const OrderbookDebugInfo = ({
|
||||
midPrice?: string;
|
||||
}) => (
|
||||
<Fragment>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '0',
|
||||
borderTop: '1px solid rgba(255,0,0,0.5)',
|
||||
background: 'black',
|
||||
width: '100%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="absolute left-0 bottom-0 font-mono"
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#FFF',
|
||||
background: '#000',
|
||||
padding: '2px',
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-1/2 left-0 border-t border-t-black w-full" />
|
||||
<div className="text-xs p-2 bg-black/80 text-white absolute left-0 bottom-6 font-mono">
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
@@ -493,12 +475,7 @@ export const Orderbook = ({
|
||||
|
||||
const tableBody =
|
||||
data && data.length !== 0 ? (
|
||||
<div
|
||||
className="grid grid-cols-4 gap-[0.3125rem] text-right"
|
||||
style={{
|
||||
gridAutoRows: '17px',
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-4 gap-1 text-right auto-rows-[17px]">
|
||||
{data.map((data, i) => (
|
||||
<OrderbookRow
|
||||
key={data.price}
|
||||
@@ -559,14 +536,13 @@ export const Orderbook = ({
|
||||
onDoubleClick={() => setDebug(!debug)}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 grid grid-cols-4 gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full"
|
||||
style={{ gridAutoRows: '17px' }}
|
||||
className="absolute top-0 grid grid-cols-4 auto-rows-[17px] gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full"
|
||||
ref={headerElement}
|
||||
>
|
||||
<div>{t('Bid vol')}</div>
|
||||
<div>{t('Ask vol')}</div>
|
||||
<div>{t('Price')}</div>
|
||||
<div className="pr-[2px] whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<div className="pr-1 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{t('Cumulative vol')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -605,8 +581,7 @@ export const Orderbook = ({
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t-[1px] border-default mt-2 z-10 bg-white dark:bg-black w-full"
|
||||
style={{ gridAutoRows: '20px' }}
|
||||
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t border-default mt-2 z-10 bg-white dark:bg-black w-full"
|
||||
ref={footerElement}
|
||||
>
|
||||
<div className="col-span-2">
|
||||
|
||||
@@ -74,7 +74,7 @@ export const FeesBreakdown = ({
|
||||
: '-';
|
||||
};
|
||||
return (
|
||||
<dl className="grid grid-cols-5">
|
||||
<dl className="grid grid-cols-6">
|
||||
<dt className="col-span-2">{t('Infrastructure fee')}</dt>
|
||||
{feeFactors && (
|
||||
<dd className="text-right col-span-1">
|
||||
@@ -83,7 +83,7 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.infrastructureFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Liquidity fee')}</dt>
|
||||
@@ -94,7 +94,7 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.liquidityFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Maker fee')}</dt>
|
||||
@@ -105,7 +105,7 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.makerFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Total fees')}</dt>
|
||||
@@ -114,7 +114,7 @@ export const FeesBreakdown = ({
|
||||
{totalFeesPercentage(feeFactors)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(totalFees)} {symbol || ''}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@@ -93,7 +93,9 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
const headerClassName = 'uppercase text-lg';
|
||||
const dayVolume = calcCandleVolume(market);
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
@@ -129,16 +131,27 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
{
|
||||
title: t('Market price'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={pick(
|
||||
market.data,
|
||||
'name',
|
||||
'markPrice',
|
||||
'bestBidPrice',
|
||||
'bestOfferPrice'
|
||||
)}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
/>
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
...pick(
|
||||
market.data,
|
||||
'name',
|
||||
'markPrice',
|
||||
'bestBidPrice',
|
||||
'bestOfferPrice'
|
||||
),
|
||||
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
/>
|
||||
<p className="text-xs">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -189,6 +202,10 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const marketSpecPanels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
@@ -224,13 +241,21 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: asset ? (
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={true}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
<>
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={true}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
<p className="text-xs">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
),
|
||||
@@ -342,31 +367,40 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
),
|
||||
LPVolumeMin:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
LPVolumeMax:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders count towards a commitment they have to be
|
||||
within either the liquidity or price monitoring bounds (whichever is
|
||||
tighter).`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ export const tooltipMapping: Record<string, ReactNode> = {
|
||||
markPrice: t(
|
||||
'A concept derived from traditional markets. It is a calculated value for the ‘current market price’ on a market.'
|
||||
),
|
||||
quoteUnit: t(
|
||||
`The underlying that is being priced by the market, described by the market's oracle.`
|
||||
),
|
||||
openInterest: t(
|
||||
'The volume of all open positions in a given market (the sum of the size of all positions greater than 0).'
|
||||
),
|
||||
@@ -98,7 +101,4 @@ export const tooltipMapping: Record<string, ReactNode> = {
|
||||
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
|
||||
),
|
||||
suppliedStake: t('The current amount of liquidity supplied for this market.'),
|
||||
liquidityPriceRange: t(
|
||||
`Percentage move up and down from the mid price, which specifies the range of price levels over which automated liquidity provision orders will be deployed.`
|
||||
),
|
||||
};
|
||||
|
||||
@@ -152,6 +152,8 @@ export const OrderListManager = ({
|
||||
isReadOnly={isReadOnly}
|
||||
hasActiveOrder={hasActiveOrder}
|
||||
blockLoadDebounceMillis={100}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -323,6 +323,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="text-right font-mono"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
@@ -347,6 +348,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="text-right font-mono"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable */
|
||||
process.env.TZ = 'UTC';
|
||||
export default {
|
||||
displayName: 'react-helpers',
|
||||
preset: '../../jest.preset.js',
|
||||
|
||||
@@ -10,7 +10,9 @@ const MIN_FRACTION_DIGITS = 2;
|
||||
const MAX_FRACTION_DIGITS = 20;
|
||||
|
||||
export function toDecimal(numberOfDecimals: number) {
|
||||
return 1 / Math.pow(10, numberOfDecimals);
|
||||
return new BigNumber(1)
|
||||
.dividedBy(Math.pow(10, numberOfDecimals))
|
||||
.toString(10);
|
||||
}
|
||||
|
||||
export function toBigNum(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
import classNames from 'classnames';
|
||||
import { memo } from 'react';
|
||||
import { BID_COLOR, ASK_COLOR } from './vol-cell';
|
||||
import { NumericCell } from './numeric-cell';
|
||||
import { addDecimalsFormatNumber } from '../format';
|
||||
|
||||
export interface CumulativeVolProps {
|
||||
@@ -15,11 +14,7 @@ export interface CumulativeVolProps {
|
||||
positionDecimalPlaces: number;
|
||||
}
|
||||
|
||||
export interface ICumulativeVolCellProps extends ICellRendererParams {
|
||||
value: CumulativeVolProps;
|
||||
}
|
||||
|
||||
export const CumulativeVol = React.memo(
|
||||
export const CumulativeVol = memo(
|
||||
({
|
||||
relativeAsk,
|
||||
relativeBid,
|
||||
@@ -27,7 +22,6 @@ export const CumulativeVol = React.memo(
|
||||
bid,
|
||||
indicativeVolume,
|
||||
testId,
|
||||
className,
|
||||
positionDecimalPlaces,
|
||||
}: CumulativeVolProps) => {
|
||||
const askBar = relativeAsk ? (
|
||||
@@ -40,7 +34,7 @@ export const CumulativeVol = React.memo(
|
||||
backgroundColor: ASK_COLOR,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
) : null;
|
||||
const bidBar = relativeBid ? (
|
||||
<div
|
||||
@@ -53,25 +47,48 @@ export const CumulativeVol = React.memo(
|
||||
backgroundColor: BID_COLOR,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const volume = indicativeVolume ? (
|
||||
<span className="relative">
|
||||
({addDecimalsFormatNumber(indicativeVolume, positionDecimalPlaces ?? 0)}
|
||||
<span>
|
||||
(
|
||||
<NumericCell
|
||||
value={Number(indicativeVolume)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
indicativeVolume,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
)
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative">
|
||||
{ask ? addDecimalsFormatNumber(ask, positionDecimalPlaces ?? 0) : null}
|
||||
<span>
|
||||
{ask ? (
|
||||
<NumericCell
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
ask,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{ask && bid ? '/' : null}
|
||||
{bid ? addDecimalsFormatNumber(bid, positionDecimalPlaces ?? 0) : null}
|
||||
{bid ? (
|
||||
<NumericCell
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
bid,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('h-full relative', className)}
|
||||
className="relative font-mono pr-1"
|
||||
data-testid={testId || 'cumulative-vol'}
|
||||
>
|
||||
{askBar}
|
||||
@@ -83,9 +100,3 @@ export const CumulativeVol = React.memo(
|
||||
);
|
||||
|
||||
CumulativeVol.displayName = 'CumulativeVol';
|
||||
|
||||
export const CumulativeVolCell = ({ value }: ICumulativeVolCellProps) => (
|
||||
<CumulativeVol {...value} />
|
||||
);
|
||||
|
||||
CumulativeVolCell.displayName = 'CumulativeVolCell';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { DateRangeFilterProps } from './date-range-filter';
|
||||
import { DateRangeFilter } from './date-range-filter';
|
||||
|
||||
const commonProps = {
|
||||
filterChangedCallback: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DateRangeFilter', () => {
|
||||
it('should be properly rendered', async () => {
|
||||
const defaultRangeFilter = {
|
||||
start: '2023-02-14T13:53:01+01:00',
|
||||
end: '2023-02-21T13:53:01+01:00',
|
||||
};
|
||||
const displayStartValue = '2023-02-14T12:53:01.000';
|
||||
const displayEndValue = '2023-02-21T12:53:01.000';
|
||||
render(
|
||||
<DateRangeFilter
|
||||
{...(commonProps as unknown as DateRangeFilterProps)}
|
||||
defaultRangeFilter={defaultRangeFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('Start')).toHaveValue(displayStartValue);
|
||||
expect(screen.getByLabelText('End')).toHaveValue(displayEndValue);
|
||||
|
||||
expect(commonProps.filterChangedCallback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,113 +1,259 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||
import { formatForInput } from '../format/date';
|
||||
import {
|
||||
isBefore,
|
||||
subDays,
|
||||
addDays,
|
||||
differenceInDays,
|
||||
formatRFC3339,
|
||||
min,
|
||||
max,
|
||||
isValid,
|
||||
} from 'date-fns';
|
||||
import { t } from '../i18n';
|
||||
import { formatForInput } from '../format/date';
|
||||
|
||||
const defaultFilterValue: Schema.DateRange = {};
|
||||
export interface DateRangeFilterProps extends IFilterParams {
|
||||
defaultRangeFilter?: Schema.DateRange;
|
||||
maxSubDays?: number;
|
||||
maxNextDays?: number;
|
||||
maxDaysRange?: number;
|
||||
}
|
||||
|
||||
export const DateRangeFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
const [value, setValue] = useState<Schema.DateRange>(defaultFilterValue);
|
||||
export const DateRangeFilter = forwardRef(
|
||||
(props: DateRangeFilterProps, ref) => {
|
||||
const defaultDates = props?.defaultRangeFilter || defaultFilterValue;
|
||||
const [value, setValue] = useState<Schema.DateRange>(defaultDates);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
|
||||
const minStartDate =
|
||||
props?.maxSubDays !== undefined
|
||||
? formatForInput(subDays(Date.now(), props.maxSubDays))
|
||||
: '';
|
||||
const maxStartDate =
|
||||
props?.maxNextDays !== undefined
|
||||
? formatForInput(addDays(Date.now(), props.maxNextDays))
|
||||
: '';
|
||||
const minEndDate =
|
||||
value.start && props?.maxDaysRange !== undefined
|
||||
? formatForInput(new Date(value.start))
|
||||
: minStartDate || value.start
|
||||
? formatForInput(new Date(value.start))
|
||||
: '';
|
||||
const endDateCandidates = [];
|
||||
if (props.maxNextDays !== undefined) {
|
||||
endDateCandidates.push(addDays(new Date(), props.maxNextDays));
|
||||
}
|
||||
if (props.maxDaysRange !== undefined && value.start) {
|
||||
endDateCandidates.push(
|
||||
addDays(new Date(value.start), props.maxDaysRange)
|
||||
);
|
||||
}
|
||||
const maxEndDate = endDateCandidates.length
|
||||
? formatForInput(min(endDateCandidates))
|
||||
: maxStartDate;
|
||||
return [minStartDate, maxStartDate, minEndDate, maxEndDate];
|
||||
}, [props.maxSubDays, props.maxDaysRange, props.maxNextDays, value.start]);
|
||||
// expose AG Grid Filter Lifecycle callbacks
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
const rowValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
if (
|
||||
value.start &&
|
||||
rowValue &&
|
||||
new Date(rowValue) <= new Date(value.start)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
value.end &&
|
||||
rowValue &&
|
||||
new Date(rowValue) >= new Date(value.end)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// expose AG Grid Filter Lifecycle callbacks
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
const rowValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
if (
|
||||
value.start &&
|
||||
rowValue &&
|
||||
new Date(rowValue) <= new Date(value.start)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
value.end &&
|
||||
rowValue &&
|
||||
new Date(rowValue) >= new Date(value.end)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isFilterActive() {
|
||||
return value.start || value.end;
|
||||
},
|
||||
|
||||
isFilterActive() {
|
||||
return value.start || value.end;
|
||||
},
|
||||
getModel() {
|
||||
if (!this.isFilterActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
if (!this.isFilterActive()) {
|
||||
return null;
|
||||
}
|
||||
return { value };
|
||||
},
|
||||
|
||||
return { value };
|
||||
},
|
||||
|
||||
setModel(model?: { value: Schema.DateRange } | null) {
|
||||
setValue(model?.value || defaultFilterValue);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue({
|
||||
...value,
|
||||
[event.target.name]:
|
||||
event.target.value &&
|
||||
new Date(event.target.value).toISOString().replace('Z', '000000Z'),
|
||||
setModel(model?: { value: Schema.DateRange } | null) {
|
||||
setValue(
|
||||
model?.value || props?.defaultRangeFilter || defaultFilterValue
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
const validate = (
|
||||
name: string,
|
||||
timeValue: Date,
|
||||
update?: Schema.DateRange
|
||||
) => {
|
||||
if (
|
||||
props.maxSubDays !== undefined &&
|
||||
isBefore(new Date(timeValue), subDays(Date.now(), props.maxSubDays + 1))
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The earliest data that can be queried is %s days ago.',
|
||||
String(props.maxSubDays)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (props?.maxDaysRange !== undefined) {
|
||||
const contrvalue =
|
||||
name === 'start'
|
||||
? update?.end || value.end
|
||||
: update?.start || value.start;
|
||||
if (
|
||||
Math.abs(
|
||||
differenceInDays(new Date(timeValue), new Date(contrvalue))
|
||||
) > props.maxDaysRange
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The maximum time range that can be queried is %s days.',
|
||||
String(props.maxDaysRange)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props?.filterChangedCallback();
|
||||
}, [value, props]);
|
||||
const checkForEndDate = (
|
||||
endDate: Date | undefined,
|
||||
startDate: Date | undefined
|
||||
) => {
|
||||
const endDateCandidates: Date[] = [];
|
||||
if (props.maxDaysRange !== undefined && isValid(startDate)) {
|
||||
endDateCandidates.push(addDays(startDate as Date, props.maxDaysRange));
|
||||
}
|
||||
if (props.maxNextDays !== undefined) {
|
||||
endDateCandidates.push(addDays(Date.now(), props.maxNextDays));
|
||||
}
|
||||
if (isValid(endDate)) {
|
||||
endDateCandidates.push(endDate as Date);
|
||||
}
|
||||
return endDate && startDate
|
||||
? formatRFC3339(max([startDate, min(endDateCandidates)]))
|
||||
: undefined;
|
||||
};
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value: dateValue, name } = event.target;
|
||||
const date = new Date(dateValue || defaultDates[name as 'start' | 'end']);
|
||||
let update = { [name]: isValid(date) ? formatRFC3339(date) : undefined };
|
||||
const startCheckDate = name === 'start' ? date : new Date(value.start);
|
||||
const endCheckDate =
|
||||
name === 'start'
|
||||
? new Date(value.end)
|
||||
: isValid(date)
|
||||
? date
|
||||
: new Date(maxEndDate);
|
||||
const endDate = isValid(endCheckDate) ? endCheckDate : undefined;
|
||||
const startDate = isValid(startCheckDate) ? startCheckDate : undefined;
|
||||
update = { ...update, end: checkForEndDate(endDate, startDate) };
|
||||
|
||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||
return (
|
||||
<div className="ag-filter-body-wrapper">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="start">
|
||||
<span className="block">{t('Start')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
value={start}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="block" key="end">
|
||||
<span className="block">{t('End')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
value={end}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div className="ag-filter-apply-panel">
|
||||
<button
|
||||
type="button"
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => setValue(defaultFilterValue)}
|
||||
if (validate(name, date, update)) {
|
||||
setValue((curr) => ({
|
||||
...curr,
|
||||
...update,
|
||||
}));
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
props?.filterChangedCallback();
|
||||
}, [value, props]);
|
||||
|
||||
const notification = useMemo(() => {
|
||||
const not = error ? (
|
||||
<div
|
||||
className="text-sm flex items-center first-letter:uppercase mt-2 border-danger text-danger"
|
||||
role="alert"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{error}
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||
return (
|
||||
<div className="ag-filter-body-wrapper inline-block min-w-fit">
|
||||
{notification}
|
||||
<div className="ag-filter-apply-panel">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="start">
|
||||
<span className="block mb-1">{t('Start')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
value={start || ''}
|
||||
onChange={onChange}
|
||||
min={minStartDate}
|
||||
max={maxStartDate}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="end">
|
||||
<span className="block mb-1">{t('End')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
value={end || ''}
|
||||
onChange={onChange}
|
||||
min={minEndDate}
|
||||
max={maxEndDate}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="ag-filter-apply-panel">
|
||||
<button
|
||||
type="button"
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => {
|
||||
setError('');
|
||||
setValue(defaultDates);
|
||||
}}
|
||||
>
|
||||
{t('Reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './summary-rows';
|
||||
export * from './vol-cell';
|
||||
export * from './set-filter';
|
||||
export * from './date-range-filter';
|
||||
export * from './numeric-cell';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { NumericCell } from './numeric-cell';
|
||||
|
||||
describe('NumericCell', () => {
|
||||
const testId = 'cell';
|
||||
it('Displays formatted value', () => {
|
||||
const valueFormatted = '111.00';
|
||||
render(
|
||||
<NumericCell
|
||||
value={111}
|
||||
valueFormatted={valueFormatted}
|
||||
testId={testId}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId(testId)).toHaveTextContent(valueFormatted);
|
||||
expect(screen.getByText('00')).toBeInTheDocument();
|
||||
expect(screen.getByText('00')).toHaveClass('opacity-60');
|
||||
});
|
||||
|
||||
it('Displays 0', () => {
|
||||
render(<NumericCell value={0} valueFormatted="0.00" testId={testId} />);
|
||||
expect(screen.getByTestId(testId)).toHaveTextContent('0.00');
|
||||
});
|
||||
|
||||
it('Displays - if value is not a number', () => {
|
||||
render(<NumericCell value={null} valueFormatted="" testId={testId} />);
|
||||
expect(screen.getByTestId(testId)).toHaveTextContent('-');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { getDecimalSeparator, isNumeric } from '../format';
|
||||
|
||||
interface NumericCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a numeric value in a consistent way for data grid
|
||||
* use, right aligned, monospace and decimals deemphasised
|
||||
*/
|
||||
export const NumericCell = forwardRef<HTMLSpanElement, NumericCellProps>(
|
||||
({ value, valueFormatted, testId }, ref) => {
|
||||
if (!isNumeric(value)) {
|
||||
return (
|
||||
<span ref={ref} data-testid={testId}>
|
||||
-
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const decimalSeparator = getDecimalSeparator();
|
||||
const valueSplit: string[] = decimalSeparator
|
||||
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
|
||||
: [`${value}`];
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
data-testid={testId}
|
||||
title={valueFormatted}
|
||||
>
|
||||
{valueSplit[0]}
|
||||
{valueSplit[1] ? decimalSeparator : null}
|
||||
{valueSplit[1] ? (
|
||||
<span className="opacity-60">{valueSplit[1]}</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,15 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import * as React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { PriceCell } from './price-cell';
|
||||
|
||||
describe('<PriceCell />', () => {
|
||||
describe('PriceCell', () => {
|
||||
it('Displays formatted value', () => {
|
||||
render(<PriceCell value={100} valueFormatted="100.00" />);
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('100.00');
|
||||
expect(screen.getByTestId('price')).toHaveAttribute('title', '100.00');
|
||||
});
|
||||
|
||||
it('Displays 0', () => {
|
||||
render(<PriceCell value={0} valueFormatted="0.00" />);
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('0.00');
|
||||
@@ -19,4 +19,14 @@ describe('<PriceCell />', () => {
|
||||
render(<PriceCell value={null} valueFormatted="" />);
|
||||
expect(screen.getByTestId('price')).toHaveTextContent('-');
|
||||
});
|
||||
|
||||
it('can be clicked if given an onClick handler', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const value = 100;
|
||||
render(
|
||||
<PriceCell value={value} valueFormatted="100.00" onClick={mockOnClick} />
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('price'));
|
||||
expect(mockOnClick).toHaveBeenCalledWith(value);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user