Commit 696853b6 authored by varasev's avatar varasev Committed by GitHub

Merge branch 'main' into va-tx-actions

parents c8cb7ec1 a77404c0
node_modules node_modules
node_modules_linux node_modules_linux
\ No newline at end of file
playwright/envs.js
name: Run E2E tests k8s name: Run E2E tests k8s
on: on:
# push: pull_request_review:
pull_request: types: [submitted]
workflow_dispatch: workflow_dispatch:
env: env:
...@@ -14,59 +14,13 @@ env: ...@@ -14,59 +14,13 @@ env:
BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}} BASTION_SSH_KEY: ${{secrets.BASTION_SSH_KEY}}
jobs: jobs:
push_to_registry:
name: Push Docker image to registry
runs-on: ubuntu-latest
outputs:
shortSha: ${{ steps.output-step.outputs.short-sha }}
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/blockscout/frontend
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
- name: Add outputs
run: |
echo "::set-output name=short-sha::${{ env.SHORT_SHA }}"
id: output-step
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
deploy_and_tests: deploy_and_tests:
needs: push_to_registry if: github.event.review.state == 'approved'
# needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
with: with:
appNamespace: e2e-front-$GITHUB_SHA_SHORT appNamespace: e2e-front-$GITHUB_SHA_SHORT
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }} frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_REF_NAME_SLUG
blockscoutIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT blockscoutIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT
frontendIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT frontendIngressHost: e2e-blockscout-$GITHUB_SHA_SHORT
gethIngressHost: e2e-geth-$GITHUB_SHA_SHORT gethIngressHost: e2e-geth-$GITHUB_SHA_SHORT
......
...@@ -57,8 +57,7 @@ jobs: ...@@ -57,8 +57,7 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
cache-from: type=gha cache-from: type=registry,ref=ghcr.io/blockscout/frontend::buildcache
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_REF_NAME_SLUG }} tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_REF_NAME_SLUG }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
...@@ -69,12 +68,12 @@ jobs: ...@@ -69,12 +68,12 @@ jobs:
needs: push_to_registry needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with: with:
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack,DOCKER_IMAGE=prerelease-$GITHUB_REF_NAME_SLUG env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack,DOCKER_IMAGE=prerelease-$GITHUB_REF_NAME_SLUG,K8S_DOMAIN=blockscout-main.test.aws-k8s.blockscout.com
globalEnv: review globalEnv: review
appNamespace: review-front-$GITHUB_REF_NAME_SLUG appNamespace: review-front-$GITHUB_REF_NAME_SLUG
blockscoutIngressHost: blockscout blockscoutIngressHost: review-front
frontendIngressHost: blockscout frontendIngressHost: review-front
frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_REF_NAME_SLUG frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_REF_NAME_SLUG
gethIngressHost: geth gethIngressHost: review-front-geth
scVerifierIngressHost: sc-verifier scVerifierIngressHost: review-front-sc-verifier
secrets: inherit secrets: inherit
...@@ -44,8 +44,8 @@ jobs: ...@@ -44,8 +44,8 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
cache-from: type=gha cache-from: type=registry,ref=ghcr.io/blockscout/frontend::buildcache
cache-to: type=gha,mode=max cache-to: type=registry,ref=ghcr.io/blockscout/frontend::buildcache,mode=max
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
......
...@@ -5,7 +5,7 @@ blockscout: ...@@ -5,7 +5,7 @@ blockscout:
app: blockscout app: blockscout
enabled: true enabled: true
image: image:
_default: blockscout/blockscout:latest _default: &image blockscout/blockscout:frontend-main
replicas: replicas:
app: 1 app: 1
docker: docker:
...@@ -15,15 +15,19 @@ blockscout: ...@@ -15,15 +15,19 @@ blockscout:
init: init:
enabled: true enabled: true
image: image:
_default: blockscout/blockscout:latest _default: *image
service: service:
# ClusterIP, NodePort or LoadBalancer # ClusterIP, NodePort or LoadBalancer
type: ClusterIP type: ClusterIP
# enable ingress # enable ingress
ingress: ingress:
enabled: true enabled: true
annotations: {} annotations:
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2' # - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
nginx.ingress.kubernetes.io/cors-allow-origin: "https://*.blockscout-main.test.aws-k8s.blockscout.com, https://*.test.aws-k8s.blockscout.com, http://localhost:3000"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH
nginx.ingress.kubernetes.io/enable-cors: "true"
host: host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com _default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
...@@ -144,6 +148,8 @@ blockscout: ...@@ -144,6 +148,8 @@ blockscout:
_default: 'true' _default: 'true'
APPS: APPS:
_default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]' _default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]'
SESSION_COOKIE_DOMAIN:
_default: blockscout-main.test.aws-k8s.blockscout.com
postgres: postgres:
enabled: true enabled: true
...@@ -151,12 +157,12 @@ postgres: ...@@ -151,12 +157,12 @@ postgres:
port: 5432 port: 5432
command: '["docker-entrypoint.sh", "-c"]' command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]' args: '["max_connections=500"]'
# strategy: Recreate # strategy: Recreate
persistence: true persistence: true
storage: 500Gi storage: 1000Gi
resources: resources:
limits: limits:
......
...@@ -394,4 +394,4 @@ frontend: ...@@ -394,4 +394,4 @@ frontend:
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
_default: blockscout.com _default: blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS: NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: [{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}] _default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]"
import type { MetaMaskInpageProvider } from '@metamask/providers';
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
}
}
export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined) {
if (typeof val === 'string' && filterValues.includes(val as unknown as FilterType)) {
return val as unknown as FilterType;
}
}
export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined) {
const valArray = [];
if (typeof val === 'string') {
valArray.push(...val.split(','));
}
if (Array.isArray(val)) {
val.forEach(el => valArray.push(...el.split(',')));
}
return valArray.filter(el => filterValues.includes(el as unknown as FilterType)) as unknown as Array<FilterType>;
}
...@@ -29,7 +29,7 @@ export default function useNavItems() { ...@@ -29,7 +29,7 @@ export default function useNavItems() {
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: true }, { text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other' // there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/ // examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later // at this stage custom menu items is under development, we will implement it later
......
...@@ -3,9 +3,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; ...@@ -3,9 +3,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash'; import { pick, omit } from 'lodash';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll, scroller } from 'react-scroll';
import { PAGINATION_FIELDS } from 'types/api/pagination'; import { PAGINATION_FIELDS, PAGINATION_FILTERS_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination'; import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -16,9 +16,17 @@ interface Params<QueryName extends PaginatedQueryKeys> { ...@@ -16,9 +16,17 @@ interface Params<QueryName extends PaginatedQueryKeys> {
queryIds?: Array<string>; queryIds?: Array<string>;
filters?: PaginationFilters<QueryName>; filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>; options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
scroll?: { elem: string; offset: number };
} }
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({ queryName, filters, options, apiPath, queryIds }: Params<QueryName>) { export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({
queryName,
filters,
options,
apiPath,
queryIds,
scroll,
}: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName]; const paginationFields = PAGINATION_FIELDS[queryName];
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
...@@ -31,6 +39,10 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -31,6 +39,10 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ]; const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>( const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
queryKey, queryKey,
async() => { async() => {
...@@ -65,15 +77,15 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -65,15 +77,15 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
animateScroll.scrollToTop({ duration: 0 }); scrollToTop();
setPage(prev => prev + 1); setPage(prev => prev + 1);
}); });
}, [ data?.next_page_params, page, pageParams.length, router ]); }, [ data?.next_page_params, page, pageParams.length, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
// returning to the first page // returning to the first page
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = {}; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, paginationFields, 'page'); nextPageQuery = omit(router.query, paginationFields, 'page');
canGoBackwards.current = true; canGoBackwards.current = true;
...@@ -85,21 +97,51 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -85,21 +97,51 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.query = nextPageQuery; router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
animateScroll.scrollToTop({ duration: 0 }); scrollToTop();
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.clear(); page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
}); });
}, [ router, page, paginationFields, pageParams, queryClient ]); }, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.clear(); queryClient.removeQueries({ queryKey: [ queryName ] });
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
animateScroll.scrollToTop({ duration: 0 }); queryClient.removeQueries({ queryKey: [ queryName ] });
scrollToTop();
setPage(1); setPage(1);
setPageParams([ ]); setPageParams([ ]);
canGoBackwards.current = true; canGoBackwards.current = true;
window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from
// so have to remove it but with some delay :)
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
}, 100);
});
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
const newQuery = omit(router.query, PAGINATION_FIELDS[queryName], 'page', PAGINATION_FILTERS_FIELDS[queryName]);
if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) {
newQuery[key] = Array.isArray(value) ? value.join(',') : (value || '');
}
});
}
router.push(
{
pathname: router.pathname,
query: newQuery,
},
undefined,
{ shallow: true },
).then(() => {
setPage(1);
setPageParams([ ]);
scrollToTop();
}); });
}, [ queryClient, router, paginationFields ]); }, [ queryName, router, scrollToTop, setPageParams, setPage ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0; const hasPaginationParams = Object.keys(currPageParams).length > 0;
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
...@@ -129,5 +171,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -129,5 +171,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
}, 0); }, 0);
}, []); }, []);
return { ...queryResult, pagination }; return { ...queryResult, pagination, onFilterChange };
} }
import type { Channel } from 'phoenix'; import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
...@@ -19,16 +20,6 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e ...@@ -19,16 +20,6 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
handler: (payload: Payload) => void; handler: (payload: Payload) => void;
} }
export interface AddressCoinBalancePayload {
coin_balance: {
block_number: number;
block_timestamp?: string;
delta?: string;
transaction_hash?: string | null;
value?: string;
};
}
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketMessage { export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
...@@ -40,6 +31,6 @@ export namespace SocketMessage { ...@@ -40,6 +31,6 @@ export namespace SocketMessage {
export type AddressCurrentCoinBalance = export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', AddressCoinBalancePayload>; export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
export const base = {
block_number: 30367643,
block_timestamp: '2022-12-11T17:55:20Z',
delta: '-5568096000000000',
transaction_hash: null,
value: '107014805905725000000',
};
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/blocks-validated${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/coin-balance-history-by-day`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/coin-balance-history${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/internal-transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/token-transfers${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -2,7 +2,7 @@ import type { TestFixture, Page } from '@playwright/test'; ...@@ -2,7 +2,7 @@ import type { TestFixture, Page } from '@playwright/test';
import type { WebSocket } from 'ws'; import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import type { AddressCoinBalancePayload } from 'lib/socket/types'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
type ReturnType = () => Promise<WebSocket>; type ReturnType = () => Promise<WebSocket>;
...@@ -54,7 +54,7 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => { ...@@ -54,7 +54,7 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
}); });
}; };
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: AddressCoinBalancePayload): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
......
...@@ -16,6 +16,6 @@ ...@@ -16,6 +16,6 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts", "decs.d.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"],
"exclude": ["node_modules", "node_modules_linux"], "exclude": ["node_modules", "node_modules_linux"],
} }
import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { TokenInfo } from './tokenInfo'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address { export interface Address {
block_number_balance_updated_at: number | null; block_number_balance_updated_at: number | null;
...@@ -31,3 +36,63 @@ export interface AddressTokenBalance { ...@@ -31,3 +36,63 @@ export interface AddressTokenBalance {
token_id: string | null; token_id: string | null;
value: string; value: string;
} }
export interface AddressTransactionsResponse {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
} | null;
}
export const AddressFromToFilterValues = [ 'from', 'to' ] as const;
export type AddressFromToFilter = typeof AddressFromToFilterValues[number] | undefined;
export type AddressTxsFilters = {
filter: AddressFromToFilter;
}
export interface AddressTokenTransferResponse {
items: Array<TokenTransfer>;
next_page_params: TokenTransferPagination | null;
}
export type AddressTokenTransferFilters = {
filter: AddressFromToFilter;
type: Array<TokenType>;
}
export interface AddressCoinBalanceHistoryItem {
block_number: number;
block_timestamp: string;
delta: string;
transaction_hash: string | null;
value: string;
}
export interface AddressCoinBalanceHistoryResponse {
items: Array<AddressCoinBalanceHistoryItem>;
next_page_params: {
block_number: number;
items_count: number;
};
}
export interface AddressBlocksValidatedResponse {
items: Array<Block>;
next_page_params: {
block_number: number;
items_count: number;
};
}
export interface AddressInternalTxsResponse {
items: Array<InternalTransaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_index: number;
} | null;
}
import type {
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressTxsFilters,
AddressTokenTransferFilters,
AddressCoinBalanceHistoryResponse,
AddressBlocksValidatedResponse,
AddressInternalTxsResponse,
} from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log'; import type { LogsResponse } from 'types/api/log';
...@@ -8,6 +17,9 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -8,6 +17,9 @@ import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull'; import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys = export type PaginatedQueryKeys =
QueryKeys.addressTxs |
QueryKeys.addressTokenTransfers |
QueryKeys.addressInternalTxs |
QueryKeys.blocks | QueryKeys.blocks |
QueryKeys.blocksReorgs | QueryKeys.blocksReorgs |
QueryKeys.blocksUncles | QueryKeys.blocksUncles |
...@@ -16,24 +28,33 @@ export type PaginatedQueryKeys = ...@@ -16,24 +28,33 @@ export type PaginatedQueryKeys =
QueryKeys.txsPending | QueryKeys.txsPending |
QueryKeys.txInternals | QueryKeys.txInternals |
QueryKeys.txLogs | QueryKeys.txLogs |
QueryKeys.txTokenTransfers; QueryKeys.txTokenTransfers |
QueryKeys.addressCoinBalanceHistory |
QueryKeys.addressBlocksValidated;
export type PaginatedResponse<Q extends PaginatedQueryKeys> = export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse : Q extends QueryKeys.addressInternalTxs ? AddressInternalTxsResponse :
Q extends QueryKeys.blockTxs ? BlockTransactionsResponse : Q extends QueryKeys.addressTxs ? AddressTransactionsResponse :
Q extends QueryKeys.txsValidate ? TransactionsResponseValidated : Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferResponse :
Q extends QueryKeys.txsPending ? TransactionsResponsePending : Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse :
Q extends QueryKeys.txInternals ? InternalTransactionsResponse : Q extends QueryKeys.blockTxs ? BlockTransactionsResponse :
Q extends QueryKeys.txLogs ? LogsResponse : Q extends QueryKeys.txsValidate ? TransactionsResponseValidated :
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse : Q extends QueryKeys.txsPending ? TransactionsResponsePending :
never Q extends QueryKeys.txInternals ? InternalTransactionsResponse :
Q extends QueryKeys.txLogs ? LogsResponse :
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
Q extends QueryKeys.addressCoinBalanceHistory ? AddressCoinBalanceHistoryResponse :
Q extends QueryKeys.addressBlocksValidated ? AddressBlocksValidatedResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> = export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlockFilters : Q extends (QueryKeys.addressTxs | QueryKeys.addressInternalTxs) ? AddressTxsFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters : Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferFilters :
Q extends QueryKeys.txsPending ? TTxsFilters : Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters : Q extends QueryKeys.txsValidate ? TTxsFilters :
never Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params']; export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
...@@ -42,6 +63,9 @@ type PaginationFields = { ...@@ -42,6 +63,9 @@ type PaginationFields = {
} }
export const PAGINATION_FIELDS: PaginationFields = { export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.addressTxs]: [ 'block_number', 'items_count', 'index' ],
[QueryKeys.addressInternalTxs]: [ 'block_number', 'items_count', 'index', 'transaction_index' ],
[QueryKeys.addressTokenTransfers]: [ 'block_number', 'items_count', 'index', 'transaction_hash' ],
[QueryKeys.blocks]: [ 'block_number', 'items_count' ], [QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ], [QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksUncles]: [ 'block_number', 'items_count' ], [QueryKeys.blocksUncles]: [ 'block_number', 'items_count' ],
...@@ -51,4 +75,27 @@ export const PAGINATION_FIELDS: PaginationFields = { ...@@ -51,4 +75,27 @@ export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ], [QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ],
[QueryKeys.txTokenTransfers]: [ 'block_number', 'items_count', 'transaction_hash', 'index' ], [QueryKeys.txTokenTransfers]: [ 'block_number', 'items_count', 'transaction_hash', 'index' ],
[QueryKeys.txLogs]: [ 'items_count', 'transaction_hash', 'index' ], [QueryKeys.txLogs]: [ 'items_count', 'transaction_hash', 'index' ],
[QueryKeys.addressCoinBalanceHistory]: [ 'items_count', 'block_number' ],
[QueryKeys.addressBlocksValidated]: [ 'items_count', 'block_number' ],
};
type PaginationFiltersFields = {
[K in PaginatedQueryKeys]: Array<KeysOfObjectOrNull<PaginationFilters<K>>>
}
export const PAGINATION_FILTERS_FIELDS: PaginationFiltersFields = {
[QueryKeys.addressTxs]: [ 'filter' ],
[QueryKeys.addressInternalTxs]: [ 'filter' ],
[QueryKeys.addressTokenTransfers]: [ 'filter', 'type' ],
[QueryKeys.addressCoinBalanceHistory]: [],
[QueryKeys.addressBlocksValidated]: [],
[QueryKeys.blocks]: [ 'type' ],
[QueryKeys.txsValidate]: [ 'filter', 'type', 'method' ],
[QueryKeys.txsPending]: [ 'filter', 'type', 'method' ],
[QueryKeys.txTokenTransfers]: [ 'type' ],
[QueryKeys.blocksReorgs]: [],
[QueryKeys.blocksUncles]: [],
[QueryKeys.blockTxs]: [],
[QueryKeys.txInternals]: [],
[QueryKeys.txLogs]: [],
}; };
...@@ -38,14 +38,16 @@ interface TokenTransferBase { ...@@ -38,14 +38,16 @@ interface TokenTransferBase {
to: AddressParam; to: AddressParam;
} }
export type TokenTransferPagination = {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
}
export interface TokenTransferResponse { export interface TokenTransferResponse {
items: Array<TokenTransfer>; items: Array<TokenTransfer>;
next_page_params: { next_page_params: TokenTransferPagination | null;
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
} | null;
} }
export interface TokenTransferFilters { export interface TokenTransferFilters {
......
import type { AddressParam } from './addressParams';
export type TransactionReward = {
types: Array<string>;
emission_reward: string;
block_hash: string;
from: AddressParam;
to: AddressParam;
}
...@@ -24,4 +24,10 @@ export enum QueryKeys { ...@@ -24,4 +24,10 @@ export enum QueryKeys {
address='address', address='address',
addressCounters='address-counters', addressCounters='address-counters',
addressTokenBalances='address-token-balances', addressTokenBalances='address-token-balances',
addressCoinBalanceHistory='address-coin-balance-history',
addressCoinBalanceHistoryByDay='address-coin-balance-history-by-day',
addressTxs='addressTxs',
addressTokenTransfers='addressTokenTransfers',
addressBlocksValidated='address-blocks-validated',
addressInternalTxs='address-internal-txs',
} }
...@@ -19,7 +19,6 @@ export enum StatsIntervalId { ...@@ -19,7 +19,6 @@ export enum StatsIntervalId {
} }
export type StatsChart = { export type StatsChart = {
visible?: boolean;
id: string; id: string;
title: string; title: string;
description: string; description: string;
......
import { Box, Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressBlocksValidatedResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import SocketAlert from 'ui/shared/SocketAlert';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedSkeletonMobile from './blocksValidated/AddressBlocksValidatedSkeletonMobile';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
interface Props {
addressQuery: UseQueryResult<Address>;
}
const AddressBlocksValidated = ({ addressQuery }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient();
const query = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/blocks-validated`,
queryName: QueryKeys.addressBlocksValidated,
options: {
enabled: Boolean(addressQuery.data),
},
});
const handleSocketError = React.useCallback(() => {
setSocketAlert(true);
}, []);
const handleNewSocketMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
setSocketAlert(false);
queryClient.setQueryData(
[ QueryKeys.addressBlocksValidated, { page: query.pagination.page } ],
(prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) {
return;
}
return {
...prevData,
items: [ payload.block, ...prevData.items ],
};
});
}, [ query.pagination.page, queryClient ]);
const channel = useSocketChannel({
topic: `blocks:${ addressQuery.data?.hash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: addressQuery.isLoading || addressQuery.isError || !addressQuery.data.hash || query.pagination.page !== 1,
});
useSocketMessage({
channel,
event: 'new_block',
handler: handleNewSocketMessage,
});
const content = (() => {
if (query.isLoading) {
return (
<>
<Hide below="lg">
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/>
</Hide>
<Show below="lg">
<AddressBlocksValidatedSkeletonMobile/>
</Show>
</>
);
}
if (query.isError) {
return <DataFetchAlert/>;
}
if (query.data.items.length === 0) {
return 'There is no validated blocks for this address';
}
return (
<>
<Hide below="lg">
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="17%">Block</Th>
<Th width="17%">Age</Th>
<Th width="16%">Txn</Th>
<Th width="25%">GasUsed</Th>
<Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressBlocksValidatedTableItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg">
{ query.data.items.map((item) => (
<AddressBlocksValidatedListItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
</Show>
</>
);
})();
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
return (
<Box>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) }
{ socketAlert && <SocketAlert mb={ 6 }/> }
{ content }
</Box>
);
};
export default React.memo(AddressBlocksValidated);
import { useQueryClient } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import SocketAlert from 'ui/shared/SocketAlert';
import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart';
import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory';
interface Props {
addressQuery: UseQueryResult<Address>;
}
const AddressCoinBalance = ({ addressQuery }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient();
const coinBalanceQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/coin-balance-history`,
queryName: QueryKeys.addressCoinBalanceHistory,
options: {
enabled: Boolean(addressQuery.data),
},
});
const handleSocketError = React.useCallback(() => {
setSocketAlert(true);
}, []);
const handleNewSocketMessage: SocketMessage.AddressCoinBalance['handler'] = React.useCallback((payload) => {
setSocketAlert(false);
queryClient.setQueryData(
[ QueryKeys.addressCoinBalanceHistory, { page: coinBalanceQuery.pagination.page } ],
(prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) {
return;
}
return {
...prevData,
items: [
payload.coin_balance,
...prevData.items,
],
};
});
}, [ coinBalanceQuery.pagination.page, queryClient ]);
const channel = useSocketChannel({
topic: `addresses:${ addressQuery.data?.hash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: addressQuery.isLoading || addressQuery.isError || !addressQuery.data.hash || coinBalanceQuery.pagination.page !== 1,
});
useSocketMessage({
channel,
event: 'coin_balance',
handler: handleNewSocketMessage,
});
return (
<>
{ socketAlert && <SocketAlert mb={ 6 }/> }
<AddressCoinBalanceChart/>
<AddressCoinBalanceHistory query={ coinBalanceQuery }/>
</>
);
};
export default React.memo(AddressCoinBalance);
...@@ -10,7 +10,6 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -10,7 +10,6 @@ import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import metamaskIcon from 'icons/metamask.svg';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -22,6 +21,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; ...@@ -22,6 +21,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink'; import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton'; import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton'; import AddressFavoriteButton from './details/AddressFavoriteButton';
...@@ -73,7 +73,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -73,7 +73,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash } { isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
</Text> </Text>
<CopyToClipboard text={ addressQuery.data.hash }/> <CopyToClipboard text={ addressQuery.data.hash }/>
<Icon as={ metamaskIcon } boxSize={ 6 } ml={ 2 }/> { addressQuery.data.is_contract && addressQuery.data.token && <AddressAddToMetaMask ml={ 2 } token={ addressQuery.data.token }/> }
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/> <AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/>
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/> <AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/>
</Flex> </Flex>
...@@ -89,7 +89,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -89,7 +89,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
<Grid <Grid
mt={ 8 } mt={ 8 }
columnGap={ 8 } columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }} rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden" templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
> >
<AddressNameInfo data={ addressQuery.data }/> <AddressNameInfo data={ addressQuery.data }/>
...@@ -108,7 +108,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -108,7 +108,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
title="Tokens" title="Tokens"
hint="All tokens in the account and total value." hint="All tokens in the account and total value."
alignSelf="center" alignSelf="center"
py="2px" py={ 0 }
> >
<TokenSelect/> <TokenSelect/>
</DetailsInfoItem> </DetailsInfoItem>
...@@ -143,7 +143,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -143,7 +143,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
title="Last balance update" title="Last balance update"
hint="Block number in which the address was updated." hint="Block number in which the address was updated."
alignSelf="center" alignSelf="center"
py={{ base: 0, lg: 1 }} py={{ base: '2px', lg: 1 }}
> >
<Link <Link
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) } href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) }
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp';
import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = `/node-api/addresses/${ ADDRESS_HASH }/internal-transactions`;
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(internalTxsMock.baseResponse),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressInternalTxs/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot();
});
import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import AddressTxsFilter from './AddressTxsFilter';
import AddressIntTxsList from './internals/AddressIntTxsList';
const SCROLL_ELEM = 'address-internas-txs';
const SCROLL_OFFSET = -100;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressInternalTxs = () => {
const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const queryId = router.query.id;
const queryIdArray = castArray(queryId);
const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange } = useQueryWithPages({
apiPath: `/node-api/addresses/${ queryId }/internal-transactions`,
queryName: QueryKeys.addressInternalTxs,
queryIds: queryIdArray,
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val);
setFilterValue(newVal);
onFilterChange({ filter: newVal });
}, [ onFilterChange ]);
if (isLoading) {
return (
<>
<Show below="lg"><AddressIntTxsSkeletonMobile/></Show>
<Hide below="lg"><AddressIntTxsSkeletonDesktop/></Hide>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0 && !filterValue) {
return <Text as="span">There are no internal transactions for this address.</Text>;
}
let content;
if (data.items.length === 0) {
content = <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
} else {
content = (
<>
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ queryIdStr }/>
</Show>
<Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ queryIdStr }/>
</Hide>
</>
);
}
return (
<Element name={ SCROLL_ELEM }>
<ActionBar mt={ -6 }>
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
{ !isPaginatorHidden && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
{ content }
</Element>
);
};
export default AddressInternalTxs;
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => {
const router = useRouter();
const hash = router.query.id;
return (
<TokenTransfer
path={ `/node-api/addresses/${ hash }/token-transfers` }
queryName={ QueryKeys.addressTokenTransfers }
queryIds={ castArray(router.query.id) }
baseAddress={ typeof hash === 'string' ? hash : undefined }
/>
);
};
export default AddressTokenTransfers;
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { base as txMock } from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import AddressTxs from './AddressTxs';
const API_URL = '/node-api/addresses/0xd789a607CEac2f0E14867de4EB15b15C9FFB5859/transactions';
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock, txMock ], next_page_params: { block: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent';
import AddressTxsFilter from './AddressTxsFilter';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const SCROLL_ELEM = 'address-txs';
const SCROLL_OFFSET = -100;
const AddressTxs = () => {
const router = useRouter();
const isMobile = useIsMobile();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ router.query.id }/transactions`,
queryName: QueryKeys.addressTxs,
queryIds: castArray(router.query.id),
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val);
setFilterValue(newVal);
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
const isPaginatorHidden =
!addressTxsQuery.isLoading &&
!addressTxsQuery.isError &&
addressTxsQuery.pagination.page === 1 &&
!addressTxsQuery.pagination.hasNextPage;
const filter = (
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
);
return (
<Element name={ SCROLL_ELEM }>
{ !isMobile && (
<ActionBar mt={ -6 }>
{ filter }
{ !isPaginatorHidden && <Pagination { ...addressTxsQuery.pagination }/> }
</ActionBar>
) }
<TxsContent
filter={ filter }
query={ addressTxsQuery }
showSocketInfo={ false }
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
/>
</Element>
);
};
export default AddressTxs;
import {
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import FilterButton from 'ui/shared/FilterButton';
interface Props {
isActive: boolean;
defaultFilter: AddressFromToFilter;
onFilterChange: (nextValue: string | Array<string>) => void;
}
const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) => {
const { isOpen, onToggle } = useDisclosure();
return (
<Menu>
<MenuButton>
<FilterButton
isActive={ isOpen || isActive }
onClick={ onToggle }
/>
</MenuButton>
<MenuList zIndex={ 2 }>
<MenuOptionGroup defaultValue={ defaultFilter || 'all' } title="Address" type="radio" onChange={ onFilterChange }>
<MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="from">From</MenuItemOption>
<MenuItemOption value="to">To</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
);
};
export default React.memo(AddressTxsFilter);
import { Link, Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
page: number;
};
const AddressBlocksValidatedListItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.height) });
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
const totalReward = getBlockTotalReward(props);
return (
<AccountListItemMobile rowGap={ 2 }>
<Flex justifyContent="space-between" w="100%">
<Link href={ blockUrl } fontWeight="700">{ props.height }</Link>
<Text variant="secondary">{ timeAgo }</Text>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn</Text>
<Text variant="secondary">{ props.tx_count }</Text>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Gas used</Text>
<Text variant="secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }/>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward }</Text>
</Flex>
</AccountListItemMobile>
);
};
export default React.memo(AddressBlocksValidatedListItem);
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AddressBlocksValidatedSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex justifyContent="space-between" w="100%" h={ 6 }>
<Skeleton w="100px"/>
<Skeleton w="100px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="40px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="70px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="100px"/>
<Skeleton w="120px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default AddressBlocksValidatedSkeletonMobile;
import { Link, Td, Tr, Text, Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Block } from 'types/api/block';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
page: number;
};
const AddressBlocksValidatedTableItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.height) });
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
const totalReward = getBlockTotalReward(props);
return (
<Tr>
<Td>
<Link href={ blockUrl } fontWeight="700">{ props.height }</Link>
</Td>
<Td>
<Text variant="secondary">{ timeAgo }</Text>
</Td>
<Td>
<Text fontWeight="500">{ props.tx_count }</Text>
</Td>
<Td>
<Flex alignItems="center" columnGap={ 2 }>
<Box flexBasis="80px">{ BigNumber(props.gas_used || 0).toFormat() }</Box>
<Utilization colorScheme="gray" value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }/>
</Flex>
</Td>
<Td isNumeric display="flex" justifyContent="end">
{ totalReward }
</Td>
</Tr>
);
};
export default React.memo(AddressBlocksValidatedTableItem);
import { Box } from '@chakra-ui/react';
import React from 'react';
const AddressCoinBalanceChart = () => {
// chart will be added after stats feature is finalized
return <Box p={ 4 } borderColor="gray.200" borderRadius="md" borderWidth="1px">Here will be coin balance chart</Box>;
};
export default AddressCoinBalanceChart;
import { Box, Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import appConfig from 'configs/app/config';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressCoinBalanceListItem from './AddressCoinBalanceListItem';
import AddressCoinBalanceSkeletonMobile from './AddressCoinBalanceSkeletonMobile';
import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem';
interface Props {
query: UseQueryResult<AddressCoinBalanceHistoryResponse> & {
pagination: PaginationProps;
};
}
const AddressCoinBalanceHistory = ({ query }: Props) => {
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const content = (() => {
if (query.isLoading) {
return (
<>
<Hide below="lg">
<SkeletonTable columns={ [ '25%', '25%', '25%', '25%', '120px' ] }/>
</Hide>
<Show below="lg">
<AddressCoinBalanceSkeletonMobile/>
</Show>
</>
);
}
if (query.isError) {
return <DataFetchAlert/>;
}
return (
<>
<Hide below="lg">
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="25%">Block</Th>
<Th width="25%">Txn</Th>
<Th width="25%">Age</Th>
<Th width="25%" isNumeric pr={ 1 }/>
<Th width="120px" isNumeric>Balance { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressCoinBalanceTableItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg">
{ query.data.items.map((item) => (
<AddressCoinBalanceListItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
</Show>
</>
);
})();
return (
<Box mt={ 8 }>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) }
{ content }
</Box>
);
};
export default React.memo(AddressCoinBalanceHistory);
import { Link, Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import appConfig from 'configs/app/config';
import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
};
const AddressCoinBalanceListItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.block_number) });
const deltaBn = BigNumber(props.delta).div(WEI);
const isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
return (
<AccountListItemMobile rowGap={ 2 }>
<Flex justifyContent="space-between" w="100%">
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).toFixed(8) } { appConfig.network.currency.symbol }</Text>
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFixed(8) }
</Text>
</StatHelpText>
</Stat>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Block</Text>
<Link href={ blockUrl } fontWeight="700">{ props.block_number }</Link>
</Flex>
{ props.transaction_hash && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txs</Text>
<Address maxW="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
</Address>
</Flex>
) }
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Age</Text>
<Text variant="secondary">{ timeAgo }</Text>
</Flex>
</AccountListItemMobile>
);
};
export default React.memo(AddressCoinBalanceListItem);
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AddressCoinBalanceSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex justifyContent="space-between" w="100%" h={ 6 }>
<Skeleton w="170px"/>
<Skeleton w="120px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="80px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="150px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="30px"/>
<Skeleton w="60px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default AddressCoinBalanceSkeletonMobile;
import { Link, Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
};
const AddressCoinBalanceTableItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.block_number) });
const deltaBn = BigNumber(props.delta).div(WEI);
const isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
return (
<Tr>
<Td>
<Link href={ blockUrl } fontWeight="700">{ props.block_number }</Link>
</Td>
<Td>
{ props.transaction_hash ?
(
<Address maxW="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
</Address>
) :
<Text fontWeight="700">-</Text>
}
</Td>
<Td>
<Text variant="secondary">{ timeAgo }</Text>
</Td>
<Td isNumeric pr={ 1 }>
<Text>{ BigNumber(props.value).div(WEI).toFixed(8) }</Text>
</Td>
<Td isNumeric display="flex" justifyContent="end">
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFixed(8) }
</Text>
</StatHelpText>
</Stat>
</Td>
</Tr>
);
};
export default React.memo(AddressCoinBalanceTableItem);
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import metamaskIcon from 'icons/metamask.svg';
import useToast from 'lib/hooks/useToast';
interface Props {
className?: string;
token: TokenInfo;
}
const AddressAddToMetaMask = ({ className, token }: Props) => {
const toast = useToast();
const handleClick = React.useCallback(async() => {
try {
const wasAdded = await window.ethereum.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more!
options: {
address: token.address,
symbol: token.symbol,
decimals: Number(token.decimals) || 18,
},
},
});
if (wasAdded) {
toast({
position: 'top-right',
title: 'Success',
description: 'Successfully added token to MetaMask',
status: 'success',
variant: 'subtle',
isClosable: true,
});
}
} catch (error) {
toast({
position: 'top-right',
title: 'Error',
description: (error as Error)?.message || 'Something went wrong',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}
}, [ toast, token ]);
if (token.type !== 'ERC-20' || !('ethereum' in window)) {
return null;
}
return (
<Tooltip label="Add token to MetaMask">
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ metamaskIcon } boxSize={ 6 }/>
</Box>
</Tooltip>
);
};
export default React.memo(chakra(AddressAddToMetaMask));
...@@ -8,7 +8,7 @@ const AddressDetailsSkeleton = () => { ...@@ -8,7 +8,7 @@ const AddressDetailsSkeleton = () => {
<Box> <Box>
<Flex align="center"> <Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/> <SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w="420px" ml={ 2 }/> <Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 }/>
<Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/> <Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/> <Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/> <Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import AddressIntTxsListItem from 'ui/address/internals/AddressIntTxsListItem';
type Props = {
data: Array<InternalTransaction>;
currentAddress: string;
}
const AddressIntTxsList = ({ data, currentAddress }: Props) => {
return (
<Box>
{ data.map((item) => <AddressIntTxsListItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/>) }
</Box>
);
};
export default AddressIntTxsList;
import { Flex, Tag, Icon, Box, HStack, Text, Link } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string };
const TxInternalsListItem = ({
type,
from,
to,
value,
success,
error,
created_contract: createdContract,
transaction_hash: txnHash,
block,
timestamp,
currentAddress,
}: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
const isOut = Boolean(currentAddress && currentAddress === from.hash);
const isIn = Boolean(currentAddress && currentAddress === to?.hash);
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
<Flex justifyContent="space-between" width="100%">
<AddressLink fontWeight="700" hash={ txnHash } truncation="constant" type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text>
</Flex>
<HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text>
<Link href={ link('block', { id: block.toString() }) }>{ block }</Link>
</HStack>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ toData.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash }/>
</Address>
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
<Text fontSize="sm" variant="secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text>
</HStack>
</AccountListItemMobile>
);
};
export default TxInternalsListItem;
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] }/>
);
};
export default TxInternalsSkeletonDesktop;
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="100%" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default TxInternalsSkeletonMobile;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressIntTxsTableItem from './AddressIntTxsTableItem';
interface Props {
data: Array<InternalTransaction>;
currentAddress: string;
}
const AddressIntTxsTable = ({ data, currentAddress }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="15%">Parent txn hash</Th>
<Th width="15%">Type</Th>
<Th width="10%">Block</Th>
<Th width="20%">From</Th>
<Th width="48px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="20%" isNumeric>
Value { appConfig.network.currency.symbol }
</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<AddressIntTxsTableItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/>
)) }
</Tbody>
</Table>
);
};
export default AddressIntTxsTable;
import { Tr, Td, Tag, Icon, Box, Flex, Text, Link } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string }
const AddressIntTxsTableItem = ({
type,
from,
to,
value,
success,
error,
created_contract: createdContract,
transaction_hash: txnHash,
block,
timestamp,
currentAddress,
}: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
const isOut = Boolean(currentAddress && currentAddress === from.hash);
const isIn = Boolean(currentAddress && currentAddress === to?.hash);
return (
<Tr alignItems="top">
<Td verticalAlign="middle">
<Flex rowGap={ 3 } flexWrap="wrap">
<AddressLink fontWeight="700" hash={ txnHash } type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex rowGap={ 2 } flexWrap="wrap">
{ typeTitle && (
<Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box>
) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Link href={ link('block', { id: block.toString() }) }>{ block }</Link>
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td px={ 0 } verticalAlign="middle">
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
}
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ toData.hash }/>
<AddressLink hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Td>
</Tr>
);
};
export default React.memo(AddressIntTxsTableItem);
...@@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -184,9 +185,7 @@ test.describe('socket', () => { ...@@ -184,9 +185,7 @@ test.describe('socket', () => {
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1'); const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'coin_balance', { socketServer.sendMessage(socket, channel, 'coin_balance', {
coin_balance: { coin_balance: coinBalanceMock.base,
block_number: 1,
},
}); });
const button = page.getByRole('button', { name: /select/i }); const button = page.getByRole('button', { name: /select/i });
......
import { Box, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -71,7 +71,7 @@ const TokenSelect = () => { ...@@ -71,7 +71,7 @@ const TokenSelect = () => {
} }
return ( return (
<> <Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}>
{ isMobile ? { isMobile ?
<TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> : <TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/> <TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
...@@ -83,11 +83,10 @@ const TokenSelect = () => { ...@@ -83,11 +83,10 @@ const TokenSelect = () => {
size="sm" size="sm"
pl="6px" pl="6px"
pr="6px" pr="6px"
ml={ 3 }
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> } icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
/> />
</Tooltip> </Tooltip>
</> </Flex>
); );
}; };
......
...@@ -82,10 +82,10 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -82,10 +82,10 @@ const BlocksContent = ({ type, query }: Props) => {
if (query.isLoading) { if (query.isLoading) {
return ( return (
<> <>
<Show below="lg" key="skeleton-mobile"> <Show below="lg" key="skeleton-mobile" ssr={ false }>
<BlocksSkeletonMobile/> <BlocksSkeletonMobile/>
</Show> </Show>
<Hide below="lg" key="skeleton-desktop"> <Hide below="lg" key="skeleton-desktop" ssr={ false }>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/> <SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Hide> </Hide>
</> </>
...@@ -103,8 +103,8 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -103,8 +103,8 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ query.data.items }/></Show> <Show below="lg" key="content-mobile" ssr={ false }><BlocksList data={ query.data.items }/></Show>
<Hide below="lg" key="content-desktop"><BlocksTable data={ query.data.items } top={ 80 } page={ 1 }/></Hide> <Hide below="lg" key="content-desktop" ssr={ false }><BlocksTable data={ query.data.items } top={ 80 } page={ 1 }/></Hide>
</> </>
); );
...@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<> <>
{ isMobile && !isPaginatorHidden && ( { isMobile && !isPaginatorHidden && (
<ActionBar> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
) } ) }
......
...@@ -138,13 +138,13 @@ const LatestBlocksItem = ({ tx }: Props) => { ...@@ -138,13 +138,13 @@ const LatestBlocksItem = ({ tx }: Props) => {
</Address> </Address>
</Flex> </Flex>
<Flex fontSize="sm" justifyContent="end" flexDirection={{ base: 'column', lg: 'row' }}> <Flex fontSize="sm" justifyContent="end" flexDirection={{ base: 'column', lg: 'row' }}>
<Box mr={{ base: 0, lg: 2 }} mb={{ base: 2, lg: 0 }}> <Box mr={{ base: 0, lg: 3 }} mb={{ base: 2, lg: 0 }}>
<Text as="span">Value { appConfig.network.currency.symbol } </Text> <Text as="span">Value { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text>
</Box> </Box>
<Box> <Box>
<Text as="span">Fee { appConfig.network.currency.symbol } </Text> <Text as="span">Fee { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text>
</Box> </Box>
</Flex> </Flex>
</Box> </Box>
......
...@@ -8,7 +8,12 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -8,7 +8,12 @@ import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
...@@ -32,11 +37,14 @@ const AddressPageContent = () => { ...@@ -32,11 +37,14 @@ const AddressPageContent = () => {
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'txs', title: 'Transactions', component: null }, { id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: null }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txn', title: 'Internal txn', component: null }, { id: 'internal_txn', title: 'Internal txn', component: <AddressInternalTxs/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: null }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> },
// temporary show this tab in all address
// later api will return info about available tabs
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated addressQuery={ addressQuery }/> },
]; ];
return ( return (
......
...@@ -2,16 +2,28 @@ import { Tag, chakra } from '@chakra-ui/react'; ...@@ -2,16 +2,28 @@ import { Tag, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
baseAddress: string; isIn: boolean;
addressFrom: string; isOut: boolean;
className?: string; className?: string;
} }
const InOutTag = ({ baseAddress, addressFrom, className }: Props) => { const InOutTag = ({ isIn, isOut, className }: Props) => {
const isOut = addressFrom === baseAddress; if (!isIn && !isOut) {
return null;
}
const colorScheme = isOut ? 'orange' : 'green'; const colorScheme = isOut ? 'orange' : 'green';
return <Tag className={ className } colorScheme={ colorScheme }>{ isOut ? 'OUT' : 'IN' }</Tag>; return (
<Tag
className={ className }
colorScheme={ colorScheme }
display="flex"
justifyContent="center"
>
{ isOut ? 'OUT' : 'IN' }
</Tag>
);
}; };
export default React.memo(chakra(InOutTag)); export default React.memo(chakra(InOutTag));
...@@ -40,19 +40,6 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) => ...@@ -40,19 +40,6 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) =>
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
useEffect(() => {
if (router.isReady) {
let tabIndex = 0;
if (router.query.tab) {
tabIndex = tabs.findIndex(({ id }) => id === router.query.tab);
if (tabIndex < 0) {
tabIndex = 0;
}
}
setActiveTabIndex(tabIndex);
}
}, [ tabs, router ]);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile); const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
const isSticky = useIsSticky(listRef, 5, stickyEnabled); const isSticky = useIsSticky(listRef, 5, stickyEnabled);
...@@ -68,6 +55,38 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) => ...@@ -68,6 +55,38 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) =>
); );
}, [ tabs, router ]); }, [ tabs, router ]);
useEffect(() => {
if (router.isReady) {
let tabIndex = 0;
if (router.query.tab) {
tabIndex = tabs.findIndex(({ id }) => id === router.query.tab);
if (tabIndex < 0) {
tabIndex = 0;
}
}
setActiveTabIndex(tabIndex);
}
}, [ tabs, router, activeTabIndex ]);
useEffect(() => {
if (activeTabIndex < tabs.length && isMobile) {
window.setTimeout(() => {
const activeTabRef = tabsRefs[activeTabIndex];
if (activeTabRef.current && listRef.current) {
const activeTabRect = activeTabRef.current.getBoundingClientRect();
listRef.current.scrollTo({
left: activeTabRect.left + listRef.current.scrollLeft - 16,
behavior: 'smooth',
});
}
// have to wait until DOM is updated and all styles to tabs is applied
}, 300);
}
// run only when tab index or device type is updated
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ activeTabIndex, isMobile ]);
return ( return (
<Tabs <Tabs
variant="soft-rounded" variant="soft-rounded"
......
...@@ -51,19 +51,20 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -51,19 +51,20 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
}, [ tabs, disabled ]); }, [ tabs, disabled ]);
React.useEffect(() => { React.useEffect(() => {
setTabsRefs(disabled ? [] : tabsList.map((_, index) => tabsRefs[index] || React.createRef())); setTabsRefs(tabsList.map((_, index) => tabsRefs[index] || React.createRef()));
setTabsCut(disabled ? tabs.length : 0);
// update refs only when disabled prop changes // update refs only when disabled prop changes
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ disabled ]); }, [ disabled ]);
React.useEffect(() => { React.useEffect(() => {
if (tabsRefs.length > 0) { if (tabsRefs.length > 0 && !disabled) {
setTabsCut(calculateCut()); setTabsCut(calculateCut());
} }
}, [ calculateCut, tabsRefs ]); }, [ calculateCut, disabled, tabsRefs ]);
React.useEffect(() => { React.useEffect(() => {
if (tabsRefs.length === 0) { if (tabsRefs.length === 0 || disabled) {
return; return;
} }
...@@ -76,7 +77,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -76,7 +77,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
return function cleanup() { return function cleanup() {
resizeObserver.unobserve(document.body); resizeObserver.unobserve(document.body);
}; };
}, [ calculateCut, tabsRefs.length ]); }, [ calculateCut, disabled, tabsRefs.length ]);
return React.useMemo(() => { return React.useMemo(() => {
return { return {
......
import { Text, Alert, Link, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
}
const SocketAlert = ({ className }: Props) => {
return (
<Alert status="warning" className={ className }>
<Text whiteSpace="pre">Connection lost, click </Text>
<Link href={ window.document.location.href }>to load newer records</Link>
</Alert>
);
};
export default chakra(SocketAlert);
import { Hide, Show, Text } from '@chakra-ui/react'; import { Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll';
import type { AddressTokenTransferFilters, AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransferFilters } from 'types/api/tokenTransfer';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
...@@ -17,11 +24,21 @@ import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; ...@@ -17,11 +24,21 @@ import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile'; import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
import { TOKEN_TYPE } from './helpers';
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
const SCROLL_ELEM = 'token-transfers';
const SCROLL_OFFSET = -100;
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
path: string; path: string;
queryName: QueryKeys.txTokenTransfers; queryName: QueryKeys.txTokenTransfers | QueryKeys.addressTokenTransfers;
queryIds?: Array<string>; queryIds?: Array<string>;
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
...@@ -29,20 +46,33 @@ interface Props { ...@@ -29,20 +46,33 @@ interface Props {
} }
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => { const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => {
const [ filters, setFilters ] = React.useState<Array<TokenType>>([]); const router = useRouter();
const { isError, isLoading, data, pagination } = useQueryWithPages({ const [ filters, setFilters ] = React.useState<AddressTokenTransferFilters & TokenTransferFilters>(
{ type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) },
);
const { isError, isLoading, data, pagination, onFilterChange } = useQueryWithPages({
apiPath: path, apiPath: path,
queryName, queryName,
queryIds, queryIds,
options: { enabled: !isDisabled }, options: { enabled: !isDisabled },
filters: filters.length ? { type: filters } : undefined, filters: filters,
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
}); });
const handleFilterChange = React.useCallback((nextValue: Array<TokenType>) => { const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
setFilters(nextValue); onFilterChange({ ...filters, type: nextValue });
}, []); setFilters((prevState) => ({ ...prevState, type: nextValue }));
}, [ filters, onFilterChange ]);
const handleAddressFilterChange = React.useCallback((nextValue: string) => {
const filterVal = getAddressFilterValue(nextValue);
onFilterChange({ ...filters, filter: filterVal });
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]);
const isActionBarHidden = filters.length === 0 && !data?.items.length; const numActiveFilters = filters.type.length + (filters.filter ? 1 : 0);
const isActionBarHidden = !numActiveFilters && !data?.items.length;
const content = (() => { const content = (() => {
if (isLoading || isLoadingProp) { if (isLoading || isLoadingProp) {
...@@ -65,7 +95,7 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -65,7 +95,7 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (!data.items?.length && filters.length === 0) { if (!data.items?.length && !numActiveFilters) {
return <Text as="span">There are no token transfers</Text>; return <Text as="span">There are no token transfers</Text>;
} }
...@@ -87,15 +117,22 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -87,15 +117,22 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
})(); })();
return ( return (
<> <Element name={ SCROLL_ELEM }>
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<TokenTransferFilter defaultFilters={ filters } onFilterChange={ handleFilterChange } appliedFiltersNum={ filters.length }/> <TokenTransferFilter
defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters }
withAddressFilter={ Boolean(baseAddress) }
onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter }
/>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
) } ) }
{ content } { content }
</> </Element>
); );
}; };
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react'; import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
CheckboxGroup,
Checkbox,
Text,
useDisclosure,
Radio,
RadioGroup,
Stack,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/tokenInfo';
import FilterButton from 'ui/shared/FilterButton'; import FilterButton from 'ui/shared/FilterButton';
...@@ -9,13 +23,25 @@ import { TOKEN_TYPE } from './helpers'; ...@@ -9,13 +23,25 @@ import { TOKEN_TYPE } from './helpers';
interface Props { interface Props {
appliedFiltersNum?: number; appliedFiltersNum?: number;
defaultFilters: Array<TokenType>; defaultTypeFilters: Array<TokenType>;
onFilterChange: (nextValue: Array<TokenType>) => void; onTypeFilterChange: (nextValue: Array<TokenType>) => void;
withAddressFilter?: boolean;
onAddressFilterChange?: (nextValue: string) => void;
defaultAddressFilter?: AddressFromToFilter;
} }
const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => { const TokenTransferFilter = ({
onTypeFilterChange,
defaultTypeFilters,
appliedFiltersNum,
withAddressFilter,
onAddressFilterChange,
defaultAddressFilter,
}: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
...@@ -27,8 +53,27 @@ const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Pr ...@@ -27,8 +53,27 @@ const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Pr
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="200px"> <PopoverContent w="200px">
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }> <PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
{ withAddressFilter && (
<>
<Text variant="secondary" fontWeight={ 600 }>Address</Text>
<RadioGroup
size="lg"
onChange={ onAddressFilterChange }
defaultValue={ defaultAddressFilter || 'all' }
paddingBottom={ 4 }
borderBottom="1px solid"
borderColor={ borderColor }
>
<Stack spacing={ 4 }>
<Radio value="all"><Text fontSize="md">All</Text></Radio>
<Radio value="from"><Text fontSize="md">From</Text></Radio>
<Radio value="to"><Text fontSize="md">To</Text></Radio>
</Stack>
</RadioGroup>
</>
) }
<Text variant="secondary" fontWeight={ 600 }>Type</Text> <Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }> <CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) } { TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup> </CheckboxGroup>
</PopoverBody> </PopoverBody>
...@@ -37,4 +82,4 @@ const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Pr ...@@ -37,4 +82,4 @@ const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Pr
); );
}; };
export default React.memo(TokenTransfer); export default React.memo(TokenTransferFilter);
...@@ -53,7 +53,7 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -53,7 +53,7 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
} }
<Address width={ addressWidth }> <Address width={ addressWidth }>
......
...@@ -59,7 +59,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA ...@@ -59,7 +59,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA
</Td> </Td>
{ baseAddress && ( { baseAddress && (
<Td px={ 0 }> <Td px={ 0 }>
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center" mt="3px"/> <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" mt="3px"/>
</Td> </Td>
) } ) }
<Td> <Td>
......
...@@ -18,10 +18,9 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -18,10 +18,9 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
const borderColor = useToken('colors', 'blue.200'); const borderColor = useToken('colors', 'blue.200');
const ref = React.useRef(null); const ref = React.useRef(null);
const isPressed = React.useRef(false); const isActive = React.useRef(false);
const startX = React.useRef<number>(); const startX = React.useRef<number>();
const endX = React.useRef<number>(); const endX = React.useRef<number>();
const startIndex = React.useRef<number>(0);
const getIndexByX = React.useCallback((x: number) => { const getIndexByX = React.useCallback((x: number) => {
const xDate = scale.invert(x); const xDate = scale.invert(x);
...@@ -51,20 +50,33 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -51,20 +50,33 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
.attr('width', Math.abs(diffX)); .attr('width', Math.abs(diffX));
}, []); }, []);
const handelMouseUp = React.useCallback(() => { const handleSelect = React.useCallback((x0: number, x1: number) => {
isPressed.current = false; const index0 = getIndexByX(x0);
const index1 = getIndexByX(x1);
if (Math.abs(index0 - index1) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index0, index1), Math.max(index0, index1) ]);
}
}, [ getIndexByX, onSelect ]);
const cleanUp = React.useCallback(() => {
isActive.current = false;
startX.current = undefined; startX.current = undefined;
endX.current = undefined;
d3.select(ref.current).attr('opacity', 0); d3.select(ref.current).attr('opacity', 0);
}, [ ]);
if (!endX.current) { const handelMouseUp = React.useCallback(() => {
if (!isActive.current) {
return; return;
} }
const index = getIndexByX(endX.current); if (startX.current && endX.current) {
if (Math.abs(index - startIndex.current) > SELECTION_THRESHOLD) { handleSelect(startX.current, endX.current);
onSelect([ Math.min(index, startIndex.current), Math.max(index, startIndex.current) ]);
} }
}, [ getIndexByX, onSelect ]);
cleanUp();
}, [ cleanUp, handleSelect ]);
React.useEffect(() => { React.useEffect(() => {
if (!anchorEl) { if (!anchorEl) {
...@@ -76,20 +88,34 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -76,20 +88,34 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
anchorD3 anchorD3
.on('mousedown.selectionX', (event: MouseEvent) => { .on('mousedown.selectionX', (event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl); const [ x ] = d3.pointer(event, anchorEl);
isPressed.current = true; isActive.current = true;
startX.current = x; startX.current = x;
const index = getIndexByX(x);
startIndex.current = index;
}) })
.on('mouseup.selectionX', handelMouseUp)
.on('mousemove.selectionX', (event: MouseEvent) => { .on('mousemove.selectionX', (event: MouseEvent) => {
if (isPressed.current) { if (isActive.current) {
const [ x ] = d3.pointer(event, anchorEl); const [ x ] = d3.pointer(event, anchorEl);
startX.current && drawSelection(startX.current, x); startX.current && drawSelection(startX.current, x);
endX.current = x; endX.current = x;
} }
}); })
.on('mouseup.selectionX', handelMouseUp)
.on('touchstart.selectionX', (event: TouchEvent) => {
const pointers = d3.pointers(event, anchorEl);
isActive.current = pointers.length === 2;
})
.on('touchmove.selectionX', (event: TouchEvent) => {
if (isActive.current) {
const pointers = d3.pointers(event, anchorEl);
if (pointers.length === 2 && Math.abs(pointers[0][0] - pointers[1][0]) > 5) {
drawSelection(pointers[0][0], pointers[1][0]);
startX.current = pointers[0][0];
endX.current = pointers[1][0];
}
}
})
.on('touchend.selectionX', handelMouseUp);
d3.select('body').on('mouseup.selectionX', function(event) { d3.select('body').on('mouseup.selectionX', function(event) {
const isOutside = startX.current !== undefined && event.target !== anchorD3.node(); const isOutside = startX.current !== undefined && event.target !== anchorD3.node();
...@@ -99,10 +125,10 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -99,10 +125,10 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
}); });
return () => { return () => {
anchorD3.on('mousedown.selectionX mouseup.selectionX mousemove.selectionX', null); anchorD3.on('.selectionX', null);
d3.select('body').on('mouseup.selectionX', null); d3.select('body').on('.selectionX', null);
}; };
}, [ anchorEl, drawSelection, getIndexByX, handelMouseUp ]); }, [ anchorEl, cleanUp, drawSelection, getIndexByX, handelMouseUp, handleSelect ]);
return ( return (
<g className="ChartSelectionX" ref={ ref } opacity={ 0 }> <g className="ChartSelectionX" ref={ ref } opacity={ 0 }>
......
...@@ -32,6 +32,8 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -32,6 +32,8 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
const bgColor = useToken('colors', 'blackAlpha.900'); const bgColor = useToken('colors', 'blackAlpha.900');
const ref = React.useRef(null); const ref = React.useRef(null);
const trackerId = React.useRef<number>();
const isVisible = React.useRef(false);
const drawLine = React.useCallback( const drawLine = React.useCallback(
(x: number) => { (x: number) => {
...@@ -129,67 +131,78 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -129,67 +131,78 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
}, [ drawPoints, drawLine, drawContent ]); }, [ drawPoints, drawLine, drawContent ]);
const showContent = React.useCallback(() => { const showContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 1); if (!isVisible.current) {
d3.select(ref.current) d3.select(ref.current).attr('opacity', 1);
.selectAll('.ChartTooltip__point') d3.select(ref.current)
.attr('opacity', 1); .selectAll('.ChartTooltip__point')
.attr('opacity', 1);
isVisible.current = true;
}
}, []); }, []);
const hideContent = React.useCallback(() => { const hideContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 0); d3.select(ref.current).attr('opacity', 0);
isVisible.current = false;
}, []); }, []);
const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => { const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => {
let isShown = false;
let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall; let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall;
if (isPressed) { if (isPressed) {
hideContent(); hideContent();
} }
trackPointer(event, { return trackPointer(event, {
move: (pointer) => { move: (pointer) => {
if (!pointer.point || isPressed) { if (!pointer.point || isPressed) {
return; return;
} }
draw(pointer); draw(pointer);
if (!isShown) { showContent();
showContent();
isShown = true;
}
}, },
out: () => { out: () => {
hideContent(); hideContent();
isShown = false; trackerId.current = undefined;
}, },
end: (tracker) => { end: () => {
hideContent(); hideContent();
const isOutside = tracker.sourceEvent?.offsetX && width && (tracker.sourceEvent.offsetX > width || tracker.sourceEvent.offsetX < 0); trackerId.current = undefined;
if (!isOutside && isPressed) {
window.setTimeout(() => {
createPointerTracker(event, true);
}, 0);
}
isShown = false;
isPressed = false; isPressed = false;
}, },
}); });
}, [ draw, hideContent, showContent, width ]); }, [ draw, hideContent, showContent ]);
React.useEffect(() => { React.useEffect(() => {
const anchorD3 = d3.select(anchorEl); const anchorD3 = d3.select(anchorEl);
let isMultiTouch = false; // disabling creation of new tracker in multi touch mode
anchorD3 anchorD3
.on('touchmove.tooltip', (event: PointerEvent) => event.preventDefault()) // prevent scrolling .on('touchmove.tooltip', (event: TouchEvent) => event.preventDefault()) // prevent scrolling
.on(`touchstart.tooltip`, (event: TouchEvent) => {
isMultiTouch = event.touches.length > 1;
})
.on(`touchend.tooltip`, (event: TouchEvent) => {
if (isMultiTouch && event.touches.length === 0) {
isMultiTouch = false;
}
})
.on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => { .on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => {
createPointerTracker(event); if (!isMultiTouch) {
trackerId.current = createPointerTracker(event);
}
})
.on('pointermove.tooltip', (event: PointerEvent) => {
if (event.pointerType === 'mouse' && !isMultiTouch && trackerId.current === undefined) {
trackerId.current = createPointerTracker(event);
}
}); });
return () => { return () => {
anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null); anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null);
trackerId.current && anchorD3.on(
[ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ trackerId.current }`).join(' '),
null,
);
}; };
}, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]); }, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]);
......
...@@ -16,18 +16,26 @@ export default function useChartSize(svgEl: SVGSVGElement | null, margin?: Chart ...@@ -16,18 +16,26 @@ export default function useChartSize(svgEl: SVGSVGElement | null, margin?: Chart
}, [ calculateRect ]); }, [ calculateRect ]);
React.useEffect(() => { React.useEffect(() => {
const content = window.document.querySelector('main');
if (!content) {
return;
}
let timeoutId: number; let timeoutId: number;
const resizeHandler = _debounce(() => { const resizeHandler = _debounce(() => {
setRect({ width: 0, height: 0 }); setRect({ width: 0, height: 0 });
timeoutId = window.setTimeout(() => { timeoutId = window.setTimeout(() => {
setRect(calculateRect()); setRect(calculateRect());
}, 0); }, 0);
}, 100); }, 200);
const resizeObserver = new ResizeObserver(resizeHandler); const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(content);
resizeObserver.observe(window.document.body);
resizeObserver.observe(document.body);
return function cleanup() { return function cleanup() {
resizeObserver.unobserve(document.body); resizeObserver.unobserve(content);
resizeObserver.unobserve(window.document.body);
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
}; };
}, [ calculateRect ]); }, [ calculateRect ]);
......
...@@ -14,7 +14,7 @@ export interface PointerOptions { ...@@ -14,7 +14,7 @@ export interface PointerOptions {
end?: (tracker: Pointer) => void; end?: (tracker: Pointer) => void;
} }
export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions) { export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions): number {
const tracker: Pointer = { const tracker: Pointer = {
id: event.pointerId, id: event.pointerId,
point: null, point: null,
...@@ -26,16 +26,27 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi ...@@ -26,16 +26,27 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
tracker.point = d3.pointer(event, target); tracker.point = d3.pointer(event, target);
target.setPointerCapture(id); target.setPointerCapture(id);
const untrack = (sourceEvent: PointerEvent) => {
tracker.sourceEvent = sourceEvent;
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
};
d3.select(target) d3.select(target)
.on(`touchstart.${ id }`, (sourceEvent: PointerEvent) => {
const target = sourceEvent.target as Element;
const touches = d3.pointers(sourceEvent, target);
// disable current tracker when entering multi touch mode
touches.length > 1 && untrack(sourceEvent);
})
.on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => { .on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => {
if (sourceEvent.pointerId !== id) { if (sourceEvent.pointerId !== id) {
return; return;
} }
tracker.sourceEvent = sourceEvent; untrack(sourceEvent);
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
}) })
.on(`pointermove.${ id }`, (sourceEvent) => { .on(`pointermove.${ id }`, (sourceEvent) => {
if (sourceEvent.pointerId !== id) { if (sourceEvent.pointerId !== id) {
...@@ -57,5 +68,5 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi ...@@ -57,5 +68,5 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
start?.(tracker); start?.(tracker);
return [ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ id }`); return id;
} }
...@@ -44,13 +44,16 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props) ...@@ -44,13 +44,16 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props)
gutter={ 20 } gutter={ 20 }
color={ isActive ? colors.text.active : colors.text.hover } color={ isActive ? colors.text.active : colors.text.hover }
> >
<HStack spacing={ 3 }> <HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/> <Icon as={ icon } boxSize="30px"/>
<Text <Text
variant="inherit" variant="inherit"
fontSize="sm" fontSize="sm"
lineHeight="20px" lineHeight="20px"
display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }} opacity={{ base: '1', lg: isExpanded ? '1' : '0', xl: isCollapsed ? '0' : '1' }}
transitionProperty="opacity"
transitionDuration="normal"
transitionTimingFunction="ease"
> >
{ text } { text }
</Text> </Text>
......
...@@ -77,13 +77,13 @@ const NavigationDesktop = () => { ...@@ -77,13 +77,13 @@ const NavigationDesktop = () => {
<NetworkMenu isCollapsed={ isCollapsed }/> <NetworkMenu isCollapsed={ isCollapsed }/>
</Box> </Box>
<Box as="nav" mt={ 8 }> <Box as="nav" mt={ 8 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
{ hasAccount && ( { hasAccount && (
<Box as="nav" mt={ 12 }> <Box as="nav" mt={ 12 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
......
...@@ -16,13 +16,13 @@ const NavigationMobile = () => { ...@@ -16,13 +16,13 @@ const NavigationMobile = () => {
return ( return (
<> <>
<Box as="nav" mt={ 6 }> <Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
{ isAuth && ( { isAuth && (
<Box as="nav" mt={ 6 }> <Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
......
...@@ -65,7 +65,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => { ...@@ -65,7 +65,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
src={ logo } src={ logo }
display={{ base: 'block', lg: isCollapsed === false ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }} display={{ base: 'block', lg: isCollapsed === false ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}
alt={ `${ appConfig.network.name } network logo` } alt={ `${ appConfig.network.name } network logo` }
fallback={ isLogoError || !logo ? fallbackSmallLogo : undefined } fallback={ isLogoError || !logo ? fallbackLogo : undefined }
onError={ handleLogoError } onError={ handleLogoError }
/> />
{ /* small logo */ } { /* small logo */ }
...@@ -75,7 +75,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => { ...@@ -75,7 +75,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
src={ smallLogo } src={ smallLogo }
display={{ base: 'none', lg: isCollapsed === false ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }} display={{ base: 'none', lg: isCollapsed === false ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }}
alt={ `${ appConfig.network.name } network logo` } alt={ `${ appConfig.network.name } network logo` }
fallback={ isSmallLogoError || !smallLogo ? fallbackLogo : undefined } fallback={ isSmallLogoError || !smallLogo ? fallbackSmallLogo : undefined }
onError={ handleSmallLogoError } onError={ handleSmallLogoError }
/> />
</> </>
......
...@@ -56,7 +56,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) = ...@@ -56,7 +56,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) =
return ( return (
<svg width="100%" height="100%" ref={ ref } cursor="pointer" id={ chartId }> <svg width="100%" height="100%" ref={ ref } cursor="pointer" id={ chartId }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ 1 }> <g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ innerWidth ? 1 : 0 }>
<ChartGridLine <ChartGridLine
type="horizontal" type="horizontal"
scale={ yScale } scale={ yScale }
......
...@@ -14,7 +14,7 @@ type Props = { ...@@ -14,7 +14,7 @@ type Props = {
} }
const ChartsWidgetsList = ({ charts, interval }: Props) => { const ChartsWidgetsList = ({ charts, interval }: Props) => {
const isAnyChartDisplayed = charts.some((section) => section.charts.some(chart => chart.visible)); const isAnyChartDisplayed = charts.some((section) => section.charts.length > 0);
return ( return (
<Box> <Box>
...@@ -23,7 +23,6 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => { ...@@ -23,7 +23,6 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => {
{ {
charts.map((section) => ( charts.map((section) => (
<ListItem <ListItem
display={ section.charts.every((chart) => !chart.visible) ? 'none' : 'block' }
key={ section.id } key={ section.id }
mb={ 8 } mb={ 8 }
_last={{ _last={{
...@@ -46,7 +45,6 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => { ...@@ -46,7 +45,6 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => {
{ section.charts.map((chart) => ( { section.charts.map((chart) => (
<GridItem <GridItem
key={ chart.id } key={ chart.id }
display={ chart.visible ? 'block' : 'none' }
> >
<ChartWidget <ChartWidget
id={ chart.id } id={ chart.id }
......
...@@ -9,7 +9,53 @@ export const statsChartsScheme: Array<StatsSection> = [ ...@@ -9,7 +9,53 @@ export const statsChartsScheme: Array<StatsSection> = [
id: 'new-blocks', id: 'new-blocks',
title: 'New blocks', title: 'New blocks',
description: 'New blocks number per day', description: 'New blocks number per day',
visible: true, },
{
id: 'average-block-size',
title: 'Average block size',
description: 'Average size of blocks in bytes',
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
id: 'average-transaction-fee',
title: 'Average transaction fee',
description: 'The average amount in USD spent per transaction',
},
{
id: 'transactions-fees',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
},
{
id: 'new-transactions',
title: 'Transactions fees',
description: 'New transactions number per period',
},
{
id: 'transactions-growth',
title: 'Transactions growth',
description: 'Cumulative transactions number per period',
},
],
},
{
id: 'accounts',
title: 'Accounts',
charts: [
{
id: 'active-accounts',
title: 'Active accounts',
description: 'Active accounts number per period',
},
{
id: 'accounts-growth',
title: 'Accounts growth',
description: 'Cumulative accounts number per period',
}, },
], ],
}, },
......
...@@ -25,16 +25,13 @@ export default function useStats() { ...@@ -25,16 +25,13 @@ export default function useStats() {
const filterCharts = useCallback((q: string, currentSection: StatsSectionIds) => { const filterCharts = useCallback((q: string, currentSection: StatsSectionIds) => {
const charts = statsChartsScheme const charts = statsChartsScheme
?.map((section: StatsSection) => { ?.map((section: StatsSection) => {
const charts = section.charts.map((chart: StatsChart) => ({ const charts = section.charts.filter((chart: StatsChart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart));
...chart,
visible: isSectionMatches(section, currentSection) && isChartNameMatches(q, chart),
}));
return { return {
...section, ...section,
charts, charts,
}; };
}); }).filter((section: StatsSection) => section.charts.length > 0);
setDisplayedCharts(charts || []); setDisplayedCharts(charts || []);
}, []); }, []);
......
...@@ -24,9 +24,11 @@ type Props = { ...@@ -24,9 +24,11 @@ type Props = {
query: QueryResult; query: QueryResult;
showBlockInfo?: boolean; showBlockInfo?: boolean;
showSocketInfo?: boolean; showSocketInfo?: boolean;
currentAddress?: string;
filter?: React.ReactNode;
} }
const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Props) => { const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage; const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -60,7 +62,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop ...@@ -60,7 +62,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop
{ ({ content }) => <Box>{ content }</Box> } { ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice> </TxsNewItemNotice>
) } ) }
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo }/>) } { txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo } currentAddress={ currentAddress }/>) }
</Box> </Box>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
...@@ -71,6 +73,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop ...@@ -71,6 +73,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo } showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 } top={ isPaginatorHidden ? 0 : 80 }
currentAddress={ currentAddress }
/> />
</Hide> </Hide>
</> </>
...@@ -86,6 +89,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop ...@@ -86,6 +89,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop
setSorting={ setSortByValue } setSorting={ setSortByValue }
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ !isPaginatorHidden } showPagination={ !isPaginatorHidden }
filterComponent={ filter }
/> />
) } ) }
{ content } { content }
......
...@@ -17,18 +17,14 @@ type Props = { ...@@ -17,18 +17,14 @@ type Props = {
paginationProps: PaginationProps; paginationProps: PaginationProps;
className?: string; className?: string;
showPagination?: boolean; showPagination?: boolean;
filterComponent?: React.ReactNode;
} }
const TxsHeaderMobile = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => { const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps, className, showPagination = true }: Props) => {
return ( return (
<ActionBar className={ className }> <ActionBar className={ className }>
<HStack> <HStack>
{ /* api is not implemented */ } { filterComponent }
{ /* <TxsFilters
filters={ filters }
onFiltersChange={ setFilters }
appliedFiltersNum={ 0 }
/> */ }
<TxsSorting <TxsSorting
isActive={ Boolean(sorting) } isActive={ Boolean(sorting) }
setSorting={ setSorting } setSorting={ setSorting }
......
...@@ -24,17 +24,30 @@ import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; ...@@ -24,17 +24,30 @@ import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean}) => { type Props = {
tx: Transaction;
showBlockInfo: boolean;
currentAddress?: string;
}
const TAG_WIDTH = 48;
const ARROW_WIDTH = 24;
const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300'); const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to ? tx.to : tx.created_contract; const dataTo = tx.to ? tx.to : tx.created_contract;
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
return ( return (
<> <>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}> <Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
...@@ -83,7 +96,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo ...@@ -83,7 +96,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
</Box> </Box>
) } ) }
<Flex alignItems="center" height={ 6 } mt={ 6 }> <Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)"> <Address width={ `calc((100%-${ currentAddress ? TAG_WIDTH : ARROW_WIDTH + 8 }px)/2)` }>
<AddressIcon hash={ tx.from.hash }/> <AddressIcon hash={ tx.from.hash }/>
<AddressLink <AddressLink
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -92,12 +105,15 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo ...@@ -92,12 +105,15 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
ml={ 2 } ml={ 2 }
/> />
</Address> </Address>
<Icon { (isIn || isOut) ?
as={ rightArrowIcon } <InOutTag isIn={ isIn } isOut={ isOut } width="48px" mr={ 2 }/> : (
boxSize={ 6 } <Icon
mx={ 2 } as={ rightArrowIcon }
color="gray.500" boxSize={ 6 }
/> mx={ 2 }
color="gray.500"
/>
) }
<Address width="calc((100%-40px)/2)"> <Address width="calc((100%-40px)/2)">
<AddressIcon hash={ dataTo.hash }/> <AddressIcon hash={ dataTo.hash }/>
<AddressLink <AddressLink
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import TxsTable from './TxsTable';
test('base view +@dark-mode +@desktop-xl', async({ mount }) => {
const component = await mount(
<TestApp>
{ /* eslint-disable-next-line react/jsx-no-bind */ }
<TxsTable txs={ [ txMock.base, txMock.base ] } sort={ () => () => {} } top={ 0 } showBlockInfo showSocketInfo={ false }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
...@@ -18,20 +18,21 @@ type Props = { ...@@ -18,20 +18,21 @@ type Props = {
top: number; top: number;
showBlockInfo: boolean; showBlockInfo: boolean;
showSocketInfo: boolean; showSocketInfo: boolean;
currentAddress?: string;
} }
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Props) => { const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="54px"></Th> <Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th> <Th width="18%">Txn hash</Th>
<Th width="20%">Type</Th>
<Th width="15%">Method</Th> <Th width="15%">Method</Th>
{ showBlockInfo && <Th width="11%">Block</Th> } { showBlockInfo && <Th width="11%">Block</Th> }
<Th width={{ xl: '128px', base: '66px' }}>From</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th> <Th width={{ xl: currentAddress ? '48px' : '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th> <Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric> <Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end"> <Link onClick={ sort('val') } display="flex" justifyContent="end">
...@@ -60,6 +61,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Pr ...@@ -60,6 +61,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Pr
key={ item.hash } key={ item.hash }
tx={ item } tx={ item }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -28,13 +28,23 @@ import Address from 'ui/shared/address/Address'; ...@@ -28,13 +28,23 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import InOutTag from 'ui/shared/InOutTag';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from './TxType'; import TxType from './TxType';
const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean }) => { type Props = {
tx: Transaction;
showBlockInfo: boolean;
currentAddress?: string;
}
const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const addressFrom = ( const addressFrom = (
<Address> <Address>
<Tooltip label={ tx.from.implementation_name }> <Tooltip label={ tx.from.implementation_name }>
...@@ -74,13 +84,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -74,13 +84,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
) } ) }
</Popover> </Popover>
</Td> </Td>
<Td> <Td pr={ 4 }>
<VStack alignItems="start">
<TxType types={ tx.tx_types }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td>
<VStack alignItems="start" lineHeight="24px"> <VStack alignItems="start" lineHeight="24px">
<Address width="100%"> <Address width="100%">
<AddressLink <AddressLink
...@@ -92,6 +96,12 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -92,6 +96,12 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> <Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack> </VStack>
</Td> </Td>
<Td>
<VStack alignItems="start">
<TxType types={ tx.tx_types }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td whiteSpace="nowrap"> <Td whiteSpace="nowrap">
{ tx.method ? ( { tx.method ? (
<TruncatedTextTooltip label={ tx.method }> <TruncatedTextTooltip label={ tx.method }>
...@@ -110,8 +120,11 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -110,8 +120,11 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
<Td> <Td>
{ addressFrom } { addressFrom }
</Td> </Td>
<Td> <Td px={ 0 }>
<Icon as={ rightArrowIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/> { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px" mr={ 2 }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } mx="6px" color="gray.500"/>
}
</Td> </Td>
<Td> <Td>
{ addressTo } { addressTo }
...@@ -121,14 +134,17 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -121,14 +134,17 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
<Td colSpan={ 3 }> <Td colSpan={ 3 }>
<Box> <Box>
{ addressFrom } { addressFrom }
<Icon { (isIn || isOut) ?
as={ rightArrowIcon } <InOutTag isIn={ isIn } isOut={ isOut } width="48px" my={ 2 }/> : (
boxSize={ 6 } <Icon
mt={ 2 } as={ rightArrowIcon }
mb={ 1 } boxSize={ 6 }
color="gray.500" mt={ 2 }
transform="rotate(90deg)" mb={ 1 }
/> color="gray.500"
transform="rotate(90deg)"
/>
) }
{ addressTo } { addressTo }
</Box> </Box>
</Td> </Td>
......
...@@ -2604,6 +2604,38 @@ ...@@ -2604,6 +2604,38 @@
"@jridgewell/resolve-uri" "3.1.0" "@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14" "@jridgewell/sourcemap-codec" "1.4.14"
"@metamask/object-multiplex@^1.1.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-1.2.0.tgz#38fc15c142f61939391e1b9a8eed679696c7e4f4"
integrity sha512-hksV602d3NWE2Q30Mf2Np1WfVKaGqfJRy9vpHAmelbaD0OkDt06/0KQkRR6UVYdMbTbkuEu8xN5JDUU80inGwQ==
dependencies:
end-of-stream "^1.4.4"
once "^1.4.0"
readable-stream "^2.3.3"
"@metamask/providers@^10.2.1":
version "10.2.1"
resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-10.2.1.tgz#61304940adeccc7421dcda30ffd1d834273cc77b"
integrity sha512-p2TXw2a1Nb8czntDGfeIYQnk4LLVbd5vlcb3GY//lylYlKdSqp+uUTegCvxiFblRDOT68jsY8Ib1VEEzVUOolA==
dependencies:
"@metamask/object-multiplex" "^1.1.0"
"@metamask/safe-event-emitter" "^2.0.0"
"@types/chrome" "^0.0.136"
detect-browser "^5.2.0"
eth-rpc-errors "^4.0.2"
extension-port-stream "^2.0.1"
fast-deep-equal "^2.0.1"
is-stream "^2.0.0"
json-rpc-engine "^6.1.0"
json-rpc-middleware-stream "^4.2.1"
pump "^3.0.0"
webextension-polyfill-ts "^0.25.0"
"@metamask/safe-event-emitter@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c"
integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==
"@motionone/animation@^10.12.0": "@motionone/animation@^10.12.0":
version "10.14.0" version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.14.0.tgz#2f2a3517183bb58d82e389aac777fe0850079de6" resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.14.0.tgz#2f2a3517183bb58d82e389aac777fe0850079de6"
...@@ -3226,6 +3258,14 @@ ...@@ -3226,6 +3258,14 @@
dependencies: dependencies:
"@babel/types" "^7.3.0" "@babel/types" "^7.3.0"
"@types/chrome@^0.0.136":
version "0.0.136"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.136.tgz#7c011b9f997b0156f25a140188a0c5689d3f368f"
integrity sha512-XDEiRhLkMd+SB7Iw3ZUIj/fov3wLd4HyTdLltVszkgl1dBfc3Rb7oPMVZ2Mz2TLqnF7Ow+StbR8E7r9lqpb4DA==
dependencies:
"@types/filesystem" "*"
"@types/har-format" "*"
"@types/d3-array@*": "@types/d3-array@*":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
...@@ -3441,6 +3481,18 @@ ...@@ -3441,6 +3481,18 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
"@types/filesystem@*":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf"
integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==
dependencies:
"@types/filewriter" "*"
"@types/filewriter@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee"
integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==
"@types/geojson@*": "@types/geojson@*":
version "7946.0.10" version "7946.0.10"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
...@@ -3453,6 +3505,11 @@ ...@@ -3453,6 +3505,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/har-format@*":
version "1.2.10"
resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.10.tgz#7b4e1e0ada4d17684ac3b05d601a4871cfab11fc"
integrity sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
...@@ -4923,6 +4980,11 @@ delegates@^1.0.0: ...@@ -4923,6 +4980,11 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
detect-browser@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca"
integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==
detect-newline@^3.0.0: detect-newline@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
...@@ -5074,7 +5136,7 @@ encode-utf8@^1.0.3: ...@@ -5074,7 +5136,7 @@ encode-utf8@^1.0.3:
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
end-of-stream@^1.1.0: end-of-stream@^1.1.0, end-of-stream@^1.4.4:
version "1.4.4" version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
...@@ -5584,6 +5646,13 @@ esutils@^2.0.2: ...@@ -5584,6 +5646,13 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eth-rpc-errors@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a"
integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==
dependencies:
fast-safe-stringify "^2.0.6"
ethers@^5.7.1: ethers@^5.7.1:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.1.tgz#48c83a44900b5f006eb2f65d3ba6277047fd4f33" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.1.tgz#48c83a44900b5f006eb2f65d3ba6277047fd4f33"
...@@ -5676,11 +5745,23 @@ expect@^29.0.0, expect@^29.3.1: ...@@ -5676,11 +5745,23 @@ expect@^29.0.0, expect@^29.3.1:
jest-message-util "^29.3.1" jest-message-util "^29.3.1"
jest-util "^29.3.1" jest-util "^29.3.1"
extension-port-stream@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-2.0.1.tgz#d374820c581418c2275d3c4439ade0b82c4cfac6"
integrity sha512-ltrv4Dh/979I04+D4Te6TFygfRSOc5EBzzlHRldWMS8v73V80qWluxH88hqF0qyUsBXTb8NmzlmSipcre6a+rg==
dependencies:
webextension-polyfill-ts "^0.22.0"
fast-copy@^3.0.0: fast-copy@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8" resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8"
integrity sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA== integrity sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
...@@ -5712,7 +5793,7 @@ fast-redact@^3.1.1: ...@@ -5712,7 +5793,7 @@ fast-redact@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
fast-safe-stringify@^2.1.1: fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
...@@ -6976,6 +7057,22 @@ json-parse-even-better-errors@^2.3.0: ...@@ -6976,6 +7057,22 @@ json-parse-even-better-errors@^2.3.0:
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-rpc-engine@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz#bf5ff7d029e1c1bf20cb6c0e9f348dcd8be5a393"
integrity sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==
dependencies:
"@metamask/safe-event-emitter" "^2.0.0"
eth-rpc-errors "^4.0.2"
json-rpc-middleware-stream@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-4.2.1.tgz#e5cb8795ebfd7503c6ceaa43daaf065687cc2f22"
integrity sha512-6QKayke/8lg0nTjOpRCq4JCgRx7bVybldmloIfY21HSDV0GUevcV9i8DJNvuKTJx4KR9EDIf6HTStAnEovGUvA==
dependencies:
"@metamask/safe-event-emitter" "^2.0.0"
readable-stream "^2.3.3"
json-schema-traverse@^0.4.1: json-schema-traverse@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
...@@ -8021,7 +8118,7 @@ react@18.2.0: ...@@ -8021,7 +8118,7 @@ react@18.2.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
readable-stream@^2.0.6: readable-stream@^2.0.6, readable-stream@^2.3.3:
version "2.3.7" version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
...@@ -9069,6 +9166,25 @@ web-streams-polyfill@^3.0.3: ...@@ -9069,6 +9166,25 @@ web-streams-polyfill@^3.0.3:
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
webextension-polyfill-ts@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.22.0.tgz#86cfd7bab4d9d779d98c8340983f4b691b2343f3"
integrity sha512-3P33ClMwZ/qiAT7UH1ROrkRC1KM78umlnPpRhdC/292UyoTTW9NcjJEqDsv83HbibcTB6qCtpVeuB2q2/oniHQ==
dependencies:
webextension-polyfill "^0.7.0"
webextension-polyfill-ts@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.25.0.tgz#fff041626365dbd0e29c40b197e989a55ec221ca"
integrity sha512-ikQhwwHYkpBu00pFaUzIKY26I6L87DeRI+Q6jBT1daZUNuu8dSrg5U9l/ZbqdaQ1M/TTSPKeAa3kolP5liuedw==
dependencies:
webextension-polyfill "^0.7.0"
webextension-polyfill@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.7.0.tgz#0df1120ff0266056319ce1a622b09ad8d4a56505"
integrity sha512-su48BkMLxqzTTvPSE1eWxKToPS2Tv5DLGxKexLEVpwFd6Po6N8hhSLIvG6acPAg7qERoEaDL+Y5HQJeJeml5Aw==
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment