Compare commits

..
53 changed files with 417 additions and 1238 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ module.exports = defineConfig({
vegaTokenContractAddress: '0xF41bD86d462D36b997C0bbb4D97a0a3382f205B7',
vegaTokenAddress: '0x67175Da1D5e966e40D11c4B2519392B2058373de',
txTimeout: { timeout: 70000 },
epochTimeout: { timeout: 12000 },
epochTimeout: { timeout: 10000 },
blockConfirmations: 3,
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
@@ -1,63 +0,0 @@
export const previousEpochData = {
epoch: {
id: '7611',
validatorsConnection: {
edges: [
{
node: {
id: 'cd96782bc0ad5679869cf69fe7838a92212da7f53b4a214bed68067117494122',
stakedTotal: '3154229668720612941799',
rewardScore: {
rawValidatorScore: '0.2',
performanceScore: '1',
multisigScore: '0',
validatorScore: '0.2',
normalisedScore: '0.2007216887087119',
validatorStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
__typename: 'RewardScore',
},
rankingScore: {
status: 'VALIDATOR_NODE_STATUS_TENDERMINT',
previousStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
rankingScore: '0.211713765544955625',
stakeScore: '0.2016321576618625',
performanceScore: '1',
votingPower: '2007',
__typename: 'RankingScore',
},
__typename: 'Node',
},
__typename: 'NodeEdge',
},
{
node: {
id: '887d936f797a47032eceb572a13b69b581d3b1fa595a7d021a3e3cf2a5d2acfd',
stakedTotal: '3151161904761904764551',
rewardScore: {
rawValidatorScore: '0.2',
performanceScore: '1',
multisigScore: '1',
validatorScore: '0.2',
normalisedScore: '0.2007216887087119',
validatorStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
__typename: 'RewardScore',
},
rankingScore: {
status: 'VALIDATOR_NODE_STATUS_TENDERMINT',
previousStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
rankingScore: '0.211507855409133255',
stakeScore: '0.2014360527706031',
performanceScore: '1',
votingPower: '2007',
__typename: 'RankingScore',
},
__typename: 'Node',
},
__typename: 'NodeEdge',
},
],
__typename: 'NodesConnection',
},
__typename: 'Epoch',
},
};
@@ -58,6 +58,8 @@ context(
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
ethereumWalletConnect();
// this is a workaround for #2422 which can be removed once issue is resolved
cy.associateTokensToVegaWallet('4');
vegaWalletSetSpecifiedApprovalAmount('1000');
});
@@ -1,6 +1,5 @@
/// <reference types="cypress" />
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
navigation,
verifyPageHeader,
@@ -10,7 +9,6 @@ import {
clickOnValidatorFromList,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { previousEpochData } from '../../fixtures/mocks/previous-epoch';
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
@@ -32,7 +30,6 @@ const normalisedVotingPowerToolTip =
'[data-testid="normalised-voting-power-tooltip"]';
const performancePenaltyToolTip = '[data-testid="performance-penalty-tooltip"]';
const overstakedPenaltyToolTip = '[data-testid="overstaked-penalty-tooltip"]';
const multisigPenaltyToolTip = '[data-testid="multisig-error-tooltip"]';
const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
@@ -154,22 +151,6 @@ context('Validators Page - verify elements on page', function () {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
it('Should be able to see multisig error', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'PreviousEpoch', previousEpochData);
});
waitForBeginningOfEpoch();
cy.getByTestId('total-penalty').first().realHover();
cy.get(multisigPenaltyToolTip)
.invoke('text')
.should('contain', 'Multisig penalty: 100%');
cy.getByTestId('total-penalty').eq(1).realHover();
cy.get(multisigPenaltyToolTip)
.invoke('text')
.should('contain', 'Multisig penalty: 100%');
});
}
);
@@ -282,33 +282,33 @@ context(
// 2002-SINC-016
describe('Vega wallet with assets', function () {
const assets = [
{
id: '816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc',
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: 10.0,
},
{
id: '8566db7257222b5b7ef2886394ad28b938b28680a54a169bbc795027b89d6665',
name: 'DAI (fake)',
symbol: 'fDAI',
amount: '200000',
expectedAmount: 2.0,
expectedAmount: '2.00',
},
{
id: '73174a6fb1d5802ba0ac7bd7ab79e0a3a4837b262de0a4e80815a55442692bd0',
name: 'BTC (fake)',
symbol: 'fBTC',
amount: '600000',
expectedAmount: 6.0,
expectedAmount: '6.00',
},
{
id: 'e02d4c15d790d1d2dffaf2dcd1cf06a1fe656656cf4ed18c8ce99f9e83643567',
name: 'EURO (fake)',
symbol: 'fEURO',
amount: '800000',
expectedAmount: 8.0,
expectedAmount: '8.00',
},
{
id: '816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc',
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: '10.00',
},
];
@@ -319,6 +319,12 @@ context(
cy.reload();
waitForSpinner();
cy.connectVegaWallet();
cy.get(walletContainer).within(() => {
cy.getByTestId('currency-title', txTimeout).should(
'have.length.at.least',
5
);
});
});
for (const { name, symbol, expectedAmount } of assets) {
@@ -332,10 +338,10 @@ context(
.contains(name)
.parent()
.siblings()
.then((elementAmount) => {
const displayedAmount = parseFloat(elementAmount.text());
expect(displayedAmount).be.gte(expectedAmount);
});
.invoke('text')
.should('have.length.at.least', 4)
.then(parseFloat)
.should('be.gte', parseFloat(expectedAmount));
cy.get(vegaWalletCurrencyTitle)
.contains(name)
@@ -37,7 +37,7 @@ export function navigateTo(page: navigation) {
});
} else {
return cy.get(navigation.section, { timeout: 10000 }).within(() => {
cy.get(page).eq(0).click({ force: true });
cy.get(page).eq(0).click();
});
}
}
@@ -1,15 +0,0 @@
describe('charts', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId('Depth').click();
});
it('can see market depth chart', () => {
// 6006-DEPC-001
cy.getByTestId('tab-depth').should('be.visible');
cy.get('.depth-chart-module_canvas__260De').should('be.visible');
});
});
@@ -182,7 +182,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
.contains('Liquidity monitoring parameters')
.click();
validateMarketDataRow(0, 'Triggering Ratio', '0.7');
validateMarketDataRow(0, 'Triggering Ratio', '0');
validateMarketDataRow(1, 'Time Window', '3,600');
validateMarketDataRow(2, 'Scaling Factor', '10');
});
@@ -8,7 +8,7 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -184,6 +184,7 @@ const MarketList = ({
if (error) {
return <div>{error.message}</div>;
}
return (
<AutoSizer>
{({ width, height }) => (
@@ -203,29 +204,6 @@ const MarketList = ({
);
};
interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}
const ListItem = ({
index,
style,
data,
}: {
index: number;
style: CSSProperties;
data: ListItemData;
}) => (
<MarketSelectorItem
market={data.data[index]}
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
/>
);
const List = ({
data,
loading,
@@ -234,20 +212,28 @@ const List = ({
onSelect,
noItems,
currentMarketId,
}: ListItemData & {
}: {
data: MarketMaybeWithDataAndCandles[];
loading: boolean;
width: number;
height: number;
noItems: string;
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}) => {
const itemKey = useCallback(
(index: number, data: ListItemData) => data.data[index].id,
[]
);
const itemData = useMemo(
() => ({ data, onSelect, currentMarketId }),
[data, onSelect, currentMarketId]
);
const row = ({ index, style }: { index: number; style: CSSProperties }) => {
const market = data[index];
return (
<MarketSelectorItem
market={market}
currentMarketId={currentMarketId}
style={style}
onSelect={onSelect}
/>
);
};
if (!data || loading) {
return (
<div style={{ width, height }}>
@@ -273,13 +259,11 @@ const List = ({
<FixedSizeList
className="virtualized-list"
itemCount={data.length}
itemData={itemData}
itemSize={130}
itemKey={itemKey}
width={width}
height={height}
>
{ListItem}
{row}
</FixedSizeList>
);
};
@@ -23,19 +23,6 @@ import {
ResizableGrid,
ResizableGridPanel,
} from '../../components/resizable-grid';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals();
if (!ready || ready.length === 0) {
return null;
}
return (
<span className="bg-vega-blue-450 text-white text-[10px] rounded p-[3px] pb-[2px] leading-none">
{ready.length}
</span>
);
};
export const Portfolio = () => {
const { updateTitle } = usePageTitleStore((store) => ({
@@ -116,11 +103,7 @@ export const Portfolio = () => {
<DepositsContainer />
</VegaWalletContainer>
</Tab>
<Tab
id="withdrawals"
name={t('Withdrawals')}
indicator={<WithdrawalsIndicator />}
>
<Tab id="withdrawals" name={t('Withdrawals')}>
<WithdrawalsContainer />
</Tab>
</Tabs>
@@ -3,7 +3,6 @@ import {
withdrawalProvider,
useWithdrawalDialog,
WithdrawalsTable,
useIncompleteWithdrawals,
} from '@vegaprotocol/withdraws';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
@@ -18,7 +17,6 @@ export const WithdrawalsContainer = () => {
skip: !pubKey,
});
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
const { ready, delayed } = useIncompleteWithdrawals();
return (
<VegaWalletContainer>
@@ -27,8 +25,6 @@ export const WithdrawalsContainer = () => {
data-testid="withdrawals-history"
rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No withdrawals')}
ready={ready}
delayed={delayed}
/>
</div>
{!isReadOnly && (
@@ -1,16 +1,13 @@
import type { DefaultWeb3ProviderContextShape } from '@vegaprotocol/web3';
import {
useEthereumConfig,
createConnectors,
Web3Provider as Web3ProviderInternal,
useWeb3ConnectStore,
createDefaultProvider,
} from '@vegaprotocol/web3';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
export const Web3Provider = ({ children }: { children: ReactNode }) => {
@@ -20,13 +17,10 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
const connectors = useWeb3ConnectStore((store) => store.connectors);
const initializeConnectors = useWeb3ConnectStore((store) => store.initialize);
const [defaultProvider, setDefaultProvider] = useState<
DefaultWeb3ProviderContextShape['provider'] | undefined
>(undefined);
useEffect(() => {
if (config?.chain_id) {
initializeConnectors(
return initializeConnectors(
createConnectors(
ETHEREUM_PROVIDER_URL,
Number(config?.chain_id),
@@ -35,11 +29,6 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
),
Number(config.chain_id)
);
const defaultProvider = createDefaultProvider(
ETHEREUM_PROVIDER_URL,
Number(config?.chain_id)
);
setDefaultProvider(defaultProvider);
}
}, [
config?.chain_id,
@@ -60,10 +49,7 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
}}
noDataMessage={t('Could not fetch Ethereum configuration')}
>
<Web3ProviderInternal
connectors={connectors}
defaultProvider={defaultProvider}
>
<Web3ProviderInternal connectors={connectors}>
<>{children}</>
</Web3ProviderInternal>
</AsyncRenderer>
-5
View File
@@ -3,17 +3,12 @@ import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
import { Routes } from './client-router';
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
export const ToastsManager = () => {
useUpdateNetworkParametersToasts();
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
useReadyToWithdrawalToasts({
withdrawalsLink: `${Routes.PORTFOLIO}`,
});
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
@@ -1,7 +1,6 @@
# Liquidity Provisions
fragment LiquidityProvisionFields on LiquidityProvision {
id
party {
id
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
+2 -3
View File
@@ -3,14 +3,14 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', id?: string | null, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } };
export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } };
export type LiquidityProvisionsQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', id?: string | null, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProvisionsUpdateSubscriptionVariables = Types.Exact<{
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
@@ -31,7 +31,6 @@ export type LiquidityProviderFeeShareQuery = { __typename?: 'Query', market?: {
export const LiquidityProvisionFieldsFragmentDoc = gql`
fragment LiquidityProvisionFields on LiquidityProvision {
id
party {
id
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
@@ -38,7 +38,8 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
) => {
return produce(data || [], (draft) => {
deltas?.forEach((delta) => {
const index = draft.findIndex((a) => delta.id === a.id);
const id = delta.id;
const index = draft.findIndex((a) => delta.id === id);
if (index !== -1) {
draft[index].commitmentAmount = delta.commitmentAmount;
draft[index].fee = delta.fee;
@@ -46,7 +47,6 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
draft[index].status = delta.status;
} else {
draft.unshift({
id: delta.id,
commitmentAmount: delta.commitmentAmount,
fee: delta.fee,
status: delta.status,
+2 -2
View File
@@ -188,7 +188,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }) => data.id}
getRowId={({ data }) => `${data.party.id}-${data.status}`}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
@@ -200,7 +200,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
storeKey="liquidityProvisionTable"
{...props}
columnDefs={colDefs}
/>
></AgGrid>
);
}
);
@@ -1,5 +1,6 @@
import {
addDecimalsFormatNumber,
formatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -46,10 +47,7 @@ const Row = ({
if (asPercentage) {
return formatNumberPercentage(new BigNumber(value).times(100));
}
if (assetSymbol) {
return `${Number(value).toLocaleString()} ${assetSymbol}`;
}
return Number(value).toLocaleString();
return `${formatNumber(Number(value))} ${assetSymbol}`;
};
const formattedValue = getFormattedValue(value);
@@ -94,7 +94,7 @@ export const marketInfoQuery = (
},
lpPriceRange: '0.02',
liquidityMonitoringParameters: {
triggeringRatio: '0.7',
triggeringRatio: '0',
targetStakeParameters: {
timeWindow: 3600,
scalingFactor: 10,
+3 -62
View File
@@ -1,50 +1,13 @@
import { marketDataErrorPolicyGuard } from '@vegaprotocol/data-provider';
import { makeDataProvider } from '@vegaprotocol/data-provider';
import type {
MarketsDataQuery,
MarketsDataQueryVariables,
} from './__generated__/markets-data';
import type {
MarketDataUpdateSubscription,
MarketDataUpdateFieldsFragment,
MarketDataUpdateSubscriptionVariables,
} from './__generated__/market-data';
import { MarketDataUpdateDocument } from './__generated__/market-data';
import type { MarketsDataQuery } from './__generated__/markets-data';
import { MarketsDataDocument } from './__generated__/markets-data';
import type { MarketData } from './market-data-provider';
const getData = (responseData: MarketsDataQuery | null): MarketData[] =>
const getData = (responseData: MarketsDataQuery | null): MarketData[] | null =>
responseData?.marketsConnection?.edges
.filter((edge) => edge.node.data)
.map((edge) => edge.node.data as MarketData) || [];
export const mapMarketDataUpdateToMarketData = (
delta: MarketDataUpdateFieldsFragment
): MarketData => {
const { marketId, __typename, ...marketData } = delta;
return { ...marketData, market: { id: marketId } };
};
const update = (
data: MarketData[] | null,
delta: MarketDataUpdateFieldsFragment
) => {
const updatedData = data ? [...data] : [];
const item = mapMarketDataUpdateToMarketData(delta);
const index = updatedData.findIndex(
(data) => data.market.id === item.market.id
);
if (index !== -1) {
updatedData[index] = { ...updatedData[index], ...item };
} else {
updatedData.push(item);
}
return updatedData;
};
const getDelta = (
subscriptionData: MarketDataUpdateSubscription
): MarketDataUpdateFieldsFragment => subscriptionData.marketsData[0];
.map((edge) => edge.node.data as MarketData) || null;
export const marketsDataProvider = makeDataProvider<
MarketsDataQuery,
@@ -56,25 +19,3 @@ export const marketsDataProvider = makeDataProvider<
getData,
errorPolicyGuard: marketDataErrorPolicyGuard,
});
type Variables = { marketIds: string[] };
export const marketsLiveDataProvider = makeDataProvider<
MarketsDataQuery,
MarketData[],
MarketDataUpdateSubscription,
MarketDataUpdateFieldsFragment,
Variables,
MarketDataUpdateSubscriptionVariables,
MarketsDataQueryVariables
>({
query: MarketsDataDocument,
subscriptionQuery: MarketDataUpdateDocument,
getData,
getDelta,
update,
errorPolicyGuard: marketDataErrorPolicyGuard,
getQueryVariables: () => ({}),
getSubscriptionVariables: ({ marketIds }: Variables) =>
marketIds.map((marketId) => ({ marketId })),
});
+1 -44
View File
@@ -10,15 +10,10 @@ import type {
} from './__generated__/markets';
import type { MarketsCandlesQueryVariables } from './__generated__/markets-candles';
import {
marketsDataProvider,
marketsLiveDataProvider,
mapMarketDataUpdateToMarketData,
} from './markets-data-provider';
import { marketsDataProvider } from './markets-data-provider';
import { marketDataProvider } from './market-data-provider';
import { marketsCandlesProvider } from './markets-candles-provider';
import type { MarketData } from './market-data-provider';
import type { MarketDataUpdateFieldsFragment } from './__generated__';
import type { MarketCandles } from './markets-candles-provider';
import { useMemo } from 'react';
import * as Schema from '@vegaprotocol/types';
@@ -165,44 +160,6 @@ export const allMarketsWithDataProvider = makeDerivedDataProvider<
addData(parts[0] as Market[], parts[1] as MarketData[])
);
export const allMarketsWithLiveDataProvider = makeDerivedDataProvider<
MarketMaybeWithData[],
MarketMaybeWithData,
{ marketIds: string[] }
>(
[
(callback, client, variables) =>
marketsProvider(callback, client, undefined),
marketsLiveDataProvider,
],
(partsData, variables, prevData, parts) => {
if (prevData && parts[1].isUpdate) {
const data = mapMarketDataUpdateToMarketData(parts[1].delta);
const index = prevData.findIndex(
(market) => market.id === data.market.id
);
if (index !== -1) {
const updatedData = [...prevData];
updatedData[index] = { ...updatedData[index], data };
return updatedData;
} else {
return prevData;
}
}
return addData(partsData[0] as Market[], partsData[1] as MarketData[]);
},
(data, parts) => {
if (!parts[1].isUpdate && parts[1].delta) {
return;
}
return data.find(
(market) =>
market.id ===
(parts[1].delta as MarketDataUpdateFieldsFragment).marketId
);
}
);
export type MarketMaybeWithDataAndCandles = MarketMaybeWithData &
MarketMaybeWithCandles;
+1
View File
@@ -4,3 +4,4 @@ export * from './lib/positions-data-providers';
export * from './lib/positions-table';
export * from './lib/use-market-margin';
export * from './lib/use-open-volume';
export * from './lib/use-positions-data';
@@ -14,7 +14,7 @@ import type {
MarketMaybeWithData,
MarketDataQueryVariables,
} from '@vegaprotocol/markets';
import { allMarketsWithLiveDataProvider } from '@vegaprotocol/markets';
import { allMarketsWithDataProvider } from '@vegaprotocol/markets';
import type {
PositionsQuery,
PositionFieldsFragment,
@@ -144,9 +144,8 @@ export const update = (
data: PositionFieldsFragment[] | null,
deltas: PositionsSubscriptionSubscription['positions']
) => {
const updatedData = produce(data || [], (draft) => {
return produce(data || [], (draft) => {
deltas.forEach((delta) => {
const { marketId, partyId, __typename, ...position } = delta;
const index = draft.findIndex(
(node) =>
node.market.id === delta.marketId && node.party.id === delta.partyId
@@ -155,25 +154,29 @@ export const update = (
const currNode = draft[index];
draft[index] = {
...currNode,
...position,
realisedPNL: delta.realisedPNL,
unrealisedPNL: delta.unrealisedPNL,
openVolume: delta.openVolume,
averageEntryPrice: delta.averageEntryPrice,
updatedAt: delta.updatedAt,
lossSocializationAmount: delta.lossSocializationAmount,
positionStatus: delta.positionStatus,
};
} else {
draft.unshift({
...position,
...delta,
__typename: 'Position',
market: {
__typename: 'Market',
id: delta.marketId,
},
party: {
__typename: 'Party',
id: delta.partyId,
},
});
}
});
});
return updatedData;
};
const getSubscriptionVariables = (
@@ -239,54 +242,41 @@ export const rejoinPositionData = (
| null => {
if (positions && marketsData) {
return positions.map((node) => {
const market =
marketsData?.find((market) => market.id === node.market.id) || null;
return {
...node,
market,
market:
marketsData?.find((market) => market.id === node.market.id) || null,
};
});
}
return null;
};
export const positionsMarketsProvider = makeDerivedDataProvider<
string[],
never,
PositionsQueryVariables
>([positionsDataProvider], ([positions]) => {
return Array.from(
new Set(
(positions as PositionFieldsFragment[]).map(
(position) => position.market.id
)
)
).sort();
});
export const positionsMetricsProvider = makeDerivedDataProvider<
Position[],
Position[],
PositionsQueryVariables & { marketIds: string[] }
PositionsQueryVariables
>(
[
(callback, client, variables) =>
positionsDataProvider(callback, client, { partyIds: variables.partyIds }),
positionsDataProvider,
(callback, client, variables) =>
accountsDataProvider(callback, client, {
partyId: Array.isArray(variables.partyIds)
? variables.partyIds[0]
: variables.partyIds,
}),
(callback, client, variables) =>
allMarketsWithLiveDataProvider(callback, client, {
marketIds: variables.marketIds,
}),
(callback, client) =>
allMarketsWithDataProvider(callback, client, undefined),
],
([positions, accounts, marketsData]) => {
([positions, accounts, marketsData], variables) => {
const positionsData = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsData, accounts as Account[] | null);
return sortBy(metrics, 'marketName');
if (!variables) {
return [];
}
return sortBy(
getMetrics(positionsData, accounts as Account[] | null),
'marketName'
);
},
(data, delta, previousData) =>
data.filter((row) => {
+40 -41
View File
@@ -1,13 +1,11 @@
import { useCallback } from 'react';
import { useRef } from 'react';
import { usePositionsData } from './use-positions-data';
import { PositionsTable } from './positions-table';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
positionsMetricsProvider,
positionsMarketsProvider,
} from './positions-data-providers';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface PositionsManagerProps {
@@ -26,43 +24,42 @@ export const PositionsManager = ({
storeKey,
}: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const gridRef = useRef<AgGridReact | null>(null);
const { data, error } = usePositionsData(partyIds, gridRef);
const create = useVegaTransactionStore((store) => store.create);
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
reduceOnly: true,
},
],
},
}),
[create]
);
const onClose = ({
marketId,
openVolume,
}: {
marketId: string;
openVolume: string;
}) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
reduceOnly: true,
},
],
},
});
const { data: marketIds } = useDataProvider({
dataProvider: positionsMarketsProvider,
variables: { partyIds },
});
const { data, error } = useDataProvider({
dataProvider: positionsMetricsProvider,
variables: { partyIds, marketIds: marketIds || [] },
skip: !marketIds,
const bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
disabled: noBottomPlaceholder,
});
return (
@@ -71,9 +68,11 @@ export const PositionsManager = ({
pubKey={pubKey}
pubKeys={pubKeys}
rowData={error ? [] : data}
ref={gridRef}
onMarketClick={onMarketClick}
onClose={onClose}
isReadOnly={isReadOnly}
{...bottomPlaceholderProps}
storeKey={storeKey}
multipleKeys={partyIds.length > 1}
overlayNoRowsTemplate={error ? error.message : t('No positions')}
+1 -3
View File
@@ -36,6 +36,7 @@ import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { getRowId } from './use-positions-data';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
@@ -85,9 +86,6 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell';
export const getRowId = ({ data }: { data: Position }) =>
`${data.partyId}-${data.marketId}`;
export const PositionsTable = forwardRef<AgGridReact, Props>(
(
{
@@ -0,0 +1,107 @@
import type { AgGridReact } from 'ag-grid-react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook, waitFor } from '@testing-library/react';
import { usePositionsData } from './use-positions-data';
import type { Position } from './positions-data-providers';
let mockData: Position[] = [
{
marketName: 'M1',
marketId: 'market-0',
openVolume: '1',
},
{
marketName: 'M2',
marketId: 'market-1',
openVolume: '-1985',
},
{
marketName: 'M3',
marketId: 'market-2',
openVolume: '0',
},
{
marketName: 'M4',
marketId: 'market-3',
openVolume: '0',
},
{
marketName: 'M5',
marketId: 'market-4',
openVolume: '3',
},
] as Position[];
let mockDataProviderData = {
data: mockData,
error: undefined,
loading: false,
totalCount: undefined,
};
let updateMock: jest.Mock;
const mockDataProvider = jest.fn((args) => {
updateMock = args.update;
return mockDataProviderData;
});
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((args) => mockDataProvider(args)),
}));
describe('usePositionData Hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
const mockRefreshInfiniteCache = jest.fn();
const mockGetRowNode = jest
.fn()
.mockImplementation((id: string) =>
mockData.find((position) => position.marketId === id)
);
const partyIds = ['partyId'];
const anUpdatedOne = {
marketId: 'market-1',
openVolume: '1',
};
const gridRef = {
current: {
api: {
refreshInfiniteCache: mockRefreshInfiniteCache,
getRowNode: mockGetRowNode,
getModel: () => ({ getType: () => 'infinite' }),
},
} as unknown as AgGridReact,
};
it('should return proper data', async () => {
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data?.length ?? 0).toEqual(5);
});
it('should call mockRefreshInfiniteCache', async () => {
renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
await waitFor(() => {
updateMock({ delta: [anUpdatedOne] as Position[] });
});
expect(mockRefreshInfiniteCache).toHaveBeenCalledWith();
});
it('no data should return null', () => {
mockData = [];
mockDataProviderData = {
...mockDataProviderData,
data: mockData,
loading: false,
};
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data).toEqual([]);
});
});
@@ -0,0 +1,81 @@
import { useCallback, useMemo, useRef } from 'react';
import type { RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import { positionsMetricsProvider } from './positions-data-providers';
import type { PositionsQueryVariables } from './__generated__/Positions';
import { updateGridData } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { GetRowsParams } from '@vegaprotocol/datagrid';
import isEqual from 'lodash/isEqual';
export const getRowId = ({
data,
}: {
data: Position & { isLastPlaceholder?: boolean; id?: string };
}) =>
data.isLastPlaceholder && data.id
? data.id
: `${data.partyId}-${data.marketId}`;
export const usePositionsData = (
partyIds: string[],
gridRef: RefObject<AgGridReact>
) => {
const variables = useMemo<PositionsQueryVariables>(
() => ({ partyIds }),
[partyIds]
);
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(
({ data }: { data: Position[] | null }) => {
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
return updateGridData(dataRef, data, gridRef);
}
const update: Position[] = [];
const add: Position[] = [];
data?.forEach((row) => {
const rowNode = gridRef.current?.api?.getRowNode(
getRowId({ data: row })
);
if (rowNode) {
if (!isEqual(rowNode.data, row)) {
update.push(row);
}
} else {
add.push(row);
}
});
gridRef.current?.api?.applyTransaction({
update,
add,
addIndex: 0,
});
return true;
},
[gridRef]
);
const { data, error, loading, reload } = useDataProvider({
dataProvider: positionsMetricsProvider,
update,
variables,
});
const getRows = useCallback(
async ({ successCallback, startRow, endRow }: GetRowsParams<Position>) => {
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current ? dataRef.current.length : 0;
successCallback(rowsThisBlock, lastRow);
},
[]
);
return {
data,
error,
loading,
getRows,
reload,
};
};
@@ -1,6 +1,6 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import type { ComponentProps, ReactNode } from 'react';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
import { VegaIcon, VegaIconNames } from '../icon';
import { Icon } from '../icon';
@@ -164,9 +164,10 @@ export const DropdownMenuSeparator = forwardRef<
/**
* Container element for submenus
*/
export const DropdownMenuSub = (
subProps: ComponentProps<typeof DropdownMenuPrimitive.Sub>
) => <DropdownMenuPrimitive.Sub {...subProps} />;
export const DropdownMenuSub = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Sub>,
React.ComponentProps<typeof DropdownMenuPrimitive.Sub>
>(({ ...subProps }) => <DropdownMenuPrimitive.Sub {...subProps} />);
/**
* Container within a DropdownMenuSub specifically for the content
@@ -200,9 +201,10 @@ export const DropdownMenuSubTrigger = forwardRef<
* Portal to ensure menu portions are rendered outwith where they appear in the
* DOM.
*/
export const DropdownMenuPortal = (
portalProps: ComponentProps<typeof DropdownMenuPrimitive.Portal>
) => <DropdownMenuPrimitive.Portal {...portalProps} />;
export const DropdownMenuPortal = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Portal>,
React.ComponentProps<typeof DropdownMenuPrimitive.Portal>
>(({ ...portalProps }) => <DropdownMenuPrimitive.Portal {...portalProps} />);
/**
* Wraps a regular DropdownMenuItem with copy to clip board functionality
+1 -4
View File
@@ -45,8 +45,7 @@ export const Tabs = ({
'cursor-default': isActive,
'text-neutral-400 hover:text-neutral-500 dark:hover:text-neutral-300':
!isActive,
},
'flex items-center gap-2'
}
);
const borderClass = classNames(
'absolute bottom-[-1px] left-0 w-full h-0 border-b',
@@ -59,7 +58,6 @@ export const Tabs = ({
value={child.props.id}
className={triggerClass}
>
{child.props.indicator}
{child.props.name}
<span className={borderClass} />
</TabsPrimitive.Trigger>
@@ -89,7 +87,6 @@ interface TabProps {
children: ReactNode;
id: string;
name: string;
indicator?: ReactNode;
hidden?: boolean;
}
@@ -18,8 +18,6 @@ export type ToastContent = JSX.Element | undefined;
type ToastState = 'initial' | 'showing' | 'expired';
type WithdrawalInfoMeta = { withdrawalId: string | undefined };
export type Toast = {
id: string;
intent: Intent;
@@ -28,9 +26,6 @@ export type Toast = {
onClose?: () => void;
signal?: 'close';
loader?: boolean;
hidden?: boolean;
// meta information
meta?: WithdrawalInfoMeta | undefined;
};
type ToastProps = Toast & {
@@ -13,13 +13,11 @@ import { Portal } from '@radix-ui/react-portal';
type ToastsContainerProps = {
toasts: Toasts;
order: 'asc' | 'desc';
showHidden?: boolean;
};
export const ToastsContainer = ({
toasts,
order = 'asc',
showHidden = false,
}: ToastsContainerProps) => {
const ref = useRef<HTMLDivElement>();
const closeAll = useToasts((store) => store.closeAll);
@@ -43,10 +41,6 @@ export const ToastsContainer = ({
};
}, [count, order, toasts]);
const validToasts = Object.values(toasts).filter(
(t) => !t.hidden || showHidden
);
return (
<Portal
ref={ref as Ref<HTMLDivElement>}
@@ -77,16 +71,14 @@ export const ToastsContainer = ({
'flex-col-reverse': order === 'desc',
})}
>
{validToasts.length > 0 &&
validToasts
.filter((t) => !t.hidden || showHidden)
.map((toast) => {
return (
<li key={toast.id}>
<Toast {...toast} />
</li>
);
})}
{toasts &&
Object.values(toasts).map((toast) => {
return (
<li key={toast.id}>
<Toast {...toast} />
</li>
);
})}
<Button
title={t('Dismiss all toasts')}
size="sm"
@@ -97,7 +89,7 @@ export const ToastsContainer = ({
'opacity-0 group-hover:opacity-50 hover:!opacity-100',
'text-sm text-black dark:text-white bg-white dark:bg-black hover:!bg-white hover:dark:!bg-black',
{
hidden: validToasts.length === 0,
hidden: Object.keys(toasts).length === 0,
}
)}
onClick={() => {
@@ -46,20 +46,12 @@ type Actions = {
* Arbitrary removes all toasts
*/
removeAll: () => void;
/**
* Checks if a given toasts exists in the collection
*/
hasToast: (id: string) => boolean;
/**
* Closes toast by meta
*/
closeBy: (meta: Toast['meta']) => void;
};
type ToastsStore = State & Actions;
export const useToasts = create<ToastsStore>()(
immer((set, get) => ({
immer((set) => ({
toasts: {},
count: 0,
add: (toast) =>
@@ -105,18 +97,6 @@ export const useToasts = create<ToastsStore>()(
}
}),
removeAll: () => set({ toasts: {}, count: 0 }),
hasToast: (id) => get().toasts[id] != null,
closeBy: (meta) => {
if (!meta) return;
set((state) => {
const found = Object.values(state.toasts).find((t) =>
isEqual(t.meta, meta)
);
if (found) {
found.signal = 'close';
}
});
},
}))
);
-1
View File
@@ -27,4 +27,3 @@ export * from './lib/web3-connectors';
export * from './lib/web3-provider';
export * from './lib/withdrawal-approval-dialog';
export * from './lib/withdrawal-approval-status';
export * from './lib/default-web3-provider';
-3
View File
@@ -16,6 +16,3 @@ export const getChainName = (chainId: number | null | undefined) => {
*/
export const WALLETCONNECT_PROJECT_ID =
process.env['NX_WALLETCONNECT_PROJECT_ID'] || '';
export const ETHEREUM_PROVIDER_URL =
process.env['NX_ETHEREUM_PROVIDER_URL'] || '';
@@ -1,19 +0,0 @@
import type { ethers } from 'ethers';
import { createContext, useContext } from 'react';
export type DefaultWeb3ProviderContextShape = {
provider?: ethers.providers.JsonRpcProvider;
};
export const DefaultWeb3ProviderContext = createContext<
DefaultWeb3ProviderContextShape | undefined
>(undefined);
export const useDefaultWeb3Provider = () => {
const context = useContext(DefaultWeb3ProviderContext);
if (context === undefined) {
throw new Error(
'useDefaultWeb3Provider must be used within DefaultWeb3ProviderContext'
);
}
return context;
};
+5 -22
View File
@@ -2,40 +2,23 @@ import { CollateralBridge } from '@vegaprotocol/smart-contracts';
import { useWeb3React } from '@web3-react/core';
import { useMemo } from 'react';
import { useEthereumConfig } from './use-ethereum-config';
import { useDefaultWeb3Provider } from './default-web3-provider';
import { localLoggerFactory } from '@vegaprotocol/logger';
export const useBridgeContract = (allowDefaultProvider = false) => {
const { provider: activeProvider } = useWeb3React();
const { provider: defaultProvider } = useDefaultWeb3Provider();
export const useBridgeContract = () => {
const { provider } = useWeb3React();
const { config } = useEthereumConfig();
const logger = localLoggerFactory({ application: 'web3' });
const provider = useMemo(() => {
if (!activeProvider && allowDefaultProvider) {
logger.info('will use default web3 provider');
return defaultProvider;
}
return activeProvider;
}, [activeProvider, allowDefaultProvider, defaultProvider, logger]);
// this has to be memoized, otherwise it ticks like crazy
const signer = useMemo(() => {
return !activeProvider && allowDefaultProvider
? undefined
: activeProvider?.getSigner();
}, [activeProvider, allowDefaultProvider]);
const contract = useMemo(() => {
if (!provider || !config) {
return null;
}
const signer = provider.getSigner();
return new CollateralBridge(
config.collateral_bridge_contract.address,
signer || provider
);
}, [provider, signer, config]);
}, [provider, config]);
return contract;
};
@@ -4,10 +4,7 @@ import type { MultisigControl } from '@vegaprotocol/smart-contracts';
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
import type { Token } from '@vegaprotocol/smart-contracts';
import type {
DepositBusEventFieldsFragment,
WithdrawalBusEventFieldsFragment,
} from '@vegaprotocol/wallet';
import type { DepositBusEventFieldsFragment } from '@vegaprotocol/wallet';
import type { EthTxState } from './use-ethereum-transaction';
import { EthTxStatus } from './use-ethereum-transaction';
@@ -30,7 +27,6 @@ export interface EthStoredTxState extends EthTxState {
requiresConfirmation: boolean; // whether or not the tx needs external confirmation (IE from a subscription even)
assetId?: string;
deposit?: DepositBusEventFieldsFragment;
withdrawal?: WithdrawalBusEventFieldsFragment;
}
export interface EthTransactionStore {
@@ -41,8 +37,7 @@ export interface EthTransactionStore {
args: string[],
assetId?: string,
requiredConfirmations?: number,
requiresConfirmation?: boolean,
withdrawal?: EthStoredTxState['withdrawal']
requiresConfirmation?: boolean
) => number;
update: (
id: EthStoredTxState['id'],
@@ -67,8 +62,7 @@ export const useEthTransactionStore = create<EthTransactionStore>()(
args: string[] = [],
assetId?: string,
requiredConfirmations = 1,
requiresConfirmation = false,
withdrawal = undefined
requiresConfirmation = false
) => {
const transactions = get().transactions;
const now = new Date();
@@ -88,7 +82,6 @@ export const useEthTransactionStore = create<EthTransactionStore>()(
requiredConfirmations,
requiresConfirmation,
assetId,
withdrawal,
};
set({ transactions: transactions.concat(transaction) });
return transaction.id;
@@ -162,10 +162,9 @@ const isFinal = (tx: EthStoredTxState) =>
[EthTxStatus.Confirmed, EthTxStatus.Error].includes(tx.status);
export const useEthereumTransactionToasts = () => {
const [setToast, removeToast, closeToastBy] = useToasts((store) => [
const [setToast, removeToast] = useToasts((store) => [
store.setToast,
store.remove,
store.closeBy,
]);
const dismissTx = useEthTransactionStore((state) => state.dismiss);
@@ -174,16 +173,8 @@ export const useEthereumTransactionToasts = () => {
(tx: EthStoredTxState) => () => {
dismissTx(tx.id);
removeToast(`eth-${tx.id}`);
// closes related "Funds released" toast after successful withdrawal
if (
isWithdrawTransaction(tx) &&
tx.status === EthTxStatus.Confirmed &&
tx.withdrawal
) {
closeToastBy({ withdrawalId: tx.withdrawal.id });
}
},
[closeToastBy, dismissTx, removeToast]
[dismissTx, removeToast]
);
const fromEthTransaction = useCallback(
@@ -298,19 +298,14 @@ describe('useEthWithdrawApprovalsManager', () => {
mockEthTransactionStoreState.mockReturnValue({ create });
render();
await waitForNextTick();
expect(create).toHaveBeenCalled();
expect(create.mock.calls[0][0]).toEqual({});
expect(create.mock.calls[0][1]).toEqual('withdraw_asset');
expect(create.mock.calls[0][2]).toEqual(
expect.arrayContaining([
erc20WithdrawalApproval.assetSource,
erc20WithdrawalApproval.amount,
erc20WithdrawalApproval.targetAddress,
erc20WithdrawalApproval.creation,
erc20WithdrawalApproval.nonce,
erc20WithdrawalApproval.signatures,
])
);
expect(create).toBeCalledWith({}, 'withdraw_asset', [
erc20WithdrawalApproval.assetSource,
erc20WithdrawalApproval.amount,
erc20WithdrawalApproval.targetAddress,
erc20WithdrawalApproval.creation,
erc20WithdrawalApproval.nonce,
erc20WithdrawalApproval.signatures,
]);
});
it('detect wrong chainId', () => {
@@ -91,8 +91,7 @@ export const useEthWithdrawApprovalsManager = () => {
if (threshold && amount.isGreaterThan(threshold)) {
const delaySecs = await getDelay();
const completeTimestamp =
new Date(withdrawal.createdTimestamp).getTime() +
(delaySecs as number) * 1000;
new Date(withdrawal.createdTimestamp).getTime() + delaySecs * 1000;
const now = Date.now();
if (now < completeTimestamp) {
update(transaction.id, {
@@ -140,11 +139,7 @@ export const useEthWithdrawApprovalsManager = () => {
approval.creation,
approval.nonce,
approval.signatures,
],
undefined,
undefined,
undefined,
transaction.withdrawal || undefined
]
);
})().catch((err) => {
localLoggerFactory({ application: 'web3' }).error(
+8 -31
View File
@@ -3,44 +3,21 @@ import { useCallback } from 'react';
import { localLoggerFactory } from '@vegaprotocol/logger';
/**
* The withdraw delay is a global value set on the contract bridge which may be
* changed via a proposal therefore it can be cached for a set amount of time
* (MAX_AGE) and re-retrieved when necessary.
*/
const MAX_AGE = 5 * 60 * 1000; // 5 minutes
type TimestampedDelay = { value: number | undefined; ts: number };
const DELAY: TimestampedDelay = { value: undefined, ts: 0 };
/**
* Returns a function that gets the delay in seconds that's required if the
* withdrawal amount is over the withdrawal threshold
* (contract.get_withdraw_threshold)
* Gets the delay in seconds thats required if the withdrawal amount is
* over the withdrawal threshold (contract.get_withdraw_threshold)
*/
export const useGetWithdrawDelay = () => {
const contract = useBridgeContract(true);
const logger = localLoggerFactory({ application: 'web3' });
const contract = useBridgeContract();
const getDelay = useCallback(async () => {
if (DELAY.value != null && Date.now() - DELAY.ts <= MAX_AGE) {
return DELAY.value;
}
if (!contract) {
logger.info('could not get withdraw delay: no bridge contract');
return undefined;
}
const logger = localLoggerFactory({ application: 'web3' });
try {
logger.info('get withdraw delay', { contract: contract?.toString() });
const res = await contract?.default_withdraw_delay();
logger.info(`retrieved withdraw delay: ${res} seconds`);
DELAY.value = res.toNumber();
DELAY.ts = Date.now();
return res.toNumber() as number;
return res.toNumber();
} catch (err) {
logger.error('could not get withdraw delay', err);
return undefined;
logger.error('get withdraw delay', err);
}
}, [contract, logger]);
}, [contract]);
return getDelay;
};
@@ -3,74 +3,33 @@ import { useBridgeContract } from './use-bridge-contract';
import BigNumber from 'bignumber.js';
import { addDecimal } from '@vegaprotocol/utils';
import type { WithdrawalBusEventFieldsFragment } from '@vegaprotocol/wallet';
import { localLoggerFactory } from '@vegaprotocol/logger';
type Asset = Pick<
WithdrawalBusEventFieldsFragment['asset'],
'source' | 'decimals'
>;
/**
* The withdraw threshold is a value set on the contract bridge per asset which
* may be changed via a proposal therefore it can be cached for a set amount of
* time (MAX_AGE) and re-retrieved when necessary.
*/
const MAX_AGE = 5 * 60 * 1000; // 5 minutes
export const BUILTIN_ASSET_ADDRESS = 'builtin';
export const BUILTIN_ASSET_THRESHOLD = new BigNumber(Infinity);
type TimestampedThreshold = { value: BigNumber; ts: number };
const THRESHOLDS: Record<string, TimestampedThreshold> = {};
const setThreshold = (address: string, value: BigNumber) =>
(THRESHOLDS[address] = { value, ts: Date.now() });
export const addr = (asset: Asset | undefined) =>
asset && asset.source.__typename === 'ERC20'
? asset.source.contractAddress
: BUILTIN_ASSET_ADDRESS;
/**
* Returns a function to get the threshold amount for a withdrawal.
* If a withdrawal amount is greater than this value it will incur a delay
* before being able to be completed. The delay is set on the smart contract and
* can be retrieved using contract.default_withdraw_delay
* Returns a function to get the threshold amount for a withdrawal. If a withdrawal amount
* is greater than this value it will incur a delay before being able to be completed. The delay is set
* on the smart contract and can be retrieved using contract.default_withdraw_delay
*/
export const useGetWithdrawThreshold = () => {
const logger = localLoggerFactory({ application: 'web3' });
const contract = useBridgeContract(true);
const contract = useBridgeContract();
const getThreshold = useCallback(
async (asset: Asset | undefined) => {
const contractAddress = addr(asset);
// return cached value if still valid
const thr = THRESHOLDS[contractAddress];
if (thr && Date.now() - thr.ts <= MAX_AGE) {
return thr.value;
}
if (!contract || !asset || contractAddress === BUILTIN_ASSET_ADDRESS) {
setThreshold(BUILTIN_ASSET_ADDRESS, BUILTIN_ASSET_THRESHOLD);
return BUILTIN_ASSET_THRESHOLD;
}
try {
const res = await contract.get_withdraw_threshold(contractAddress);
const value = new BigNumber(addDecimal(res.toString(), asset.decimals));
const threshold = value.isEqualTo(0)
? new BigNumber(Infinity)
: value.minus(new BigNumber(addDecimal('1', asset.decimals)));
logger.info(
`retrieved withdraw threshold for ${addr(
asset
)}: ${threshold.toString()}`
);
setThreshold(contractAddress, threshold);
return threshold;
} catch (err) {
logger.error('could not get the withdraw thresholds', err);
return undefined;
async (
asset:
| Pick<WithdrawalBusEventFieldsFragment['asset'], 'source' | 'decimals'>
| undefined
) => {
if (!contract || asset?.source.__typename !== 'ERC20') {
return new BigNumber(Infinity);
}
const res = await contract.get_withdraw_threshold(
asset.source.contractAddress
);
const value = new BigNumber(addDecimal(res.toString(), asset.decimals));
const threshold = value.isEqualTo(0)
? new BigNumber(Infinity)
: value.minus(new BigNumber(addDecimal('1', asset.decimals)));
return threshold;
},
[contract, logger]
[contract]
);
return getThreshold;
@@ -666,12 +666,6 @@ export const useVegaTransactionToasts = () => {
const closeAfter =
isFinal(tx) && !isWithdrawTransaction(tx.body) ? CLOSE_AFTER : undefined;
// marks "Funds unlocked" toast so it can be found in eth toasts
const meta =
isFinal(tx) && isWithdrawTransaction(tx.body)
? { withdrawalId: tx.withdrawal?.id }
: undefined;
return {
id: `vega-${tx.id}`,
intent,
@@ -679,7 +673,6 @@ export const useVegaTransactionToasts = () => {
loader: tx.status === VegaTxStatus.Pending,
content,
closeAfter,
meta,
};
};
-5
View File
@@ -9,11 +9,6 @@ import { initializeUrlConnector } from './url-connector';
import { WALLETCONNECT_PROJECT_ID } from './constants';
import { useWeb3ConnectStore } from './web3-connect-store';
import { theme } from '@vegaprotocol/tailwindcss-config';
import { ethers } from 'ethers';
export const createDefaultProvider = (providerUrl: string, chainId: number) => {
return new ethers.providers.JsonRpcProvider(providerUrl, chainId);
};
export const initializeCoinbaseConnector = (providerUrl: string) =>
initializeConnector<CoinbaseWallet>(
+6 -15
View File
@@ -1,21 +1,14 @@
import { useMemo } from 'react';
import { Web3ReactProvider } from '@web3-react/core';
import type { Web3ReactHooks } from '@web3-react/core';
import { Web3ReactProvider } from '@web3-react/core';
import type { Connector } from '@web3-react/types';
import type { DefaultWeb3ProviderContextShape } from './default-web3-provider';
import { DefaultWeb3ProviderContext } from './default-web3-provider';
import { useMemo } from 'react';
interface Web3ProviderProps {
children: JSX.Element | JSX.Element[];
connectors: [Connector, Web3ReactHooks][];
defaultProvider?: DefaultWeb3ProviderContextShape['provider'];
}
export const Web3Provider = ({
children,
connectors,
defaultProvider,
}: Web3ProviderProps) => {
export const Web3Provider = ({ children, connectors }: Web3ProviderProps) => {
/**
* The connectors prop passed to Web3ReactProvider must be referentially static.
* https://github.com/Uniswap/web3-react/blob/31742897f9fddb38e00e36c2516029d3df9a9c54/packages/core/src/provider.tsx#L66
@@ -27,10 +20,8 @@ export const Web3Provider = ({
);
return (
<DefaultWeb3ProviderContext.Provider value={{ provider: defaultProvider }}>
<Web3ReactProvider key={key} connectors={connectors}>
{children}
</Web3ReactProvider>
</DefaultWeb3ProviderContext.Provider>
<Web3ReactProvider key={key} connectors={connectors}>
{children}
</Web3ReactProvider>
);
};
-1
View File
@@ -11,4 +11,3 @@ export * from './lib/withdrawal-dialog';
export * from './lib/withdrawal-feedback';
export * from './lib/withdrawals-provider';
export * from './lib/withdrawals-table';
export * from './lib/use-ready-to-complete-withdrawals-toast';
@@ -1,207 +0,0 @@
import * as Types from '@vegaprotocol/types';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
import BigNumber from 'bignumber.js';
import * as web3 from '@vegaprotocol/web3';
import { renderHook, waitFor } from '@testing-library/react';
import { useIncompleteWithdrawals } from './use-ready-to-complete-withdrawals-toast';
import { MockedProvider } from '@apollo/client/testing';
import { useVegaWallet } from '@vegaprotocol/wallet';
jest.mock('@vegaprotocol/web3');
jest.mock('@vegaprotocol/wallet');
type Asset = WithdrawalFieldsFragment['asset'];
type Withdrawal = WithdrawalFieldsFragment;
const NO_THRESHOLD_ASSET: Asset = {
__typename: 'Asset',
id: 'NO_THRESHOLD_ASSET',
name: 'NO_THRESHOLD_ASSET',
symbol: 'NTA',
decimals: 1,
status: Types.AssetStatus.STATUS_ENABLED,
source: {
__typename: 'ERC20',
contractAddress: '0xnta',
},
};
const LOW_THRESHOLD_ASSET: Asset = {
__typename: 'Asset',
id: 'LOW_THRESHOLD_ASSET',
name: 'LOW_THRESHOLD_ASSET',
symbol: 'LTA',
decimals: 1,
status: Types.AssetStatus.STATUS_ENABLED,
source: {
__typename: 'ERC20',
contractAddress: '0xlta',
},
};
const HIGH_THRESHOLD_ASSET: Asset = {
__typename: 'Asset',
id: 'HIGH_THRESHOLD_ASSET',
name: 'HIGH_THRESHOLD_ASSET',
symbol: 'HTA',
decimals: 1,
status: Types.AssetStatus.STATUS_ENABLED,
source: {
__typename: 'ERC20',
contractAddress: '0xhta',
},
};
const NOW = 5000;
const DELAY = { value: 1, ts: 5000 };
const THRESHOLDS: Record<string, { value: BigNumber; ts: number }> = {
builtin: { value: new BigNumber(Infinity), ts: 5000 },
'0xnta': { value: new BigNumber(Infinity), ts: 5000 },
'0xlta': { value: new BigNumber(10), ts: 5000 },
'0xhta': { value: new BigNumber(1000), ts: 5000 },
};
const ts = (ms: number) => new Date(ms).toISOString();
// ready
const mockIncompleteW1: Withdrawal = {
id: 'w1',
status: Types.WithdrawalStatus.STATUS_FINALIZED,
amount: '10000000000',
createdTimestamp: ts(10),
pendingOnForeignChain: true,
withdrawnTimestamp: null,
txHash: null,
asset: NO_THRESHOLD_ASSET,
};
// delay (10 + 1000 < 5000), ready
const mockIncompleteW2: Withdrawal = {
id: 'w2',
status: Types.WithdrawalStatus.STATUS_FINALIZED,
amount: '100',
createdTimestamp: ts(10),
pendingOnForeignChain: true,
withdrawnTimestamp: null,
txHash: null,
asset: LOW_THRESHOLD_ASSET,
};
// delay (4500 + 1000 > 5000), below threshold, ready
const mockIncompleteW3: Withdrawal = {
id: 'w3',
status: Types.WithdrawalStatus.STATUS_FINALIZED,
amount: '1000',
createdTimestamp: ts(4500),
pendingOnForeignChain: true,
withdrawnTimestamp: null,
txHash: null,
asset: HIGH_THRESHOLD_ASSET,
};
// delay (5000 + 1000 > 5000), delayed
const mockIncompleteW4: Withdrawal = {
id: 'w4',
status: Types.WithdrawalStatus.STATUS_FINALIZED,
amount: '10000',
createdTimestamp: ts(5000),
pendingOnForeignChain: true,
withdrawnTimestamp: null,
txHash: null,
asset: HIGH_THRESHOLD_ASSET,
};
// delay (4001 + 1000 > 5000), delayed
const mockIncompleteW5: Withdrawal = {
id: 'w5',
status: Types.WithdrawalStatus.STATUS_FINALIZED,
amount: '100000',
createdTimestamp: ts(4001),
pendingOnForeignChain: true,
withdrawnTimestamp: null,
txHash: null,
asset: HIGH_THRESHOLD_ASSET,
};
// completed
const mockCompleteW1: Withdrawal = {
id: 'cw1',
status: Types.WithdrawalStatus.STATUS_FINALIZED,
amount: '1000',
createdTimestamp: ts(10),
pendingOnForeignChain: false,
withdrawnTimestamp: ts(11),
txHash: '0xcompleted',
asset: HIGH_THRESHOLD_ASSET,
};
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: () => ({
data: [
mockIncompleteW1,
mockIncompleteW2,
mockIncompleteW3,
mockIncompleteW4,
mockIncompleteW5,
mockCompleteW1,
],
}),
}));
describe('useIncompleteWithdrawals', () => {
let DATE_NOW: jest.SpyInstance<number, []>;
beforeAll(() => {
// mocks Date.now() to always return the same point in time.
DATE_NOW = jest.spyOn(Date, 'now').mockImplementation(() => NOW);
(useVegaWallet as jest.Mock).mockReturnValue({
pubKey: '0xpubkey',
isReadOnly: false,
});
(web3.useGetWithdrawThreshold as jest.Mock).mockImplementation(
() => (asset: Asset) => {
if (asset.source.__typename === 'ERC20') {
return Promise.resolve(
THRESHOLDS[asset.source.contractAddress].value
);
}
return Promise.resolve(Infinity);
}
);
(web3.useGetWithdrawDelay as jest.Mock).mockImplementation(() => () => {
return Promise.resolve(DELAY.value);
});
(web3.addr as jest.Mock).mockImplementation((asset: Asset | undefined) =>
asset && asset.source.__typename === 'ERC20'
? asset.source.contractAddress
: 'builtin'
);
});
afterAll(() => {
jest.resetAllMocks();
DATE_NOW.mockRestore();
});
it('returns a collection of ready to complete withdrawals', async () => {
const { result } = renderHook(() => useIncompleteWithdrawals(), {
wrapper: MockedProvider,
});
await waitFor(async () => {
const { ready } = result.current;
expect(ready).toHaveLength(3);
expect(ready.map((w) => w.data.id)).toContain(mockIncompleteW1.id);
expect(ready.map((w) => w.data.id)).toContain(mockIncompleteW2.id);
expect(ready.map((w) => w.data.id)).toContain(mockIncompleteW3.id);
});
});
it('returns a collection of delayed withdrawals', async () => {
const { result } = renderHook(() => useIncompleteWithdrawals(), {
wrapper: MockedProvider,
});
await waitFor(async () => {
const { delayed } = result.current;
expect(delayed).toHaveLength(2);
expect(delayed.map((w) => w.data.id)).toContain(mockIncompleteW4.id);
expect(delayed.map((w) => w.data.id)).toContain(mockIncompleteW5.id);
});
});
});
@@ -1,277 +0,0 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
useVegaWallet,
useWithdrawalApprovalQuery,
} from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { Button, Intent, Panel, ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { useNavigate } from 'react-router-dom';
import {
useEthWithdrawApprovalsStore,
useGetWithdrawDelay,
useGetWithdrawThreshold,
} from '@vegaprotocol/web3';
import { withdrawalProvider } from './withdrawals-provider';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
import uniqBy from 'lodash/uniqBy';
const CHECK_INTERVAL = 1000;
const ON_APP_START_TOAST_ID = `ready-to-withdraw`;
type UseReadyToWithdrawalToastsOptions = {
withdrawalsLink: string;
};
export type TimestampedWithdrawals = {
data: WithdrawalFieldsFragment;
timestamp: number | undefined;
}[];
export const useIncompleteWithdrawals = () => {
const [ready, setReady] = useState<TimestampedWithdrawals>([]);
const [delayed, setDelayed] = useState<TimestampedWithdrawals>([]);
const { pubKey, isReadOnly } = useVegaWallet();
const { data } = useDataProvider({
dataProvider: withdrawalProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey || isReadOnly,
});
const getDelay = useGetWithdrawDelay(); // seconds
const incompleteWithdrawals = useMemo(
() => data?.filter((w) => !w.txHash),
[data]
);
const assets = useMemo(
() =>
uniqBy(
incompleteWithdrawals?.map((w) => w.asset),
(a) => a.id
),
[incompleteWithdrawals]
);
const getThreshold = useGetWithdrawThreshold();
const checkWithdraws = useCallback(async () => {
if (assets.length === 0) return;
// trigger delay
// trigger thresholds
return await Promise.all([
getDelay(),
...assets.map((asset) => getThreshold(asset)),
]).then(([delay, ...thresholds]) => ({
delay,
thresholds: assets.reduce<Record<string, BigNumber | undefined>>(
(all, asset, index) =>
Object.assign(all, { [asset.id]: thresholds[index] }),
{}
),
}));
}, [assets, getDelay, getThreshold]);
useEffect(() => {
checkWithdraws().then((retrieved) => {
if (
!retrieved ||
retrieved.delay === undefined ||
!incompleteWithdrawals
) {
return;
}
const { thresholds, delay } = retrieved;
const timestamped = incompleteWithdrawals.map((w) => {
let timestamp = undefined;
const threshold = thresholds[w.asset.id];
if (threshold) {
timestamp = 0;
if (new BigNumber(w.amount).isGreaterThan(threshold)) {
const created = w.createdTimestamp;
timestamp = new Date(created).getTime() + (delay as number) * 1000;
}
}
return {
data: w,
timestamp,
};
});
const delayed = timestamped?.filter(
(item) => item.timestamp != null && Date.now() < item.timestamp
);
const ready = timestamped?.filter(
(item) => item.timestamp != null && Date.now() >= item.timestamp
);
setReady(ready);
setDelayed(delayed);
});
}, [checkWithdraws, incompleteWithdrawals]);
return { ready, delayed };
};
export const useReadyToWithdrawalToasts = ({
withdrawalsLink,
}: UseReadyToWithdrawalToastsOptions) => {
const [setToast, hasToast, updateToast, removeToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.update,
store.remove,
]);
const { delayed, ready } = useIncompleteWithdrawals();
const onClose = useCallback(() => {
// hides toast instead of removing is so it won't be re-added on rerender
updateToast(ON_APP_START_TOAST_ID, { hidden: true });
}, [updateToast]);
useEffect(() => {
// set on app start toast if there are withdrawals ready to complete
if (ready.length > 0) {
// set only once, unless removed
if (!hasToast(ON_APP_START_TOAST_ID)) {
const appStartToast: Toast = {
id: ON_APP_START_TOAST_ID,
intent: Intent.Warning,
content:
ready.length === 1 ? (
<SingleReadyToWithdrawToastContent withdrawal={ready[0].data} />
) : (
<MultipleReadyToWithdrawToastContent
count={ready.length}
withdrawalsLink={withdrawalsLink}
/>
),
onClose,
};
setToast(appStartToast);
}
}
// set toast whenever a withdrawal delay is passed
let interval: NodeJS.Timer;
if (delayed.length > 0) {
interval = setInterval(() => {
const ready = delayed.filter(
(item) => item.timestamp && Date.now() >= item.timestamp
);
for (const withdrawal of ready) {
const id = `complete-withdrawal-${withdrawal.data.id}`;
const toast: Toast = {
id,
intent: Intent.Warning,
content: (
<SingleReadyToWithdrawToastContent withdrawal={withdrawal.data} />
),
onClose: () => {
updateToast(id, { hidden: true });
},
};
if (!hasToast(id)) setToast(toast);
}
}, CHECK_INTERVAL);
}
return () => {
clearInterval(interval);
};
}, [
delayed,
hasToast,
onClose,
ready,
removeToast,
setToast,
updateToast,
withdrawalsLink,
]);
};
const MultipleReadyToWithdrawToastContent = ({
count,
withdrawalsLink,
}: {
count: number;
withdrawalsLink?: string;
}) => {
const navigate = useNavigate();
return (
<>
<ToastHeading>{t('Withdrawals ready')}</ToastHeading>
<p>
{t(
'Complete these %s withdrawals to release your funds',
count.toString()
)}
</p>
<p className="mt-1">
<Button
data-testid="toast-view-withdrawals"
size="xs"
onClick={() =>
withdrawalsLink ? navigate(withdrawalsLink) : undefined
}
>
{t('View withdrawals')}
</Button>
</p>
</>
);
};
const SingleReadyToWithdrawToastContent = ({
withdrawal,
}: {
withdrawal: WithdrawalFieldsFragment;
}) => {
const { createEthWithdrawalApproval } = useEthWithdrawApprovalsStore(
(state) => ({
createEthWithdrawalApproval: state.create,
})
);
const { data: approval } = useWithdrawalApprovalQuery({
variables: {
withdrawalId: withdrawal.id,
},
});
const completeButton = (
<p className="mt-1">
<Button
data-testid="toast-complete-withdrawal"
size="xs"
onClick={() => {
createEthWithdrawalApproval(
withdrawal,
approval?.erc20WithdrawalApproval
);
}}
>
{t('Complete withdrawal')}
</Button>
</p>
);
const amount = formatNumber(
toBigNum(withdrawal.amount, withdrawal.asset.decimals),
withdrawal.asset.decimals
);
return (
<>
<ToastHeading>{t('Withdrawal ready')}</ToastHeading>
<p>{t('Complete the withdrawal to release your funds')}</p>
<Panel>
<strong>
{t('Withdraw')} {amount} {withdrawal.asset.symbol}
</strong>
</Panel>
{completeButton}
</>
);
};
@@ -82,19 +82,16 @@ export const useVerifyWithdrawal = () => {
if (threshold && amount.isGreaterThan(threshold)) {
const delaySecs = await getDelay();
if (delaySecs != null) {
const completeTimestamp =
new Date(withdrawal.createdTimestamp).getTime() +
delaySecs * 1000;
const completeTimestamp =
new Date(withdrawal.createdTimestamp).getTime() + delaySecs * 1000;
if (Date.now() < completeTimestamp) {
setState({
status: ApprovalStatus.Delayed,
threshold,
completeTimestamp,
});
return false;
}
if (Date.now() < completeTimestamp) {
setState({
status: ApprovalStatus.Delayed,
threshold,
completeTimestamp,
});
return false;
}
}
@@ -62,8 +62,8 @@ export const useWithdrawAsset = (
try {
logger.info('get withdraw asset data', { asset: asset?.id });
const result = await Promise.all([getThreshold(asset), getDelay()]);
if (result[0] != null) threshold = result[0];
if (result[1] != null) delay = result[1];
threshold = result[0];
delay = result[1];
} catch (err) {
logger.error('get withdraw asset data', err);
}
@@ -5,11 +5,6 @@ import { useAccountBalance } from '@vegaprotocol/accounts';
import { WithdrawFormContainer } from './withdraw-form-container';
import * as Types from '@vegaprotocol/types';
import { useWeb3React } from '@web3-react/core';
import {
useGetWithdrawDelay,
useGetWithdrawThreshold,
} from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
let mockData: Account[] | null = null;
jest.mock('@vegaprotocol/data-provider', () => ({
@@ -32,7 +27,6 @@ jest.mock('@vegaprotocol/network-parameters', () => {
}),
};
});
jest.mock('@vegaprotocol/web3');
describe('WithdrawFormContainer', () => {
const props = {
@@ -116,12 +110,6 @@ describe('WithdrawFormContainer', () => {
accountBalance: 0,
accountDecimals: null,
});
(useGetWithdrawThreshold as jest.Mock).mockReturnValue(() =>
Promise.resolve(new BigNumber(Infinity))
);
(useGetWithdrawDelay as jest.Mock).mockReturnValue(() =>
Promise.resolve(60)
);
});
afterAll(() => {
jest.resetAllMocks();
+16 -77
View File
@@ -1,9 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import {
addDecimalsFormatNumber,
convertToCountdownString,
getDateTimeFormat,
isNumeric,
truncateByChars,
@@ -33,14 +32,9 @@ import {
useWithdrawalApprovalDialog,
} from '@vegaprotocol/web3';
import * as Schema from '@vegaprotocol/types';
import type { TimestampedWithdrawals } from './use-ready-to-complete-withdrawals-toast';
import classNames from 'classnames';
export const WithdrawalsTable = (
props: TypedDataAgGrid<WithdrawalFieldsFragment> & {
ready?: TimestampedWithdrawals;
delayed?: TimestampedWithdrawals;
}
props: TypedDataAgGrid<WithdrawalFieldsFragment>
) => {
const gridRef = useRef<AgGridReact | null>(null);
const createWithdrawApproval = useEthWithdrawApprovalsStore(
@@ -131,7 +125,6 @@ export const WithdrawalsTable = (
<AgGridColumn
headerName={t('Status')}
field="status"
cellRendererParams={{ ready: props.ready, delayed: props.delayed }}
cellRenderer="StatusCell"
/>
<AgGridColumn
@@ -218,74 +211,20 @@ export const EtherscanLinkCell = ({
);
};
export const StatusCell = ({
data,
ready,
delayed,
}: {
data: WithdrawalFieldsFragment;
ready?: TimestampedWithdrawals;
delayed?: TimestampedWithdrawals;
}) => {
const READY_TO_COMPLETE = t('Ready to complete');
const DELAYED = (readyIn: string) => t('Delayed (ready in %s)', readyIn);
const PENDING = t('Pending');
const COMPLETED = t('Completed');
const REJECTED = t('Rejected');
const FAILED = t('Failed');
const isPending = data.pendingOnForeignChain || !data.txHash;
const isReady = ready?.find((w) => w.data.id === data.id);
const isDelayed = delayed?.find((w) => w.data.id === data.id);
const determineLabel = () => {
if (isPending) {
if (isReady) {
return READY_TO_COMPLETE;
}
return PENDING;
}
if (data.status === Schema.WithdrawalStatus.STATUS_FINALIZED) {
return COMPLETED;
}
if (data.status === Schema.WithdrawalStatus.STATUS_REJECTED) {
return REJECTED;
}
return FAILED;
};
const [label, setLabel] = useState<string | undefined>(determineLabel());
useEffect(() => {
// handle countdown for delayed withdrawals
let interval: NodeJS.Timer;
if (!data || !isDelayed || isDelayed.timestamp == null || !isPending) {
return;
}
// eslint-disable-next-line prefer-const
interval = setInterval(() => {
if (isDelayed.timestamp == null) return;
const remaining = Date.now() - isDelayed.timestamp;
if (remaining < 0) {
setLabel(DELAYED(convertToCountdownString(remaining, '0:00:00:00')));
} else {
setLabel(READY_TO_COMPLETE);
}
}, 1000);
return () => {
clearInterval(interval);
};
}, [READY_TO_COMPLETE, data, delayed, isDelayed, isPending]);
return data ? (
<span
className={classNames({
'text-vega-blue-450': label === READY_TO_COMPLETE,
})}
>
{label}
</span>
) : null;
export const StatusCell = ({ data }: { data: WithdrawalFieldsFragment }) => {
if (!data) {
return null;
}
if (data.pendingOnForeignChain || !data.txHash) {
return <span>{t('Pending')}</span>;
}
if (data.status === Schema.WithdrawalStatus.STATUS_FINALIZED) {
return <span>{t('Completed')}</span>;
}
if (data.status === Schema.WithdrawalStatus.STATUS_REJECTED) {
return <span>{t('Rejected')}</span>;
}
return <span>{t('Failed')}</span>;
};
const RecipientCell = ({
+1 -1
View File
@@ -12,7 +12,7 @@
},
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"],
"accessToken": "OTY4ZjdlZTItNGIwNy00NDcyLTllZjctOWIzYTg1NWE0Yzg1fHJlYWQtd3JpdGU=",