diff --git a/apps/console-lite-e2e/src/integration/portfolio-page.test.ts b/apps/console-lite-e2e/src/integration/portfolio-page.test.ts index 68e787663..1b229c349 100644 --- a/apps/console-lite-e2e/src/integration/portfolio-page.test.ts +++ b/apps/console-lite-e2e/src/integration/portfolio-page.test.ts @@ -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"]' ) diff --git a/apps/console-lite/src/app/components/console-lite-grid/console-lite-grid.tsx b/apps/console-lite/src/app/components/console-lite-grid/console-lite-grid.tsx index 8016ca4e3..e8ee1d165 100644 --- a/apps/console-lite/src/app/components/console-lite-grid/console-lite-grid.tsx +++ b/apps/console-lite/src/app/components/console-lite-grid/console-lite-grid.tsx @@ -105,9 +105,7 @@ const ConsoleLiteGrid = ( ); }; -const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as < - T extends { id?: string } ->( +const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as ( p: Props & { ref?: React.Ref } ) => React.ReactElement; diff --git a/apps/console-lite/src/app/components/portfolio/accounts/accounts.tsx b/apps/console-lite/src/app/components/portfolio/accounts/accounts.tsx index ce2b7d489..f940c09ce 100644 --- a/apps/console-lite/src/app/components/portfolio/accounts/accounts.tsx +++ b/apps/console-lite/src/app/components/portfolio/accounts/accounts.tsx @@ -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(null); + const dataRef = useRef(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({ + 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} > - - rowData={data as AccountObj[]} + + rowData={data?.length ? undefined : []} + rowModelType={data?.length ? 'infinite' : 'clientSide'} + ref={gridRef} + datasource={{ getRows }} columnDefs={columnDefs} defaultColDef={defaultColDef} components={{ PriceCell }} diff --git a/apps/console-lite/src/app/components/portfolio/accounts/use-column-definitions.tsx b/apps/console-lite/src/app/components/portfolio/accounts/use-column-definitions.tsx index 9947b6eec..74c38f4a0 100644 --- a/apps/console-lite/src/app/components/portfolio/accounts/use-column-definitions.tsx +++ b/apps/console-lite/src/app/components/portfolio/accounts/use-column-definitions.tsx @@ -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) => + addDecimalsFormatNumber(value, data.asset.decimals), + }, + { + colId: 'used', + headerName: t('Used'), + field: 'used', + cellRenderer: 'PriceCell', + valueFormatter: ({ + value, + data, + }: VegaValueFormatterParams) => addDecimalsFormatNumber(value, data.asset.decimals), }, ]; diff --git a/apps/console-lite/src/app/components/portfolio/positions/use-column-definitions.tsx b/apps/console-lite/src/app/components/portfolio/positions/use-column-definitions.tsx index ae3e8fb9c..0be91d967 100644 --- a/apps/console-lite/src/app/components/portfolio/positions/use-column-definitions.tsx +++ b/apps/console-lite/src/app/components/portfolio/positions/use-column-definitions.tsx @@ -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 = () => ''; diff --git a/apps/console-lite/src/app/components/simple-market-list/simple-market-list.tsx b/apps/console-lite/src/app/components/simple-market-list/simple-market-list.tsx index 91661fe74..9bff7520a 100644 --- a/apps/console-lite/src/app/components/simple-market-list/simple-market-list.tsx +++ b/apps/console-lite/src/app/components/simple-market-list/simple-market-list.tsx @@ -70,6 +70,7 @@ const SimpleMarketList = () => { rowData={localData} defaultColDef={defaultColDef} handleRowClicked={handleRowClicked} + getRowId={({ data }) => data.id} /> diff --git a/apps/trading-e2e/src/integration/trading-accounts.cy.ts b/apps/trading-e2e/src/integration/trading-accounts.cy.ts index ead169d4c..6188dadad 100644 --- a/apps/trading-e2e/src/integration/trading-accounts.cy.ts +++ b/apps/trading-e2e/src/integration/trading-accounts.cy.ts @@ -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'); }); }); diff --git a/apps/trading-e2e/src/integration/trading-collateral.cy.ts b/apps/trading-e2e/src/integration/trading-collateral.cy.ts index d2682f173..b1368cd8c 100644 --- a/apps/trading-e2e/src/integration/trading-collateral.cy.ts +++ b/apps/trading-e2e/src/integration/trading-collateral.cy.ts @@ -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', diff --git a/apps/trading-e2e/src/integration/withdraw.cy.ts b/apps/trading-e2e/src/integration/withdraw.cy.ts index 1707c60d9..253174a18 100644 --- a/apps/trading-e2e/src/integration/withdraw.cy.ts +++ b/apps/trading-e2e/src/integration/withdraw.cy.ts @@ -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') diff --git a/apps/trading/pages/liquidity/[marketId].page.tsx b/apps/trading/pages/liquidity/[marketId].page.tsx index eb1700bc1..e6530fbbd 100644 --- a/apps/trading/pages/liquidity/[marketId].page.tsx +++ b/apps/trading/pages/liquidity/[marketId].page.tsx @@ -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(null); @@ -69,9 +75,11 @@ const LiquidityPage = ({ id }: { id?: string }) => {
push(`/markets/${marketId}`)}>{`${name} ${t( - 'liquidity provision' - )}`} + + + {`${name} ${t('liquidity provision')}`} + + } > { + const { keypair } = useVegaWallet(); + const [depositDialog, setDepositDialog] = useState(false); + + if (!keypair) { + return ( + +

{t('Please connect Vega wallet')}

+
+ ); + } + + return ( + +
+ + +
+
+ ); +}; + +export const AssetAccountTable = ({ partyId }: { partyId: string }) => { + const [withdrawDialog, setWithdrawDialog] = useState(false); + const [depositDialog, setDepositDialog] = useState(false); + const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); + const [assetId, setAssetId] = useState(); + return ( + <> + { + value && openAssetDetailsDialog(value); + }} + onClickWithdraw={(assetId) => { + setWithdrawDialog(true); + setAssetId(assetId); + }} + onClickDeposit={(assetId) => { + setDepositDialog(true); + setAssetId(assetId); + }} + /> + + + + ); +}; + +export interface DepositDialogProps { + assetId?: string; + depositDialog: boolean; + setDepositDialog: (open: boolean) => void; +} + +export const DepositDialog = ({ + assetId, + depositDialog, + setDepositDialog, +}: DepositDialogProps) => { + return ( + +

{t('Deposit')}

+ +
+ ); +}; diff --git a/apps/trading/pages/portfolio/index.page.tsx b/apps/trading/pages/portfolio/index.page.tsx index 8b67098ee..b29b7863f 100644 --- a/apps/trading/pages/portfolio/index.page.tsx +++ b/apps/trading/pages/portfolio/index.page.tsx @@ -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) => ({ diff --git a/libs/accounts/.storybook/main.js b/libs/accounts/.storybook/main.js new file mode 100644 index 000000000..9997fd7a1 --- /dev/null +++ b/libs/accounts/.storybook/main.js @@ -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; + }, +}; diff --git a/libs/accounts/.storybook/preview-head.html b/libs/accounts/.storybook/preview-head.html new file mode 100644 index 000000000..dd2e70030 --- /dev/null +++ b/libs/accounts/.storybook/preview-head.html @@ -0,0 +1 @@ + diff --git a/libs/accounts/.storybook/preview.js b/libs/accounts/.storybook/preview.js new file mode 100644 index 000000000..5edeee2ad --- /dev/null +++ b/libs/accounts/.storybook/preview.js @@ -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 ( +
+ + + +
+ ); + }, +]; diff --git a/libs/accounts/.storybook/styles.scss b/libs/accounts/.storybook/styles.scss new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/libs/accounts/.storybook/styles.scss @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/libs/accounts/.storybook/tsconfig.json b/libs/accounts/.storybook/tsconfig.json new file mode 100644 index 000000000..a33db6100 --- /dev/null +++ b/libs/accounts/.storybook/tsconfig.json @@ -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"] +} diff --git a/libs/accounts/project.json b/libs/accounts/project.json index c97ccc205..67e4fbe40 100644 --- a/libs/accounts/project.json +++ b/libs/accounts/project.json @@ -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 + } + } } } } diff --git a/libs/accounts/src/lib/__generated__/Accounts.ts b/libs/accounts/src/lib/__generated__/Accounts.ts new file mode 100644 index 000000000..6448e214b --- /dev/null +++ b/libs/accounts/src/lib/__generated__/Accounts.ts @@ -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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(AccountsDocument, options); + } +export function useAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(AccountsDocument, options); + } +export type AccountsQueryHookResult = ReturnType; +export type AccountsLazyQueryHookResult = ReturnType; +export type AccountsQueryResult = Apollo.QueryResult; +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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSubscription(AccountEventsDocument, options); + } +export type AccountEventsSubscriptionHookResult = ReturnType; +export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult; diff --git a/libs/accounts/src/lib/__generated___/Accounts.ts b/libs/accounts/src/lib/__generated___/Accounts.ts index 71a8dd4e6..6448e214b 100644 --- a/libs/accounts/src/lib/__generated___/Accounts.ts +++ b/libs/accounts/src/lib/__generated___/Accounts.ts @@ -108,4 +108,4 @@ export function useAccountEventsSubscription(baseOptions: Apollo.SubscriptionHoo return Apollo.useSubscription(AccountEventsDocument, options); } export type AccountEventsSubscriptionHookResult = ReturnType; -export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult; \ No newline at end of file +export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult; diff --git a/libs/accounts/src/lib/accounts-container.tsx b/libs/accounts/src/lib/accounts-container.tsx deleted file mode 100644 index 8d6b27809..000000000 --- a/libs/accounts/src/lib/accounts-container.tsx +++ /dev/null @@ -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 ( - -

{t('Please connect Vega wallet')}

-
- ); - } - - return ; -}; diff --git a/libs/accounts/src/lib/accounts-data-provider.spec.ts b/libs/accounts/src/lib/accounts-data-provider.spec.ts new file mode 100644 index 000000000..747ea4771 --- /dev/null +++ b/libs/accounts/src/lib/accounts-data-provider.spec.ts @@ -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', + }, +]; diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts index 5b360aec5..d87a6019a 100644 --- a/libs/accounts/src/lib/accounts-data-provider.ts +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -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[]) +); diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx index 9ef0c5069..231a37795 100644 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -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) => - ({ 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( - 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(null); + const dataRef = useRef(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({ + 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 ( - + {data && ( + + )} ); }; + +export default AccountManager; diff --git a/libs/accounts/src/lib/accounts-table.spec.tsx b/libs/accounts/src/lib/accounts-table.spec.tsx index 815904382..57a7fcc3d 100644 --- a/libs/accounts/src/lib/accounts-table.spec.tsx +++ b/libs/accounts/src/lib/accounts-table.spec.tsx @@ -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(); - expect(baseElement).toBeTruthy(); + render( 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(); - 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( + 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(); - 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( + 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); +}); diff --git a/libs/accounts/src/lib/accounts-table.stories.tsx b/libs/accounts/src/lib/accounts-table.stories.tsx new file mode 100644 index 000000000..9c6d1c0ea --- /dev/null +++ b/libs/accounts/src/lib/accounts-table.stories.tsx @@ -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) => ( + 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, + }, + }, + ]), +}; diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index 48185ebb2..ef0dbd621 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -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: + '', +}; + +export interface GetRowsParams extends Omit { + 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 | 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( - ({ data }, ref) => { - const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); +export const AccountTable = forwardRef( + ({ onClickAsset, onClickWithdraw, onClickDeposit, ...props }, ref) => { + const [openBreakdown, setOpenBreakdown] = useState(false); + const [breakdown, setBreakdown] = useState(null); return ( - 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', - }, - ], - }); - }} - > - - value && value.length > 0 ? ( - - ) : ( - '' - ) - } - /> - - value ? AccountTypeMapping[value as AccountType] : '-' - } - /> - - - addDecimalsFormatNumber(value, data.asset.decimals) - } - /> - + <> + data.asset.id} + ref={ref} + rowHeight={34} + tooltipShowDelay={500} + defaultColDef={{ + flex: 1, + resizable: true, + tooltipComponent: TooltipCellComponent, + sortable: true, + }} + {...props} + > + ) => { + return ( + { + onClickAsset(value); + }} + > + {value} + + ); + }} + maxWidth={300} + /> + ) => + data && + data.asset && + addDecimalsFormatNumber(value, data.asset.decimals) + } + maxWidth={300} + /> + + ) => { + return ( + { + setOpenBreakdown(!openBreakdown); + setBreakdown(value || null); + }} + > + {t('Collateral breakdown')} + + ); + }} + /> + ) => { + return ( + + ); + }} + /> + ) => { + return ( + + ); + }} + /> + + +
+

{t('Collateral breakdown')}

+ +
+
+ ); } ); - -export default AccountsTable; diff --git a/libs/accounts/src/lib/breakdown-table.spec.tsx b/libs/accounts/src/lib/breakdown-table.spec.tsx new file mode 100644 index 000000000..cfe984752 --- /dev/null +++ b/libs/accounts/src/lib/breakdown-table.spec.tsx @@ -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(); + expect(baseElement).toBeTruthy(); + }); + + it('should render correct columns', async () => { + await act(async () => { + render(); + }); + 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(); + }); + 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); + }); +}); diff --git a/libs/accounts/src/lib/breakdown-table.tsx b/libs/accounts/src/lib/breakdown-table.tsx new file mode 100644 index 000000000..fc7e7a12d --- /dev/null +++ b/libs/accounts/src/lib/breakdown-table.tsx @@ -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( + ({ data }, ref) => { + return ( + + `${data.asset.id}-${data.type}-${data.market?.id}` + } + ref={ref} + rowHeight={34} + components={{ PriceCell }} + tooltipShowDelay={500} + defaultColDef={{ + flex: 1, + resizable: true, + }} + > + ) => + AccountTypeMapping[value] + } + /> + ) => { + if (!value) return '-'; + return value; + }} + minWidth={200} + /> + + + ) => { + if (data && data.asset) { + return addDecimalsFormatNumber(value, data.asset.decimals); + } + return '-'; + }} + maxWidth={300} + /> + ) => { + if (data && data.asset) { + return addDecimalsFormatNumber(value, data.asset.decimals); + } + return '-'; + }} + maxWidth={300} + /> + + ); + } +); + +export default BreakdownTable; diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts index 6bdb90b6b..86e857552 100644 --- a/libs/accounts/src/lib/index.ts +++ b/libs/accounts/src/lib/index.ts @@ -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'; diff --git a/libs/accounts/tsconfig.json b/libs/accounts/tsconfig.json index 1eabf319c..b9096c1ed 100644 --- a/libs/accounts/tsconfig.json +++ b/libs/accounts/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "./tsconfig.spec.json" + }, + { + "path": "./.storybook/tsconfig.json" } ] } diff --git a/libs/deposits/src/index.ts b/libs/deposits/src/index.ts index 07125c4da..f41a696fd 100644 --- a/libs/deposits/src/index.ts +++ b/libs/deposits/src/index.ts @@ -1,3 +1 @@ -export * from './lib/deposit-manager'; -export * from './lib/use-deposits'; -export * from './lib/deposits-table'; +export * from './lib'; diff --git a/libs/deposits/src/lib/__generated__/DepositAsset.ts b/libs/deposits/src/lib/__generated__/DepositAsset.ts new file mode 100644 index 000000000..5113e5c4e --- /dev/null +++ b/libs/deposits/src/lib/__generated__/DepositAsset.ts @@ -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; +} diff --git a/libs/deposits/src/lib/__generated__/index.ts b/libs/deposits/src/lib/__generated__/index.ts new file mode 100644 index 000000000..28bd955ea --- /dev/null +++ b/libs/deposits/src/lib/__generated__/index.ts @@ -0,0 +1,2 @@ +export * from './Deposit'; +export * from './DepositAsset'; diff --git a/libs/deposits/src/lib/deposit-container.tsx b/libs/deposits/src/lib/deposit-container.tsx new file mode 100644 index 000000000..7a0944005 --- /dev/null +++ b/libs/deposits/src/lib/deposit-container.tsx @@ -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(DEPOSITS_QUERY, { + variables: { partyId: keypair?.pub }, + skip: !keypair?.pub, + }); + + const assets = getEnabledAssets(data); + + return ( + data={data} loading={loading} error={error}> + {assets.length ? ( + + + + ) : ( + +

{t('No assets on this network')}

+
+ )} + + ); +}; diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index db7f67061..fb18cbacb 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -77,9 +77,9 @@ export const DepositForm = ({ formState: { errors }, } = useForm({ defaultValues: { - asset: selectedAsset?.id, from: account, to: keypair?.pub, + asset: selectedAsset?.id || '', }, }); @@ -151,7 +151,11 @@ export const DepositForm = ({ !!selectedAsset || required(value), + }, + }} render={({ field }) => ( ({ 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) => ( + + ); + + 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) => ( - -); - -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')); -}; diff --git a/libs/withdraws/src/lib/withdraw-manager.tsx b/libs/withdraws/src/lib/withdraw-manager.tsx index 18d06282c..137d7de62 100644 --- a/libs/withdraws/src/lib/withdraw-manager.tsx +++ b/libs/withdraws/src/lib/withdraw-manager.tsx @@ -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 ( ) => void; } -export const useWithdrawStore = create((set: SetState) => ({ +export const useWithdrawStore = create((set) => ({ asset: undefined, balance: new BigNumber(0), min: new BigNumber(0), diff --git a/libs/withdraws/src/lib/withdrawal-dialogs.tsx b/libs/withdraws/src/lib/withdrawal-dialogs.tsx index 6dcec74b1..014d4667e 100644 --- a/libs/withdraws/src/lib/withdrawal-dialogs.tsx +++ b/libs/withdraws/src/lib/withdrawal-dialogs.tsx @@ -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" > { setWithdrawDialog(false);