chore(ledger): remove ledger entries table, adjust download form (#4611)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Maciek 2023-08-29 15:57:02 +02:00 committed by GitHub
parent 332cc302c3
commit 9ac199f59a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 591 additions and 969 deletions

View File

@ -1,59 +1,25 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress'; import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { ledgerEntriesQuery } from '@vegaprotocol/mock'; import { partyAssetsQuery } from '@vegaprotocol/mock';
describe('Portfolio page', { tags: '@smoke' }, () => { describe('Portfolio page', { tags: '@smoke' }, () => {
beforeEach(() => { beforeEach(() => {
cy.mockTradingPage();
cy.mockGQL((req) => { cy.mockGQL((req) => {
aliasGQLQuery(req, 'LedgerEntries', ledgerEntriesQuery()); aliasGQLQuery(req, 'PartyAssets', partyAssetsQuery());
}); });
cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setVegaWallet(); cy.setVegaWallet();
}); });
describe('Ledger entries', () => { describe('Ledger entries', () => {
it('List should be properly rendered', () => { it('Download form should be properly rendered', () => {
// 7007-LEEN-001
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click(); cy.getByTestId('"Ledger entries"').click();
const headers = [
'Sender',
'Account type',
'Market',
'Receiver',
'Account type',
'Market',
'Transfer type',
'Quantity',
'Asset',
'Sender account balance',
'Receiver account balance',
'Vega time',
];
cy.getByTestId('tab-ledger-entries').within(($headers) => { cy.getByTestId('tab-ledger-entries').within(($headers) => {
cy.wrap($headers) cy.wrap($headers)
.get('.ag-header-cell-text') .getByTestId('ledger-download-button')
.each(($header, i) => { .should('be.visible');
cy.wrap($header).should('have.text', headers[i]);
});
}); });
cy.get(
'[data-testid="tab-ledger-entries"] .ag-center-cols-container .ag-row'
).should('have.length', ledgerEntriesQuery().ledgerEntries.edges.length);
});
it('account filters should be callable', () => {
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
cy.get('[role="columnheader"][col-id="fromAccountType"]').realHover();
cy.get(
'[role="columnheader"][col-id="fromAccountType"] .ag-header-cell-menu-button'
).click();
cy.get('fieldset.ag-simple-filter-body-wrapper')
.should('be.visible')
.within((fields) => {
cy.wrap(fields).find('label').should('have.length', 18);
});
cy.getByTestId('"Ledger entries"').click();
cy.get('fieldset.ag-simple-filter-body-wrapper').should('not.exist');
}); });
}); });
}); });

View File

@ -1,23 +1,30 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { LedgerManager } from '@vegaprotocol/ledger'; import { LedgerExportForm } from '@vegaprotocol/ledger';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import type { DataGridSlice } from '../../stores/datagrid-store-slice'; import { useEnvironment } from '@vegaprotocol/environment';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import type { PartyAssetFieldsFragment } from '@vegaprotocol/assets';
import { create } from 'zustand'; import { usePartyAssetsQuery } from '@vegaprotocol/assets';
import { persist } from 'zustand/middleware';
export const LedgerContainer = () => { export const LedgerContainer = () => {
const VEGA_URL = useEnvironment((store) => store.VEGA_URL);
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { data, loading } = usePartyAssetsQuery({
const gridStore = useLedgerStore((store) => store.gridStore); variables: { partyId: pubKey || '' },
const updateGridStore = useLedgerStore((store) => store.updateGridStore); skip: !pubKey,
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
updateGridStore(colState);
}); });
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
if (!pubKey) { if (!pubKey) {
return ( return (
<Splash> <Splash>
@ -26,11 +33,31 @@ export const LedgerContainer = () => {
); );
} }
return <LedgerManager partyId={pubKey} gridProps={gridStoreCallbacks} />; if (!VEGA_URL) {
}; return (
<Splash>
<p>{t('Environment not configured')}</p>
</Splash>
);
}
const useLedgerStore = create<DataGridSlice>()( if (loading) {
persist(createDataGridSlice, { return (
name: 'vega_ledger_store', <div className="relative flex items-center justify-center w-full h-full">
}) <Loader />
); </div>
);
}
if (!Object.keys(assets).length) {
return (
<Splash>
<p>{t('No ledger entries to export')}</p>
</Splash>
);
}
return (
<LedgerExportForm partyId={pubKey} vegaUrl={VEGA_URL} assets={assets} />
);
};

View File

@ -24,3 +24,26 @@ query Assets {
} }
} }
} }
fragment PartyAssetFields on Asset {
id
name
symbol
status
}
query PartyAssets($partyId: ID!) {
party(id: $partyId) {
id
accountsConnection {
edges {
node {
type
asset {
...PartyAssetFields
}
}
}
}
}
}

View File

@ -10,6 +10,15 @@ export type AssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null }; export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null };
export type PartyAssetFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, status: Types.AssetStatus };
export type PartyAssetsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyAssetsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, asset: { __typename?: 'Asset', id: string, name: string, symbol: string, status: Types.AssetStatus } } } | null> | null } | null } | null };
export const AssetListFieldsFragmentDoc = gql` export const AssetListFieldsFragmentDoc = gql`
fragment AssetListFields on Asset { fragment AssetListFields on Asset {
id id
@ -28,6 +37,14 @@ export const AssetListFieldsFragmentDoc = gql`
status status
} }
`; `;
export const PartyAssetFieldsFragmentDoc = gql`
fragment PartyAssetFields on Asset {
id
name
symbol
status
}
`;
export const AssetsDocument = gql` export const AssetsDocument = gql`
query Assets { query Assets {
assetsConnection { assetsConnection {
@ -65,4 +82,49 @@ export function useAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ass
} }
export type AssetsQueryHookResult = ReturnType<typeof useAssetsQuery>; export type AssetsQueryHookResult = ReturnType<typeof useAssetsQuery>;
export type AssetsLazyQueryHookResult = ReturnType<typeof useAssetsLazyQuery>; export type AssetsLazyQueryHookResult = ReturnType<typeof useAssetsLazyQuery>;
export type AssetsQueryResult = Apollo.QueryResult<AssetsQuery, AssetsQueryVariables>; export type AssetsQueryResult = Apollo.QueryResult<AssetsQuery, AssetsQueryVariables>;
export const PartyAssetsDocument = gql`
query PartyAssets($partyId: ID!) {
party(id: $partyId) {
id
accountsConnection {
edges {
node {
type
asset {
...PartyAssetFields
}
}
}
}
}
}
${PartyAssetFieldsFragmentDoc}`;
/**
* __usePartyAssetsQuery__
*
* To run a query within a React component, call `usePartyAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyAssetsQuery` 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 } = usePartyAssetsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyAssetsQuery(baseOptions: Apollo.QueryHookOptions<PartyAssetsQuery, PartyAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(PartyAssetsDocument, options);
}
export function usePartyAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PartyAssetsQuery, PartyAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(PartyAssetsDocument, options);
}
export type PartyAssetsQueryHookResult = ReturnType<typeof usePartyAssetsQuery>;
export type PartyAssetsLazyQueryHookResult = ReturnType<typeof usePartyAssetsLazyQuery>;
export type PartyAssetsQueryResult = Apollo.QueryResult<PartyAssetsQuery, PartyAssetsQueryVariables>;

View File

@ -0,0 +1,47 @@
import merge from 'lodash/merge';
import type { PartyAssetsQuery } from './__generated__/Assets';
import * as Types from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
export const partyAssetsQuery = (
override?: PartialDeep<PartyAssetsQuery>
): PartyAssetsQuery => {
const defaultAssets: PartyAssetsQuery = {
party: {
__typename: 'Party',
id: 'partyId',
accountsConnection: {
edges: partyAccountFields.map((node) => ({
__typename: 'AccountEdge',
node,
})),
},
},
};
return merge(defaultAssets, override);
};
const partyAccountFields = [
{
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
asset: {
__typename: 'Asset',
id: 'asset-id',
symbol: 'tEURO',
name: 'Euro',
status: Types.AssetStatus.STATUS_ENABLED,
},
},
{
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
asset: {
__typename: 'Asset',
id: 'asset-id-2',
symbol: 'tDAI',
name: 'DAI',
status: Types.AssetStatus.STATUS_ENABLED,
},
},
] as const;

View File

@ -2,6 +2,7 @@
export * from '../accounts/src/lib/accounts.mock'; export * from '../accounts/src/lib/accounts.mock';
export * from '../assets/src/lib/asset.mock'; export * from '../assets/src/lib/asset.mock';
export * from '../assets/src/lib/assets.mock'; export * from '../assets/src/lib/assets.mock';
export * from '../assets/src/lib/party-assets.mock';
export * from '../candles-chart/src/lib/candles.mock'; export * from '../candles-chart/src/lib/candles.mock';
export * from '../candles-chart/src/lib/chart.mock'; export * from '../candles-chart/src/lib/chart.mock';
export * from '../deal-ticket/src/hooks/estimate-order.mock'; export * from '../deal-ticket/src/hooks/estimate-order.mock';
@ -10,7 +11,6 @@ export * from '../environment/src/utils/node.mock';
export * from '../environment/src/components/node-guard/node-guard.mock'; export * from '../environment/src/components/node-guard/node-guard.mock';
export * from '../fills/src/lib/fills.mock'; export * from '../fills/src/lib/fills.mock';
export * from '../proposals/src/lib/proposals-data-provider/proposals.mock'; export * from '../proposals/src/lib/proposals-data-provider/proposals.mock';
export * from '../ledger/src/lib/ledger-entries.mock';
export * from '../market-depth/src/lib/market-depth.mock'; export * from '../market-depth/src/lib/market-depth.mock';
export * from '../markets/src/lib/components/market-info/market-info.mock'; export * from '../markets/src/lib/components/market-info/market-info.mock';
export * from '../markets/src/lib/market-candles.mock'; export * from '../markets/src/lib/market-candles.mock';

View File

@ -1,2 +1 @@
export * from './lib/ledger-manager'; export * from './lib/ledger-export-form';
export * from './lib/__generated__/LedgerEntries';

View File

@ -1,47 +0,0 @@
fragment LedgerEntry on AggregatedLedgerEntry {
vegaTime
quantity
assetId
transferType
toAccountType
toAccountMarketId
toAccountPartyId
toAccountBalance
fromAccountType
fromAccountMarketId
fromAccountPartyId
fromAccountBalance
}
query LedgerEntries(
$partyId: ID!
$pagination: Pagination
$dateRange: DateRange
$fromAccountType: [AccountType!]
$toAccountType: [AccountType!]
) {
ledgerEntries(
filter: {
FromAccountFilter: {
partyIds: [$partyId]
accountTypes: $fromAccountType
}
ToAccountFilter: { partyIds: [$partyId], accountTypes: $toAccountType }
}
pagination: $pagination
dateRange: $dateRange
) {
edges {
node {
...LedgerEntry
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}

View File

@ -1,88 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LedgerEntryFragment = { __typename?: 'AggregatedLedgerEntry', vegaTime: any, quantity: string, assetId?: string | null, transferType?: Types.TransferType | null, toAccountType?: Types.AccountType | null, toAccountMarketId?: string | null, toAccountPartyId?: string | null, toAccountBalance: string, fromAccountType?: Types.AccountType | null, fromAccountMarketId?: string | null, fromAccountPartyId?: string | null, fromAccountBalance: string };
export type LedgerEntriesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
pagination?: Types.InputMaybe<Types.Pagination>;
dateRange?: Types.InputMaybe<Types.DateRange>;
fromAccountType?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
toAccountType?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
}>;
export type LedgerEntriesQuery = { __typename?: 'Query', ledgerEntries: { __typename?: 'AggregatedLedgerEntriesConnection', edges: Array<{ __typename?: 'AggregatedLedgerEntriesEdge', cursor: string, node: { __typename?: 'AggregatedLedgerEntry', vegaTime: any, quantity: string, assetId?: string | null, transferType?: Types.TransferType | null, toAccountType?: Types.AccountType | null, toAccountMarketId?: string | null, toAccountPartyId?: string | null, toAccountBalance: string, fromAccountType?: Types.AccountType | null, fromAccountMarketId?: string | null, fromAccountPartyId?: string | null, fromAccountBalance: string } } | null>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } };
export const LedgerEntryFragmentDoc = gql`
fragment LedgerEntry on AggregatedLedgerEntry {
vegaTime
quantity
assetId
transferType
toAccountType
toAccountMarketId
toAccountPartyId
toAccountBalance
fromAccountType
fromAccountMarketId
fromAccountPartyId
fromAccountBalance
}
`;
export const LedgerEntriesDocument = gql`
query LedgerEntries($partyId: ID!, $pagination: Pagination, $dateRange: DateRange, $fromAccountType: [AccountType!], $toAccountType: [AccountType!]) {
ledgerEntries(
filter: {FromAccountFilter: {partyIds: [$partyId], accountTypes: $fromAccountType}, ToAccountFilter: {partyIds: [$partyId], accountTypes: $toAccountType}}
pagination: $pagination
dateRange: $dateRange
) {
edges {
node {
...LedgerEntry
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
${LedgerEntryFragmentDoc}`;
/**
* __useLedgerEntriesQuery__
*
* To run a query within a React component, call `useLedgerEntriesQuery` and pass it any options that fit your needs.
* When your component renders, `useLedgerEntriesQuery` 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 } = useLedgerEntriesQuery({
* variables: {
* partyId: // value for 'partyId'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* fromAccountType: // value for 'fromAccountType'
* toAccountType: // value for 'toAccountType'
* },
* });
*/
export function useLedgerEntriesQuery(baseOptions: Apollo.QueryHookOptions<LedgerEntriesQuery, LedgerEntriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<LedgerEntriesQuery, LedgerEntriesQueryVariables>(LedgerEntriesDocument, options);
}
export function useLedgerEntriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<LedgerEntriesQuery, LedgerEntriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<LedgerEntriesQuery, LedgerEntriesQueryVariables>(LedgerEntriesDocument, options);
}
export type LedgerEntriesQueryHookResult = ReturnType<typeof useLedgerEntriesQuery>;
export type LedgerEntriesLazyQueryHookResult = ReturnType<typeof useLedgerEntriesLazyQuery>;
export type LedgerEntriesQueryResult = Apollo.QueryResult<LedgerEntriesQuery, LedgerEntriesQueryVariables>;

View File

@ -1,77 +0,0 @@
import type { Asset } from '@vegaprotocol/assets';
import { assetsMapProvider } from '@vegaprotocol/assets';
import type { Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets';
import {
makeDataProvider,
makeDerivedDataProvider,
} from '@vegaprotocol/data-provider';
import type {
LedgerEntriesQuery,
LedgerEntriesQueryVariables,
LedgerEntryFragment,
} from './__generated__/LedgerEntries';
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
export type LedgerEntry = LedgerEntryFragment & {
asset: Asset | null | undefined;
marketSender: Market | null | undefined;
marketReceiver: Market | null | undefined;
};
type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number];
const isLedgerEntryEdge = (entry: Edge): entry is NonNullable<Edge> =>
entry !== null;
const getData = (responseData: LedgerEntriesQuery | null) => {
return (
responseData?.ledgerEntries?.edges
.filter(isLedgerEntryEdge)
.map((edge) => edge.node) || []
);
};
const ledgerEntriesOnlyProvider = makeDataProvider<
LedgerEntriesQuery,
ReturnType<typeof getData>,
never,
never,
LedgerEntriesQueryVariables
>({
query: LedgerEntriesDocument,
getData,
additionalContext: {
isEnlargedTimeout: true,
},
});
export const ledgerEntriesProvider = makeDerivedDataProvider<
LedgerEntry[],
never,
LedgerEntriesQueryVariables
>(
[
ledgerEntriesOnlyProvider,
(callback, client) => assetsMapProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData) => {
const entries = partsData[0] as ReturnType<typeof getData>;
const assets = partsData[1] as Record<string, Asset>;
const markets = partsData[2] as Record<string, Market>;
return entries.map((entry) => {
const asset = entry.assetId
? (assets as Record<string, Asset>)[entry.assetId]
: null;
const marketSender = entry.fromAccountMarketId
? markets[entry.fromAccountMarketId]
: null;
const marketReceiver = entry.toAccountMarketId
? markets[entry.toAccountMarketId]
: null;
return { ...entry, asset, marketSender, marketReceiver };
});
}
);

View File

@ -1,247 +0,0 @@
import type {
LedgerEntriesQuery,
LedgerEntryFragment,
} from './__generated__/LedgerEntries';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import merge from 'lodash/merge';
export const ledgerEntriesQuery = (
override?: PartialDeep<LedgerEntriesQuery>
): LedgerEntriesQuery => {
const defaultResult: LedgerEntriesQuery = {
__typename: 'Query',
ledgerEntries: {
__typename: 'AggregatedLedgerEntriesConnection',
edges: ledgerEntries.map((node) => ({
__typename: 'AggregatedLedgerEntriesEdge',
node,
cursor: 'cursor-1',
})),
pageInfo: {
startCursor:
'eyJ2ZWdhX3RpbWUiOiIyMDIyLTExLTIzVDE3OjI3OjU2LjczNDM2NFoifQ==',
endCursor:
'eyJ2ZWdhX3RpbWUiOiIyMDIyLTExLTIzVDEzOjExOjE2LjU0NjM2M1oifQ==',
hasNextPage: false,
hasPreviousPage: false,
__typename: 'PageInfo',
},
},
};
return merge(defaultResult, override);
};
export const ledgerEntries: LedgerEntryFragment[] = [
{
vegaTime: '1669224476734364000',
quantity: '0',
assetId: 'asset-id',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: 'market-1',
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: 'market-0',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669221452175594000',
quantity: '0',
assetId: 'asset-id-2',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: 'market-0',
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: 'market-2',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209347054198000',
quantity: '0',
assetId: 'asset-id',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: 'market-3',
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: 'market-2',
fromAccountPartyId: 'sender party id',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209345512806000',
quantity: '0',
assetId: 'asset-id',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209316163397000',
quantity: '0',
assetId: 'asset-id-2',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209299051286000',
quantity: '1326783',
assetId: 'asset-id-2',
transferType: Schema.TransferType.TRANSFER_TYPE_MTM_WIN,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209151328614000',
quantity: '0',
assetId: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209117655380000',
quantity: '0',
assetId: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209082788024000',
quantity: '1326783',
assetId: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
transferType: Schema.TransferType.TRANSFER_TYPE_MTM_WIN,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209076546363000',
quantity: '0',
assetId: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T13:36:42.13989Z',
quantity: '9078407730948615',
assetId: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_LOW,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
toAccountMarketId: null,
toAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_MARGIN,
fromAccountMarketId:
'0942d767cb2cb5a795e14216e8e53c2b6f75e46dc1732c5aeda8a5aba4ad193d',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T13:35:49.257039Z',
quantity: '263142253070974',
assetId: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_LOW,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
toAccountMarketId: null,
toAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_MARGIN,
fromAccountMarketId:
'0942d767cb2cb5a795e14216e8e53c2b6f75e46dc1732c5aeda8a5aba4ad193d',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:41:22.054428Z',
quantity: '1000000000',
assetId: '4e4e80abff30cab933b8c4ac6befc618372eb76b2cbddc337eff0b4a3a4d25b8',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:39:11.516154Z',
quantity: '1000000000000',
assetId: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:37:26.832226Z',
quantity: '10000000000000000000000',
assetId: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:24:52.844901Z',
quantity: '49390000000000000000000',
assetId: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
];

View File

@ -0,0 +1,215 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createDownloadUrl, LedgerExportForm } from './ledger-export-form';
import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils';
const vegaUrl = 'https://vega-url.co.uk/querystuff';
const mockResponse = {
headers: { get: jest.fn() },
blob: () => '',
};
global.fetch = jest.fn().mockResolvedValue(mockResponse);
const assetsMock = {
['a'.repeat(64)]: 'symbol asset-id',
['b'.repeat(64)]: 'symbol asset-id-2',
};
describe('LedgerExportForm', () => {
const partyId = 'c'.repeat(64);
afterEach(() => {
jest.clearAllMocks();
});
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2023-08-10T10:10:10.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('should be properly rendered', async () => {
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
// userEvent does not work with faked timers
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('assetID should be properly change request url', async () => {
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
fireEvent.change(screen.getByTestId('select-ledger-asset'), {
target: { value: Object.keys(assetsMock)[1] },
});
expect(screen.getByText('symbol asset-id-2')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[1]
}&dateRange.startTimestamp=1691057410000000000`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('date-from should properly change request url', async () => {
const newDate = new Date(1691230210000);
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByLabelText('Date from')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Date from'), {
target: { value: formatForInput(newDate) },
});
expect(screen.getByTestId('date-from')).toHaveValue(
`${formatForInput(newDate)}.000`
);
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=${toNanoSeconds(newDate)}`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('date-to should properly change request url', async () => {
const newDate = new Date(1691230210000);
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByLabelText('Date to')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Date to'), {
target: { value: formatForInput(newDate) },
});
expect(screen.getByTestId('date-to')).toHaveValue(
`${formatForInput(newDate)}.000`
);
fireEvent.click(screen.getByTestId('ledger-download-button'));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000&dateRange.endTimestamp=${toNanoSeconds(
newDate
)}`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
});
describe('createDownloadUrl', () => {
const fromTimestamp = 1690848000000;
const toTimestamp = 1691107200000;
const args = {
protohost: 'https://vega-url.co.uk',
partyId: 'a'.repeat(64),
assetId: 'b'.repeat(64),
dateFrom: new Date(fromTimestamp).toISOString(),
};
it('formats url with without an end date', () => {
expect(createDownloadUrl(args)).toEqual(
`${args.protohost}/api/v2/ledgerentry/export?partyId=${
args.partyId
}&assetId=${args.assetId}&dateRange.startTimestamp=${toNanoSeconds(
args.dateFrom
)}`
);
});
it('formats url with with an end date', () => {
const dateTo = new Date(toTimestamp).toISOString();
expect(
createDownloadUrl({
...args,
dateTo,
})
).toEqual(
`${args.protohost}/api/v2/ledgerentry/export?partyId=${
args.partyId
}&assetId=${args.assetId}&dateRange.startTimestamp=${toNanoSeconds(
args.dateFrom
)}&dateRange.endTimestamp=${toNanoSeconds(dateTo)}`
);
});
it('should throw if invalid args are provided', () => {
// invalid url
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, protohost: 'foo' });
}).toThrow();
// invalid partyId
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, partyId: 'z'.repeat(64) });
}).toThrow();
});
});

View File

@ -0,0 +1,172 @@
import { useRef, useState } from 'react';
import { z } from 'zod';
import {
Button,
Loader,
TradingFormGroup,
TradingInput,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { toNanoSeconds, VEGA_ID_REGEX } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/logger';
import { formatForInput } from '@vegaprotocol/utils';
import { subDays } from 'date-fns';
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
const getProtoHost = (vegaurl: string) => {
const loc = new URL(vegaurl);
return `${loc.protocol}//${loc.host}`;
};
const downloadSchema = z.object({
protohost: z.string().url().nonempty(),
partyId: z.string().regex(VEGA_ID_REGEX).nonempty(),
assetId: z.string().regex(VEGA_ID_REGEX).nonempty(),
dateFrom: z.string().nonempty(),
dateTo: z.string().optional(),
});
export const createDownloadUrl = (args: z.infer<typeof downloadSchema>) => {
// check args from form inputs
downloadSchema.parse(args);
const params = new URLSearchParams();
params.append('partyId', args.partyId);
params.append('assetId', args.assetId);
params.append('dateRange.startTimestamp', toNanoSeconds(args.dateFrom));
if (args.dateTo) {
params.append('dateRange.endTimestamp', toNanoSeconds(args.dateTo));
}
const url = new URL(args.protohost);
url.pathname = '/api/v2/ledgerentry/export';
url.search = params.toString();
return url.toString();
};
interface Props {
partyId: string;
vegaUrl: string;
assets: Record<string, string>;
}
export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const now = useRef(new Date());
const [dateFrom, setDateFrom] = useState(() => {
return formatForInput(subDays(now.current, 7));
});
const [dateTo, setDateTo] = useState('');
const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current);
const [isDownloading, setIsDownloading] = useState(false);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId || isDownloading);
const assetDropDown = (
<TradingSelect
id="select-ledger-asset"
value={assetId}
onChange={(e) => {
setAssetId(e.target.value);
}}
className="w-full"
data-testid="select-ledger-asset"
disabled={isDownloading}
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
{assets[assetKey]}
</option>
))}
</TradingSelect>
);
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
setIsDownloading(true);
const resp = await fetch(link);
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
const blob = await resp.blob();
if (blob) {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
} finally {
setIsDownloading(false);
}
};
if (!protohost || Object.keys(assets).length === 0) {
return null;
}
return (
<form onSubmit={startDownload} className="p-4 w-[350px]">
<h2 className="mb-4">{t('Export ledger entries')}</h2>
<TradingFormGroup label={t('Select asset')} labelFor="asset">
{assetDropDown}
</TradingFormGroup>
<TradingFormGroup label={t('Date from')} labelFor="date-from">
<TradingInput
type="datetime-local"
data-testid="date-from"
id="date-from"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
disabled={disabled}
max={maxFromDate}
/>
</TradingFormGroup>
<TradingFormGroup label={t('Date to')} labelFor="date-to">
<TradingInput
type="datetime-local"
data-testid="date-to"
id="date-to"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
disabled={disabled}
max={maxToDate}
/>
</TradingFormGroup>
<div className="relative text-sm" title={t('Download all to .csv file')}>
{isDownloading && (
<div
className="absolute flex items-center justify-center w-full h-full"
data-testid="download-spinner"
>
<Loader size="small" />
</div>
)}
<Button
variant="primary"
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
{t('Download')}
</Button>
</div>
</form>
);
};

View File

@ -1,85 +0,0 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LedgerExportLink } from './ledger-export-link';
import * as Types from '@vegaprotocol/types';
import { ledgerEntries } from './ledger-entries.mock';
import type { LedgerEntry } from './ledger-entries-data-provider';
const VEGA_URL = 'https://vega-url.co.uk/querystuff';
const mockEnvironment = jest.fn(() => VEGA_URL);
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: jest.fn(() => mockEnvironment()),
}));
const asset = {
id: 'assetID',
name: 'assetName',
symbol: 'assetSymbol',
decimals: 1,
quantum: '1',
status: Types.AssetStatus,
source: {
__typename: 'ERC20',
contractAddress: 'contractAddres',
lifetimeLimit: 'lifetimeLimit',
withdrawThreshold: 'withdraw',
},
};
describe('LedgerExportLink', () => {
const partyId = 'partyId';
const entries = ledgerEntries.map((entry) => {
return {
...entry,
asset: entry.assetId
? {
...asset,
id: entry.assetId,
name: `name ${entry.assetId}`,
symbol: `symbol ${entry.assetId}`,
}
: null,
marketSender: null,
marketReceiver: null,
} as LedgerEntry;
});
it('should be properly rendered', async () => {
render(<LedgerExportLink partyId={partyId} entries={entries} />);
await waitFor(() => {
expect(screen.getByRole('link')).toBeInTheDocument();
expect(screen.getByRole('link')).toHaveAttribute(
'href',
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=asset-id`
);
expect(
screen.getByRole('button', { name: /^symbol asset-id/ })
).toBeInTheDocument();
});
});
it('should be properly change link url', async () => {
render(<LedgerExportLink partyId={partyId} entries={entries} />);
await waitFor(() => {
expect(
screen.getByRole('button', { name: /^symbol asset-id/ })
).toBeInTheDocument();
});
act(() => {
userEvent.click(screen.getByRole('button', { name: /^symbol asset-id/ }));
});
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
act(() => {
userEvent.click(
screen.getByRole('menuitem', { name: /^symbol asset-id-2/ })
);
});
await waitFor(() => {
expect(screen.getByRole('link')).toHaveAttribute(
'href',
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=asset-id-2`
);
});
});
});

View File

@ -1,71 +0,0 @@
import type { LedgerEntry } from './ledger-entries-data-provider';
import { useMemo, useState } from 'react';
import { useEnvironment } from '@vegaprotocol/environment';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Link,
Button,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
const getProtoHost = (vegaurl: string) => {
const loc = new URL(vegaurl);
return `${loc.protocol}//${loc.host}`;
};
export const LedgerExportLink = ({
partyId,
entries,
}: {
partyId: string;
entries: LedgerEntry[];
}) => {
const assets = entries.reduce((aggr, item) => {
if (item.asset && !(item.asset.id in aggr)) {
aggr[item.asset.id] = item.asset.symbol;
}
return aggr;
}, {} as Record<string, string>);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const VEGA_URL = useEnvironment((store) => store.VEGA_URL);
const protohost = VEGA_URL ? getProtoHost(VEGA_URL) : '';
const assetDropDown = useMemo(() => {
return (
<DropdownMenu
trigger={<DropdownMenuTrigger>{assets[assetId]}</DropdownMenuTrigger>}
>
<DropdownMenuContent>
{Object.keys(assets).map((assetKey) => (
<DropdownMenuItem
key={assetKey}
onSelect={() => setAssetId(assetKey)}
>
{assets[assetKey]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [assetId, assets]);
if (!protohost || !entries || entries.length === 0) {
return null;
}
return (
<div className="flex shrink items-stretch gap-2 p-2">
<div className="flex items-center">Export all</div>
{assetDropDown}
<Link
className="text-sm"
title={t('Download all to .csv file')}
href={`${protohost}/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${assetId}`}
>
<Button size="sm">{t('Download')}</Button>
</Link>
</div>
);
};

View File

@ -1,73 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community';
import { useCallback, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns';
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
import { LedgerTable } from './ledger-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { LedgerExportLink } from './ledger-export-link';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
export interface Filter {
vegaTime?: {
value: Schema.DateRange;
};
fromAccountType?: { value: Types.AccountType[] };
toAccountType?: { value: Types.AccountType[] };
}
const defaultFilter = {
vegaTime: {
value: { start: formatRFC3339(subDays(Date.now(), 7)) },
},
};
export const LedgerManager = ({
partyId,
gridProps,
}: {
partyId: string;
gridProps: ReturnType<typeof useDataGridEvents>;
}) => {
const [filter, setFilter] = useState<Filter>(defaultFilter);
const variables = useMemo<LedgerEntriesQueryVariables>(
() => ({
partyId,
dateRange: filter?.vegaTime?.value,
pagination: {
first: 10,
},
}),
[partyId, filter?.vegaTime?.value]
);
const { data, error } = useDataProvider({
dataProvider: ledgerEntriesProvider,
variables,
skip: !variables.partyId,
});
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() };
setFilter(updatedFilter);
gridProps.onFilterChanged(event);
},
[gridProps]
);
return (
<div className="h-full relative">
<LedgerTable
rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No entries')}
{...gridProps}
onFilterChanged={onFilterChanged}
/>
{data && <LedgerExportLink entries={data} partyId={partyId} />}
</div>
);
};

View File

@ -1,215 +0,0 @@
import {
addDecimalsFormatNumber,
fromNanoSeconds,
getDateTimeFormat,
truncateByChars,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type {
VegaValueFormatterParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
DateRangeFilter,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community';
import type { Market } from '@vegaprotocol/markets';
import {
AccountTypeMapping,
DescriptionTransferTypeMapping,
TransferTypeMapping,
} from '@vegaprotocol/types';
import type { LedgerEntry } from './ledger-entries-data-provider';
import { useMemo } from 'react';
import { formatRFC3339, subDays } from 'date-fns';
export const TransferTooltipCellComponent = ({
value,
}: {
value: Types.TransferType;
}) => {
return (
<p className="max-w-sm px-4 py-2 z-20 rounded text-sm break-word">
{value ? DescriptionTransferTypeMapping[value] : ''}
</p>
);
};
const defaultValue = { start: formatRFC3339(subDays(Date.now(), 7)) };
const dateRangeFilterParams = {
maxNextDays: 0,
defaultValue,
};
const defaultColDef = {
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
export const LedgerTable = (props: LedgerEntryProps) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Sender'),
field: 'fromAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'fromAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) =>
value && (
<MarketNameCell value={value} data={data?.marketSender as Market} />
),
},
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'toAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) =>
value && (
<MarketNameCell
value={value}
data={data?.marketReceiver as Market}
/>
),
},
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
value ? TransferTypeMapping[value] : '',
},
{
headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
},
],
[]
);
return (
<AgGrid
tooltipShowDelay={500}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
{...props}
/>
);
};

View File

@ -4414,6 +4414,8 @@ export type StopOrderFilter = {
dateRange?: InputMaybe<DateRange>; dateRange?: InputMaybe<DateRange>;
/** Zero or more expiry strategies to filter by */ /** Zero or more expiry strategies to filter by */
expiryStrategy?: InputMaybe<Array<StopOrderExpiryStrategy>>; expiryStrategy?: InputMaybe<Array<StopOrderExpiryStrategy>>;
/** Filter for live stop orders only */
liveOnly?: InputMaybe<Scalars['Boolean']>;
/** Zero or more market IDs to filter by */ /** Zero or more market IDs to filter by */
markets?: InputMaybe<Array<Scalars['ID']>>; markets?: InputMaybe<Array<Scalars['ID']>>;
/** Zero or more party IDs to filter by */ /** Zero or more party IDs to filter by */

View File

@ -15,8 +15,9 @@ export const ethereumAddress = (value: string) => {
return true; return true;
}; };
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
export const vegaPublicKey = (value: string) => { export const vegaPublicKey = (value: string) => {
if (!/^[A-Fa-f0-9]{64}$/i.test(value)) { if (!VEGA_ID_REGEX.test(value)) {
return t('Invalid Vega key'); return t('Invalid Vega key');
} }
return true; return true;

View File

@ -0,0 +1,11 @@
# Ledger entries
## Ledger entries tab in portfolio
When I enter on ledger entries tab in portfolio page
- **Must** see a form for download report file (<a name="7007-LEEN-001" href="#7007-LEEN-001">7007-LEEN-001</a>)
- in the form **Must** see a dropdown for select an asset, in which reports will be downloaded (<a name="7007-LEEN-002" href="#7007-LEEN-002">7007-LEEN-002</a>)
- in the form **Must** see inputs for select time period, in which reports will be downloaded (<a name="7007-LEEN-003" href="#7007-LEEN-003">7007-LEEN-003</a>)
- default preselected period **Must** be the last 7 days (<a name="7007-LEEN-004" href="#7007-LEEN-004">7007-LEEN-004</a>)
- during download a loader component **Must** be visible and all interactive elements in the form **Must** be disabled (<a name="7007-LEEN-005" href="#7007-LEEN-005">7007-LEEN-005</a>)