Compare commits

...

31 Commits

Author SHA1 Message Date
Madalina Raicu
db312e4420
chore: #847 add get account data test 2022-09-22 23:02:08 +01:00
Madalina Raicu
bb081c7378
fix: #847 added tooltip and updated filtering 2022-09-22 22:54:18 +01:00
Madalina Raicu
473245023b
feat: #847 add storybook 2022-09-22 22:48:29 +01:00
Madalina Raicu
9c39277ea2
fix: #847 fix progress bar in accounts and positions 2022-09-22 21:16:00 +01:00
Madalina Raicu
2c0e7d0ab2
Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-22 19:34:52 +01:00
Madalina Raicu
8ac7d840d3
feat: #847 added useDepositAsset and useWithdrawAsset 2022-09-22 19:34:26 +01:00
Madalina Raicu
e743df7ee7
Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-22 17:55:56 +01:00
Madalina Raicu
bae0a160c5
fix: #847 UI tweaks around accounts container 2022-09-22 17:30:05 +01:00
Madalina Raicu
5edb33c3cd
fix: container moved, progress bar in helpers 2022-09-22 14:22:02 +01:00
Madalina Raicu
d9032d3171
Merge branch 'chore/ignore-apollo-errors' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-21 13:59:56 +01:00
Madalina Raicu
205806d047
Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-21 13:58:50 +01:00
Bartłomiej Głownia
74edfaf9ca chore: ignore apollo errors - to be reverted after API will be fixed 2022-09-21 14:50:34 +02:00
Madalina Raicu
b96c86dfab
feat: add storybook set up 2022-09-21 13:43:56 +01:00
Madalina Raicu
3098882035
fix: revert update on account fields 2022-09-20 21:54:30 +01:00
Madalina Raicu
de1a2d5105
Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-20 18:03:20 +01:00
Madalina Raicu
37cf269732
Merge branch 'feat/847-portfolio-update-collateral-tab' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-20 14:13:10 +01:00
Madalina Raicu
371c574597
Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feat/847-portfolio-update-collateral-tab 2022-09-20 14:12:01 +01:00
m.ray
80c91ce8dd
fix: #847 update deposit-form.spec.tsx 2022-09-20 14:00:13 +01:00
Madalina Raicu
bd836b0d83
fix: #847 use only bigint no bignumber, remove NaN check 2022-09-20 01:40:15 +01:00
Madalina Raicu
bdcfeb9db3
fix: #847 pass asset id as default value 2022-09-20 01:13:30 +01:00
Madalina Raicu
a985d9e42e
fix: #847 default select deposit & withdraw 2022-09-19 23:33:56 +01:00
Madalina Raicu
9c87293b02
fix: #847 default select deposit & withdraw 2022-09-19 23:29:44 +01:00
Madalina Raicu
31c7e1cf65
fix: #847 integration tests 2022-09-19 19:41:17 +01:00
Madalina Raicu
232fadb03c
fix: #847 remove global reward from incoming - needs to be party specific 2022-09-19 18:15:12 +01:00
Madalina Raicu
a402075cf0
fix: #847 remove disabledSelect
to fix withdraw and deposit dialogs
2022-09-19 17:49:31 +01:00
Madalina Raicu
0b9bbaf052
fix: #847 add deposit new asset button 2022-09-19 17:40:02 +01:00
Madalina Raicu
6bd2b15423
fix: #847 add styling fixes 2022-09-19 17:12:40 +01:00
Madalina Raicu
b589f627d9
feat: #847 show deposited value, avaliable and percentage used 2022-09-19 13:50:49 +01:00
Madalina Raicu
e5d3adcbe0
fix: #847 add deposit asset type and fix tests 2022-09-19 12:54:26 +01:00
Madalina Raicu
ed9594c0f8
feat: #847 add collateral tables 2022-09-19 09:54:22 +01:00
Madalina Raicu
99d5b56167
feat: #847 show progress bar, margin accounts, no used/deposited 2022-09-15 18:25:52 +01:00
52 changed files with 1737 additions and 407 deletions

View File

@ -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',

View File

@ -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')

View File

@ -69,9 +69,10 @@ 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>
<button
className="hover:underline"
onClick={() => push(`/markets/${marketId}`)}
>{`${name} ${t('liquidity provision')}`}</button>
}
>
<HeaderStat

View File

@ -11,7 +11,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 {
import { TradingModeTooltip } from '../../components/trading-mode-tooltip';
import { useRouter } from 'next/router';
import { Header, HeaderStat } from '../../components/header';
import { AccountsContainer } from '../portfolio/accounts-container';
const TradingViews = {
Candles: CandlesChartContainer,

View File

@ -0,0 +1,105 @@
import { useState } from 'react';
import { Button, 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">
<header className="flex justify-between items-center p-4">
<h1 className="text-lg text-black dark:text-white">
{t('Collateral')}
</h1>
<Button
size="sm"
onClick={() => setDepositDialog(true)}
data-testid="deposit-new-dialog-button"
>
{t('Deposit new asset')}
</Button>
</header>
<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 { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } =
useAssetDetailsDialogStore();
const [assetId, setAssetId] = useState<string>();
return (
<>
<AccountManager
partyId={partyId}
onClickAsset={(value) => {
if (value) {
setAssetDetailsDialogOpen(true);
setAssetDetailsDialogSymbol(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>
);
};

View File

@ -1,7 +1,6 @@
import { t } 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';
@ -10,6 +9,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { DepositsContainer } from './deposits-container';
import { ResizableGrid } from '@vegaprotocol/ui-toolkit';
import { LayoutPriority } from 'allotment';
import { AccountsContainer } from './accounts-container';
const Portfolio = () => {
const wrapperClasses = 'h-full max-h-full flex flex-col';

View File

@ -13,7 +13,6 @@ export const WithdrawalsContainer = () => {
const { withdrawals, loading, error } = useWithdrawals();
const [withdrawDialog, setWithdrawDialog] = useState(false);
console.log('render');
return (
<Web3Container>
<VegaWalletContainer>

View 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;
},
};

View File

@ -0,0 +1 @@
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />

View 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>
);
},
];

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View 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"]
}

View File

@ -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
}
}
}
}
}

View 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>;

View File

@ -0,0 +1 @@
export * from './Accounts';

View File

@ -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>;

View File

@ -0,0 +1 @@
export * from './Accounts';

View File

@ -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} />;
};

View File

@ -0,0 +1,272 @@
import { AccountType } from '@vegaprotocol/types';
import { getAccountData } from './accounts-data-provider';
import type { AccountFieldsFragment } from './__generated__';
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 = [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: 'XYZalpha',
symbol: 'XYZalpha',
},
available: '10001000000',
balance: '10001000000',
breakdown: [],
deposited: '10001000000',
market: null,
type: 'ACCOUNT_TYPE_GENERAL',
used: '0',
},
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '4000000000000001006031',
balance: '4000000000000001006031',
breakdown: [],
deposited: '4000000000000001006031',
market: null,
type: 'ACCOUNT_TYPE_GENERAL',
used: '0',
},
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
},
available: '5000186156',
balance: '406922',
breakdown: [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
},
available: '5000186156',
balance: '406922',
deposited: '5000593078',
market: {
__typename: 'Market',
id: '9c1ee71959e566c484fcea796513137f8a02219cca2e973b7ae72dc29d099581',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'AAVEDAI Monthly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '406922',
},
],
deposited: '5000593078',
market: {
__typename: 'Market',
id: '9c1ee71959e566c484fcea796513137f8a02219cca2e973b7ae72dc29d099581',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'AAVEDAI Monthly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '406922',
},
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
symbol: 'tEURO',
},
available: '2993437206',
balance: '2781397',
breakdown: [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
symbol: 'tEURO',
},
available: '2993437206',
balance: '2781397',
deposited: '2996218603',
market: {
__typename: 'Market',
id: 'd90fd7c746286625504d7a3f5f420a280875acd3cd611676d9e70acc675f4540',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'Tesla Quarterly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '2781397',
},
],
deposited: '2996218603',
market: {
__typename: 'Market',
id: 'd90fd7c746286625504d7a3f5f420a280875acd3cd611676d9e70acc675f4540',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'Tesla Quarterly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '2781397',
},
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '993ed98f4f770d91a796faab1738551193ba45c62341d20597df70fea6704ede',
symbol: 'tUSDC',
},
available: '1990351587',
balance: '1990351587',
breakdown: [],
deposited: '1990351587',
market: null,
type: 'ACCOUNT_TYPE_GENERAL',
used: '0',
},
];

View File

@ -10,6 +10,15 @@ import type {
AccountEventsSubscription,
} from './__generated___/Accounts';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import { AccountType } from '@vegaprotocol/types';
export interface AccountFields extends AccountFieldsFragment {
available: string;
used: string;
deposited: string;
balance: string;
breakdown?: AccountFields[];
}
function isAccount(
account:
@ -28,6 +37,33 @@ export const getId = (
? `${account.type}-${account.asset.id}-${account.market?.id ?? 'null'}`
: `${account.type}-${account.assetId}-${account.marketId}`;
const IN_ACCOUNT_TYPES = [
AccountType.ACCOUNT_TYPE_GENERAL,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
];
const OUT_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 getData = (
responseData: AccountsQuery
): AccountFieldsFragment[] | null => {
return responseData.party?.accounts ?? null;
};
const getDelta = (
subscriptionData: AccountEventsSubscription
): AccountEventsSubscription['accounts'] => subscriptionData.accounts;
const update = (
data: AccountFieldsFragment[],
deltas: AccountEventsSubscription['accounts']
@ -40,21 +76,12 @@ const update = (
draft[index].balance = delta.balance;
} else {
// #TODO handle new account
// draft.push(delta);
}
});
});
};
const getData = (
responseData: AccountsQuery
): AccountFieldsFragment[] | null => {
return responseData.party?.accounts ?? null;
};
const getDelta = (
subscriptionData: AccountEventsSubscription
): AccountEventsSubscription['accounts'] => subscriptionData.accounts;
export const accountsDataProvider = makeDataProvider<
AccountsQuery,
AccountFieldsFragment[],
@ -67,3 +94,46 @@ export const accountsDataProvider = makeDataProvider<
getData,
getDelta,
});
const getSymbols = (data: AccountFieldsFragment[]) =>
Array.from(new Set(data.map((a) => a.asset.symbol))).sort();
export const getAccountData = (
data: AccountFieldsFragment[]
): AccountFields[] => {
const collateralData = getSymbols(data).map((assetSymbol) => {
const assetData = data.filter((a) => a.asset.symbol === assetSymbol);
const deposited = assetData
.filter((a) => [AccountType.ACCOUNT_TYPE_GENERAL].includes(a.type))
.reduce((acc, a) => acc + BigInt(a.balance), BigInt(0));
const incoming = assetData
.filter((a) => IN_ACCOUNT_TYPES.includes(a.type))
.reduce((acc, a) => acc + BigInt(a.balance), BigInt(0));
const used = assetData
.filter((a) => OUT_ACCOUNT_TYPES.includes(a.type))
.reduce((acc, a) => acc + BigInt(a.balance), BigInt(0));
const depositRow: AccountFields = {
...assetData[0],
available: (incoming - used).toString(),
deposited: deposited.toString(),
used: used.toString(),
};
const accountRows = assetData
.filter((a) => ![AccountType.ACCOUNT_TYPE_GENERAL].includes(a.type))
.map((a) => ({
...a,
available: (incoming - BigInt(a.balance)).toString(),
deposited: deposited.toString(),
used: a.balance.toString(),
}));
return { ...depositRow, breakdown: accountRows };
});
return collateralData;
};

View File

@ -1,79 +1,84 @@
import React, { useRef, useMemo } from 'react';
import { produce } from 'immer';
import merge from 'lodash/merge';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useDataProvider, addSummaryRows } from '@vegaprotocol/react-helpers';
import type {
AccountFieldsFragment,
AccountEventsSubscription,
} from './__generated___/Accounts';
import type { Asset } from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { AgGridReact } from 'ag-grid-react';
import produce from 'immer';
import merge from 'lodash/merge';
import { useRef, useCallback } from 'react';
import {
AccountsTable,
getGroupId,
getGroupSummaryRow,
} from './accounts-table';
import { accountsDataProvider, getId } from './accounts-data-provider';
getId,
accountsDataProvider,
getAccountData,
} from './accounts-data-provider';
import { AccountTable } from './accounts-table';
import type {
AccountEventsSubscription,
AccountFieldsFragment,
} from './__generated__';
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 variables = useMemo(() => ({ partyId }), [partyId]);
const update = accountsManagerUpdate(gridRef);
const { data, error, loading } = useDataProvider<
const update = useCallback(
({ 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,
});
}
return true;
},
[gridRef]
);
const { data: collateralData } = useDataProvider<
AccountFieldsFragment[],
AccountEventsSubscription['accounts']
>({ dataProvider: accountsDataProvider, update, variables });
>({ dataProvider: accountsDataProvider, update, variables: { partyId } });
const data = collateralData && getAccountData(collateralData);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<AccountsTable ref={gridRef} data={data} />
</AsyncRenderer>
<AccountTable
data={data}
ref={gridRef}
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
/>
);
};
export default AccountManager;

View File

@ -1,9 +1,10 @@
import AccountsTable from './accounts-table';
import AccountsTable from './breakdown-table';
import { act, render, screen, waitFor } from '@testing-library/react';
import type { AccountFieldsFragment } from './__generated___/Accounts';
import { Schema as Types } from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import { getAccountData } from './accounts-data-provider';
const singleRow: AccountFieldsFragment = {
const singleRow: AccountFields = {
__typename: 'Account',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
balance: '125600000',
@ -24,6 +25,9 @@ const singleRow: AccountFieldsFragment = {
symbol: 'tBTC',
decimals: 5,
},
available: '125600000',
used: '125600000',
deposited: '125600000',
};
const singleRowData = [singleRow];
@ -40,12 +44,12 @@ describe('AccountsTable', () => {
render(<AccountsTable data={singleRowData} />);
await waitFor(async () => {
const headers = await screen.getAllByRole('columnheader');
expect(headers).toHaveLength(4);
expect(headers).toHaveLength(5);
expect(
headers.map((h) =>
h.querySelector('[ref="eText"]')?.textContent?.trim()
)
).toEqual(['Asset', 'Type', 'Market', 'Balance']);
).toEqual(['Asset', 'Deposited', 'Used', '', '', '']);
});
});
});
@ -67,4 +71,63 @@ describe('AccountsTable', () => {
});
});
});
it('should get correct account data', () => {
const result = getAccountData([singleRow]);
const expected = [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '-125600000',
balance: '125600000',
breakdown: [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '-125600000',
balance: '125600000',
deposited: '0',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '125600000',
},
],
deposited: '0',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '125600000',
},
];
expect(result).toEqual(expected);
});
});

View File

@ -0,0 +1,123 @@
import type { Story, Meta } from '@storybook/react';
import { AccountType } from 'libs/types/src/__generated__/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,
},
},
]),
};

View File

@ -1,174 +1,190 @@
import { forwardRef } from 'react';
import { forwardRef, useState } from 'react';
import type {
ColumnApi,
GroupCellRendererParams,
ValueFormatterParams,
} from 'ag-grid-community';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import type { ValueProps } 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 type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import { getId } from './accounts-data-provider';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { AccountType } from '@vegaprotocol/types';
import { AccountTypeMapping } from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import type { AccountFieldsFragment } from './__generated___/Accounts';
import BreakdownTable from './breakdown-table';
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 max = BigInt(data.deposited);
const range = max > min ? max : min;
return {
low: addDecimalsFormatNumber(min.toString(), data.asset.decimals, 2),
high: addDecimalsFormatNumber(max.toString(), data.asset.decimals, 2),
value: range ? Number((min * BigInt(100)) / range) : 0,
intent: data.lowMarginLevel ? Intent.Warning : undefined,
};
};
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>',
};
interface AccountsTableValueFormatterParams extends ValueFormatterParams {
data: AccountFieldsFragment;
}
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 const assetDecimalsFormatter = ({
value,
data,
}: AccountsTableValueFormatterParams) =>
addDecimalsFormatNumber(value, data.asset.decimals);
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;
};
export interface AccountTableProps extends AgGridReactProps {
data: AccountFields[] | null;
onClickAsset: (asset?: string | Asset) => void;
onClickWithdraw?: (assetId?: string) => void;
onClickDeposit?: (assetId?: string) => void;
}
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 { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } =
useAssetDetailsDialogStore();
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
({ data, onClickAsset, onClickWithdraw, onClickDeposit }, 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={() => {
setAssetDetailsDialogOpen(true);
setAssetDetailsDialogSymbol(value);
}}
>
{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%' }}
rowData={data}
getRowId={({ data }) => getId(data)}
ref={ref}
rowHeight={34}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
}}
>
<AgGridColumn
headerName={t('Asset')}
field="asset.symbol"
headerTooltip={t(
'Asset is the collateral that is deposited into the Vega protocol.'
)}
cellRenderer={({ value }: ValueFormatterParams) => {
return (
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickAsset(value);
}}
>
{value}
</ButtonLink>
);
}}
maxWidth={300}
/>
<AgGridColumn
headerName={t('Balance')}
headerTooltip={t(
'This is the general account balance for the given asset.'
)}
field="deposited"
valueFormatter={assetDecimalsFormatter}
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 }: GroupCellRendererParams) => {
return (
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(value);
}}
>
{t('Collateral breakdown')}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName=""
field="deposit"
maxWidth={200}
cellRenderer={({ data }: GroupCellRendererParams) => {
return (
<Button
size="xs"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</Button>
);
}}
/>
<AgGridColumn
headerName=""
field="withdraw"
maxWidth={200}
cellRenderer={({ data }: GroupCellRendererParams) => {
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;

View File

@ -0,0 +1,133 @@
import BreakdownTable from './breakdown-table';
import { act, render, screen, waitFor } 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 () => {
await act(async () => {
const { baseElement } = render(<BreakdownTable data={[]} />);
expect(baseElement).toBeTruthy();
});
});
it('should render correct columns', async () => {
act(async () => {
render(<BreakdownTable data={singleRowData} />);
await waitFor(async () => {
const headers = await screen.getAllByRole('columnheader');
expect(headers).toHaveLength(3);
expect(
headers.map((h) =>
h.querySelector('[ref="eText"]')?.textContent?.trim()
)
).toEqual(['Market', 'Used', 'Type']);
});
});
});
it('should apply correct formatting', async () => {
act(async () => {
render(<BreakdownTable 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]);
});
});
});
});
it('should get correct account data', () => {
const result = getAccountData([singleRow]);
const expected = [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '-125600000',
balance: '125600000',
breakdown: [
{
__typename: 'Account',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '-125600000',
balance: '125600000',
deposited: '0',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '125600000',
},
],
deposited: '0',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
},
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '125600000',
},
];
expect(result).toEqual(expected);
});
});

View File

@ -0,0 +1,70 @@
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
import type { ValueFormatterParams } from 'ag-grid-community';
import { PriceCell, t } from '@vegaprotocol/react-helpers';
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 { getId } from './accounts-data-provider';
import { AccountTypeMapping } from '@vegaprotocol/types';
import type { AccountType } from '@vegaprotocol/types';
import {
progressBarHeaderComponentParams,
progressBarValueFormatter,
} from './accounts-table';
interface BreakdownTableProps extends AgGridReactProps {
data: AccountFields[] | null;
style?: CSSProperties;
}
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
({ data }, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No collateral breakdown')}
rowData={data}
getRowId={({ data }) => getId(data)}
ref={ref}
rowHeight={34}
components={{ PriceCell }}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
resizable: true,
}}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.name"
valueFormatter="value || '—'"
maxWidth={300}
/>
<AgGridColumn
headerName={t('Used')}
field="used"
flex={2}
maxWidth={500}
headerComponentParams={progressBarHeaderComponentParams}
cellRendererSelector={progressBarCellRendererSelector}
valueFormatter={progressBarValueFormatter}
/>
<AgGridColumn
headerName={t('Type')}
field="type"
maxWidth={300}
valueFormatter={({ value }: ValueFormatterParams) =>
AccountTypeMapping[value as AccountType]
}
/>
</AgGrid>
);
}
);
export default BreakdownTable;

View File

@ -1,6 +1,7 @@
export * from './__generated___/Accounts';
export * from './accounts-container';
export * from './__generated__';
// export * from './__generated___';
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';

View File

@ -20,6 +20,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./.storybook/tsconfig.json"
}
]
}

View File

@ -1,3 +1 @@
export * from './lib/deposit-manager';
export * from './lib/use-deposits';
export * from './lib/deposits-table';
export * from './lib';

View 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;
}

View File

@ -0,0 +1,5 @@
export * from './DepositEvent';
export * from './DepositEventSub';
export * from './DepositFields';
export * from './Deposits';
export * from './DepositsQuery';

View File

@ -0,0 +1 @@
export * from './Deposit';

View 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>
);
};

View File

@ -78,9 +78,9 @@ export const DepositForm = ({
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
asset: selectedAsset?.id,
from: account,
to: keypair?.pub,
asset: selectedAsset?.id || '',
},
});
@ -152,7 +152,11 @@ export const DepositForm = ({
<Controller
control={control}
name="asset"
rules={{ validate: { required } }}
rules={{
validate: {
required,
},
}}
render={({ field }) => (
<Select
id="asset"
@ -161,6 +165,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) => (

View File

@ -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>

View File

@ -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

View File

@ -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),

View File

@ -0,0 +1,17 @@
export * from './__generated__';
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';

View File

@ -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) => {

View File

@ -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}

View File

@ -33,8 +33,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>
</>
);

View File

@ -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')}

View File

@ -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"]
}

View File

@ -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

View File

@ -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,
};
};

View File

@ -1 +1,2 @@
export * from './grid-progress-bar';
export * from './progress-bar';

View File

@ -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}

View File

@ -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,
},
}}
render={({ field }) => (
<Select
{...field}

View File

@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/react-helpers';
import { formatNumber, t } from '@vegaprotocol/react-helpers';
import BigNumber from 'bignumber.js';
import { formatDistanceToNow } from 'date-fns';
@ -35,7 +35,7 @@ export const WithdrawLimits = ({
<tbody>
<tr data-testid="balance-available">
<th className="text-left font-normal">{t('Balance available')}</th>
<td className="text-right">{balance.toString()}</td>
<td className="text-right">{formatNumber(balance)}</td>
</tr>
<tr data-testid="withdrawal-threshold">
<th className="text-left font-normal">

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import sortBy from 'lodash/sortBy';
import { WithdrawForm } from './withdraw-form';
import type { WithdrawalArgs } from './use-create-withdraw';
@ -12,22 +12,16 @@ import { captureException } from '@sentry/react';
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
import { useWithdrawStore } from './withdraw-store';
export interface WithdrawManagerProps {
assets: Asset[];
accounts: Account[];
submit: (args: WithdrawalArgs) => void;
}
export const WithdrawManager = ({
assets,
accounts,
submit,
}: WithdrawManagerProps) => {
const useWithdrawAsset = (
assets: Asset[],
accounts: Account[],
assetId?: string
) => {
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
// 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) => {
@ -63,6 +57,28 @@ export const WithdrawManager = ({
[accounts, assets, update, getThreshold, getDelay]
);
useEffect(() => {
handleSelectAsset(assetId || '');
}, [assetId, handleSelectAsset]);
return { asset, balance, min, threshold, delay, handleSelectAsset };
};
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, handleSelectAsset } =
useWithdrawAsset(assets, accounts, assetId);
return (
<WithdrawForm
selectedAsset={asset}

View File

@ -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),

View File

@ -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);