* feat: #847 show progress bar, margin accounts, no used/deposited * feat: #847 add collateral tables * fix: #847 add deposit asset type and fix tests * feat: #847 show deposited value, avaliable and percentage used * fix: #847 add styling fixes * fix: #847 add deposit new asset button * fix: #847 remove disabledSelect to fix withdraw and deposit dialogs * fix: #847 remove global reward from incoming - needs to be party specific * fix: #847 integration tests * fix: #847 default select deposit & withdraw * fix: #847 default select deposit & withdraw * fix: #847 pass asset id as default value * fix: #847 use only bigint no bignumber, remove NaN check * fix: #847 update deposit-form.spec.tsx * fix: revert update on account fields * feat: add storybook set up * chore: ignore apollo errors - to be reverted after API will be fixed * fix: container moved, progress bar in helpers * fix: #847 UI tweaks around accounts container * feat: #847 added useDepositAsset and useWithdrawAsset * fix: #847 fix progress bar in accounts and positions * feat: #847 add storybook * fix: #847 added tooltip and updated filtering * chore: #847 add get account data test * fix: #847 fix lint and type in account story * fix: #847 update data provider * fix: #847 fix get account data dry & lp link * fix: #847 fix breakdown table test * fix: #847 account data provider test * fix: #847 remove deposit new asset button - subscription does not display a sset data * fix: #847 add defaultValue in select otherwise default is not set up * feat: #847 update data provider update method and tables * fix: #847 fix accounts tests * fix: #847 remove unused getRows * fix: add decimals * fix: #847 fix imports * fix: update ids * Update apps/trading/pages/liquidity/[marketId].page.tsx * fix: #847 accounts update method check delta * fix: #847 use vega value formatters and cell renderers * fix: #847 fix imports * fix: #847 handle new account else block comment * fix: accounts and breakdown tables * fix(#847): account data provider improvments * fix: #847 fix formatters null check and add param * fix: #847 fix withdraw test and mock the hook * fix: #847 fix console lite grid select market test * fix: console lite build * fix: revert withdraw limits * fix: remove redundant waitFor use vega cell renderer * fix: breakdown display only use accounts * fix: breakdown display only use accounts * fix: updated accounts table * fix: move update inside try useWithdrawAsset * fix: update trading-accounts test * fix: portfolio-page.test.ts Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
This commit is contained in:
parent
835bf36393
commit
907a4e256e
@ -56,7 +56,7 @@ describe('Portfolio page', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 3);
|
||||
cy.get(
|
||||
'.ag-center-cols-container [row-id="ACCOUNT_TYPE_GENERAL-asset-id-null"]'
|
||||
)
|
||||
|
@ -105,9 +105,7 @@ const ConsoleLiteGrid = <T extends { id?: string }>(
|
||||
);
|
||||
};
|
||||
|
||||
const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as <
|
||||
T extends { id?: string }
|
||||
>(
|
||||
const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as <T>(
|
||||
p: Props<T> & { ref?: React.Ref<AgGridReact> }
|
||||
) => React.ReactElement;
|
||||
|
||||
|
@ -1,16 +1,10 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useMemo, useRef, useCallback } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { PriceCell, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
AccountFieldsFragment,
|
||||
AccountEventsSubscription,
|
||||
} from '@vegaprotocol/accounts';
|
||||
import {
|
||||
accountsDataProvider,
|
||||
accountsManagerUpdate,
|
||||
getId,
|
||||
} from '@vegaprotocol/accounts';
|
||||
import type { AccountFields } from '@vegaprotocol/accounts';
|
||||
import { aggregatedAccountsDataProvider, getId } from '@vegaprotocol/accounts';
|
||||
import type { IGetRowsParams } from 'ag-grid-community';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
@ -20,20 +14,43 @@ import { NO_DATA_MESSAGE } from '../../../constants';
|
||||
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||
import { useAccountColumnDefinitions } from '.';
|
||||
|
||||
interface AccountObj extends AccountFieldsFragment {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const AccountsManager = () => {
|
||||
const { partyId = '' } = useOutletContext<{ partyId: string }>();
|
||||
const { isOpen, symbol, setOpen } = useAssetDetailsDialogStore();
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const dataRef = useRef<AccountFields[] | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const update = useMemo(() => accountsManagerUpdate(gridRef), []);
|
||||
const { data, error, loading } = useDataProvider<
|
||||
AccountFieldsFragment[],
|
||||
AccountEventsSubscription['accounts']
|
||||
>({ dataProvider: accountsDataProvider, update, variables });
|
||||
const update = useCallback(
|
||||
({ data }: { data: AccountFields[] | null }) => {
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
if (dataRef.current?.length) {
|
||||
dataRef.current = data;
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
const { data, error, loading } = useDataProvider<AccountFields[], never>({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
update,
|
||||
variables,
|
||||
});
|
||||
dataRef.current = data;
|
||||
const getRows = async ({
|
||||
successCallback,
|
||||
startRow,
|
||||
endRow,
|
||||
}: IGetRowsParams) => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
};
|
||||
const { columnDefs, defaultColDef } = useAccountColumnDefinitions();
|
||||
return (
|
||||
<>
|
||||
@ -43,8 +60,11 @@ const AccountsManager = () => {
|
||||
data={data}
|
||||
noDataMessage={NO_DATA_MESSAGE}
|
||||
>
|
||||
<ConsoleLiteGrid<AccountObj>
|
||||
rowData={data as AccountObj[]}
|
||||
<ConsoleLiteGrid<AccountFields>
|
||||
rowData={data?.length ? undefined : []}
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
ref={gridRef}
|
||||
datasource={{ getRows }}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
components={{ PriceCell }}
|
||||
|
@ -1,45 +1,9 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import type { SummaryRow } from '@vegaprotocol/react-helpers';
|
||||
import type { AccountFieldsFragment } from '@vegaprotocol/accounts';
|
||||
import type { AccountFields } from '@vegaprotocol/accounts';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type {
|
||||
ColDef,
|
||||
GroupCellRendererParams,
|
||||
ValueFormatterParams,
|
||||
} from 'ag-grid-community';
|
||||
import type { AccountType } from '@vegaprotocol/types';
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
|
||||
interface AccountsTableValueFormatterParams extends ValueFormatterParams {
|
||||
data: AccountFieldsFragment;
|
||||
}
|
||||
|
||||
const comparator = (
|
||||
valueA: string,
|
||||
valueB: string,
|
||||
nodeA: { data: AccountFieldsFragment & SummaryRow },
|
||||
nodeB: { data: AccountFieldsFragment & SummaryRow },
|
||||
isInverted: boolean
|
||||
) => {
|
||||
if (valueA < valueB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (valueA > valueB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nodeA.data.__summaryRow) {
|
||||
return isInverted ? -1 : 1;
|
||||
}
|
||||
|
||||
if (nodeB.data.__summaryRow) {
|
||||
return isInverted ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
import type { ColDef, GroupCellRendererParams } from 'ag-grid-community';
|
||||
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const useAccountColumnDefinitions = () => {
|
||||
const { open } = useAssetDetailsDialogStore();
|
||||
@ -49,7 +13,6 @@ const useAccountColumnDefinitions = () => {
|
||||
colId: 'account-asset',
|
||||
headerName: t('Asset'),
|
||||
field: 'asset.symbol',
|
||||
comparator,
|
||||
headerClass: 'uppercase justify-start',
|
||||
cellClass: 'uppercase flex h-full items-center md:pl-4',
|
||||
cellRenderer: ({ value }: GroupCellRendererParams) =>
|
||||
@ -71,27 +34,25 @@ const useAccountColumnDefinitions = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
colId: 'type',
|
||||
headerName: t('Type'),
|
||||
field: 'type',
|
||||
cellClass: 'uppercase !flex h-full items-center',
|
||||
valueFormatter: ({ value }: ValueFormatterParams) =>
|
||||
value ? AccountTypeMapping[value as AccountType] : '-',
|
||||
},
|
||||
{
|
||||
colId: 'market',
|
||||
headerName: t('Market'),
|
||||
cellClass: 'uppercase !flex h-full items-center',
|
||||
field: 'market.tradableInstrument.instrument.name',
|
||||
valueFormatter: "value || '—'",
|
||||
},
|
||||
{
|
||||
colId: 'balance',
|
||||
headerName: t('Balance'),
|
||||
field: 'balance',
|
||||
cellClass: 'uppercase !flex h-full items-center',
|
||||
colId: 'deposited',
|
||||
headerName: t('Deposited'),
|
||||
field: 'deposited',
|
||||
cellRenderer: 'PriceCell',
|
||||
valueFormatter: ({ value, data }: AccountsTableValueFormatterParams) =>
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'deposited'>) =>
|
||||
addDecimalsFormatNumber(value, data.asset.decimals),
|
||||
},
|
||||
{
|
||||
colId: 'used',
|
||||
headerName: t('Used'),
|
||||
field: 'used',
|
||||
cellRenderer: 'PriceCell',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'used'>) =>
|
||||
addDecimalsFormatNumber(value, data.asset.decimals),
|
||||
},
|
||||
];
|
||||
|
@ -11,7 +11,7 @@ import type {
|
||||
PositionsTableValueFormatterParams,
|
||||
Position,
|
||||
} from '@vegaprotocol/positions';
|
||||
import { AmountCell, ProgressBarCell } from '@vegaprotocol/positions';
|
||||
import { AmountCell } from '@vegaprotocol/positions';
|
||||
import type {
|
||||
CellRendererSelectorResult,
|
||||
ICellRendererParams,
|
||||
@ -20,7 +20,7 @@ import type {
|
||||
ColDef,
|
||||
} from 'ag-grid-community';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const EmptyCell = () => '';
|
||||
|
||||
|
@ -70,6 +70,7 @@ const SimpleMarketList = () => {
|
||||
rowData={localData}
|
||||
defaultColDef={defaultColDef}
|
||||
handleRowClicked={handleRowClicked}
|
||||
getRowId={({ data }) => data.id}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
</div>
|
||||
|
@ -28,19 +28,13 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('tab-accounts')
|
||||
.should('be.visible')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'General');
|
||||
.find('[col-id="breakdown"]')
|
||||
.should('have.text', 'Collateral breakdown');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.should('be.visible')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="market.tradableInstrument.instrument.name"]')
|
||||
.should('have.text', '—');
|
||||
|
||||
cy.getByTestId('tab-accounts')
|
||||
.should('be.visible')
|
||||
.get(tradingAccountRowId)
|
||||
.find('[col-id="balance"]')
|
||||
.find('[col-id="deposited"]')
|
||||
.should('have.text', '1,000.00000');
|
||||
});
|
||||
});
|
||||
|
@ -19,9 +19,7 @@ describe('collateral', { tags: '@smoke' }, () => {
|
||||
it('renders collateral', () => {
|
||||
connectVegaWallet();
|
||||
cy.getByTestId(collateralTab).click();
|
||||
cy.get(assetSymbolColumn).each(($symbol) => {
|
||||
cy.wrap($symbol).invoke('text').should('not.be.empty');
|
||||
});
|
||||
cy.getByTestId(assetSymbolColumn).first().click();
|
||||
cy.get(assetTypeColumn).should('contain.text', 'General');
|
||||
cy.get(assetMarketName).should(
|
||||
'contain.text',
|
||||
|
@ -70,7 +70,7 @@ describe('withdraw', { tags: '@smoke' }, () => {
|
||||
it('can set amount using use maximum button', () => {
|
||||
cy.get(assetSelectField).select('Asset 0');
|
||||
cy.getByTestId(useMaximumAmount).click();
|
||||
cy.get(amountField).should('have.value', '1000.00000');
|
||||
cy.get(amountField).should('have.value', '1,000.00000');
|
||||
});
|
||||
|
||||
it('triggers transaction when submitted', () => {
|
||||
@ -87,7 +87,7 @@ describe('withdraw', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('balance-available')
|
||||
.should('contain.text', 'Balance available')
|
||||
.find('td')
|
||||
.should('have.text', '1000');
|
||||
.should('have.text', '1,000');
|
||||
cy.getByTestId('withdrawal-threshold')
|
||||
.should('contain.text', 'Delayed withdrawal threshold')
|
||||
.find('td')
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { LiquidityTable, useLiquidityProvision } from '@vegaprotocol/liquidity';
|
||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import { LiquidityProvisionStatus } from '@vegaprotocol/types';
|
||||
import { AsyncRenderer, Tab, Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Tab,
|
||||
Tabs,
|
||||
Link as UiToolkitLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { Header, HeaderStat } from '../../components/header';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { tooltipMapping } from '@vegaprotocol/market-info';
|
||||
import Link from 'next/link';
|
||||
|
||||
const LiquidityPage = ({ id }: { id?: string }) => {
|
||||
const { query, push } = useRouter();
|
||||
const { query } = useRouter();
|
||||
const { keypair } = useVegaWallet();
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
|
||||
@ -69,9 +75,11 @@ const LiquidityPage = ({ id }: { id?: string }) => {
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<Header
|
||||
title={
|
||||
<button onClick={() => push(`/markets/${marketId}`)}>{`${name} ${t(
|
||||
'liquidity provision'
|
||||
)}`}</button>
|
||||
<Link href={`/markets/${marketId}`} passHref={true}>
|
||||
<UiToolkitLink>
|
||||
{`${name} ${t('liquidity provision')}`}
|
||||
</UiToolkitLink>
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<HeaderStat
|
||||
|
@ -13,7 +13,6 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Market_market } from './__generated__/Market';
|
||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
import {
|
||||
@ -41,6 +40,7 @@ import {
|
||||
} from '@vegaprotocol/types';
|
||||
import { TradingModeTooltip } from '../../components/trading-mode-tooltip';
|
||||
import { Header, HeaderStat } from '../../components/header';
|
||||
import { AccountsContainer } from '../portfolio/accounts-container';
|
||||
|
||||
const TradingViews = {
|
||||
Candles: CandlesChartContainer,
|
||||
|
89
apps/trading/pages/portfolio/accounts-container.tsx
Normal file
89
apps/trading/pages/portfolio/accounts-container.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { useState } from 'react';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { WithdrawalDialogs } from '@vegaprotocol/withdraws';
|
||||
import { Web3Container } from '@vegaprotocol/web3';
|
||||
import { DepositContainer } from '@vegaprotocol/deposits';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountManager } from '@vegaprotocol/accounts';
|
||||
|
||||
export const AccountsContainer = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const [depositDialog, setDepositDialog] = useState(false);
|
||||
|
||||
if (!keypair) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('Please connect Vega wallet')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Web3Container>
|
||||
<div className="h-full">
|
||||
<AssetAccountTable partyId={keypair.pub} />
|
||||
<DepositDialog
|
||||
depositDialog={depositDialog}
|
||||
setDepositDialog={setDepositDialog}
|
||||
/>
|
||||
</div>
|
||||
</Web3Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const AssetAccountTable = ({ partyId }: { partyId: string }) => {
|
||||
const [withdrawDialog, setWithdrawDialog] = useState(false);
|
||||
const [depositDialog, setDepositDialog] = useState(false);
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const [assetId, setAssetId] = useState<string>();
|
||||
return (
|
||||
<>
|
||||
<AccountManager
|
||||
partyId={partyId}
|
||||
onClickAsset={(value) => {
|
||||
value && openAssetDetailsDialog(value);
|
||||
}}
|
||||
onClickWithdraw={(assetId) => {
|
||||
setWithdrawDialog(true);
|
||||
setAssetId(assetId);
|
||||
}}
|
||||
onClickDeposit={(assetId) => {
|
||||
setDepositDialog(true);
|
||||
setAssetId(assetId);
|
||||
}}
|
||||
/>
|
||||
<WithdrawalDialogs
|
||||
assetId={assetId}
|
||||
withdrawDialog={withdrawDialog}
|
||||
setWithdrawDialog={setWithdrawDialog}
|
||||
/>
|
||||
<DepositDialog
|
||||
assetId={assetId}
|
||||
depositDialog={depositDialog}
|
||||
setDepositDialog={setDepositDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface DepositDialogProps {
|
||||
assetId?: string;
|
||||
depositDialog: boolean;
|
||||
setDepositDialog: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const DepositDialog = ({
|
||||
assetId,
|
||||
depositDialog,
|
||||
setDepositDialog,
|
||||
}: DepositDialogProps) => {
|
||||
return (
|
||||
<Dialog open={depositDialog} onChange={setDepositDialog}>
|
||||
<h1 className="text-2xl mb-4">{t('Deposit')}</h1>
|
||||
<DepositContainer assetId={assetId} />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import { t, titlefy } from '@vegaprotocol/react-helpers';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||
import { ResizableGridPanel, Tab, Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import { WithdrawalsContainer } from './withdrawals-container';
|
||||
import { FillsContainer } from '@vegaprotocol/fills';
|
||||
@ -12,6 +11,7 @@ import { DepositsContainer } from './deposits-container';
|
||||
import { ResizableGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { AccountsContainer } from './accounts-container';
|
||||
|
||||
const Portfolio = () => {
|
||||
const { update } = useGlobalStore((store) => ({
|
||||
|
28
libs/accounts/.storybook/main.js
Normal file
28
libs/accounts/.storybook/main.js
Normal file
@ -0,0 +1,28 @@
|
||||
const rootMain = require('../../../.storybook/main');
|
||||
|
||||
module.exports = {
|
||||
...rootMain,
|
||||
|
||||
core: { ...rootMain.core, builder: 'webpack5' },
|
||||
|
||||
stories: [
|
||||
...rootMain.stories,
|
||||
'../src/lib/**/*.stories.mdx',
|
||||
'../src/lib/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
...rootMain.addons,
|
||||
'@nrwl/react/plugins/storybook',
|
||||
'storybook-addon-themes',
|
||||
],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
// apply any global webpack configs that might have been specified in .storybook/main.js
|
||||
if (rootMain.webpackFinal) {
|
||||
config = await rootMain.webpackFinal(config, { configType });
|
||||
}
|
||||
|
||||
// add your own webpack tweaks if needed
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
1
libs/accounts/.storybook/preview-head.html
Normal file
1
libs/accounts/.storybook/preview-head.html
Normal file
@ -0,0 +1 @@
|
||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
50
libs/accounts/.storybook/preview.js
Normal file
50
libs/accounts/.storybook/preview.js
Normal file
@ -0,0 +1,50 @@
|
||||
import './styles.scss';
|
||||
import { ThemeContext } from '@vegaprotocol/react-helpers';
|
||||
import { useEffect, useState } from 'react';
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
backgrounds: { disable: true },
|
||||
themes: {
|
||||
default: 'dark',
|
||||
list: [
|
||||
{ name: 'dark', class: ['dark', 'bg-black'], color: '#000' },
|
||||
{ name: 'light', class: '', color: '#FFF' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story, context) => {
|
||||
// storybook-addon-themes doesn't seem to provide the current selected
|
||||
// theme in context, we need to provide it in JS as some components
|
||||
// rely on it for rendering
|
||||
const [theme, setTheme] = useState(context.parameters.themes.default);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
if (mutationList.length) {
|
||||
const body = mutationList[0].target;
|
||||
if (body.classList.contains('dark')) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { attributes: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: 500 }}>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Story />
|
||||
</ThemeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
];
|
3
libs/accounts/.storybook/styles.scss
Normal file
3
libs/accounts/.storybook/styles.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
20
libs/accounts/.storybook/tsconfig.json
Normal file
20
libs/accounts/.storybook/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": ""
|
||||
},
|
||||
"files": [
|
||||
"../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../**/*.spec.ts",
|
||||
"../**/*.spec.js",
|
||||
"../**/*.spec.tsx",
|
||||
"../**/*.spec.jsx",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"include": ["../src/**/*", "*.js"]
|
||||
}
|
@ -38,6 +38,37 @@
|
||||
"jestConfig": "libs/accounts/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"executor": "@nrwl/storybook:storybook",
|
||||
"options": {
|
||||
"uiFramework": "@storybook/react",
|
||||
"port": 4400,
|
||||
"config": {
|
||||
"configFolder": "libs/accounts/.storybook"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"executor": "@nrwl/storybook:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"uiFramework": "@storybook/react",
|
||||
"outputPath": "dist/storybook/accounts",
|
||||
"config": {
|
||||
"configFolder": "libs/accounts/.storybook"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
111
libs/accounts/src/lib/__generated__/Accounts.ts
generated
Normal file
111
libs/accounts/src/lib/__generated__/Accounts.ts
generated
Normal file
@ -0,0 +1,111 @@
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type AccountFieldsFragment = { __typename?: 'Account', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } };
|
||||
|
||||
export type AccountsQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, accounts?: Array<{ __typename?: 'Account', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } }> | null } | null };
|
||||
|
||||
export type AccountEventsSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountEventsSubscription = { __typename?: 'Subscription', accounts: Array<{ __typename?: 'AccountUpdate', type: Types.AccountType, balance: string, assetId: string, marketId?: string | null }> };
|
||||
|
||||
export const AccountFieldsFragmentDoc = gql`
|
||||
fragment AccountFields on Account {
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const AccountsDocument = gql`
|
||||
query Accounts($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
accounts {
|
||||
...AccountFields
|
||||
}
|
||||
}
|
||||
}
|
||||
${AccountFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useAccountsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useAccountsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAccountsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAccountsQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAccountsQuery(baseOptions: Apollo.QueryHookOptions<AccountsQuery, AccountsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<AccountsQuery, AccountsQueryVariables>(AccountsDocument, options);
|
||||
}
|
||||
export function useAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountsQuery, AccountsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<AccountsQuery, AccountsQueryVariables>(AccountsDocument, options);
|
||||
}
|
||||
export type AccountsQueryHookResult = ReturnType<typeof useAccountsQuery>;
|
||||
export type AccountsLazyQueryHookResult = ReturnType<typeof useAccountsLazyQuery>;
|
||||
export type AccountsQueryResult = Apollo.QueryResult<AccountsQuery, AccountsQueryVariables>;
|
||||
export const AccountEventsDocument = gql`
|
||||
subscription AccountEvents($partyId: ID!) {
|
||||
accounts(partyId: $partyId) {
|
||||
type
|
||||
balance
|
||||
assetId
|
||||
marketId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useAccountEventsSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useAccountEventsSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAccountEventsSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAccountEventsSubscription({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAccountEventsSubscription(baseOptions: Apollo.SubscriptionHookOptions<AccountEventsSubscription, AccountEventsSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<AccountEventsSubscription, AccountEventsSubscriptionVariables>(AccountEventsDocument, options);
|
||||
}
|
||||
export type AccountEventsSubscriptionHookResult = ReturnType<typeof useAccountEventsSubscription>;
|
||||
export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult<AccountEventsSubscription>;
|
@ -108,4 +108,4 @@ export function useAccountEventsSubscription(baseOptions: Apollo.SubscriptionHoo
|
||||
return Apollo.useSubscription<AccountEventsSubscription, AccountEventsSubscriptionVariables>(AccountEventsDocument, options);
|
||||
}
|
||||
export type AccountEventsSubscriptionHookResult = ReturnType<typeof useAccountEventsSubscription>;
|
||||
export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult<AccountEventsSubscription>;
|
||||
export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult<AccountEventsSubscription>;
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountsManager } from './accounts-manager';
|
||||
|
||||
export const AccountsContainer = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
|
||||
if (!keypair) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('Please connect Vega wallet')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountsManager partyId={keypair.pub} />;
|
||||
};
|
243
libs/accounts/src/lib/accounts-data-provider.spec.ts
Normal file
243
libs/accounts/src/lib/accounts-data-provider.spec.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
import type { AccountFieldsFragment } from './__generated___/Accounts';
|
||||
|
||||
describe('getAccountData', () => {
|
||||
it('should return the correct aggregated data', () => {
|
||||
const data = getAccountData(accounts);
|
||||
expect(data).toEqual(accountResult);
|
||||
});
|
||||
});
|
||||
|
||||
const accounts: AccountFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
balance: '2781397',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: 'd90fd7c746286625504d7a3f5f420a280875acd3cd611676d9e70acc675f4540',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Tesla Quarterly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
balance: '406922',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '9c1ee71959e566c484fcea796513137f8a02219cca2e973b7ae72dc29d099581',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '10001000000',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'XYZalpha',
|
||||
symbol: 'XYZalpha',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '1990351587',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '993ed98f4f770d91a796faab1738551193ba45c62341d20597df70fea6704ede',
|
||||
symbol: 'tUSDC',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '2996218603',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '5000593078',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '4000000000000001006031',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const accountResult: AccountFields[] = [
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
available: '4000000000000001006031',
|
||||
balance: '4000000000000001006031',
|
||||
breakdown: [],
|
||||
deposited: '4000000000000001006031',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
used: '0',
|
||||
},
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
},
|
||||
available: '5000593078',
|
||||
balance: '5000593078',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'Account',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
},
|
||||
available: '5000593078',
|
||||
balance: '406922',
|
||||
deposited: '5001000000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '9c1ee71959e566c484fcea796513137f8a02219cca2e973b7ae72dc29d099581',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
used: '406922',
|
||||
},
|
||||
],
|
||||
deposited: '5001000000',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
used: '406922',
|
||||
},
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
},
|
||||
available: '2996218603',
|
||||
balance: '2996218603',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'Account',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
},
|
||||
available: '2996218603',
|
||||
balance: '2781397',
|
||||
deposited: '2999000000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: 'd90fd7c746286625504d7a3f5f420a280875acd3cd611676d9e70acc675f4540',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Tesla Quarterly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
used: '2781397',
|
||||
},
|
||||
],
|
||||
deposited: '2999000000',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
used: '2781397',
|
||||
},
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '993ed98f4f770d91a796faab1738551193ba45c62341d20597df70fea6704ede',
|
||||
symbol: 'tUSDC',
|
||||
},
|
||||
available: '1990351587',
|
||||
balance: '1990351587',
|
||||
breakdown: [],
|
||||
deposited: '1990351587',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
used: '0',
|
||||
},
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: 'XYZalpha',
|
||||
symbol: 'XYZalpha',
|
||||
},
|
||||
available: '10001000000',
|
||||
balance: '10001000000',
|
||||
breakdown: [],
|
||||
deposited: '10001000000',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
used: '0',
|
||||
},
|
||||
];
|
@ -9,7 +9,11 @@ import type {
|
||||
AccountsQuery,
|
||||
AccountEventsSubscription,
|
||||
} from './__generated___/Accounts';
|
||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
|
||||
function isAccount(
|
||||
account:
|
||||
@ -67,3 +71,77 @@ export const accountsDataProvider = makeDataProvider<
|
||||
getData,
|
||||
getDelta,
|
||||
});
|
||||
|
||||
export interface AccountFields extends AccountFieldsFragment {
|
||||
available: string;
|
||||
used: string;
|
||||
deposited: string;
|
||||
balance: string;
|
||||
breakdown?: AccountFields[];
|
||||
}
|
||||
|
||||
const USE_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
AccountType.ACCOUNT_TYPE_BOND,
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
AccountType.ACCOUNT_TYPE_FEES_MAKER,
|
||||
AccountType.ACCOUNT_TYPE_PENDING_TRANSFERS,
|
||||
];
|
||||
|
||||
const getAssetIds = (data: AccountFieldsFragment[]) =>
|
||||
Array.from(new Set(data.map((a) => a.asset.id))).sort();
|
||||
|
||||
const getTotalBalance = (accounts: AccountFieldsFragment[]) =>
|
||||
accounts.reduce((acc, a) => acc + BigInt(a.balance), BigInt(0));
|
||||
|
||||
export const getAccountData = (
|
||||
data: AccountFieldsFragment[]
|
||||
): AccountFields[] => {
|
||||
return getAssetIds(data).map((assetId) => {
|
||||
const accounts = data.filter((a) => a.asset.id === assetId);
|
||||
return accounts && getAssetAccountAggregation(accounts, assetId);
|
||||
});
|
||||
};
|
||||
|
||||
const getAssetAccountAggregation = (
|
||||
accountList: AccountFieldsFragment[],
|
||||
assetId: string
|
||||
): AccountFields => {
|
||||
const accounts = accountList.filter((a) => a.asset.id === assetId);
|
||||
const available = getTotalBalance(
|
||||
accounts.filter((a) => a.type === AccountType.ACCOUNT_TYPE_GENERAL)
|
||||
);
|
||||
|
||||
const used = getTotalBalance(
|
||||
accounts.filter((a) => USE_ACCOUNT_TYPES.includes(a.type))
|
||||
);
|
||||
|
||||
const balanceAccount: AccountFields = {
|
||||
asset: accounts[0].asset,
|
||||
balance: available.toString(),
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
available: available.toString(),
|
||||
used: used.toString(),
|
||||
deposited: (available + used).toString(),
|
||||
};
|
||||
|
||||
const breakdown = accounts
|
||||
.filter((a) => USE_ACCOUNT_TYPES.includes(a.type))
|
||||
.map((a) => ({
|
||||
...a,
|
||||
asset: accounts[0].asset,
|
||||
deposited: balanceAccount.deposited,
|
||||
available: balanceAccount.available,
|
||||
used: a.balance,
|
||||
}));
|
||||
return { ...balanceAccount, breakdown };
|
||||
};
|
||||
|
||||
export const aggregatedAccountsDataProvider = makeDerivedDataProvider<
|
||||
AccountFields[],
|
||||
never
|
||||
>(
|
||||
[accountsDataProvider],
|
||||
(parts) => parts[0] && getAccountData(parts[0] as AccountFieldsFragment[])
|
||||
);
|
||||
|
@ -1,79 +1,75 @@
|
||||
import React, { useRef, useMemo } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import merge from 'lodash/merge';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDataProvider, addSummaryRows } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
AccountFieldsFragment,
|
||||
AccountEventsSubscription,
|
||||
} from './__generated___/Accounts';
|
||||
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import {
|
||||
AccountsTable,
|
||||
getGroupId,
|
||||
getGroupSummaryRow,
|
||||
} from './accounts-table';
|
||||
import { accountsDataProvider, getId } from './accounts-data-provider';
|
||||
import { useRef, useMemo, useCallback } from 'react';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { aggregatedAccountsDataProvider } from './accounts-data-provider';
|
||||
import type { GetRowsParams } from './accounts-table';
|
||||
import { AccountTable } from './accounts-table';
|
||||
|
||||
interface AccountsManagerProps {
|
||||
interface AccountManagerProps {
|
||||
partyId: string;
|
||||
onClickAsset: (asset?: string | Asset) => void;
|
||||
onClickWithdraw?: (assetId?: string) => void;
|
||||
onClickDeposit?: (assetId?: string) => void;
|
||||
}
|
||||
|
||||
export const accountsManagerUpdate =
|
||||
(gridRef: React.RefObject<AgGridReact>) =>
|
||||
({ delta: deltas }: { delta: AccountEventsSubscription['accounts'] }) => {
|
||||
const update: AccountFieldsFragment[] = [];
|
||||
const add: AccountFieldsFragment[] = [];
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
const api = gridRef.current.api;
|
||||
deltas.forEach((delta) => {
|
||||
const rowNode = api.getRowNode(getId(delta));
|
||||
if (rowNode) {
|
||||
const updatedData = produce<AccountFieldsFragment>(
|
||||
rowNode.data,
|
||||
(draft: AccountFieldsFragment) => {
|
||||
merge(draft, delta);
|
||||
}
|
||||
);
|
||||
if (updatedData !== rowNode.data) {
|
||||
update.push(updatedData);
|
||||
}
|
||||
} else {
|
||||
// #TODO handle new account (or leave it to data provider to handle it)
|
||||
}
|
||||
});
|
||||
if (update.length || add.length) {
|
||||
gridRef.current.api.applyTransactionAsync({
|
||||
update,
|
||||
add,
|
||||
addIndex: 0,
|
||||
});
|
||||
}
|
||||
if (add.length) {
|
||||
addSummaryRows(
|
||||
gridRef.current.api,
|
||||
gridRef.current.columnApi,
|
||||
getGroupId,
|
||||
getGroupSummaryRow
|
||||
);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
|
||||
export const AccountManager = ({
|
||||
onClickAsset,
|
||||
onClickWithdraw,
|
||||
onClickDeposit,
|
||||
partyId,
|
||||
}: AccountManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const dataRef = useRef<AccountFields[] | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const update = useMemo(() => accountsManagerUpdate(gridRef), []);
|
||||
const { data, error, loading } = useDataProvider<
|
||||
AccountFieldsFragment[],
|
||||
AccountEventsSubscription['accounts']
|
||||
>({ dataProvider: accountsDataProvider, update, variables });
|
||||
const update = useCallback(
|
||||
({ data }: { data: AccountFields[] | null }) => {
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
if (dataRef.current?.length) {
|
||||
dataRef.current = data;
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
const { data, error, loading } = useDataProvider<AccountFields[], never>({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
update,
|
||||
variables,
|
||||
});
|
||||
dataRef.current = data;
|
||||
const getRows = async ({
|
||||
successCallback,
|
||||
startRow,
|
||||
endRow,
|
||||
}: GetRowsParams) => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
};
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
<AccountsTable ref={gridRef} data={data} />
|
||||
{data && (
|
||||
<AccountTable
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.length ? undefined : []}
|
||||
ref={gridRef}
|
||||
datasource={{ getRows }}
|
||||
onClickAsset={onClickAsset}
|
||||
onClickDeposit={onClickDeposit}
|
||||
onClickWithdraw={onClickWithdraw}
|
||||
/>
|
||||
)}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountManager;
|
||||
|
@ -1,9 +1,10 @@
|
||||
import AccountsTable from './accounts-table';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { AccountFieldsFragment } from './__generated___/Accounts';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
import { AccountTable } from './accounts-table';
|
||||
|
||||
const singleRow: AccountFieldsFragment = {
|
||||
const singleRow: AccountFields = {
|
||||
__typename: 'Account',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
balance: '125600000',
|
||||
@ -24,47 +25,101 @@ const singleRow: AccountFieldsFragment = {
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
available: '125600000',
|
||||
used: '125600000',
|
||||
deposited: '125600000',
|
||||
};
|
||||
const singleRowData = [singleRow];
|
||||
|
||||
describe('AccountsTable', () => {
|
||||
it('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(<AccountsTable data={[]} />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
render(<AccountTable rowData={[]} onClickAsset={() => null} />);
|
||||
});
|
||||
const headers = await screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(6);
|
||||
expect(
|
||||
headers?.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual(['Asset', 'Deposited', 'Used', '', '', '']);
|
||||
});
|
||||
|
||||
it('should render correct columns', async () => {
|
||||
act(async () => {
|
||||
render(<AccountsTable data={singleRowData} />);
|
||||
await waitFor(async () => {
|
||||
const headers = await screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(4);
|
||||
expect(
|
||||
headers.map((h) =>
|
||||
h.querySelector('[ref="eText"]')?.textContent?.trim()
|
||||
)
|
||||
).toEqual(['Asset', 'Type', 'Market', 'Balance']);
|
||||
});
|
||||
await act(async () => {
|
||||
render(
|
||||
<AccountTable rowData={singleRowData} onClickAsset={() => null} />
|
||||
);
|
||||
});
|
||||
const headers = await screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(6);
|
||||
expect(
|
||||
headers?.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual(['Asset', 'Deposited', 'Used', '', '', '']);
|
||||
});
|
||||
|
||||
it('should apply correct formatting', async () => {
|
||||
act(async () => {
|
||||
render(<AccountsTable data={singleRowData} />);
|
||||
await waitFor(async () => {
|
||||
const cells = await screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'tBTC',
|
||||
singleRow.type,
|
||||
'BTCUSD Monthly (30 Jun 2022)',
|
||||
'1,256.00000',
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
await act(async () => {
|
||||
render(
|
||||
<AccountTable rowData={singleRowData} onClickAsset={() => null} />
|
||||
);
|
||||
});
|
||||
const cells = await screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'tBTC',
|
||||
'1,256.00000',
|
||||
'1,256.00001,256.0000',
|
||||
'Collateral breakdown',
|
||||
'Deposit',
|
||||
'Withdraw',
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get correct account data', () => {
|
||||
const result = getAccountData([singleRow]);
|
||||
const expected = [
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
available: '0',
|
||||
balance: '0',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'Account',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
available: '0',
|
||||
balance: '125600000',
|
||||
deposited: '125600000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'BTCUSD Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'ACCOUNT_TYPE_MARGIN',
|
||||
used: '125600000',
|
||||
},
|
||||
],
|
||||
deposited: '125600000',
|
||||
type: 'ACCOUNT_TYPE_GENERAL',
|
||||
used: '125600000',
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
123
libs/accounts/src/lib/accounts-table.stories.tsx
Normal file
123
libs/accounts/src/lib/accounts-table.stories.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
import { AccountTable } from './accounts-table';
|
||||
|
||||
export default {
|
||||
component: AccountTable,
|
||||
title: 'AccountsTable',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => (
|
||||
<AccountTable data={args.data} onClickAsset={() => null} />
|
||||
);
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
data: getAccountData([
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
balance: '2781397',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: 'd90fd7c746286625504d7a3f5f420a280875acd3cd611676d9e70acc675f4540',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Tesla Quarterly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
balance: '406922',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '9c1ee71959e566c484fcea796513137f8a02219cca2e973b7ae72dc29d099581',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '10001000000',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'XYZalpha',
|
||||
symbol: 'XYZalpha',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '1990351587',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '993ed98f4f770d91a796faab1738551193ba45c62341d20597df70fea6704ede',
|
||||
symbol: 'tUSDC',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '2996218603',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
|
||||
symbol: 'tEURO',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '5000593078',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '4000000000000001006031',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
]),
|
||||
};
|
@ -1,172 +1,204 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
ColumnApi,
|
||||
GroupCellRendererParams,
|
||||
ValueFormatterParams,
|
||||
} from 'ag-grid-community';
|
||||
ValueProps,
|
||||
VegaICellRendererParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
PriceCell,
|
||||
addDecimalsFormatNumber,
|
||||
t,
|
||||
addSummaryRows,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { SummaryRow } from '@vegaprotocol/react-helpers';
|
||||
Button,
|
||||
ButtonLink,
|
||||
Dialog,
|
||||
Intent,
|
||||
progressBarCellRendererSelector,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { AccountFieldsFragment } from './__generated___/Accounts';
|
||||
import { getId } from './accounts-data-provider';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type { AccountType } from '@vegaprotocol/types';
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
|
||||
import BreakdownTable from './breakdown-table';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
|
||||
interface AccountsTableProps {
|
||||
data: AccountFieldsFragment[] | null;
|
||||
export const progressBarValueFormatter = ({
|
||||
data,
|
||||
node,
|
||||
}: ValueFormatterParams): ValueProps['valueFormatted'] | undefined => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.used);
|
||||
const mid = BigInt(data.available);
|
||||
const max = BigInt(data.deposited);
|
||||
const range = max > min ? max : min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(min.toString(), data.asset.decimals, 4),
|
||||
high: addDecimalsFormatNumber(mid.toString(), data.asset.decimals, 4),
|
||||
value: range ? Number((min * BigInt(100)) / range) : 0,
|
||||
intent: Intent.Warning,
|
||||
};
|
||||
};
|
||||
|
||||
export const progressBarHeaderComponentParams = {
|
||||
template:
|
||||
'<div class="ag-cell-label-container" role="presentation">' +
|
||||
` <span>${t('Available')}</span>` +
|
||||
' <span ref="eText" class="ag-header-cell-text"></span>' +
|
||||
'</div>',
|
||||
};
|
||||
|
||||
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
|
||||
successCallback(rowsThisBlock: AccountFields[], lastRow?: number): void;
|
||||
}
|
||||
|
||||
interface AccountsTableValueFormatterParams extends ValueFormatterParams {
|
||||
data: AccountFieldsFragment;
|
||||
export interface Datasource extends IDatasource {
|
||||
getRows(params: GetRowsParams): void;
|
||||
}
|
||||
|
||||
export const getGroupId = (
|
||||
data: AccountFieldsFragment & SummaryRow,
|
||||
columnApi: ColumnApi
|
||||
) => {
|
||||
if (data.__summaryRow) {
|
||||
return null;
|
||||
}
|
||||
const sortColumnId = columnApi.getColumnState().find((c) => c.sort)?.colId;
|
||||
switch (sortColumnId) {
|
||||
case 'asset.symbol':
|
||||
return data.asset.id;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
export interface AccountTableProps extends AgGridReactProps {
|
||||
rowData?: AccountFields[] | null;
|
||||
datasource?: Datasource;
|
||||
onClickAsset: (asset: string | Asset) => void;
|
||||
onClickWithdraw?: (assetId: string) => void;
|
||||
onClickDeposit?: (assetId: string) => void;
|
||||
}
|
||||
|
||||
export const getGroupSummaryRow = (
|
||||
data: AccountFieldsFragment[],
|
||||
columnApi: ColumnApi
|
||||
): Partial<AccountFieldsFragment & SummaryRow> | null => {
|
||||
if (!data.length) {
|
||||
return null;
|
||||
}
|
||||
const sortColumnId = columnApi.getColumnState().find((c) => c.sort)?.colId;
|
||||
switch (sortColumnId) {
|
||||
case 'asset.symbol':
|
||||
return {
|
||||
__summaryRow: true,
|
||||
balance: data
|
||||
.reduce((a, i) => a + (parseFloat(i.balance) || 0), 0)
|
||||
.toString(),
|
||||
asset: data[0].asset,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const comparator = (
|
||||
valueA: string,
|
||||
valueB: string,
|
||||
nodeA: { data: AccountFieldsFragment & SummaryRow },
|
||||
nodeB: { data: AccountFieldsFragment & SummaryRow },
|
||||
isInverted: boolean
|
||||
) => {
|
||||
if (valueA < valueB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (valueA > valueB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nodeA.data.__summaryRow) {
|
||||
return isInverted ? -1 : 1;
|
||||
}
|
||||
|
||||
if (nodeB.data.__summaryRow) {
|
||||
return isInverted ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const AccountsTable = forwardRef<AgGridReact, AccountsTableProps>(
|
||||
({ data }, ref) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
({ onClickAsset, onClickWithdraw, onClickDeposit, ...props }, ref) => {
|
||||
const [openBreakdown, setOpenBreakdown] = useState(false);
|
||||
const [breakdown, setBreakdown] = useState<AccountFields[] | null>(null);
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No accounts')}
|
||||
rowData={data}
|
||||
getRowId={({ data }) => getId(data)}
|
||||
ref={ref}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
}}
|
||||
components={{ PriceCell }}
|
||||
onSortChanged={({ api, columnApi }) => {
|
||||
addSummaryRows(api, columnApi, getGroupId, getGroupSummaryRow);
|
||||
}}
|
||||
onGridReady={(event) => {
|
||||
event.columnApi.applyColumnState({
|
||||
state: [
|
||||
{
|
||||
colId: 'asset.symbol',
|
||||
sort: 'asc',
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Asset')}
|
||||
field="asset.symbol"
|
||||
sortable
|
||||
sortingOrder={['asc', 'desc']}
|
||||
comparator={comparator}
|
||||
cellRenderer={({ value }: GroupCellRendererParams) =>
|
||||
value && value.length > 0 ? (
|
||||
<button
|
||||
className="hover:underline"
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Type')}
|
||||
field="type"
|
||||
valueFormatter={({ value }: ValueFormatterParams) =>
|
||||
value ? AccountTypeMapping[value as AccountType] : '-'
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="market.tradableInstrument.instrument.name"
|
||||
valueFormatter="value || '—'"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Balance')}
|
||||
field="balance"
|
||||
cellRenderer="PriceCell"
|
||||
type="rightAligned"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: AccountsTableValueFormatterParams) =>
|
||||
addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
}
|
||||
/>
|
||||
</AgGrid>
|
||||
<>
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No accounts')}
|
||||
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
|
||||
ref={ref}
|
||||
rowHeight={34}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
tooltipComponent: TooltipCellComponent,
|
||||
sortable: true,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Asset')}
|
||||
field="asset.symbol"
|
||||
headerTooltip={t(
|
||||
'Asset is the collateral that is deposited into the Vega protocol.'
|
||||
)}
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="deposit"
|
||||
onClick={() => {
|
||||
onClickAsset(value);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</ButtonLink>
|
||||
);
|
||||
}}
|
||||
maxWidth={300}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Deposited')}
|
||||
field="deposited"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'deposited'>) =>
|
||||
data &&
|
||||
data.asset &&
|
||||
addDecimalsFormatNumber(value, data.asset.decimals)
|
||||
}
|
||||
maxWidth={300}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Used')}
|
||||
field="used"
|
||||
flex={2}
|
||||
maxWidth={500}
|
||||
headerComponentParams={progressBarHeaderComponentParams}
|
||||
cellRendererSelector={progressBarCellRendererSelector}
|
||||
valueFormatter={progressBarValueFormatter}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName=""
|
||||
field="breakdown"
|
||||
maxWidth={150}
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<AccountFields, 'breakdown'>) => {
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="breakdown"
|
||||
onClick={() => {
|
||||
setOpenBreakdown(!openBreakdown);
|
||||
setBreakdown(value || null);
|
||||
}}
|
||||
>
|
||||
{t('Collateral breakdown')}
|
||||
</ButtonLink>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName=""
|
||||
field="deposit"
|
||||
maxWidth={200}
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields, 'asset'>) => {
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
data-testid="deposit"
|
||||
onClick={() => {
|
||||
onClickDeposit && onClickDeposit(data.asset.id);
|
||||
}}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName=""
|
||||
field="withdraw"
|
||||
maxWidth={200}
|
||||
cellRenderer={({
|
||||
data,
|
||||
}: VegaICellRendererParams<AccountFields, 'asset'>) => {
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
data-testid="withdraw"
|
||||
onClick={() =>
|
||||
onClickWithdraw && onClickWithdraw(data.asset.id)
|
||||
}
|
||||
>
|
||||
{t('Withdraw')}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
|
||||
<div className="h-[35vh] w-full m-auto flex flex-col">
|
||||
<h1 className="text-xl mb-4">{t('Collateral breakdown')}</h1>
|
||||
<BreakdownTable data={breakdown} domLayout="autoHeight" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default AccountsTable;
|
||||
|
114
libs/accounts/src/lib/breakdown-table.spec.tsx
Normal file
114
libs/accounts/src/lib/breakdown-table.spec.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import BreakdownTable from './breakdown-table';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { Schema as Types } from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
|
||||
const singleRow: AccountFields = {
|
||||
__typename: 'Account',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
balance: '125600000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'BTCUSD Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
available: '125600000',
|
||||
used: '125600000',
|
||||
deposited: '125600000',
|
||||
};
|
||||
const singleRowData = [singleRow];
|
||||
|
||||
describe('BreakdownTable', () => {
|
||||
it('should render successfully', async () => {
|
||||
const { baseElement } = render(<BreakdownTable data={[]} />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render correct columns', async () => {
|
||||
await act(async () => {
|
||||
render(<BreakdownTable data={singleRowData} />);
|
||||
});
|
||||
const headers = await screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(5);
|
||||
expect(
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual(['Account type', 'Market', 'Used', 'Deposited', 'Balance']);
|
||||
});
|
||||
|
||||
it('should apply correct formatting', async () => {
|
||||
await act(async () => {
|
||||
render(<BreakdownTable data={singleRowData} />);
|
||||
});
|
||||
const cells = await screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'Margin',
|
||||
'BTCUSD Monthly (30 Jun 2022)',
|
||||
'1,256.00001,256.0000',
|
||||
'1,256.00000',
|
||||
'1,256.00000',
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get correct account data', () => {
|
||||
const result = getAccountData([singleRow]);
|
||||
const expected = [
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
available: '0',
|
||||
balance: '0',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'Account',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
available: '0',
|
||||
balance: '125600000',
|
||||
deposited: '125600000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'BTCUSD Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'ACCOUNT_TYPE_MARGIN',
|
||||
used: '125600000',
|
||||
},
|
||||
],
|
||||
deposited: '125600000',
|
||||
type: 'ACCOUNT_TYPE_GENERAL',
|
||||
used: '125600000',
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
111
libs/accounts/src/lib/breakdown-table.tsx
Normal file
111
libs/accounts/src/lib/breakdown-table.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { forwardRef } from 'react';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
PriceCell,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
progressBarCellRendererSelector,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import {
|
||||
progressBarHeaderComponentParams,
|
||||
progressBarValueFormatter,
|
||||
} from './accounts-table';
|
||||
|
||||
interface BreakdownTableProps extends AgGridReactProps {
|
||||
data: AccountFields[] | null;
|
||||
}
|
||||
|
||||
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
({ data }, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('Collateral not used')}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: AccountFields }) =>
|
||||
`${data.asset.id}-${data.type}-${data.market?.id}`
|
||||
}
|
||||
ref={ref}
|
||||
rowHeight={34}
|
||||
components={{ PriceCell }}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
}}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Account type')}
|
||||
field="type"
|
||||
maxWidth={300}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<AccountFields, 'type'>) =>
|
||||
AccountTypeMapping[value]
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="market.tradableInstrument.instrument.name"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
AccountFields,
|
||||
'market.tradableInstrument.instrument.name'
|
||||
>) => {
|
||||
if (!value) return '-';
|
||||
return value;
|
||||
}}
|
||||
minWidth={200}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Used')}
|
||||
field="used"
|
||||
flex={2}
|
||||
maxWidth={500}
|
||||
headerComponentParams={progressBarHeaderComponentParams}
|
||||
cellRendererSelector={progressBarCellRendererSelector}
|
||||
valueFormatter={progressBarValueFormatter}
|
||||
/>
|
||||
|
||||
<AgGridColumn
|
||||
headerName={t('Deposited')}
|
||||
field="deposited"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'deposited'>) => {
|
||||
if (data && data.asset) {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
}
|
||||
return '-';
|
||||
}}
|
||||
maxWidth={300}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Balance')}
|
||||
field="balance"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'balance'>) => {
|
||||
if (data && data.asset) {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
}
|
||||
return '-';
|
||||
}}
|
||||
maxWidth={300}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default BreakdownTable;
|
@ -1,6 +1,6 @@
|
||||
export * from './__generated___/Accounts';
|
||||
export * from './accounts-container';
|
||||
export * from './__generated__/Accounts';
|
||||
export * from './accounts-data-provider';
|
||||
export * from './accounts-manager';
|
||||
export * from './accounts-table';
|
||||
export * from './asset-balance';
|
||||
export * from './accounts-manager';
|
||||
export * from './breakdown-table';
|
||||
|
@ -20,6 +20,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./.storybook/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,3 +1 @@
|
||||
export * from './lib/deposit-manager';
|
||||
export * from './lib/use-deposits';
|
||||
export * from './lib/deposits-table';
|
||||
export * from './lib';
|
||||
|
72
libs/deposits/src/lib/__generated__/DepositAsset.ts
generated
Normal file
72
libs/deposits/src/lib/__generated__/DepositAsset.ts
generated
Normal file
@ -0,0 +1,72 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AssetStatus } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: DepositAsset
|
||||
// ====================================================
|
||||
|
||||
export interface DepositAsset_assetsConnection_edges_node_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface DepositAsset_assetsConnection_edges_node_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type DepositAsset_assetsConnection_edges_node_source = DepositAsset_assetsConnection_edges_node_source_BuiltinAsset | DepositAsset_assetsConnection_edges_node_source_ERC20;
|
||||
|
||||
export interface DepositAsset_assetsConnection_edges_node {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The status of the asset in the Vega network
|
||||
*/
|
||||
status: AssetStatus;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: DepositAsset_assetsConnection_edges_node_source;
|
||||
}
|
||||
|
||||
export interface DepositAsset_assetsConnection_edges {
|
||||
__typename: "AssetEdge";
|
||||
node: DepositAsset_assetsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface DepositAsset_assetsConnection {
|
||||
__typename: "AssetsConnection";
|
||||
/**
|
||||
* The assets
|
||||
*/
|
||||
edges: (DepositAsset_assetsConnection_edges | null)[] | null;
|
||||
}
|
||||
|
||||
export interface DepositAsset {
|
||||
/**
|
||||
* The list of all assets in use in the Vega network or the specified asset if ID is provided
|
||||
*/
|
||||
assetsConnection: DepositAsset_assetsConnection;
|
||||
}
|
2
libs/deposits/src/lib/__generated__/index.ts
generated
Normal file
2
libs/deposits/src/lib/__generated__/index.ts
generated
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './Deposit';
|
||||
export * from './DepositAsset';
|
62
libs/deposits/src/lib/deposit-container.tsx
Normal file
62
libs/deposits/src/lib/deposit-container.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { Web3Container } from '@vegaprotocol/web3';
|
||||
import { DepositManager } from './deposit-manager';
|
||||
import { getEnabledAssets, t } from '@vegaprotocol/react-helpers';
|
||||
import type { DepositAsset } from './__generated__/DepositAsset';
|
||||
|
||||
const DEPOSITS_QUERY = gql`
|
||||
query DepositAsset {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
status
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Fetches data required for the Deposit page
|
||||
*/
|
||||
export const DepositContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const { keypair } = useVegaWallet();
|
||||
|
||||
const { data, loading, error } = useQuery<DepositAsset>(DEPOSITS_QUERY, {
|
||||
variables: { partyId: keypair?.pub },
|
||||
skip: !keypair?.pub,
|
||||
});
|
||||
|
||||
const assets = getEnabledAssets(data);
|
||||
|
||||
return (
|
||||
<AsyncRenderer<DepositAsset> data={data} loading={loading} error={error}>
|
||||
{assets.length ? (
|
||||
<Web3Container>
|
||||
<DepositManager
|
||||
assetId={assetId}
|
||||
assets={assets}
|
||||
isFaucetable={VEGA_ENV !== Networks.MAINNET}
|
||||
/>
|
||||
</Web3Container>
|
||||
) : (
|
||||
<Splash>
|
||||
<p>{t('No assets on this network')}</p>
|
||||
</Splash>
|
||||
)}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -77,9 +77,9 @@ export const DepositForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
asset: selectedAsset?.id,
|
||||
from: account,
|
||||
to: keypair?.pub,
|
||||
asset: selectedAsset?.id || '',
|
||||
},
|
||||
});
|
||||
|
||||
@ -151,7 +151,11 @@ export const DepositForm = ({
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{ validate: { required } }}
|
||||
rules={{
|
||||
validate: {
|
||||
required: (value) => !!selectedAsset || required(value),
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
id="asset"
|
||||
@ -160,6 +164,7 @@ export const DepositForm = ({
|
||||
field.onChange(e);
|
||||
onSelectAsset(e.target.value);
|
||||
}}
|
||||
value={selectedAsset?.id || ''}
|
||||
>
|
||||
<option value="">{t('Please select')}</option>
|
||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
|
||||
interface DepositLimitsProps {
|
||||
@ -36,7 +36,9 @@ export const DepositLimits = ({
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Balance available')}</th>
|
||||
<td className="text-right">{balance ? balance.toString() : 0}</td>
|
||||
<td className="text-right">
|
||||
{balance ? formatNumber(balance) : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="text-left font-normal">
|
||||
@ -46,7 +48,7 @@ export const DepositLimits = ({
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Deposited')}</th>
|
||||
<td className="text-right">{deposited.toString()}</td>
|
||||
<td className="text-right">{formatNumber(deposited)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Remaining')}</th>
|
||||
|
@ -4,21 +4,43 @@ import sortBy from 'lodash/sortBy';
|
||||
import { useSubmitApproval } from './use-submit-approval';
|
||||
import { useSubmitFaucet } from './use-submit-faucet';
|
||||
import { useDepositStore } from './deposit-store';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useDepositBalances } from './use-deposit-balances';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface DepositManagerProps {
|
||||
assetId?: string;
|
||||
assets: Asset[];
|
||||
isFaucetable: boolean;
|
||||
}
|
||||
|
||||
const useDepositAsset = (assets: Asset[], assetId?: string) => {
|
||||
const { asset, balance, allowance, deposited, max, update } =
|
||||
useDepositStore();
|
||||
|
||||
const handleSelectAsset = useCallback(
|
||||
(id: string) => {
|
||||
const asset = assets.find((a) => a.id === id);
|
||||
update({ asset });
|
||||
},
|
||||
[assets, update]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleSelectAsset(assetId || '');
|
||||
}, [assetId, handleSelectAsset]);
|
||||
|
||||
return { asset, balance, allowance, deposited, max, handleSelectAsset };
|
||||
};
|
||||
|
||||
export const DepositManager = ({
|
||||
assetId,
|
||||
assets,
|
||||
isFaucetable,
|
||||
}: DepositManagerProps) => {
|
||||
const { asset, balance, allowance, deposited, max, update } =
|
||||
useDepositStore();
|
||||
const { asset, balance, allowance, deposited, max, handleSelectAsset } =
|
||||
useDepositAsset(assets, assetId);
|
||||
|
||||
useDepositBalances(isFaucetable);
|
||||
|
||||
// Set up approve transaction
|
||||
@ -30,15 +52,6 @@ export const DepositManager = ({
|
||||
// Set up faucet transaction
|
||||
const faucet = useSubmitFaucet();
|
||||
|
||||
const handleSelectAsset = useCallback(
|
||||
(id: string) => {
|
||||
const asset = assets.find((a) => a.id === id);
|
||||
if (!asset) return;
|
||||
update({ asset });
|
||||
},
|
||||
[assets, update]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DepositForm
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { SetState } from 'zustand';
|
||||
import create from 'zustand';
|
||||
|
||||
interface DepositStore {
|
||||
@ -12,7 +11,7 @@ interface DepositStore {
|
||||
update: (state: Partial<DepositStore>) => void;
|
||||
}
|
||||
|
||||
export const useDepositStore = create((set: SetState<DepositStore>) => ({
|
||||
export const useDepositStore = create<DepositStore>((set) => ({
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
deposited: new BigNumber(0),
|
||||
|
16
libs/deposits/src/lib/index.ts
Normal file
16
libs/deposits/src/lib/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export * from './__generated__';
|
||||
export * from './deposit-container';
|
||||
export * from './deposit-form';
|
||||
export * from './deposit-limits';
|
||||
export * from './deposit-manager';
|
||||
export * from './deposit-store';
|
||||
export * from './deposits-table';
|
||||
export * from './use-deposit-balances';
|
||||
export * from './use-deposits';
|
||||
export * from './use-get-allowance';
|
||||
export * from './use-get-balance-of-erc20-token';
|
||||
export * from './use-get-deposit-maximum';
|
||||
export * from './use-get-deposited-amount';
|
||||
export * from './use-submit-approval';
|
||||
export * from './use-submit-deposit';
|
||||
export * from './use-submit-faucet';
|
@ -9,7 +9,7 @@ import { useGetDepositedAmount } from './use-get-deposited-amount';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
|
||||
|
||||
/**
|
||||
* Hook which fetches all the balances required for despoiting
|
||||
* Hook which fetches all the balances required for depositing
|
||||
* whenever the asset changes in the form
|
||||
*/
|
||||
export const useDepositBalances = (isFaucetable: boolean) => {
|
||||
|
@ -44,7 +44,7 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate="No liquidity provisions"
|
||||
overlayNoRowsTemplate={t('No liquidity provisions')}
|
||||
getRowId={({ data }) => data.party}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
|
@ -30,8 +30,9 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
/>
|
||||
))}
|
||||
</AsyncRenderer>
|
||||
|
||||
<Dialog>
|
||||
<p>Your position was not closed! This is still not implemented. </p>
|
||||
<p>Your position was not closed! This is still not implemented.</p>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
|
@ -7,6 +7,8 @@ import type {
|
||||
ICellRendererParams,
|
||||
CellRendererSelectorResult,
|
||||
} from 'ag-grid-community';
|
||||
import type { ValueProps as PriceCellProps } from '@vegaprotocol/ui-toolkit';
|
||||
import { EmptyCell, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
PriceFlashCell,
|
||||
addDecimalsFormatNumber,
|
||||
@ -17,7 +19,7 @@ import {
|
||||
signedNumberCssClass,
|
||||
signedNumberCssClassRules,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
|
||||
@ -64,33 +66,6 @@ export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
|
||||
return (valueFormatted && valueFormatted[0]) || undefined;
|
||||
};
|
||||
|
||||
export interface PriceCellProps {
|
||||
valueFormatted?: {
|
||||
low: string;
|
||||
high: string;
|
||||
value: number;
|
||||
intent?: Intent;
|
||||
};
|
||||
}
|
||||
|
||||
export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
|
||||
return valueFormatted ? (
|
||||
<>
|
||||
<div className="flex justify-between leading-tight font-mono">
|
||||
<div>{valueFormatted.low}</div>
|
||||
<div>{valueFormatted.high}</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={valueFormatted.value}
|
||||
intent={valueFormatted.intent}
|
||||
className="mt-2 w-full"
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
ProgressBarCell.displayName = 'PriceFlashCell';
|
||||
|
||||
export interface AmountCellProps {
|
||||
valueFormatted?: Pick<
|
||||
Position,
|
||||
@ -140,14 +115,33 @@ const ButtonCell = ({
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyCell = () => '';
|
||||
const progressBarValueFormatter = ({
|
||||
data,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams):
|
||||
| PriceCellProps['valueFormatted']
|
||||
| undefined => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.averageEntryPrice);
|
||||
const max = BigInt(data.liquidationPrice);
|
||||
const mid = BigInt(data.markPrice);
|
||||
const range = max - min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(min.toString(), data.marketDecimalPlaces),
|
||||
high: addDecimalsFormatNumber(max.toString(), data.marketDecimalPlaces),
|
||||
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
|
||||
intent: data.lowMarginLevel ? Intent.Warning : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
({ onClose, ...props }, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate="No positions"
|
||||
overlayNoRowsTemplate={t('No positions')}
|
||||
getRowId={getRowId}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
@ -254,6 +248,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
'</div>',
|
||||
}}
|
||||
flex={2}
|
||||
headerTooltip={t(
|
||||
'Liquidation prices are based on the amount of collateral you have available, the risk of your position and the liquidity on the order book. They can change rapidly based on the profit and loss of your positions and any changes to collateral from opening/closing other positions and making deposits/withdrawals.'
|
||||
)}
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
@ -261,32 +258,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams):
|
||||
| PriceCellProps['valueFormatted']
|
||||
| undefined => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.averageEntryPrice);
|
||||
const max = BigInt(data.liquidationPrice);
|
||||
const mid = BigInt(data.markPrice);
|
||||
const range = max - min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(
|
||||
min.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
high: addDecimalsFormatNumber(
|
||||
max.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
|
||||
intent: data.lowMarginLevel ? Intent.Warning : undefined,
|
||||
};
|
||||
}}
|
||||
valueFormatter={progressBarValueFormatter}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Leverage')}
|
||||
@ -354,7 +326,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
headerTooltip={t('P&L excludes any fees paid.')}
|
||||
headerTooltip={t(
|
||||
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
|
||||
)}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
@ -372,6 +346,9 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
headerTooltip={t(
|
||||
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
|
||||
)}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
|
@ -2,19 +2,22 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
"files": [
|
||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../node_modules/@nrwl/next/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts",
|
||||
"**/*.test.jsx",
|
||||
"jest.config.ts"
|
||||
]
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
@ -8,16 +8,18 @@ export interface AccordionItemProps {
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface AccordionProps {
|
||||
panels: AccordionItemProps[];
|
||||
export interface AccordionPanelProps extends AccordionItemProps {
|
||||
itemId: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const Accordion = ({ panels }: AccordionProps) => {
|
||||
export interface AccordionProps {
|
||||
panels?: AccordionItemProps[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Accordion = ({ panels, children }: AccordionProps) => {
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
const triggerClassNames = classNames(
|
||||
'w-full py-2',
|
||||
'flex items-center justify-between border-b border-neutral-500'
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
@ -25,31 +27,50 @@ export const Accordion = ({ panels }: AccordionProps) => {
|
||||
value={values}
|
||||
onValueChange={setValues}
|
||||
>
|
||||
{panels.map(({ title, content }, i) => (
|
||||
<AccordionPrimitive.Item value={`item-${i + 1}`} key={`item-${i + 1}`}>
|
||||
<AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={triggerClassNames}
|
||||
>
|
||||
<span data-testid="accordion-title">{title}</span>
|
||||
<AccordionChevron
|
||||
active={values.includes(`item-${i + 1}`)}
|
||||
aria-hidden
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Content data-testid="accordion-content-ref">
|
||||
<div className="py-4 text-sm" data-testid="accordion-content">
|
||||
{content}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
</AccordionPrimitive.Item>
|
||||
{panels?.map(({ title, content }, i) => (
|
||||
<AccordionItem
|
||||
key={`item-${i + 1}`}
|
||||
itemId={`item-${i + 1}`}
|
||||
title={title}
|
||||
content={content}
|
||||
active={values.includes(`item-${i + 1}`)}
|
||||
/>
|
||||
))}
|
||||
{children}
|
||||
</AccordionPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccordionItem = ({
|
||||
title,
|
||||
content,
|
||||
itemId,
|
||||
active,
|
||||
}: AccordionPanelProps) => {
|
||||
const triggerClassNames = classNames(
|
||||
'w-full py-2',
|
||||
'flex items-center justify-between border-b border-neutral-500'
|
||||
);
|
||||
return (
|
||||
<AccordionPrimitive.Item value={itemId}>
|
||||
<AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={triggerClassNames}
|
||||
>
|
||||
<span data-testid="accordion-title">{title}</span>
|
||||
<AccordionChevron active={active} aria-hidden />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Content data-testid="accordion-content-ref">
|
||||
<div className="py-4 text-sm" data-testid="accordion-content">
|
||||
{content}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
</AccordionPrimitive.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccordionChevron = ({ active }: { active: boolean }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
@ -12,8 +12,8 @@ export * from './dropdown-menu';
|
||||
export * from './form-group';
|
||||
export * from './icon';
|
||||
export * from './indicator';
|
||||
export * from './input';
|
||||
export * from './input-error';
|
||||
export * from './input';
|
||||
export * from './key-value-table';
|
||||
export * from './link';
|
||||
export * from './loader';
|
||||
@ -24,6 +24,7 @@ export * from './progress-bar';
|
||||
export * from './radio-group';
|
||||
export * from './resizable-grid';
|
||||
export * from './select';
|
||||
export * from './slider';
|
||||
export * from './sparkline';
|
||||
export * from './splash';
|
||||
export * from './syntax-highlighter';
|
||||
@ -33,4 +34,3 @@ export * from './theme-switcher';
|
||||
export * from './toggle';
|
||||
export * from './tooltip';
|
||||
export * from './vega-logo';
|
||||
export * from './slider';
|
||||
|
@ -0,0 +1,41 @@
|
||||
import type {
|
||||
CellRendererSelectorResult,
|
||||
ICellRendererParams,
|
||||
} from 'ag-grid-community';
|
||||
import type { Intent } from '../../utils/intent';
|
||||
import { ProgressBar } from './progress-bar';
|
||||
|
||||
export interface ValueProps {
|
||||
valueFormatted?: {
|
||||
low: string;
|
||||
high: string;
|
||||
value: number;
|
||||
intent?: Intent;
|
||||
};
|
||||
}
|
||||
|
||||
export const EmptyCell = () => '';
|
||||
|
||||
export const ProgressBarCell = ({ valueFormatted }: ValueProps) => {
|
||||
return valueFormatted ? (
|
||||
<>
|
||||
<div className="flex justify-between leading-tight font-mono">
|
||||
<div>{valueFormatted.low}</div>
|
||||
<div>{valueFormatted.high}</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={valueFormatted.value}
|
||||
intent={valueFormatted.intent}
|
||||
className="mt-2 w-full"
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const progressBarCellRendererSelector = (
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
|
||||
};
|
||||
};
|
@ -1 +1,2 @@
|
||||
export * from './grid-progress-bar';
|
||||
export * from './progress-bar';
|
||||
|
58
libs/withdraws/src/lib/use-withdraw-asset.tsx
Normal file
58
libs/withdraws/src/lib/use-withdraw-asset.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { captureException } from '@sentry/react';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { Account } from './types';
|
||||
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
|
||||
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
|
||||
import { useWithdrawStore } from './withdraw-store';
|
||||
|
||||
export const useWithdrawAsset = (
|
||||
assets: Asset[],
|
||||
accounts: Account[],
|
||||
assetId?: string
|
||||
) => {
|
||||
const { asset, balance, min, threshold, delay, update } = useWithdrawStore();
|
||||
const getThreshold = useGetWithdrawThreshold();
|
||||
const getDelay = useGetWithdrawDelay();
|
||||
|
||||
// Every time an asset is selected we need to find the corresponding
|
||||
// account, balance, min viable amount and delay threshold
|
||||
const handleSelectAsset = useCallback(
|
||||
async (id: string) => {
|
||||
const asset = assets.find((a) => a.id === id);
|
||||
const account = accounts.find(
|
||||
(a) =>
|
||||
a.type === AccountType.ACCOUNT_TYPE_GENERAL &&
|
||||
a.asset.id === asset?.id
|
||||
);
|
||||
const balance =
|
||||
account && asset
|
||||
? new BigNumber(addDecimal(account.balance, asset.decimals))
|
||||
: new BigNumber(0);
|
||||
const min = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
// Query collateral bridge for threshold for selected asset
|
||||
// and subsequent delay if withdrawal amount is larger than it
|
||||
let threshold;
|
||||
let delay;
|
||||
try {
|
||||
const result = await Promise.all([getThreshold(asset), getDelay()]);
|
||||
threshold = result[0];
|
||||
delay = result[1];
|
||||
update({ asset, balance, min, threshold, delay });
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
}
|
||||
},
|
||||
[accounts, assets, update, getThreshold, getDelay]
|
||||
);
|
||||
useEffect(() => {
|
||||
handleSelectAsset(assetId || '');
|
||||
}, [assetId, handleSelectAsset]);
|
||||
|
||||
return { asset, balance, min, threshold, delay, handleSelectAsset };
|
||||
};
|
@ -51,9 +51,11 @@ const WITHDRAW_FORM_QUERY = gql`
|
||||
interface WithdrawFormContainerProps {
|
||||
partyId?: string;
|
||||
submit: (args: WithdrawalArgs) => void;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export const WithdrawFormContainer = ({
|
||||
assetId,
|
||||
partyId,
|
||||
submit,
|
||||
}: WithdrawFormContainerProps) => {
|
||||
@ -82,6 +84,7 @@ export const WithdrawFormContainer = ({
|
||||
|
||||
return (
|
||||
<WithdrawManager
|
||||
assetId={assetId}
|
||||
assets={assets}
|
||||
accounts={data.party?.accounts || []}
|
||||
submit={submit}
|
||||
|
@ -58,7 +58,7 @@ export const WithdrawForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
asset: selectedAsset?.id,
|
||||
asset: selectedAsset?.id || '',
|
||||
to: address,
|
||||
},
|
||||
});
|
||||
@ -98,7 +98,11 @@ export const WithdrawForm = ({
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{ validate: { required } }}
|
||||
rules={{
|
||||
validate: {
|
||||
required: (value) => !!selectedAsset || required(value),
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
|
@ -3,14 +3,32 @@ import userEvent from '@testing-library/user-event';
|
||||
import { generateAccount, generateAsset } from './test-helpers';
|
||||
import type { WithdrawManagerProps } from './withdraw-manager';
|
||||
import { WithdrawManager } from './withdraw-manager';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Account } from './types';
|
||||
|
||||
const asset = generateAsset();
|
||||
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
|
||||
jest.mock('@web3-react/core', () => ({
|
||||
useWeb3React: () => ({ account: ethereumAddress }),
|
||||
}));
|
||||
|
||||
jest.mock('./use-withdraw-asset', () => ({
|
||||
useWithdrawAsset: (
|
||||
assets: Asset[],
|
||||
accounts: Account[],
|
||||
assetId?: string
|
||||
) => ({
|
||||
asset,
|
||||
balance: new BigNumber(1),
|
||||
min: new BigNumber(0.0000001),
|
||||
threshold: new BigNumber(1000),
|
||||
delay: 10,
|
||||
handleSelectAsset: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./use-get-withdraw-threshold', () => ({
|
||||
useGetWithdrawThreshold: () => {
|
||||
return () => Promise.resolve(new BigNumber(100));
|
||||
@ -23,77 +41,82 @@ jest.mock('./use-get-withdraw-delay', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
let props: WithdrawManagerProps;
|
||||
describe('WithdrawManager', () => {
|
||||
let props: WithdrawManagerProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
assets: [generateAsset()],
|
||||
accounts: [generateAccount()],
|
||||
submit: jest.fn(),
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
assets: [asset],
|
||||
accounts: [generateAccount()],
|
||||
submit: jest.fn(),
|
||||
assetId: asset.id,
|
||||
};
|
||||
});
|
||||
|
||||
const generateJsx = (props: WithdrawManagerProps) => (
|
||||
<WithdrawManager {...props} />
|
||||
);
|
||||
|
||||
it('calls submit if valid form submission', async () => {
|
||||
render(generateJsx(props));
|
||||
await act(async () => {
|
||||
await submitValid();
|
||||
});
|
||||
expect(await props.submit).toHaveBeenCalledWith({
|
||||
amount: '1000',
|
||||
asset: props.assets[0].id,
|
||||
receiverAddress: ethereumAddress,
|
||||
availableTimestamp: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly', async () => {
|
||||
render(generateJsx(props));
|
||||
|
||||
// Set other fields to be valid
|
||||
fireEvent.change(screen.getByLabelText('Asset'), {
|
||||
target: { value: props.assets[0].id },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||
target: { value: ethereumAddress },
|
||||
});
|
||||
|
||||
// Min amount
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
).toBeInTheDocument();
|
||||
expect(props.submit).not.toBeCalled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.00001' },
|
||||
});
|
||||
|
||||
// Max amount (balance is 1)
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '2' },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in account')
|
||||
).toBeInTheDocument();
|
||||
expect(props.submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
const submitValid = async () => {
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Asset'),
|
||||
props.assets[0].id
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||
target: { value: ethereumAddress },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.01' },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
};
|
||||
});
|
||||
|
||||
const generateJsx = (props: WithdrawManagerProps) => (
|
||||
<WithdrawManager {...props} />
|
||||
);
|
||||
|
||||
it('calls submit if valid form submission', async () => {
|
||||
render(generateJsx(props));
|
||||
await act(async () => {
|
||||
await submitValid();
|
||||
});
|
||||
expect(props.submit).toHaveBeenCalledWith({
|
||||
amount: '1000',
|
||||
asset: props.assets[0].id,
|
||||
receiverAddress: ethereumAddress,
|
||||
availableTimestamp: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly', async () => {
|
||||
render(generateJsx(props));
|
||||
|
||||
// Set other fields to be valid
|
||||
fireEvent.change(screen.getByLabelText('Asset'), {
|
||||
target: { value: props.assets[0].id },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||
target: { value: ethereumAddress },
|
||||
});
|
||||
|
||||
// Min amount
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
|
||||
expect(props.submit).not.toBeCalled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.00001' },
|
||||
});
|
||||
|
||||
// Max amount (balance is 1)
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '2' },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in account')
|
||||
).toBeInTheDocument();
|
||||
expect(props.submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
const submitValid = async () => {
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Asset'),
|
||||
props.assets[0].id
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||
target: { value: ethereumAddress },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.01' },
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
};
|
||||
|
@ -1,68 +1,25 @@
|
||||
import { useCallback } from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { WithdrawForm } from './withdraw-form';
|
||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Account } from './types';
|
||||
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
|
||||
import { captureException } from '@sentry/react';
|
||||
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
|
||||
import { useWithdrawStore } from './withdraw-store';
|
||||
import { useWithdrawAsset } from './use-withdraw-asset';
|
||||
|
||||
export interface WithdrawManagerProps {
|
||||
assets: Asset[];
|
||||
accounts: Account[];
|
||||
submit: (args: WithdrawalArgs) => void;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export const WithdrawManager = ({
|
||||
assets,
|
||||
accounts,
|
||||
submit,
|
||||
assetId,
|
||||
}: WithdrawManagerProps) => {
|
||||
const { asset, balance, min, threshold, delay, update } = useWithdrawStore();
|
||||
const getThreshold = useGetWithdrawThreshold();
|
||||
const getDelay = useGetWithdrawDelay();
|
||||
|
||||
// Everytime an asset is selected we need to find the corresponding
|
||||
// account, balance, min viable amount and delay threshold
|
||||
const handleSelectAsset = useCallback(
|
||||
async (id: string) => {
|
||||
const asset = assets.find((a) => a.id === id);
|
||||
const account = accounts.find(
|
||||
(a) =>
|
||||
a.type === AccountType.ACCOUNT_TYPE_GENERAL &&
|
||||
a.asset.id === asset?.id
|
||||
);
|
||||
const balance =
|
||||
account && asset
|
||||
? new BigNumber(addDecimal(account.balance, asset.decimals))
|
||||
: new BigNumber(0);
|
||||
const min = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
// Query collateral bridge for threshold for selected asset
|
||||
// and subsequent delay if withdrawal amount is larger than it
|
||||
let threshold;
|
||||
let delay;
|
||||
|
||||
try {
|
||||
const result = await Promise.all([getThreshold(asset), getDelay()]);
|
||||
threshold = result[0];
|
||||
delay = result[1];
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
}
|
||||
|
||||
update({ asset, balance, min, threshold, delay });
|
||||
},
|
||||
[accounts, assets, update, getThreshold, getDelay]
|
||||
);
|
||||
|
||||
const { asset, balance, min, threshold, delay, handleSelectAsset } =
|
||||
useWithdrawAsset(assets, accounts, assetId);
|
||||
return (
|
||||
<WithdrawForm
|
||||
selectedAsset={asset}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { SetState } from 'zustand';
|
||||
import create from 'zustand';
|
||||
|
||||
interface WithdrawStore {
|
||||
export interface WithdrawStore {
|
||||
asset: Asset | undefined;
|
||||
balance: BigNumber;
|
||||
min: BigNumber;
|
||||
@ -12,7 +11,7 @@ interface WithdrawStore {
|
||||
update: (state: Partial<WithdrawStore>) => void;
|
||||
}
|
||||
|
||||
export const useWithdrawStore = create((set: SetState<WithdrawStore>) => ({
|
||||
export const useWithdrawStore = create<WithdrawStore>((set) => ({
|
||||
asset: undefined,
|
||||
balance: new BigNumber(0),
|
||||
min: new BigNumber(0),
|
||||
|
@ -9,9 +9,11 @@ import { WithdrawalFeedback } from './withdrawal-feedback';
|
||||
export const WithdrawalDialogs = ({
|
||||
withdrawDialog,
|
||||
setWithdrawDialog,
|
||||
assetId,
|
||||
}: {
|
||||
withdrawDialog: boolean;
|
||||
setWithdrawDialog: (open: boolean) => void;
|
||||
assetId?: string;
|
||||
}) => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const createWithdraw = useCreateWithdraw();
|
||||
@ -25,6 +27,7 @@ export const WithdrawalDialogs = ({
|
||||
size="small"
|
||||
>
|
||||
<WithdrawFormContainer
|
||||
assetId={assetId}
|
||||
partyId={keypair?.pub}
|
||||
submit={(args) => {
|
||||
setWithdrawDialog(false);
|
||||
|
Loading…
Reference in New Issue
Block a user