From 5947511ec71d4de9ef56f891d1c03637cd3973d5 Mon Sep 17 00:00:00 2001 From: juan Date: Wed, 6 Dec 2023 11:33:44 -0300 Subject: [PATCH 01/30] noves integration --- icons/lightning.svg | 3 + lib/hooks/useFetchDescribe.tsx | 56 ++++ lib/hooks/useFetchTranslate.tsx | 56 ++++ lib/metadata/getPageOgType.ts | 3 + lib/metadata/templates/description.ts | 3 + lib/metadata/templates/title.ts | 3 + lib/mixpanel/getPageType.ts | 3 + lib/utils/numberHelpers.ts | 44 +++ lib/utils/stringHelpers.ts | 41 +++ nextjs/nextjs-routes.d.ts | 5 +- pages/api/describe.ts | 31 ++ pages/api/history.ts | 35 +++ pages/api/translate.ts | 31 ++ stubs/history.ts | 10 + stubs/translate.ts | 12 + stubs/translateClassified.ts | 22 ++ stubs/translateRaw.ts | 12 + types/translateApi.ts | 89 ++++++ ui/address/AddressAccountHistory.tsx | 198 +++++++++++++ ui/address/AddressTxs.tsx | 1 + ui/address/history/AccountHistoryFilter.tsx | 88 ++++++ ui/address/history/useFetchHistory.tsx | 63 ++++ .../history/useFetchHistoryWithPages.tsx | 196 ++++++++++++ ui/address/history/utils.ts | 13 + ui/pages/Address.tsx | 7 + ui/pages/Transaction.tsx | 80 ++++- ui/shared/accountHistory/FromToComponent.tsx | 117 ++++++++ ui/shared/entities/address/AddressEntity.tsx | 7 +- ui/shared/entities/token/TokenEntity.tsx | 7 +- ui/shared/filters/FilterButton.tsx | 8 +- ui/tx/TxAssetFlows.tsx | 160 ++++++++++ ui/tx/assetFlows/ActionCard.tsx | 116 ++++++++ ui/tx/assetFlows/TokenTransferSnippet.tsx | 46 +++ ui/tx/assetFlows/TokensCard.tsx | 54 ++++ .../assetFlows/utils/generateFlowViewData.ts | 278 ++++++++++++++++++ ui/txs/TxType.tsx | 12 +- ui/txs/TxsContent.tsx | 4 + ui/txs/TxsListItem.tsx | 8 +- ui/txs/TxsTable.tsx | 3 + ui/txs/TxsTableItem.tsx | 8 +- 40 files changed, 1912 insertions(+), 21 deletions(-) create mode 100644 icons/lightning.svg create mode 100644 lib/hooks/useFetchDescribe.tsx create mode 100644 lib/hooks/useFetchTranslate.tsx create mode 100644 lib/utils/numberHelpers.ts create mode 100644 lib/utils/stringHelpers.ts create mode 100644 pages/api/describe.ts create mode 100644 pages/api/history.ts create mode 100644 pages/api/translate.ts create mode 100644 stubs/history.ts create mode 100644 stubs/translate.ts create mode 100644 stubs/translateClassified.ts create mode 100644 stubs/translateRaw.ts create mode 100644 types/translateApi.ts create mode 100644 ui/address/AddressAccountHistory.tsx create mode 100644 ui/address/history/AccountHistoryFilter.tsx create mode 100644 ui/address/history/useFetchHistory.tsx create mode 100644 ui/address/history/useFetchHistoryWithPages.tsx create mode 100644 ui/address/history/utils.ts create mode 100644 ui/shared/accountHistory/FromToComponent.tsx create mode 100644 ui/tx/TxAssetFlows.tsx create mode 100644 ui/tx/assetFlows/ActionCard.tsx create mode 100644 ui/tx/assetFlows/TokenTransferSnippet.tsx create mode 100644 ui/tx/assetFlows/TokensCard.tsx create mode 100644 ui/tx/assetFlows/utils/generateFlowViewData.ts diff --git a/icons/lightning.svg b/icons/lightning.svg new file mode 100644 index 0000000000..77d80ed9b8 --- /dev/null +++ b/icons/lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/hooks/useFetchDescribe.tsx b/lib/hooks/useFetchDescribe.tsx new file mode 100644 index 0000000000..2b707bf125 --- /dev/null +++ b/lib/hooks/useFetchDescribe.tsx @@ -0,0 +1,56 @@ +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +import type { DescribeResponse } from 'types/translateApi'; + +import config from 'configs/app'; +import type { Params as FetchParams } from 'lib/hooks/useFetch'; + +import useFetch from './useFetch'; + +export interface ApiFetchParams { + queryParams?: Record | number | undefined>; + fetchParams?: Pick; +} + +export interface TranslateError { + payload?: { + txHash: string; + }; + status: Response['status']; + statusText: Response['statusText']; +} + +interface Params extends ApiFetchParams { + queryOptions?: Omit, 'queryKey' | 'queryFn'>; +} + +export default function useFetchDescribe( + txHash: string | null, + { queryOptions }: Params = {}, +) { + const fetch = useFetch(); + + const url = new URL('/node-api/describe', config.app.baseUrl); + + const body = { + txHash, + }; + + return useQuery({ + queryKey: [ 'describe', txHash, body ], + queryFn: async() => { + // all errors and error typing is handled by react-query + // so error response will never go to the data + // that's why we are safe here to do type conversion "as Promise>" + if (!txHash) { + return undefined as unknown as DescribeResponse; + } + return fetch(url.toString(), { + method: 'POST', + body, + }) as Promise; + }, + ...queryOptions, + }); +} diff --git a/lib/hooks/useFetchTranslate.tsx b/lib/hooks/useFetchTranslate.tsx new file mode 100644 index 0000000000..f10e13f717 --- /dev/null +++ b/lib/hooks/useFetchTranslate.tsx @@ -0,0 +1,56 @@ +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +import type { ResponseData } from 'types/translateApi'; + +import config from 'configs/app'; +import type { Params as FetchParams } from 'lib/hooks/useFetch'; + +import useFetch from './useFetch'; + +export interface ApiFetchParams { + queryParams?: Record | number | undefined>; + fetchParams?: Pick; +} + +export interface TranslateError { + payload?: { + txHash: string; + }; + status: Response['status']; + statusText: Response['statusText']; +} + +interface Params extends ApiFetchParams { + queryOptions?: Omit, 'queryKey' | 'queryFn'>; +} + +export default function useFetchTranslate( + txHash: string | null, + { queryOptions }: Params = {}, +) { + const fetch = useFetch(); + + const url = new URL('/node-api/translate', config.app.baseUrl); + + const body = { + txHash, + }; + + return useQuery({ + queryKey: [ 'translate', txHash, body ], + queryFn: async() => { + // all errors and error typing is handled by react-query + // so error response will never go to the data + // that's why we are safe here to do type conversion "as Promise>" + if (!txHash) { + return undefined as unknown as ResponseData; + } + return fetch(url.toString(), { + method: 'POST', + body, + }) as Promise; + }, + ...queryOptions, + }); +} diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 5da92870b1..94806ec8e7 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -42,6 +42,9 @@ const OG_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Regular page', + '/api/translate': 'Regular page', + '/api/history': 'Regular page', + '/api/describe': 'Regular page', '/api/media-type': 'Regular page', '/api/proxy': 'Regular page', '/api/csrf': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 25ced418dd..3b8359c80d 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -45,6 +45,9 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, + '/api/translate': DEFAULT_TEMPLATE, + '/api/history': DEFAULT_TEMPLATE, + '/api/describe': DEFAULT_TEMPLATE, '/api/media-type': DEFAULT_TEMPLATE, '/api/proxy': DEFAULT_TEMPLATE, '/api/csrf': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index a47e94cf5e..783c3da653 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -40,6 +40,9 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': 'login', + '/api/translate': 'node API media type', + '/api/history': 'node API media type', + '/api/describe': 'node API media type', '/api/media-type': 'node API media type', '/api/proxy': 'node API proxy', '/api/csrf': 'node API CSRF token', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index dec0e8dc30..b52df60220 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -40,6 +40,9 @@ export const PAGE_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Login', + '/api/translate': 'Node API: Media type', + '/api/history': 'Node API: Media type', + '/api/describe': 'Node API: Media type', '/api/media-type': 'Node API: Media type', '/api/proxy': 'Node API: Proxy', '/api/csrf': 'Node API: CSRF token', diff --git a/lib/utils/numberHelpers.ts b/lib/utils/numberHelpers.ts new file mode 100644 index 0000000000..792f70b082 --- /dev/null +++ b/lib/utils/numberHelpers.ts @@ -0,0 +1,44 @@ +import _ from 'lodash'; + +export function formatNumberString(number: string, decimalPlaces: number) { + const [ whole, decimal ] = number.split('.'); + if (decimal) { + return `${ whole }.${ decimal.slice(0, decimalPlaces) }`; + } + return whole; +} + +export function formatNumberIsNeeded(number: string, decimalPlaces: number) { + // eslint-disable-next-line + const [whole, decimal] = number.split('.'); + if (decimal) { + return decimal.length > decimalPlaces; + } + return false; +} + +export function truncateMiddle(text: string, startLength: number, endLength: number): string { + if (text.length <= startLength + endLength + 3) { + return text; + } + return `${ text.substring(0, startLength) }...${ text.slice(-Math.abs(endLength)) }`; +} + +export function roundNumberIfNeeded(number: string, decimalPlaces: number) { + if (formatNumberIsNeeded(number, decimalPlaces)) { + const rounded = _.round(parseFloat(number), decimalPlaces); + + if (rounded === 0) { + const positiveNumbers = [ '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; + + const numberIndices = positiveNumbers.map((n => number.indexOf(n))).filter(n => n !== -1).sort((a, b) => a - b); + + const subStr = number.substring(0, numberIndices[0] + 1); + + return '~' + subStr; + } + + return '~' + rounded.toString(); + } + return number; +} diff --git a/lib/utils/stringHelpers.ts b/lib/utils/stringHelpers.ts new file mode 100644 index 0000000000..6a37a105b8 --- /dev/null +++ b/lib/utils/stringHelpers.ts @@ -0,0 +1,41 @@ +export function camelCaseToSentence(camelCaseString: string | undefined) { + if (!camelCaseString) { + return ''; + } + + let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2'); + sentence = sentence.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'); + sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1); + sentence = capitalizeAcronyms(sentence); + return sentence; +} + +function capitalizeAcronyms(sentence: string) { + const acronymList = [ 'NFT' ]; // add more acronyms here if needed + + const words = sentence.split(' '); + + const capitalizedWords = words.map((word) => { + const acronym = word.toUpperCase(); + if (acronymList.includes(acronym)) { + return acronym.toUpperCase(); + } + return word; + }); + + return capitalizedWords.join(' '); +} + +export function truncateMiddle(string: string, startLength: number, endLength: number): string { + const text = string || ''; + + if (!text) { + return ''; + } + + if (text.length <= startLength + endLength + 3) { + return text; + } + + return `${ text.substring(0, startLength) }...${ text.slice(-Math.abs(endLength)) }`; +} diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index a39e9e3884..1dbebe3ad5 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -17,12 +17,15 @@ declare module "nextjs-routes" { | DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }> | DynamicRoute<"/address/[hash]", { "hash": string }> | StaticRoute<"/api/csrf"> + | StaticRoute<"/api/describe"> | StaticRoute<"/api/healthz"> + | StaticRoute<"/api/history"> | StaticRoute<"/api/media-type"> | StaticRoute<"/api/proxy"> + | StaticRoute<"/api/translate"> | StaticRoute<"/api-docs"> - | DynamicRoute<"/apps/[id]", { "id": string }> | StaticRoute<"/apps"> + | DynamicRoute<"/apps/[id]", { "id": string }> | StaticRoute<"/auth/auth0"> | StaticRoute<"/auth/profile"> | StaticRoute<"/auth/unverified-email"> diff --git a/pages/api/describe.ts b/pages/api/describe.ts new file mode 100644 index 0000000000..b534f1adcc --- /dev/null +++ b/pages/api/describe.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import nodeFetch from 'node-fetch'; + +import { getEnvValue } from 'configs/app/utils'; + +const translateEnpoint = getEnvValue('TRANSLATE_ENDPOINT') as string; +const translateApiKey = getEnvValue('TRANSLATE_API_KEY') as string; +const translateSelectedChain = getEnvValue('TRANSLATE_SELECTED_CHAIN') as string; + +const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { + if (nextReq.method !== 'POST') { + return nextRes.status(404).send({ + success: false, + message: 'Method not supported', + }); + } + const { txHash } = nextReq.body; + + const url = `${ translateEnpoint }/evm/${ translateSelectedChain }/describeTx/${ txHash }`; + const headers = { + apiKey: translateApiKey, + }; + + const apiRes = await nodeFetch(url, { + headers, + }); + + nextRes.status(apiRes.status).send(apiRes.body); +}; + +export default handler; diff --git a/pages/api/history.ts b/pages/api/history.ts new file mode 100644 index 0000000000..5a71c9457a --- /dev/null +++ b/pages/api/history.ts @@ -0,0 +1,35 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import nodeFetch from 'node-fetch'; +import queryString from 'querystring'; + +import { getEnvValue } from 'configs/app/utils'; + +const translateEnpoint = getEnvValue('TRANSLATE_ENDPOINT') as string; +const translateApiKey = getEnvValue('TRANSLATE_API_KEY') as string; +const translateSelectedChain = getEnvValue('TRANSLATE_SELECTED_CHAIN') as string; + +const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { + if (nextReq.method !== 'POST') { + return nextRes.status(404).send({ + success: false, + message: 'Method not supported', + }); + } + const { address } = nextReq.body; + const query = queryString.stringify(nextReq.query); + + const fetchParams = query ? query : `viewAsAccountAddress=${ address }`; + + const url = `${ translateEnpoint }/evm/${ translateSelectedChain }/txs/${ address }?${ fetchParams }`; + const headers = { + apiKey: translateApiKey, + }; + + const apiRes = await nodeFetch(url, { + headers, + }); + + nextRes.status(apiRes.status).send(apiRes.body); +}; + +export default handler; diff --git a/pages/api/translate.ts b/pages/api/translate.ts new file mode 100644 index 0000000000..edebf4ace7 --- /dev/null +++ b/pages/api/translate.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import nodeFetch from 'node-fetch'; + +import { getEnvValue } from 'configs/app/utils'; + +const translateEnpoint = getEnvValue('TRANSLATE_ENDPOINT') as string; +const translateApiKey = getEnvValue('TRANSLATE_API_KEY') as string; +const translateSelectedChain = getEnvValue('TRANSLATE_SELECTED_CHAIN') as string; + +const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { + if (nextReq.method !== 'POST') { + return nextRes.status(404).send({ + success: false, + message: 'Method not supported', + }); + } + const { txHash } = nextReq.body; + + const url = `${ translateEnpoint }/evm/${ translateSelectedChain }/tx/${ txHash }`; + const headers = { + apiKey: translateApiKey, + }; + + const apiRes = await nodeFetch(url, { + headers, + }); + + nextRes.status(apiRes.status).send(apiRes.body); +}; + +export default handler; diff --git a/stubs/history.ts b/stubs/history.ts new file mode 100644 index 0000000000..da99774bc1 --- /dev/null +++ b/stubs/history.ts @@ -0,0 +1,10 @@ +import type { AccountHistoryResponse } from 'types/translateApi'; + +import { TRANSLATE } from './translate'; + +export const HISTORY: AccountHistoryResponse = { + hasNextPage: true, + items: [ TRANSLATE ], + pageNumber: 1, + pageSize: 10, +}; diff --git a/stubs/translate.ts b/stubs/translate.ts new file mode 100644 index 0000000000..0c1d9c488c --- /dev/null +++ b/stubs/translate.ts @@ -0,0 +1,12 @@ +import type { ResponseData } from 'types/translateApi'; + +import { TRANSLATE_CLASSIFIED } from './translateClassified'; +import { TRANSLATE_RAW } from './translateRaw'; + +export const TRANSLATE: ResponseData = { + accountAddress: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + chain: 'base', + txTypeVersion: 2, + rawTransactionData: TRANSLATE_RAW, + classificationData: TRANSLATE_CLASSIFIED, +}; diff --git a/stubs/translateClassified.ts b/stubs/translateClassified.ts new file mode 100644 index 0000000000..494ed834e6 --- /dev/null +++ b/stubs/translateClassified.ts @@ -0,0 +1,22 @@ +import type { ClassificationData } from 'types/translateApi'; + +export const TRANSLATE_CLASSIFIED: ClassificationData = { + description: 'Sent 0.04 ETH', + received: [ { + action: 'Sent Token', + amount: '45', + from: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + to: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + token: { + address: '', + name: 'ETH', + symbol: 'ETH', + decimals: 18, + }, + } ], + sent: [], + source: { + type: '', + }, + type: '0x2', +}; diff --git a/stubs/translateRaw.ts b/stubs/translateRaw.ts new file mode 100644 index 0000000000..daba82675b --- /dev/null +++ b/stubs/translateRaw.ts @@ -0,0 +1,12 @@ +import type { RawTransactionData } from 'types/translateApi'; + +export const TRANSLATE_RAW: RawTransactionData = { + blockNumber: 1, + fromAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + gas: 2, + gasPrice: 3, + timestamp: 20000, + toAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + transactionFee: 2, + transactionHash: '0x128b79937a0eDE33258992c9668455f997f1aF24', +}; diff --git a/types/translateApi.ts b/types/translateApi.ts new file mode 100644 index 0000000000..61da855362 --- /dev/null +++ b/types/translateApi.ts @@ -0,0 +1,89 @@ +export interface ResponseData { + txTypeVersion: number; + chain: string; + accountAddress: string; + classificationData: ClassificationData; + rawTransactionData: RawTransactionData; +} + +export interface ClassificationData { + type: string; + description: string; + sent: Array; + received: Array; + source: { + type: string | null; + }; + message?: string; +} + +export interface SentReceived { + action: string; + amount: string; + to: To; + from: From; + token?: Token; + nft?: Nft; +} + +export interface Token { + symbol: string; + name: string; + decimals: number; + address: string; + id?: string; +} + +export interface Nft { + name: string; + id: string; + symbol: string; + address: string; +} + +export interface From { + name: string | null; + address: string; +} + +export interface To { + name: string | null; + address: string; +} + +export interface RawTransactionData { + transactionHash: string; + fromAddress: string; + toAddress: string; + blockNumber: number; + gas: number; + gasPrice: number; + transactionFee: TransactionFee | number; + timestamp: number; +} + +export interface TransactionFee { + amount: string; + currency: string; +} + +export interface AccountHistoryResponse { + hasNextPage: boolean; + items: Array; + pageNumber: number; + pageSize: number; + nextPageUrl?: string; +} + +export const HistorySentReceivedFilterValues = [ 'received', 'sent' ] as const; + +export type HistorySentReceivedFilter = typeof HistorySentReceivedFilterValues[number] | undefined; + +export interface HistoryFilters { + filter?: HistorySentReceivedFilter; +} + +export interface DescribeResponse { + type: string; + description: string; +} diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx new file mode 100644 index 0000000000..2556b090a1 --- /dev/null +++ b/ui/address/AddressAccountHistory.tsx @@ -0,0 +1,198 @@ +import { Box, Hide, Show, Skeleton, StackDivider, Table, TableContainer, + Tbody, Td, Text, Th, Thead, Tr, VStack, useColorModeValue } from '@chakra-ui/react'; +import utc from 'dayjs/plugin/utc'; +import { AnimatePresence, motion } from 'framer-motion'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { HistorySentReceivedFilter } from 'types/translateApi'; +import { HistorySentReceivedFilterValues } from 'types/translateApi'; + +import lightning from 'icons/lightning.svg'; +import dayjs from 'lib/date/dayjs'; +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { TRANSLATE } from 'stubs/translate'; +import { getFromTo } from 'ui/shared/accountHistory/FromToComponent'; +import ActionBar from 'ui/shared/ActionBar'; +import Icon from 'ui/shared/chakra/Icon'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import LinkInternal from 'ui/shared/LinkInternal'; +import Pagination from 'ui/shared/pagination/Pagination'; + +import AccountHistoryFilter from './history/AccountHistoryFilter'; +import useFetchHistoryWithPages from './history/useFetchHistoryWithPages'; +import { generateHistoryStub } from './history/utils'; + +dayjs.extend(utc); + +const getFilterValue = (getFilterValueFromQuery).bind(null, HistorySentReceivedFilterValues); + +type Props = { + scrollRef?: React.RefObject; +} + +const AddressAccountHistory = ({ scrollRef }: Props) => { + const router = useRouter(); + + const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); + + const addressColor = useColorModeValue('gray.500', 'whiteAlpha.600'); + + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + const { data, isError, pagination, isPlaceholderData } = useFetchHistoryWithPages({ + address: currentAddress, + scrollRef, + options: { + placeholderData: generateHistoryStub(TRANSLATE, 10, { hasNextPage: true, pageSize: 10, pageNumber: 1 }), + }, + }); + + const handleFilterChange = React.useCallback((val: string | Array) => { + + const newVal = getFilterValue(val); + setFilterValue(newVal); + }, [ ]); + + const actionBar = ( + + + + + + ); + + const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? getFromTo(i, currentAddress, true) === filterValue : i); + + const content = ( + + + }> + { filteredData?.map((tx, i) => ( + + + + + + + Action + + + + { dayjs(tx.rawTransactionData.timestamp * 1000).utc().fromNow() } + + + + + + + { tx.classificationData.description } + + + + + { getFromTo(tx, currentAddress) } + + + )) } + + + + + + + + + + + + + + + + { filteredData?.map((tx, i) => ( + + + + + + )) } + + +
+ Age + + Action + + From/To +
+ + + { dayjs(tx.rawTransactionData.timestamp * 1000).utc().fromNow() } + + + + + + + + + { tx.classificationData.description } + + + + + + + { getFromTo(tx, currentAddress) } + +
+
+
+
+ ); + + return ( + <> + { /* should stay before tabs to scroll up with pagination */ } + + + + + ); +}; + +export default AddressAccountHistory; diff --git a/ui/address/AddressTxs.tsx b/ui/address/AddressTxs.tsx index 3dcbec39f9..660f3993f8 100644 --- a/ui/address/AddressTxs.tsx +++ b/ui/address/AddressTxs.tsx @@ -187,6 +187,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { socketInfoAlert={ socketAlert } socketInfoNum={ newItemsCount } top={ 80 } + translate /> ); diff --git a/ui/address/history/AccountHistoryFilter.tsx b/ui/address/history/AccountHistoryFilter.tsx new file mode 100644 index 0000000000..322cb5bbc6 --- /dev/null +++ b/ui/address/history/AccountHistoryFilter.tsx @@ -0,0 +1,88 @@ +import { + Menu, + MenuButton, + MenuList, + MenuOptionGroup, + MenuItemOption, + useDisclosure, +} from '@chakra-ui/react'; +import React from 'react'; + +import type { HistorySentReceivedFilter } from 'types/translateApi'; + +import Check from 'icons/check.svg'; +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import Icon from 'ui/shared/chakra/Icon'; +import FilterButton from 'ui/shared/filters/FilterButton'; + +interface Props { + isActive: boolean; + defaultFilter: HistorySentReceivedFilter; + onFilterChange: (nextValue: string | Array) => void; + isLoading?: boolean; +} + +const AccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => { + const { isOpen, onToggle } = useDisclosure(); + const isInitialLoading = useIsInitialLoading(isLoading); + + const onCloseMenu = React.useCallback(() => { + if (isOpen) { + onToggle(); + } + }, [ isOpen, onToggle ]); + + return ( + + + + + + + + ) } + > + All + + + ) } + > + Received from + + + ) } + > + Sent to + + + + + ); +}; + +export default React.memo(AccountHistoryFilter); diff --git a/ui/address/history/useFetchHistory.tsx b/ui/address/history/useFetchHistory.tsx new file mode 100644 index 0000000000..5a98d1f18f --- /dev/null +++ b/ui/address/history/useFetchHistory.tsx @@ -0,0 +1,63 @@ +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +import type { AccountHistoryResponse } from 'types/translateApi'; + +import config from 'configs/app'; +import type { Params as FetchParams } from 'lib/hooks/useFetch'; +import useFetch from 'lib/hooks/useFetch'; + +export interface ApiFetchParams { + queryParams?: Record | number | undefined>; + fetchParams?: Pick; +} + +export interface TranslateHistory { + payload?: { + address: string; + }; + status: Response['status']; + statusText: Response['statusText']; +} + +interface Params extends ApiFetchParams { + queryOptions?: Omit, 'queryKey' | 'queryFn'>; +} + +export default function useFetchHistory( + address: string | null, + page: number, + { queryOptions, queryParams }: Params = {}, +) { + const fetch = useFetch(); + + const url = new URL('/node-api/history', config.app.baseUrl); + + queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { + // there are some pagination params that can be null or false for the next page + value !== undefined && value !== '' && url.searchParams.append(key, String(value)); + }); + + const body = { + address, + page, + }; + + return useQuery({ + queryKey: [ 'history', address, { ...queryParams }, body ], + queryFn: async() => { + // all errors and error typing is handled by react-query + // so error response will never go to the data + // that's why we are safe here to do type conversion "as Promise>" + if (!address) { + return undefined as unknown as AccountHistoryResponse; + } + return fetch(url.toString(), { + method: 'POST', + body, + + }) as Promise; + }, + ...queryOptions, + }); +} diff --git a/ui/address/history/useFetchHistoryWithPages.tsx b/ui/address/history/useFetchHistoryWithPages.tsx new file mode 100644 index 0000000000..83d6f6ea05 --- /dev/null +++ b/ui/address/history/useFetchHistoryWithPages.tsx @@ -0,0 +1,196 @@ +import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; +import omit from 'lodash/omit'; +import { useRouter } from 'next/router'; +import queryString from 'querystring'; +import React, { useCallback } from 'react'; +import { animateScroll } from 'react-scroll'; + +import type { AccountHistoryResponse, HistoryFilters } from 'types/translateApi'; +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import type { PaginatedResources, PaginationFilters, PaginationSorting } from 'lib/api/resources'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import type { TranslateHistory } from './useFetchHistory'; +import useFetchHistory from './useFetchHistory'; + +export interface Params { + address: string; + options?: Omit, 'queryKey' | 'queryFn'>; + filters?: HistoryFilters; + sorting?: PaginationSorting; + scrollRef?: React.RefObject; +} + +type NextPageParams = Record; + +function getPaginationParamsFromQuery(queryString: string | Array | undefined) { + if (queryString) { + try { + return JSON.parse(decodeURIComponent(getQueryParamString(queryString))) as NextPageParams; + } catch (error) {} + } + + return {}; +} + +export type QueryWithPagesResult = +UseQueryResult & +{ + onFilterChange: (filters: PaginationFilters) => void; + pagination: PaginationParams; +} + +export default function useFetchHistoryWithPages({ + address, + filters, + options, + sorting, + scrollRef, +}: Params): QueryWithPagesResult { + const queryClient = useQueryClient(); + const router = useRouter(); + + const [ page, setPage ] = React.useState(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1); + const [ pageParams, setPageParams ] = React.useState>({ + [page]: getPaginationParamsFromQuery(router.query.next_page_params), + }); + const [ hasPages, setHasPages ] = React.useState(page > 1); + + const isMounted = React.useRef(false); + const canGoBackwards = React.useRef(!router.query.page); + const queryParams = React.useMemo(() => [ { ...pageParams[page], ...filters, ...sorting } ], [ pageParams, page, filters, sorting ])[0]; + + const scrollToTop = useCallback(() => { + scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 }); + }, [ scrollRef ]); + + const queryResult = useFetchHistory(address, page, { + queryParams, + queryOptions: { + staleTime: page === 1 ? 0 : Infinity, + ...options, + }, + }); + const { data } = queryResult; + + const queryKey = React.useMemo(() => [ 'history', address, page, { ...queryParams } ], [ address, page, queryParams ]); + + const onNextPageClick = useCallback(() => { + if (!data?.nextPageUrl) { + // we hide next page button if no next_page_params + return; + } + const pageQuery = data.nextPageUrl || ''; + const nextPageParams = queryString.parse(pageQuery.split('?').pop() || ''); + setPageParams((prev) => ({ + ...prev, + [page + 1]: nextPageParams as NextPageParams, + })); + setPage(prev => prev + 1); + + const nextPageQuery = { + ...router.query, + page: String(page + 1), + next_page_params: encodeURIComponent(JSON.stringify(nextPageParams)), + }; + + setHasPages(true); + scrollToTop(); + router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); + }, [ data?.nextPageUrl, page, router, scrollToTop ]); + + const onPrevPageClick = useCallback(() => { + // returning to the first page + // we dont have pagination params for the first page + let nextPageQuery: typeof router.query = { ...router.query }; + if (page === 2) { + nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]); + canGoBackwards.current = true; + } else { + nextPageQuery.next_page_params = encodeURIComponent(JSON.stringify(pageParams[page - 1])); + nextPageQuery.page = String(page - 1); + } + + scrollToTop(); + router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) + .then(() => { + setPage(prev => prev - 1); + page === 2 && queryClient.removeQueries({ queryKey }); + }); + }, [ pageParams, router, page, scrollToTop, queryClient, queryKey ]); + + const resetPage = useCallback(() => { + queryClient.removeQueries({ queryKey }); + + scrollToTop(); + const nextRouterQuery = omit(router.query, [ 'next_page_params', 'page' ]); + router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => { + queryClient.removeQueries({ queryKey }); + setPage(1); + setPageParams({}); + 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, type: 'inactive' }); + }, 100); + }); + }, [ queryClient, router, scrollToTop, queryKey ]); + + const onFilterChange = useCallback((newFilters: PaginationFilters | undefined) => { + const newQuery = omit(router.query, 'next_page_params', 'page'); + if (newFilters) { + Object.entries(newFilters).forEach(([ key, value ]) => { + if (value && value.length) { + newQuery[key] = Array.isArray(value) ? value.join(',') : (value || ''); + } + }); + } + scrollToTop(); + router.push( + { + pathname: router.pathname, + query: newQuery, + }, + undefined, + { shallow: true }, + ).then(() => { + setHasPages(false); + setPage(1); + setPageParams({}); + }); + }, [ router, scrollToTop ]); + + const hasNextPage = data?.hasNextPage ? data.hasNextPage : false; + + const pagination = { + page, + onNextPageClick, + onPrevPageClick, + resetPage, + hasPages, + hasNextPage, + canGoBackwards: canGoBackwards.current, + isLoading: queryResult.isPlaceholderData, + isVisible: hasPages || hasNextPage, + }; + + React.useEffect(() => { + if (page !== 1 && isMounted.current) { + queryClient.cancelQueries({ queryKey }); + setPage(1); + } + // hook should run only when queryName has changed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ address ]); + + React.useEffect(() => { + window.setTimeout(() => { + isMounted.current = true; + }, 0); + }, []); + + return { ...queryResult, pagination, onFilterChange }; +} diff --git a/ui/address/history/utils.ts b/ui/address/history/utils.ts new file mode 100644 index 0000000000..ec64971c1f --- /dev/null +++ b/ui/address/history/utils.ts @@ -0,0 +1,13 @@ +import type { AccountHistoryResponse } from 'types/translateApi'; +import type { ArrayElement } from 'types/utils'; + +export function generateHistoryStub( + stub: ArrayElement, + num = 50, + rest: Omit, +) { + return { + items: Array(num).fill(stub), + ...rest, + }; +} diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index eb4b95dc36..84f3b409f0 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -12,6 +12,7 @@ import useContractTabs from 'lib/hooks/useContractTabs'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; import getQueryParamString from 'lib/router/getQueryParamString'; import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address'; +import AddressAccountHistory from 'ui/address/AddressAccountHistory'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressContract from 'ui/address/AddressContract'; @@ -72,6 +73,12 @@ const AddressPageContent = () => { count: addressTabsCountersQuery.data?.transactions_count, component: , }, + { + id: 'history', + title: 'Account history', + //count: , + component: , + }, config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ? { id: 'withdrawals', diff --git a/ui/pages/Transaction.tsx b/ui/pages/Transaction.tsx index 894006b550..b8a6d72a94 100644 --- a/ui/pages/Transaction.tsx +++ b/ui/pages/Transaction.tsx @@ -1,22 +1,28 @@ +import { Box, Skeleton, Text } from '@chakra-ui/react'; import { useRouter } from 'next/router'; -import React from 'react'; +import React, { useCallback } from 'react'; import type { RoutedTab } from 'ui/shared/Tabs/types'; import config from 'configs/app'; +import lightning from 'icons/lightning.svg'; import useApiQuery from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/contexts/app'; +import useFetchTranslate from 'lib/hooks/useFetchTranslate'; import getQueryParamString from 'lib/router/getQueryParamString'; +import { TRANSLATE } from 'stubs/translate'; import { TX } from 'stubs/tx'; -import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TextAd from 'ui/shared/ad/TextAd'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; +import Icon from 'ui/shared/chakra/Icon'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import EntityTags from 'ui/shared/EntityTags'; -import NetworkExplorers from 'ui/shared/NetworkExplorers'; import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; +import TokenTransferSnippet from 'ui/tx/assetFlows/TokenTransferSnippet'; +import { getFlowCount, getSplittedDescription } from 'ui/tx/assetFlows/utils/generateFlowViewData'; +import TxAssetFlows from 'ui/tx/TxAssetFlows'; import TxDetails from 'ui/tx/TxDetails'; import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped'; import TxInternals from 'ui/tx/TxInternals'; @@ -31,6 +37,14 @@ const TransactionPageContent = () => { const hash = getQueryParamString(router.query.hash); + const fetchTranslate = useFetchTranslate(hash, { + queryOptions: { + placeholderData: TRANSLATE, + }, + }); + + const { data: translateData, isError, isPlaceholderData: isTranslatePlaceholder } = fetchTranslate; + const { data, isPlaceholderData } = useApiQuery('tx', { pathParams: { hash }, queryOptions: { @@ -41,6 +55,7 @@ const TransactionPageContent = () => { const tabs: Array = [ { id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: }, + { id: 'asset_flows', title: 'Asset Flows', component: , count: getFlowCount(translateData) }, config.features.suave.isEnabled && data?.wrapped ? { id: 'wrapped', title: 'Regular tx details', component: } : undefined, @@ -73,12 +88,59 @@ const TransactionPageContent = () => { }; }, [ appProps.referrer ]); + const getTransactionDescription = useCallback(() => { + if (!isError && translateData) { + if (translateData.classificationData.description) { + const description = getSplittedDescription(translateData); + + return description.map((item, i) => ( + <> + + { i === 0 && ( + + ) } + { item.text } + + { item.hasId ? ( + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + + ) : + item.token && ( + + ) } + + + )); + } else { + return 'Error fetching transaction description'; + } + } else { + return 'Error fetching transaction description'; + } + }, [ isError, translateData ]); + const titleSecondRow = ( - <> - - { !data?.tx_tag && } - - + + + { getTransactionDescription() } + + ); return ( diff --git a/ui/shared/accountHistory/FromToComponent.tsx b/ui/shared/accountHistory/FromToComponent.tsx new file mode 100644 index 0000000000..f29233bf9d --- /dev/null +++ b/ui/shared/accountHistory/FromToComponent.tsx @@ -0,0 +1,117 @@ +import { Box, Tag, TagLabel } from '@chakra-ui/react'; +import React from 'react'; + +import type { ResponseData, SentReceived } from 'types/translateApi'; + +import type { FlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; + +import AddressEntity from '../entities/address/AddressEntity'; + +export const fromToComponent = (text: string, address: string, getValue?: boolean, name?: string | null, truncate?: number) => { + const isSent = text.startsWith('Sent'); + + if (getValue) { + return text.split(' ').shift()?.toLowerCase(); + } + return ( + + + + { text } + + Received from + + + + + + + ); +}; + +export const getFromTo = (txData: ResponseData, currentAddress: string, getValue?: boolean) => { + const raw = txData.rawTransactionData; + const sent = txData.classificationData.sent; + let sentFound: Array = []; + if (sent && sent[0]) { + sentFound = sent + .filter((sent) => sent.from.address.toLocaleLowerCase() === currentAddress) + .filter((sent) => sent.to.address); + } + + const received = txData.classificationData.received; + let receivedFound: Array = []; + if (received && received[0]) { + receivedFound = received + .filter((received) => received.to.address.toLocaleLowerCase() === currentAddress) + .filter((received) => received.from.address); + } + + if (sentFound[0] && receivedFound[0]) { + if (sentFound.length === receivedFound.length) { + if (raw.toAddress.toLocaleLowerCase() === currentAddress) { + return fromToComponent('Received from', raw.fromAddress, getValue); + } + + if (raw.fromAddress.toLocaleLowerCase() === currentAddress) { + return fromToComponent('Sent to', raw.toAddress, getValue); + } + } + if (sentFound.length > receivedFound.length) { + return fromToComponent('Sent to', sentFound[0].to.address, getValue); + } else { + return fromToComponent('Received from', receivedFound[0].from.address, getValue); + } + } + + if (sent && sentFound[0]) { + return fromToComponent('Sent to', sentFound[0].to.address, getValue); + } + + if (received && receivedFound[0]) { + return fromToComponent('Received from', receivedFound[0].from.address, getValue); + } + + if (raw.toAddress && raw.toAddress.toLocaleLowerCase() === currentAddress) { + return fromToComponent('Received from', raw.fromAddress, getValue); + } + + if (raw.fromAddress && raw.fromAddress.toLocaleLowerCase() === currentAddress) { + return fromToComponent('Sent to', raw.toAddress, getValue); + } + + if (!raw.toAddress && raw.fromAddress) { + return fromToComponent('Received from', raw.fromAddress, getValue); + } + + if (!raw.fromAddress && raw.toAddress) { + return fromToComponent('Sent to', raw.toAddress, getValue); + } + + return fromToComponent('Sent to', currentAddress, getValue); +}; + +export const getActionFromTo = (item: FlowViewItem, truncate = 7) => { + if (item.action.flowDirection === 'toRight') { + return fromToComponent('Sent to', item.rightActor.address, false, item.rightActor.name, truncate); + } + return fromToComponent('Received from', item.rightActor.address, false, item.rightActor.name, truncate); +}; diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 5eabca3825..92bec5ba8d 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -10,6 +10,7 @@ import { route } from 'nextjs-routes'; import iconSafe from 'icons/brands/safe.svg'; import iconContractVerified from 'icons/contract_verified.svg'; import iconContract from 'icons/contract.svg'; +import { truncateMiddle } from 'lib/utils/numberHelpers'; import * as EntityBase from 'ui/shared/entities/base/components'; import { getIconProps } from '../base/utils'; @@ -98,7 +99,7 @@ const Icon = (props: IconProps) => { ); }; -type ContentProps = Omit & Pick; +type ContentProps = Omit & Pick; const Content = chakra((props: ContentProps) => { if (props.address.name) { @@ -121,7 +122,7 @@ const Content = chakra((props: ContentProps) => { return ( ); }); @@ -142,6 +143,8 @@ const Container = EntityBase.Container; export interface EntityProps extends EntityBase.EntityBaseProps { address: Pick; isSafeAddress?: boolean; + truncate?: number; + truncateEnd?: number; } const AddressEntry = (props: EntityProps) => { diff --git a/ui/shared/entities/token/TokenEntity.tsx b/ui/shared/entities/token/TokenEntity.tsx index 53ff91f289..2c775f141f 100644 --- a/ui/shared/entities/token/TokenEntity.tsx +++ b/ui/shared/entities/token/TokenEntity.tsx @@ -86,7 +86,7 @@ const Content = chakra((props: ContentProps) => { ); }); -type SymbolProps = Pick; +type SymbolProps = Pick; const Symbol = (props: SymbolProps) => { const symbol = props.token.symbol; @@ -109,8 +109,8 @@ const Symbol = (props: SymbolProps) => { { symbol } @@ -139,6 +139,7 @@ export interface EntityProps extends EntityBase.EntityBaseProps { noSymbol?: boolean; jointSymbol?: boolean; onlySymbol?: boolean; + noTruncate?: boolean; } const TokenEntity = (props: EntityProps) => { diff --git a/ui/shared/filters/FilterButton.tsx b/ui/shared/filters/FilterButton.tsx index 31fbe61ee5..7db39481b8 100644 --- a/ui/shared/filters/FilterButton.tsx +++ b/ui/shared/filters/FilterButton.tsx @@ -12,9 +12,10 @@ interface Props { appliedFiltersNum?: number; onClick: () => void; as?: As; + border?: boolean; } -const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as }: Props, ref: React.ForwardedRef) => { +const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as, border }: Props, ref: React.ForwardedRef) => { const badgeColor = useColorModeValue('white', 'black'); const badgeBgColor = useColorModeValue('blue.700', 'gray.50'); @@ -35,6 +36,11 @@ const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as }: P px={ 1.5 } flexShrink={ 0 } as={ as } + _active={{ + color: '#4299E1', + borderColor: border ? '#4299E1' : 'transparent', + bg: 'blue.50', + }} > { FilterIcon } Filter diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/TxAssetFlows.tsx new file mode 100644 index 0000000000..84b163b373 --- /dev/null +++ b/ui/tx/TxAssetFlows.tsx @@ -0,0 +1,160 @@ +import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Box, Skeleton, Text, Show, Hide, VStack, StackDivider, Divider } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import _ from 'lodash'; +import React, { useState } from 'react'; + +import type { ResponseData } from 'types/translateApi'; +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import lightning from 'icons/lightning.svg'; +import type { TranslateError } from 'lib/hooks/useFetchTranslate'; +import { getActionFromTo } from 'ui/shared/accountHistory/FromToComponent'; +import ActionBar from 'ui/shared/ActionBar'; +import Icon from 'ui/shared/chakra/Icon'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import Pagination from 'ui/shared/pagination/Pagination'; + +import ActionCard from './assetFlows/ActionCard'; +import { generateFlowViewData } from './assetFlows/utils/generateFlowViewData'; + +interface FlowViewProps { + data: UseQueryResult; +} + +export default function TxAssetFlows(props: FlowViewProps) { + const { data: queryData, isPlaceholderData, isError } = props.data; + + const [ page, setPage ] = useState(1); + + const ViewData = queryData ? generateFlowViewData(queryData) : []; + const chunkedViewData = _.chunk(ViewData, 10); + + const paginationProps: PaginationParams = { + onNextPageClick: () => setPage(page + 1), + onPrevPageClick: () => setPage(page - 1), + resetPage: () => setPage(1), + canGoBackwards: true, + isLoading: isPlaceholderData, + page: page, + hasNextPage: Boolean(chunkedViewData[page]), + hasPages: true, + isVisible: true, + }; + + const data = chunkedViewData [page - 1]; + + const actionBar = ( + + + + + Wallet + + + + + + + + + + + + ); + + const content = ( + +
+ + + { data?.length && } + }> + { data?.map((item, i) => ( + + + + + + + Action + + + + + + + + + { getActionFromTo(item) } + + + )) } + + + + + + + + + + + + + + { data?.map((item, i) => ( + + + + + )) } + +
+ Actions + + From/To +
+ + + + + + { getActionFromTo(item, 50) } + +
+
+
+
+
+
+ ); + + return ( + + ); +} diff --git a/ui/tx/assetFlows/ActionCard.tsx b/ui/tx/assetFlows/ActionCard.tsx new file mode 100644 index 0000000000..1b057245b0 --- /dev/null +++ b/ui/tx/assetFlows/ActionCard.tsx @@ -0,0 +1,116 @@ +import { Box, Hide, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Show, Text, useColorModeValue } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React from 'react'; + +import lightning from 'icons/lightning.svg'; +import { roundNumberIfNeeded } from 'lib/utils/numberHelpers'; +import { camelCaseToSentence } from 'lib/utils/stringHelpers'; +import Icon from 'ui/shared/chakra/Icon'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; + +import TokensCard from './TokensCard'; +import type { Action, FlowViewItem } from './utils/generateFlowViewData'; + +interface Props { + item: FlowViewItem; +} + +const ActionCard: FC = ({ item }) => { + const popoverBg = useColorModeValue('gray.700', 'gray.300'); + + const getTokenData = (action: Action) => { + const name = action.nft?.name || action.token?.name; + const symbol = action.nft?.symbol || action.token?.symbol; + + const token = { + name: name, + symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol, + address: action.nft?.address || action.token?.address, + }; + + return token; + }; + + return ( + <> + + + + { camelCaseToSentence(item.action.label) } + + + { roundNumberIfNeeded(item.action.amount?.toString() || '', 3) } + + + + + + + + + + + + { camelCaseToSentence(item.action.label) } + + + { roundNumberIfNeeded(item.action.amount?.toString() || '', 3) } + + + + + + + + + + + + + + + + + ); +}; + +export default React.memo(ActionCard); diff --git a/ui/tx/assetFlows/TokenTransferSnippet.tsx b/ui/tx/assetFlows/TokenTransferSnippet.tsx new file mode 100644 index 0000000000..0e3ca46651 --- /dev/null +++ b/ui/tx/assetFlows/TokenTransferSnippet.tsx @@ -0,0 +1,46 @@ +import { Flex } from '@chakra-ui/react'; +import React from 'react'; + +import NftEntity from 'ui/shared/entities/nft/NftEntity'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; + +interface Token { + address: string | undefined; + id?: string | undefined; + name: string | undefined; + symbol: string | undefined; +} + +interface Props { + token: Token; + tokenId: string; +} + +const TokenTransferSnippet = ({ token, tokenId }: Props) => { + return ( + + + + + ); +}; + +export default React.memo(TokenTransferSnippet); diff --git a/ui/tx/assetFlows/TokensCard.tsx b/ui/tx/assetFlows/TokensCard.tsx new file mode 100644 index 0000000000..bf85d8974d --- /dev/null +++ b/ui/tx/assetFlows/TokensCard.tsx @@ -0,0 +1,54 @@ +import { Box, Text, useColorModeValue } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React from 'react'; + +import type { Nft, Token } from 'types/translateApi'; + +import CopyToClipboard from 'ui/shared/CopyToClipboard'; + +interface Props { + amount?: string; + token: Token | Nft | undefined; +} + +const TokensCard: FC = ({ token, amount }) => { + const textColor = useColorModeValue('white', 'blackAlpha.900'); + + if (!token) { + return null; + } + + const showTokenName = token.symbol !== token.name; + const showTokenAddress = Boolean(token.address.match(/^0x[a-fA-F\d]{40}$/)); + + return ( + + + + { amount } + + + { token.symbol } + + + + { showTokenName && ( + + { token.name } + + ) } + + { showTokenAddress && ( + + + { token.address } + + + + ) } + + + ); +}; + +export default React.memo(TokensCard); diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.ts b/ui/tx/assetFlows/utils/generateFlowViewData.ts new file mode 100644 index 0000000000..2796d56550 --- /dev/null +++ b/ui/tx/assetFlows/utils/generateFlowViewData.ts @@ -0,0 +1,278 @@ +import _ from 'lodash'; + +import type { Nft, ResponseData, SentReceived, Token } from 'types/translateApi'; + +export interface Action { + label: string; + amount: string | undefined; + flowDirection: 'toLeft' | 'toRight'; + nft: Nft | undefined; + token: Token | undefined; +} +export interface FlowViewItem { + action: Action; + rightActor: { + address: string; + name: string | null; + }; + leftActor: { + address: string; + name: string | null; + }; + accountAddress: string; +} + +export function generateFlowViewData(data: ResponseData): Array { + const perspectiveAddress = data.accountAddress.toLowerCase(); + + const sent = data.classificationData.sent || []; + const received = data.classificationData.received || []; + + const txItems = [ ...sent, ...received ]; + + const paidGasIndex = _.findIndex(txItems, (item) => item.action === 'paidGas'); + if (paidGasIndex >= 0) { + const element = txItems.splice(paidGasIndex, 1)[0]; + element.to.name = 'Validators'; + txItems.splice(txItems.length, 0, element); + } + + const flowViewData = txItems.map((item) => { + const action = { + label: item.action, + amount: item.amount || undefined, + flowDirection: getFlowDirection(item, perspectiveAddress), + nft: item.nft || undefined, + token: item.token || undefined, + }; + + if (item.from.name && item.from.name.includes('(this wallet)')) { + item.from.name = item.from.name.split('(this wallet)')[0]; + } + + if (item.to.name && item.to.name.includes('(this wallet)')) { + item.to.name = item.to.name.split('(this wallet)')[0]; + } + + const rightActor = getRightActor(item, perspectiveAddress); + + const leftActor = getLeftActor(item, perspectiveAddress); + + return { action, rightActor, leftActor, accountAddress: perspectiveAddress }; + }); + + return flowViewData; +} + +function getRightActor(item: SentReceived, perspectiveAddress: string) { + if (!item.to.address || item.to.address.toLowerCase() !== perspectiveAddress) { + return { address: item.to.address, name: item.to.name }; + } + + return { address: item.from.address, name: item.from.name }; +} + +function getLeftActor(item: SentReceived, perspectiveAddress: string) { + if (item.to.address && item.to.address.toLowerCase() === perspectiveAddress) { + return { address: item.to.address, name: item.to.name }; + } + + return { address: item.from.address, name: item.from.name }; +} + +function getFlowDirection(item: SentReceived, perspectiveAddress: string): 'toLeft' | 'toRight' { + // return "toLeft" or "toRight" + if (item.to.address && item.to.address.toLowerCase() === perspectiveAddress) { + return 'toLeft'; + } + + if (item.from.address && item.from.address.toLowerCase() === perspectiveAddress) { + return 'toRight'; + } + + return 'toLeft'; // default case +} + +interface TokensData { + nameList: Array; + symbolList: Array; + idList: Array; + names: { + [x: string]: { + name: string | undefined; + symbol: string | undefined; + address: string | undefined; + id?: string | undefined; + }; + }; + symbols: { + [x: string]: { + name: string | undefined; + symbol: string | undefined; + address: string | undefined; + id: string | undefined; + }; + }; +} + +export function getTokensData(data: ResponseData): TokensData { + const sent = data.classificationData.sent || []; + const received = data.classificationData.received || []; + + const txItems = [ ...sent, ...received ]; + + const tokens = txItems.map((item) => { + const name = item.nft?.name || item.token?.name; + const symbol = item.nft?.symbol || item.token?.symbol; + + const token = { + name: name, + symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol, + address: item.nft?.address || item.token?.address, + id: item.nft?.id || item.token?.id, + }; + + return token; + }); + + const tokensGroupByname = _.groupBy(tokens, 'name'); + const tokensGroupBySymbol = _.groupBy(tokens, 'symbol'); + const tokensGroupById = _.groupBy(tokens, 'id'); + + const mappedNames = _.mapValues(tokensGroupByname, (i) => { + return i[0]; + }); + + const mappedSymbols = _.mapValues(tokensGroupBySymbol, (i) => { + return i[0]; + }); + + const mappedIds = _.mapValues(tokensGroupById, (i) => { + return i[0]; + }); + + const nameList = _.keysIn(mappedNames).filter(i => i !== 'undefined'); + const symbolList = _.keysIn(mappedSymbols).filter(i => i !== 'undefined'); + const idList = _.keysIn(mappedIds).filter(i => i !== 'undefined'); + + return { + nameList, + symbolList, + idList, + names: mappedNames, + symbols: mappedSymbols, + }; +} + +export const getSplittedDescription = (translateData: ResponseData) => { + const description = translateData.classificationData.description; + const removeEndDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; + let parsedDescription = ' ' + removeEndDot; + const tokenData = getTokensData(translateData); + + const idsMatched = tokenData.idList.filter(id => parsedDescription.includes(`#${ id }`)); + const namesMatched = tokenData.nameList.filter(name => parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); + let symbolsMatched = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); + + symbolsMatched = symbolsMatched.filter(symbol => !namesMatched.includes(tokenData.symbols[symbol]?.name || '')); + + let indicesSorted: Array = []; + let namesMapped; + let symbolsMapped; + + if (idsMatched.length) { + namesMatched.forEach(name => { + const hasId = idsMatched.includes(tokenData.names[name].id || ''); + if (hasId) { + parsedDescription = parsedDescription.replaceAll(`#${ tokenData.names[name].id }`, ''); + } + }); + + symbolsMatched.forEach(name => { + const hasId = idsMatched.includes(tokenData.symbols[name].id || ''); + if (hasId) { + parsedDescription = parsedDescription.replaceAll(`#${ tokenData.symbols[name].id }`, ''); + } + }); + } + + if (namesMatched.length) { + namesMapped = namesMatched.map(name => { + const searchString = ` ${ name.toUpperCase() }`; + let hasId = false; + + if (idsMatched.length) { + hasId = idsMatched.includes(tokenData.names[name].id || ''); + } + + return { + name, + hasId, + indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, + token: tokenData.names[name], + }; + }); + + namesMapped.forEach(i => indicesSorted.push(...i.indices)); + } + + if (symbolsMatched.length) { + symbolsMapped = symbolsMatched.map(name => { + const searchString = ` ${ name.toUpperCase() }`; + let hasId = false; + + if (idsMatched.length) { + hasId = idsMatched.includes(tokenData.symbols[name].id || ''); + } + + return { + name, + hasId, + indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, + token: tokenData.symbols[name], + }; + }); + + symbolsMapped.forEach(i => indicesSorted.push(...i.indices)); + } + + indicesSorted = _.uniq(indicesSorted.sort((a, b) => a - b)); + + const tokenWithIndices = _.uniqBy(_.concat(namesMapped, symbolsMapped), 'name'); + + const descriptionSplitted = indicesSorted.map((a, i) => { + const item = tokenWithIndices.find(t => t?.indices.includes(a)); + + if (i === 0) { + return { + token: item?.token, + text: parsedDescription.substring(0, a), + hasId: item?.hasId, + }; + } else { + const startIndex = indicesSorted[i - 1] + (tokenWithIndices.find(t => t?.indices.includes(indicesSorted[i - 1]))?.name.length || 0); + return { + token: item?.token, + text: parsedDescription.substring(startIndex + 1, a), + hasId: item?.hasId, + }; + } + }); + + const lastIndex = indicesSorted[indicesSorted.length - 1]; + const startIndex = lastIndex + (tokenWithIndices.find(t => t?.indices.includes(lastIndex))?.name.length || 0); + const restString = parsedDescription.substring(startIndex + 1); + + if (restString) { + descriptionSplitted.push({ text: restString, token: undefined, hasId: false }); + } + + return descriptionSplitted; +}; + +export const getFlowCount = (data: ResponseData | undefined) => { + if (!data) { + return 0; + } + return generateFlowViewData(data).length; +}; diff --git a/ui/txs/TxType.tsx b/ui/txs/TxType.tsx index fe856096dc..4cee9df244 100644 --- a/ui/txs/TxType.tsx +++ b/ui/txs/TxType.tsx @@ -2,18 +2,22 @@ import React from 'react'; import type { TransactionType } from 'types/api/transaction'; +import { camelCaseToSentence } from 'lib/utils/stringHelpers'; import Tag from 'ui/shared/chakra/Tag'; export interface Props { types: Array; isLoading?: boolean; + translateLabel?: string; } const TYPES_ORDER = [ 'rootstock_remasc', 'rootstock_bridge', 'token_creation', 'contract_creation', 'token_transfer', 'contract_call', 'coin_transfer' ]; -const TxType = ({ types, isLoading }: Props) => { +const TxType = ({ types, isLoading, translateLabel }: Props) => { const typeToShow = types.sort((t1, t2) => TYPES_ORDER.indexOf(t1) - TYPES_ORDER.indexOf(t2))[0]; + const filteredTypes = [ 'unclassified' ]; + let label; let colorScheme; @@ -52,6 +56,12 @@ const TxType = ({ types, isLoading }: Props) => { } + if (translateLabel) { + if (!filteredTypes.includes(translateLabel)) { + label = camelCaseToSentence(translateLabel); + } + } + return ( { label } diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index fb81ae48b3..46f9bb17be 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -26,6 +26,7 @@ type Props = { filterValue?: AddressFromToFilter; enableTimeIncrement?: boolean; top?: number; + translate?: boolean; } const TxsContent = ({ @@ -39,6 +40,7 @@ const TxsContent = ({ currentAddress, enableTimeIncrement, top, + translate, }: Props) => { const { data, isPlaceholderData, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const isMobile = useIsMobile(); @@ -63,6 +65,7 @@ const TxsContent = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isPlaceholderData } + translate={ translate } /> )) } @@ -80,6 +83,7 @@ const TxsContent = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isPlaceholderData } + translate={ translate } /> diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index 4d1b72eb69..a1fcd5b735 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -11,6 +11,7 @@ import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; import rightArrowIcon from 'icons/arrows/east.svg'; import getValueWithUnit from 'lib/getValueWithUnit'; +import useFetchDescribe from 'lib/hooks/useFetchDescribe'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import { space } from 'lib/html-entities'; import Icon from 'ui/shared/chakra/Icon'; @@ -31,12 +32,13 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; + translate?: boolean; } const TAG_WIDTH = 48; const ARROW_WIDTH = 24; -const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => { +const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement, translate }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); @@ -44,11 +46,13 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); + const { data: describeData } = useFetchDescribe(translate ? tx.hash : null); + return ( - + diff --git a/ui/txs/TxsTable.tsx b/ui/txs/TxsTable.tsx index 7af0f4c013..e742f115a4 100644 --- a/ui/txs/TxsTable.tsx +++ b/ui/txs/TxsTable.tsx @@ -24,6 +24,7 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; + translate?: boolean; } const TxsTable = ({ @@ -38,6 +39,7 @@ const TxsTable = ({ currentAddress, enableTimeIncrement, isLoading, + translate, }: Props) => { return ( @@ -94,6 +96,7 @@ const TxsTable = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isLoading } + translate={ translate } /> )) } diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index 42443476ef..4cb3044b19 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -15,6 +15,7 @@ import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; import rightArrowIcon from 'icons/arrows/east.svg'; +import useFetchDescribe from 'lib/hooks/useFetchDescribe'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import Icon from 'ui/shared/chakra/Icon'; import Tag from 'ui/shared/chakra/Tag'; @@ -36,15 +37,18 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; + translate?: boolean; } -const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading }: Props) => { +const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading, translate }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isIn = Boolean(currentAddress && currentAddress === dataTo?.hash); const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); + const { data: describeData } = useFetchDescribe(translate ? tx.hash : null); + const addressFrom = ( @@ -195,4 +196,4 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { ); }; -export default AddressAccountHistory; +export default NovesAddressAccountHistory; diff --git a/ui/address/history/useFetchHistory.tsx b/ui/address/noves/NovesUseFetchHistory.tsx similarity index 73% rename from ui/address/history/useFetchHistory.tsx rename to ui/address/noves/NovesUseFetchHistory.tsx index 5a98d1f18f..6ae64586b3 100644 --- a/ui/address/history/useFetchHistory.tsx +++ b/ui/address/noves/NovesUseFetchHistory.tsx @@ -1,7 +1,7 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import type { AccountHistoryResponse } from 'types/translateApi'; +import type { NovesAccountHistoryResponse } from 'types/novesApi'; import config from 'configs/app'; import type { Params as FetchParams } from 'lib/hooks/useFetch'; @@ -21,17 +21,17 @@ export interface TranslateHistory { } interface Params extends ApiFetchParams { - queryOptions?: Omit, 'queryKey' | 'queryFn'>; + queryOptions?: Omit, 'queryKey' | 'queryFn'>; } -export default function useFetchHistory( +export default function NovesUseFetchHistory( address: string | null, page: number, { queryOptions, queryParams }: Params = {}, ) { const fetch = useFetch(); - const url = new URL('/node-api/history', config.app.baseUrl); + const url = new URL('/node-api/noves/history', config.app.baseUrl); queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { // there are some pagination params that can be null or false for the next page @@ -43,20 +43,20 @@ export default function useFetchHistory( page, }; - return useQuery({ + return useQuery({ queryKey: [ 'history', address, { ...queryParams }, body ], queryFn: async() => { // all errors and error typing is handled by react-query // so error response will never go to the data // that's why we are safe here to do type conversion "as Promise>" if (!address) { - return undefined as unknown as AccountHistoryResponse; + return undefined as unknown as NovesAccountHistoryResponse; } return fetch(url.toString(), { method: 'POST', body, - }) as Promise; + }) as Promise; }, ...queryOptions, }); diff --git a/ui/address/history/useFetchHistoryWithPages.tsx b/ui/address/noves/NovesUseFetchHistoryWithPages.tsx similarity index 90% rename from ui/address/history/useFetchHistoryWithPages.tsx rename to ui/address/noves/NovesUseFetchHistoryWithPages.tsx index 83d6f6ea05..7e42652771 100644 --- a/ui/address/history/useFetchHistoryWithPages.tsx +++ b/ui/address/noves/NovesUseFetchHistoryWithPages.tsx @@ -6,19 +6,19 @@ import queryString from 'querystring'; import React, { useCallback } from 'react'; import { animateScroll } from 'react-scroll'; -import type { AccountHistoryResponse, HistoryFilters } from 'types/translateApi'; +import type { NovesAccountHistoryResponse, NovesHistoryFilters } from 'types/novesApi'; import type { PaginationParams } from 'ui/shared/pagination/types'; import type { PaginatedResources, PaginationFilters, PaginationSorting } from 'lib/api/resources'; import getQueryParamString from 'lib/router/getQueryParamString'; -import type { TranslateHistory } from './useFetchHistory'; -import useFetchHistory from './useFetchHistory'; +import type { TranslateHistory } from './NovesUseFetchHistory'; +import NovesUseFetchHistory from './NovesUseFetchHistory'; export interface Params { address: string; - options?: Omit, 'queryKey' | 'queryFn'>; - filters?: HistoryFilters; + options?: Omit, 'queryKey' | 'queryFn'>; + filters?: NovesHistoryFilters; sorting?: PaginationSorting; scrollRef?: React.RefObject; } @@ -36,13 +36,13 @@ function getPaginationParamsFromQuery(queryString: string | Array | unde } export type QueryWithPagesResult = -UseQueryResult & +UseQueryResult & { onFilterChange: (filters: PaginationFilters) => void; pagination: PaginationParams; } -export default function useFetchHistoryWithPages({ +export default function NovesUseFetchHistoryWithPages({ address, filters, options, @@ -66,7 +66,7 @@ export default function useFetchHistoryWithPages, + num = 50, + rest: Omit, +) { + return { + items: Array(num).fill(stub), + ...rest, + }; +} diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index 84f3b409f0..490177cf7d 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -12,7 +12,6 @@ import useContractTabs from 'lib/hooks/useContractTabs'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; import getQueryParamString from 'lib/router/getQueryParamString'; import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address'; -import AddressAccountHistory from 'ui/address/AddressAccountHistory'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressContract from 'ui/address/AddressContract'; @@ -25,6 +24,7 @@ import AddressTxs from 'ui/address/AddressTxs'; import AddressWithdrawals from 'ui/address/AddressWithdrawals'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressQrCode from 'ui/address/details/AddressQrCode'; +import NovesAddressAccountHistory from 'ui/address/noves/NovesAddressAccountHistory'; import SolidityscanReport from 'ui/address/SolidityscanReport'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TextAd from 'ui/shared/ad/TextAd'; @@ -77,7 +77,7 @@ const AddressPageContent = () => { id: 'history', title: 'Account history', //count: , - component: , + component: , }, config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ? { diff --git a/ui/pages/Transaction.tsx b/ui/pages/Transaction.tsx index b8a6d72a94..48d48678fa 100644 --- a/ui/pages/Transaction.tsx +++ b/ui/pages/Transaction.tsx @@ -1,28 +1,24 @@ -import { Box, Skeleton, Text } from '@chakra-ui/react'; import { useRouter } from 'next/router'; -import React, { useCallback } from 'react'; +import React from 'react'; import type { RoutedTab } from 'ui/shared/Tabs/types'; import config from 'configs/app'; -import lightning from 'icons/lightning.svg'; import useApiQuery from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/contexts/app'; -import useFetchTranslate from 'lib/hooks/useFetchTranslate'; import getQueryParamString from 'lib/router/getQueryParamString'; -import { TRANSLATE } from 'stubs/translate'; +import { NOVES_TRANSLATE } from 'stubs/noves/Novestranslate'; import { TX } from 'stubs/tx'; import TextAd from 'ui/shared/ad/TextAd'; -import Icon from 'ui/shared/chakra/Icon'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import EntityTags from 'ui/shared/EntityTags'; import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; -import TokenTransferSnippet from 'ui/tx/assetFlows/TokenTransferSnippet'; -import { getFlowCount, getSplittedDescription } from 'ui/tx/assetFlows/utils/generateFlowViewData'; -import TxAssetFlows from 'ui/tx/TxAssetFlows'; +import NovesTxAssetFlows from 'ui/tx/Noves/NovesTxAssetFlows'; +import NovesTxTitleSecondRow from 'ui/tx/Noves/NovesTxTitleSecondRow'; +import NovesUseFetchTranslate from 'ui/tx/Noves/NovesUseFetchTranslate'; +import { NovesGetFlowCount } from 'ui/tx/Noves/utils/NovesGenerateFlowViewData'; import TxDetails from 'ui/tx/TxDetails'; import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped'; import TxInternals from 'ui/tx/TxInternals'; @@ -37,13 +33,13 @@ const TransactionPageContent = () => { const hash = getQueryParamString(router.query.hash); - const fetchTranslate = useFetchTranslate(hash, { + const fetchTranslate = NovesUseFetchTranslate(hash, { queryOptions: { - placeholderData: TRANSLATE, + placeholderData: NOVES_TRANSLATE, }, }); - const { data: translateData, isError, isPlaceholderData: isTranslatePlaceholder } = fetchTranslate; + const { data: translateData } = fetchTranslate; const { data, isPlaceholderData } = useApiQuery('tx', { pathParams: { hash }, @@ -55,7 +51,9 @@ const TransactionPageContent = () => { const tabs: Array = [ { id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: }, - { id: 'asset_flows', title: 'Asset Flows', component: , count: getFlowCount(translateData) }, + config.features.noves.isEnabled ? + { id: 'asset_flows', title: 'Asset Flows', component: , count: NovesGetFlowCount(translateData) } : + undefined, config.features.suave.isEnabled && data?.wrapped ? { id: 'wrapped', title: 'Regular tx details', component: } : undefined, @@ -88,60 +86,7 @@ const TransactionPageContent = () => { }; }, [ appProps.referrer ]); - const getTransactionDescription = useCallback(() => { - if (!isError && translateData) { - if (translateData.classificationData.description) { - const description = getSplittedDescription(translateData); - - return description.map((item, i) => ( - <> - - { i === 0 && ( - - ) } - { item.text } - - { item.hasId ? ( - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - - ) : - item.token && ( - - ) } - - - )); - } else { - return 'Error fetching transaction description'; - } - } else { - return 'Error fetching transaction description'; - } - }, [ isError, translateData ]); - - const titleSecondRow = ( - - - { getTransactionDescription() } - - - ); + const titleSecondRow = ; return ( <> diff --git a/ui/shared/Noves/NovesFromToComponent.tsx b/ui/shared/Noves/NovesFromToComponent.tsx new file mode 100644 index 0000000000..0525845ba9 --- /dev/null +++ b/ui/shared/Noves/NovesFromToComponent.tsx @@ -0,0 +1,68 @@ +import { Box, Tag, TagLabel } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React, { useEffect, useState } from 'react'; + +import type { NovesResponseData } from 'types/novesApi'; + +import type { NovesFlowViewItem } from 'ui/tx/Noves/utils/NovesGenerateFlowViewData'; + +import AddressEntity from '../entities/address/AddressEntity'; +import type { FromToData } from './utils'; +import { NovesGetActionFromTo, NovesGetFromTo } from './utils'; + +interface Props { + txData?: NovesResponseData; + currentAddress?: string; + item?: NovesFlowViewItem; +} + +const NovesFromToComponent: FC = ({ txData, currentAddress = '', item }) => { + const [ data, setData ] = useState({ text: 'Sent to', address: '' }); + + useEffect(() => { + let fromTo; + + if (txData) { + fromTo = NovesGetFromTo(txData, currentAddress); + setData(fromTo); + } else if (item) { + fromTo = NovesGetActionFromTo(item); + setData(fromTo); + } + }, [ currentAddress, item, txData ]); + + const isSent = data.text.startsWith('Sent'); + + const addressObj = { hash: data.address || '', name: data.name || '' }; + + return ( + + + + { data.text } + + Received from + + + + + + + ); +}; + +export default NovesFromToComponent; diff --git a/ui/shared/Noves/utils.ts b/ui/shared/Noves/utils.ts new file mode 100644 index 0000000000..0017d6387d --- /dev/null +++ b/ui/shared/Noves/utils.ts @@ -0,0 +1,88 @@ +import type { NovesResponseData, NovesSentReceived } from 'types/novesApi'; + +import type { NovesFlowViewItem } from 'ui/tx/Noves/utils/NovesGenerateFlowViewData'; + +export interface FromToData { + text: string; + address: string; + name?: string | null; +} + +export const NovesGetFromTo = (txData: NovesResponseData, currentAddress: string): FromToData => { + const raw = txData.rawTransactionData; + const sent = txData.classificationData.sent; + let sentFound: Array = []; + if (sent && sent[0]) { + sentFound = sent + .filter((sent) => sent.from.address.toLocaleLowerCase() === currentAddress) + .filter((sent) => sent.to.address); + } + + const received = txData.classificationData.received; + let receivedFound: Array = []; + if (received && received[0]) { + receivedFound = received + .filter((received) => received.to.address.toLocaleLowerCase() === currentAddress) + .filter((received) => received.from.address); + } + + if (sentFound[0] && receivedFound[0]) { + if (sentFound.length === receivedFound.length) { + if (raw.toAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Received from', address: raw.fromAddress }; + } + + if (raw.fromAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Sent to', address: raw.toAddress }; + } + } + if (sentFound.length > receivedFound.length) { + return { text: 'Sent to', address: sentFound[0].to.address } ; + } else { + return { text: 'Received from', address: receivedFound[0].from.address } ; + } + } + + if (sent && sentFound[0]) { + return { text: 'Sent to', address: sentFound[0].to.address } ; + } + + if (received && receivedFound[0]) { + return { text: 'Received from', address: receivedFound[0].from.address }; + } + + if (raw.toAddress && raw.toAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Received from', address: raw.fromAddress }; + } + + if (raw.fromAddress && raw.fromAddress.toLocaleLowerCase() === currentAddress) { + return { text: 'Sent to', address: raw.toAddress }; + } + + if (!raw.toAddress && raw.fromAddress) { + return { text: 'Received from', address: raw.fromAddress }; + } + + if (!raw.fromAddress && raw.toAddress) { + return { text: 'Sent to', address: raw.toAddress }; + } + + return { text: 'Sent to', address: currentAddress }; +}; + +export const NovesGetFromToValue = (txData: NovesResponseData, currentAddress: string) => { + const fromTo = NovesGetFromTo(txData, currentAddress); + + return fromTo.text.split(' ').shift()?.toLowerCase(); +}; + +export const NovesGetActionFromTo = (item: NovesFlowViewItem): FromToData => { + if (item.action.flowDirection === 'toRight') { + return { + text: 'Sent to', address: item.rightActor.address, name: item.rightActor.name, + }; + } + return { + text: 'Received from', address: item.rightActor.address, name: item.rightActor.name, + }; +}; diff --git a/ui/shared/accountHistory/FromToComponent.tsx b/ui/shared/accountHistory/FromToComponent.tsx deleted file mode 100644 index f29233bf9d..0000000000 --- a/ui/shared/accountHistory/FromToComponent.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Box, Tag, TagLabel } from '@chakra-ui/react'; -import React from 'react'; - -import type { ResponseData, SentReceived } from 'types/translateApi'; - -import type { FlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; - -import AddressEntity from '../entities/address/AddressEntity'; - -export const fromToComponent = (text: string, address: string, getValue?: boolean, name?: string | null, truncate?: number) => { - const isSent = text.startsWith('Sent'); - - if (getValue) { - return text.split(' ').shift()?.toLowerCase(); - } - return ( - - - - { text } - - Received from - - - - - - - ); -}; - -export const getFromTo = (txData: ResponseData, currentAddress: string, getValue?: boolean) => { - const raw = txData.rawTransactionData; - const sent = txData.classificationData.sent; - let sentFound: Array = []; - if (sent && sent[0]) { - sentFound = sent - .filter((sent) => sent.from.address.toLocaleLowerCase() === currentAddress) - .filter((sent) => sent.to.address); - } - - const received = txData.classificationData.received; - let receivedFound: Array = []; - if (received && received[0]) { - receivedFound = received - .filter((received) => received.to.address.toLocaleLowerCase() === currentAddress) - .filter((received) => received.from.address); - } - - if (sentFound[0] && receivedFound[0]) { - if (sentFound.length === receivedFound.length) { - if (raw.toAddress.toLocaleLowerCase() === currentAddress) { - return fromToComponent('Received from', raw.fromAddress, getValue); - } - - if (raw.fromAddress.toLocaleLowerCase() === currentAddress) { - return fromToComponent('Sent to', raw.toAddress, getValue); - } - } - if (sentFound.length > receivedFound.length) { - return fromToComponent('Sent to', sentFound[0].to.address, getValue); - } else { - return fromToComponent('Received from', receivedFound[0].from.address, getValue); - } - } - - if (sent && sentFound[0]) { - return fromToComponent('Sent to', sentFound[0].to.address, getValue); - } - - if (received && receivedFound[0]) { - return fromToComponent('Received from', receivedFound[0].from.address, getValue); - } - - if (raw.toAddress && raw.toAddress.toLocaleLowerCase() === currentAddress) { - return fromToComponent('Received from', raw.fromAddress, getValue); - } - - if (raw.fromAddress && raw.fromAddress.toLocaleLowerCase() === currentAddress) { - return fromToComponent('Sent to', raw.toAddress, getValue); - } - - if (!raw.toAddress && raw.fromAddress) { - return fromToComponent('Received from', raw.fromAddress, getValue); - } - - if (!raw.fromAddress && raw.toAddress) { - return fromToComponent('Sent to', raw.toAddress, getValue); - } - - return fromToComponent('Sent to', currentAddress, getValue); -}; - -export const getActionFromTo = (item: FlowViewItem, truncate = 7) => { - if (item.action.flowDirection === 'toRight') { - return fromToComponent('Sent to', item.rightActor.address, false, item.rightActor.name, truncate); - } - return fromToComponent('Received from', item.rightActor.address, false, item.rightActor.name, truncate); -}; diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 92bec5ba8d..5eabca3825 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -10,7 +10,6 @@ import { route } from 'nextjs-routes'; import iconSafe from 'icons/brands/safe.svg'; import iconContractVerified from 'icons/contract_verified.svg'; import iconContract from 'icons/contract.svg'; -import { truncateMiddle } from 'lib/utils/numberHelpers'; import * as EntityBase from 'ui/shared/entities/base/components'; import { getIconProps } from '../base/utils'; @@ -99,7 +98,7 @@ const Icon = (props: IconProps) => { ); }; -type ContentProps = Omit & Pick; +type ContentProps = Omit & Pick; const Content = chakra((props: ContentProps) => { if (props.address.name) { @@ -122,7 +121,7 @@ const Content = chakra((props: ContentProps) => { return ( ); }); @@ -143,8 +142,6 @@ const Container = EntityBase.Container; export interface EntityProps extends EntityBase.EntityBaseProps { address: Pick; isSafeAddress?: boolean; - truncate?: number; - truncateEnd?: number; } const AddressEntry = (props: EntityProps) => { diff --git a/ui/shared/entities/token/TokenEntity.tsx b/ui/shared/entities/token/TokenEntity.tsx index 2c775f141f..53ff91f289 100644 --- a/ui/shared/entities/token/TokenEntity.tsx +++ b/ui/shared/entities/token/TokenEntity.tsx @@ -86,7 +86,7 @@ const Content = chakra((props: ContentProps) => { ); }); -type SymbolProps = Pick; +type SymbolProps = Pick; const Symbol = (props: SymbolProps) => { const symbol = props.token.symbol; @@ -109,8 +109,8 @@ const Symbol = (props: SymbolProps) => { { symbol } @@ -139,7 +139,6 @@ export interface EntityProps extends EntityBase.EntityBaseProps { noSymbol?: boolean; jointSymbol?: boolean; onlySymbol?: boolean; - noTruncate?: boolean; } const TokenEntity = (props: EntityProps) => { diff --git a/ui/shared/filters/FilterButton.tsx b/ui/shared/filters/FilterButton.tsx index 7db39481b8..31fbe61ee5 100644 --- a/ui/shared/filters/FilterButton.tsx +++ b/ui/shared/filters/FilterButton.tsx @@ -12,10 +12,9 @@ interface Props { appliedFiltersNum?: number; onClick: () => void; as?: As; - border?: boolean; } -const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as, border }: Props, ref: React.ForwardedRef) => { +const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as }: Props, ref: React.ForwardedRef) => { const badgeColor = useColorModeValue('white', 'black'); const badgeBgColor = useColorModeValue('blue.700', 'gray.50'); @@ -36,11 +35,6 @@ const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as, bor px={ 1.5 } flexShrink={ 0 } as={ as } - _active={{ - color: '#4299E1', - borderColor: border ? '#4299E1' : 'transparent', - bg: 'blue.50', - }} > { FilterIcon } Filter diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/Noves/NovesTxAssetFlows.tsx similarity index 84% rename from ui/tx/TxAssetFlows.tsx rename to ui/tx/Noves/NovesTxAssetFlows.tsx index 84b163b373..fe76ca7f81 100644 --- a/ui/tx/TxAssetFlows.tsx +++ b/ui/tx/Noves/NovesTxAssetFlows.tsx @@ -3,31 +3,31 @@ import type { UseQueryResult } from '@tanstack/react-query'; import _ from 'lodash'; import React, { useState } from 'react'; -import type { ResponseData } from 'types/translateApi'; +import type { NovesResponseData } from 'types/novesApi'; import type { PaginationParams } from 'ui/shared/pagination/types'; import lightning from 'icons/lightning.svg'; -import type { TranslateError } from 'lib/hooks/useFetchTranslate'; -import { getActionFromTo } from 'ui/shared/accountHistory/FromToComponent'; import ActionBar from 'ui/shared/ActionBar'; import Icon from 'ui/shared/chakra/Icon'; import DataListDisplay from 'ui/shared/DataListDisplay'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import NovesFromToComponent from 'ui/shared/Noves/NovesFromToComponent'; import Pagination from 'ui/shared/pagination/Pagination'; +import type { NovesTranslateError } from 'ui/tx/Noves/NovesUseFetchTranslate'; -import ActionCard from './assetFlows/ActionCard'; -import { generateFlowViewData } from './assetFlows/utils/generateFlowViewData'; +import NovesActionCard from './components/NovesActionCard'; +import { NovesGenerateFlowViewData } from './utils/NovesGenerateFlowViewData'; interface FlowViewProps { - data: UseQueryResult; + data: UseQueryResult; } -export default function TxAssetFlows(props: FlowViewProps) { +export default function NovesTxAssetFlows(props: FlowViewProps) { const { data: queryData, isPlaceholderData, isError } = props.data; const [ page, setPage ] = useState(1); - const ViewData = queryData ? generateFlowViewData(queryData) : []; + const ViewData = queryData ? NovesGenerateFlowViewData(queryData) : []; const chunkedViewData = _.chunk(ViewData, 10); const paginationProps: PaginationParams = { @@ -91,8 +91,8 @@ export default function TxAssetFlows(props: FlowViewProps) { display="flex" fontSize="xl" mr="5px" - color="#718096" - _dark={{ color: '#92a2bb' }} + color="gray.500" + _dark={{ color: 'gray.400' }} /> Action @@ -101,10 +101,10 @@ export default function TxAssetFlows(props: FlowViewProps) { - + - { getActionFromTo(item) } + )) } @@ -129,12 +129,12 @@ export default function TxAssetFlows(props: FlowViewProps) { diff --git a/ui/tx/Noves/NovesTxTitleSecondRow.tsx b/ui/tx/Noves/NovesTxTitleSecondRow.tsx new file mode 100644 index 0000000000..b45ae0cec5 --- /dev/null +++ b/ui/tx/Noves/NovesTxTitleSecondRow.tsx @@ -0,0 +1,80 @@ +import { Box, Skeleton, Text } from '@chakra-ui/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import type { FC } from 'react'; +import React from 'react'; + +import type { NovesResponseData } from 'types/novesApi'; + +import lightning from 'icons/lightning.svg'; +import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; +import Icon from 'ui/shared/chakra/Icon'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; +import NetworkExplorers from 'ui/shared/NetworkExplorers'; +import NovesTokenTransferSnippet from 'ui/tx/Noves/components/NovesTokenTransferSnippet'; +import type { NovesTranslateError } from 'ui/tx/Noves/NovesUseFetchTranslate'; +import { NovesGetSplittedDescription } from 'ui/tx/Noves/utils/NovesGetSplittedDescription'; + +interface Props { + fetchTranslate: UseQueryResult; + hash: string; + txTag: string | null | undefined; +} + +const NovesTxTitleSecondRow: FC = ({ fetchTranslate, hash, txTag }) => { + + const { data, isError, isPlaceholderData } = fetchTranslate; + + if (isPlaceholderData || isError || !data?.classificationData.description) { + return ( + + + { txTag && } + + + ); + } + + const description = NovesGetSplittedDescription(data); + + return ( + + + { description.map((item, i) => ( + <> + + { i === 0 && ( + + ) } + { item.text } + + { item.hasId && item.token ? ( + + ) : + item.token && ( + + ) } + + )) } + + + ); +}; + +export default NovesTxTitleSecondRow; diff --git a/lib/hooks/useFetchTranslate.tsx b/ui/tx/Noves/NovesUseFetchTranslate.tsx similarity index 65% rename from lib/hooks/useFetchTranslate.tsx rename to ui/tx/Noves/NovesUseFetchTranslate.tsx index f10e13f717..7a46a13a94 100644 --- a/lib/hooks/useFetchTranslate.tsx +++ b/ui/tx/Noves/NovesUseFetchTranslate.tsx @@ -1,19 +1,19 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import type { ResponseData } from 'types/translateApi'; +import type { NovesResponseData } from 'types/novesApi'; import config from 'configs/app'; import type { Params as FetchParams } from 'lib/hooks/useFetch'; -import useFetch from './useFetch'; +import useFetch from '../../../lib/hooks/useFetch'; export interface ApiFetchParams { queryParams?: Record | number | undefined>; fetchParams?: Pick; } -export interface TranslateError { +export interface NovesTranslateError { payload?: { txHash: string; }; @@ -22,34 +22,34 @@ export interface TranslateError { } interface Params extends ApiFetchParams { - queryOptions?: Omit, 'queryKey' | 'queryFn'>; + queryOptions?: Omit, 'queryKey' | 'queryFn'>; } -export default function useFetchTranslate( +export default function NovesUseFetchTranslate( txHash: string | null, { queryOptions }: Params = {}, ) { const fetch = useFetch(); - const url = new URL('/node-api/translate', config.app.baseUrl); + const url = new URL('/node-api/noves/translate', config.app.baseUrl); const body = { txHash, }; - return useQuery({ + return useQuery({ queryKey: [ 'translate', txHash, body ], queryFn: async() => { // all errors and error typing is handled by react-query // so error response will never go to the data // that's why we are safe here to do type conversion "as Promise>" if (!txHash) { - return undefined as unknown as ResponseData; + return undefined as unknown as NovesResponseData; } return fetch(url.toString(), { method: 'POST', body, - }) as Promise; + }) as Promise; }, ...queryOptions, }); diff --git a/ui/tx/assetFlows/ActionCard.tsx b/ui/tx/Noves/components/NovesActionCard.tsx similarity index 77% rename from ui/tx/assetFlows/ActionCard.tsx rename to ui/tx/Noves/components/NovesActionCard.tsx index 1b057245b0..773482574f 100644 --- a/ui/tx/assetFlows/ActionCard.tsx +++ b/ui/tx/Noves/components/NovesActionCard.tsx @@ -3,22 +3,20 @@ import type { FC } from 'react'; import React from 'react'; import lightning from 'icons/lightning.svg'; -import { roundNumberIfNeeded } from 'lib/utils/numberHelpers'; -import { camelCaseToSentence } from 'lib/utils/stringHelpers'; import Icon from 'ui/shared/chakra/Icon'; import TokenEntity from 'ui/shared/entities/token/TokenEntity'; -import TokensCard from './TokensCard'; -import type { Action, FlowViewItem } from './utils/generateFlowViewData'; +import type { NovesAction, NovesFlowViewItem } from '../utils/NovesGenerateFlowViewData'; +import NovesTokensCard from './NovesTokensCard'; interface Props { - item: FlowViewItem; + item: NovesFlowViewItem; } -const ActionCard: FC = ({ item }) => { +const NovesActionCard: FC = ({ item }) => { const popoverBg = useColorModeValue('gray.700', 'gray.300'); - const getTokenData = (action: Action) => { + const getTokenData = (action: NovesAction) => { const name = action.nft?.name || action.token?.name; const symbol = action.nft?.symbol || action.token?.symbol; @@ -36,10 +34,10 @@ const ActionCard: FC = ({ item }) => { - { camelCaseToSentence(item.action.label) } + { item.action.label } - { roundNumberIfNeeded(item.action.amount?.toString() || '', 3) } + { item.action.amount } = ({ item }) => { display="flex" fontSize="xl" mr="5px" - color="#718096" - _dark={{ color: '#92a2bb' }} + color="gray.500" + _dark={{ color: 'gray.400' }} /> - { camelCaseToSentence(item.action.label) } + { item.action.label } - { roundNumberIfNeeded(item.action.amount?.toString() || '', 3) } + { item.action.amount } = ({ item }) => { - @@ -113,4 +111,4 @@ const ActionCard: FC = ({ item }) => { ); }; -export default React.memo(ActionCard); +export default React.memo(NovesActionCard); diff --git a/ui/tx/assetFlows/TokenTransferSnippet.tsx b/ui/tx/Noves/components/NovesTokenTransferSnippet.tsx similarity index 88% rename from ui/tx/assetFlows/TokenTransferSnippet.tsx rename to ui/tx/Noves/components/NovesTokenTransferSnippet.tsx index 0e3ca46651..d3e564f2af 100644 --- a/ui/tx/assetFlows/TokenTransferSnippet.tsx +++ b/ui/tx/Noves/components/NovesTokenTransferSnippet.tsx @@ -16,7 +16,7 @@ interface Props { tokenId: string; } -const TokenTransferSnippet = ({ token, tokenId }: Props) => { +const NovesTokenTransferSnippet = ({ token, tokenId }: Props) => { return ( { ); }; -export default React.memo(TokenTransferSnippet); +export default React.memo(NovesTokenTransferSnippet); diff --git a/ui/tx/assetFlows/TokensCard.tsx b/ui/tx/Noves/components/NovesTokensCard.tsx similarity index 85% rename from ui/tx/assetFlows/TokensCard.tsx rename to ui/tx/Noves/components/NovesTokensCard.tsx index bf85d8974d..c3c4818904 100644 --- a/ui/tx/assetFlows/TokensCard.tsx +++ b/ui/tx/Noves/components/NovesTokensCard.tsx @@ -2,16 +2,16 @@ import { Box, Text, useColorModeValue } from '@chakra-ui/react'; import type { FC } from 'react'; import React from 'react'; -import type { Nft, Token } from 'types/translateApi'; +import type { NovesNft, NovesToken } from 'types/novesApi'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; interface Props { amount?: string; - token: Token | Nft | undefined; + token: NovesToken | NovesNft | undefined; } -const TokensCard: FC = ({ token, amount }) => { +const NovesTokensCard: FC = ({ token, amount }) => { const textColor = useColorModeValue('white', 'blackAlpha.900'); if (!token) { @@ -51,4 +51,4 @@ const TokensCard: FC = ({ token, amount }) => { ); }; -export default React.memo(TokensCard); +export default React.memo(NovesTokensCard); diff --git a/ui/tx/Noves/utils/NovesGenerateFlowViewData.ts b/ui/tx/Noves/utils/NovesGenerateFlowViewData.ts new file mode 100644 index 0000000000..dfed45f48d --- /dev/null +++ b/ui/tx/Noves/utils/NovesGenerateFlowViewData.ts @@ -0,0 +1,102 @@ +import _ from 'lodash'; + +import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/novesApi'; + +export interface NovesAction { + label: string; + amount: string | undefined; + flowDirection: 'toLeft' | 'toRight'; + nft: NovesNft | undefined; + token: NovesToken | undefined; +} + +export interface NovesFlowViewItem { + action: NovesAction; + rightActor: { + address: string; + name: string | null; + }; + leftActor: { + address: string; + name: string | null; + }; + accountAddress: string; +} + +export function NovesGenerateFlowViewData(data: NovesResponseData): Array { + const perspectiveAddress = data.accountAddress.toLowerCase(); + + const sent = data.classificationData.sent || []; + const received = data.classificationData.received || []; + + const txItems = [ ...sent, ...received ]; + + const paidGasIndex = _.findIndex(txItems, (item) => item.action === 'paidGas'); + if (paidGasIndex >= 0) { + const element = txItems.splice(paidGasIndex, 1)[0]; + element.to.name = 'Validators'; + txItems.splice(txItems.length, 0, element); + } + + const flowViewData = txItems.map((item) => { + const action = { + label: item.actionFormatted, + amount: item.amount || undefined, + flowDirection: getFlowDirection(item, perspectiveAddress), + nft: item.nft || undefined, + token: item.token || undefined, + }; + + if (item.from.name && item.from.name.includes('(this wallet)')) { + item.from.name = item.from.name.split('(this wallet)')[0]; + } + + if (item.to.name && item.to.name.includes('(this wallet)')) { + item.to.name = item.to.name.split('(this wallet)')[0]; + } + + const rightActor = getRightActor(item, perspectiveAddress); + + const leftActor = getLeftActor(item, perspectiveAddress); + + return { action, rightActor, leftActor, accountAddress: perspectiveAddress }; + }); + + return flowViewData; +} + +function getRightActor(item: NovesSentReceived, perspectiveAddress: string) { + if (!item.to.address || item.to.address.toLowerCase() !== perspectiveAddress) { + return { address: item.to.address, name: item.to.name }; + } + + return { address: item.from.address, name: item.from.name }; +} + +function getLeftActor(item: NovesSentReceived, perspectiveAddress: string) { + if (item.to.address && item.to.address.toLowerCase() === perspectiveAddress) { + return { address: item.to.address, name: item.to.name }; + } + + return { address: item.from.address, name: item.from.name }; +} + +function getFlowDirection(item: NovesSentReceived, perspectiveAddress: string): 'toLeft' | 'toRight' { + // return "toLeft" or "toRight" + if (item.to.address && item.to.address.toLowerCase() === perspectiveAddress) { + return 'toLeft'; + } + + if (item.from.address && item.from.address.toLowerCase() === perspectiveAddress) { + return 'toRight'; + } + + return 'toLeft'; // default case +} + +export const NovesGetFlowCount = (data: NovesResponseData | undefined) => { + if (!data) { + return 0; + } + return NovesGenerateFlowViewData(data).length; +}; diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.ts b/ui/tx/Noves/utils/NovesGetSplittedDescription.ts similarity index 63% rename from ui/tx/assetFlows/utils/generateFlowViewData.ts rename to ui/tx/Noves/utils/NovesGetSplittedDescription.ts index 2796d56550..2b1aaa48d6 100644 --- a/ui/tx/assetFlows/utils/generateFlowViewData.ts +++ b/ui/tx/Noves/utils/NovesGetSplittedDescription.ts @@ -1,97 +1,6 @@ import _ from 'lodash'; -import type { Nft, ResponseData, SentReceived, Token } from 'types/translateApi'; - -export interface Action { - label: string; - amount: string | undefined; - flowDirection: 'toLeft' | 'toRight'; - nft: Nft | undefined; - token: Token | undefined; -} -export interface FlowViewItem { - action: Action; - rightActor: { - address: string; - name: string | null; - }; - leftActor: { - address: string; - name: string | null; - }; - accountAddress: string; -} - -export function generateFlowViewData(data: ResponseData): Array { - const perspectiveAddress = data.accountAddress.toLowerCase(); - - const sent = data.classificationData.sent || []; - const received = data.classificationData.received || []; - - const txItems = [ ...sent, ...received ]; - - const paidGasIndex = _.findIndex(txItems, (item) => item.action === 'paidGas'); - if (paidGasIndex >= 0) { - const element = txItems.splice(paidGasIndex, 1)[0]; - element.to.name = 'Validators'; - txItems.splice(txItems.length, 0, element); - } - - const flowViewData = txItems.map((item) => { - const action = { - label: item.action, - amount: item.amount || undefined, - flowDirection: getFlowDirection(item, perspectiveAddress), - nft: item.nft || undefined, - token: item.token || undefined, - }; - - if (item.from.name && item.from.name.includes('(this wallet)')) { - item.from.name = item.from.name.split('(this wallet)')[0]; - } - - if (item.to.name && item.to.name.includes('(this wallet)')) { - item.to.name = item.to.name.split('(this wallet)')[0]; - } - - const rightActor = getRightActor(item, perspectiveAddress); - - const leftActor = getLeftActor(item, perspectiveAddress); - - return { action, rightActor, leftActor, accountAddress: perspectiveAddress }; - }); - - return flowViewData; -} - -function getRightActor(item: SentReceived, perspectiveAddress: string) { - if (!item.to.address || item.to.address.toLowerCase() !== perspectiveAddress) { - return { address: item.to.address, name: item.to.name }; - } - - return { address: item.from.address, name: item.from.name }; -} - -function getLeftActor(item: SentReceived, perspectiveAddress: string) { - if (item.to.address && item.to.address.toLowerCase() === perspectiveAddress) { - return { address: item.to.address, name: item.to.name }; - } - - return { address: item.from.address, name: item.from.name }; -} - -function getFlowDirection(item: SentReceived, perspectiveAddress: string): 'toLeft' | 'toRight' { - // return "toLeft" or "toRight" - if (item.to.address && item.to.address.toLowerCase() === perspectiveAddress) { - return 'toLeft'; - } - - if (item.from.address && item.from.address.toLowerCase() === perspectiveAddress) { - return 'toRight'; - } - - return 'toLeft'; // default case -} +import type { NovesResponseData } from 'types/novesApi'; interface TokensData { nameList: Array; @@ -115,7 +24,7 @@ interface TokensData { }; } -export function getTokensData(data: ResponseData): TokensData { +function getTokensData(data: NovesResponseData): TokensData { const sent = data.classificationData.sent || []; const received = data.classificationData.received || []; @@ -164,7 +73,7 @@ export function getTokensData(data: ResponseData): TokensData { }; } -export const getSplittedDescription = (translateData: ResponseData) => { +export const NovesGetSplittedDescription = (translateData: NovesResponseData) => { const description = translateData.classificationData.description; const removeEndDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; let parsedDescription = ' ' + removeEndDot; @@ -269,10 +178,3 @@ export const getSplittedDescription = (translateData: ResponseData) => { return descriptionSplitted; }; - -export const getFlowCount = (data: ResponseData | undefined) => { - if (!data) { - return 0; - } - return generateFlowViewData(data).length; -}; diff --git a/lib/hooks/useFetchDescribe.tsx b/ui/txs/Noves/NovesUseFetchDescribe.tsx similarity index 67% rename from lib/hooks/useFetchDescribe.tsx rename to ui/txs/Noves/NovesUseFetchDescribe.tsx index 2b707bf125..efb504b38c 100644 --- a/lib/hooks/useFetchDescribe.tsx +++ b/ui/txs/Noves/NovesUseFetchDescribe.tsx @@ -1,12 +1,12 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import type { DescribeResponse } from 'types/translateApi'; +import type { NovesDescribeResponse } from 'types/novesApi'; import config from 'configs/app'; import type { Params as FetchParams } from 'lib/hooks/useFetch'; -import useFetch from './useFetch'; +import useFetch from '../../../lib/hooks/useFetch'; export interface ApiFetchParams { queryParams?: Record | number | undefined>; @@ -22,34 +22,34 @@ export interface TranslateError { } interface Params extends ApiFetchParams { - queryOptions?: Omit, 'queryKey' | 'queryFn'>; + queryOptions?: Omit, 'queryKey' | 'queryFn'>; } -export default function useFetchDescribe( +export default function NovesUseFetchDescribe( txHash: string | null, { queryOptions }: Params = {}, ) { const fetch = useFetch(); - const url = new URL('/node-api/describe', config.app.baseUrl); + const url = new URL('/node-api/noves/describe', config.app.baseUrl); const body = { txHash, }; - return useQuery({ + return useQuery({ queryKey: [ 'describe', txHash, body ], queryFn: async() => { // all errors and error typing is handled by react-query // so error response will never go to the data // that's why we are safe here to do type conversion "as Promise>" if (!txHash) { - return undefined as unknown as DescribeResponse; + return undefined as unknown as NovesDescribeResponse; } return fetch(url.toString(), { method: 'POST', body, - }) as Promise; + }) as Promise; }, ...queryOptions, }); diff --git a/ui/txs/TxType.tsx b/ui/txs/TxType.tsx index 4cee9df244..26e6c76213 100644 --- a/ui/txs/TxType.tsx +++ b/ui/txs/TxType.tsx @@ -2,7 +2,6 @@ import React from 'react'; import type { TransactionType } from 'types/api/transaction'; -import { camelCaseToSentence } from 'lib/utils/stringHelpers'; import Tag from 'ui/shared/chakra/Tag'; export interface Props { @@ -58,7 +57,7 @@ const TxType = ({ types, isLoading, translateLabel }: Props) => { if (translateLabel) { if (!filteredTypes.includes(translateLabel)) { - label = camelCaseToSentence(translateLabel); + label = translateLabel; } } diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index a1fcd5b735..22538a3e21 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -11,7 +11,6 @@ import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; import rightArrowIcon from 'icons/arrows/east.svg'; import getValueWithUnit from 'lib/getValueWithUnit'; -import useFetchDescribe from 'lib/hooks/useFetchDescribe'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import { space } from 'lib/html-entities'; import Icon from 'ui/shared/chakra/Icon'; @@ -23,6 +22,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import TxStatus from 'ui/shared/statusTag/TxStatus'; import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; +import NovesUseFetchDescribe from 'ui/txs/Noves/NovesUseFetchDescribe'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxType from 'ui/txs/TxType'; @@ -46,7 +46,7 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - const { data: describeData } = useFetchDescribe(translate ? tx.hash : null); + const { data: describeData } = NovesUseFetchDescribe(translate ? tx.hash : null); return ( diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index 4cb3044b19..db061b3371 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -15,7 +15,6 @@ import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; import rightArrowIcon from 'icons/arrows/east.svg'; -import useFetchDescribe from 'lib/hooks/useFetchDescribe'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import Icon from 'ui/shared/chakra/Icon'; import Tag from 'ui/shared/chakra/Tag'; @@ -27,6 +26,7 @@ import InOutTag from 'ui/shared/InOutTag'; import TxStatus from 'ui/shared/statusTag/TxStatus'; import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; +import NovesUseFetchDescribe from 'ui/txs/Noves/NovesUseFetchDescribe'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxType from './TxType'; @@ -47,7 +47,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - const { data: describeData } = useFetchDescribe(translate ? tx.hash : null); + const { data: describeData } = NovesUseFetchDescribe(translate ? tx.hash : null); const addressFrom = ( Date: Tue, 30 Jan 2024 13:07:24 -0300 Subject: [PATCH 03/30] PR changes added --- configs/app/features/index.ts | 1 - configs/app/features/noves.ts | 23 -- lib/api/resources.ts | 21 +- lib/metadata/getPageOgType.ts | 3 - lib/metadata/templates/description.ts | 3 - lib/metadata/templates/title.ts | 3 - lib/mixpanel/getPageType.ts | 3 - mocks/noves/transaction.ts | 103 +++++++++ nextjs/nextjs-routes.d.ts | 3 - pages/api/noves/describe.ts | 31 --- pages/api/noves/history.ts | 35 --- pages/api/noves/translate.ts | 31 --- stubs/noves/Novestranslate.ts | 2 +- types/{novesApi.ts => api/noves.ts} | 30 ++- types/client/txInterpretation.ts | 1 + ui/address/AddressAccountHistory.tsx | 129 ++++++++++++ ui/address/AddressAccountHistoryFilter.tsx | 55 +++++ .../AddressAccountHistoryListItem.tsx | 60 ++++++ .../AddressAccountHistoryTableItem.tsx | 60 ++++++ .../noves/NovesAccountHistoryFilter.tsx | 87 -------- .../noves/NovesAddressAccountHistory.tsx | 199 ------------------ ui/address/noves/NovesUseFetchHistory.tsx | 63 ------ .../noves/NovesUseFetchHistoryWithPages.tsx | 196 ----------------- ui/address/noves/utils.ts | 13 -- ui/pages/Address.tsx | 17 +- ui/pages/Transaction.tsx | 21 +- ui/shared/Noves/NovesFromTo.tsx | 67 ++++++ ui/shared/Noves/NovesFromToComponent.tsx | 68 ------ ui/shared/Noves/utils.test.ts | 53 +++++ ui/shared/Noves/utils.ts | 31 +-- ui/tx/Noves/NovesTxAssetFlows.tsx | 160 -------------- ui/tx/Noves/NovesTxTitleSecondRow.tsx | 80 ------- ui/tx/Noves/NovesUseFetchTranslate.tsx | 56 ----- .../utils/NovesGetSplittedDescription.ts | 180 ---------------- ui/tx/TxAssetFlows.tsx | 122 +++++++++++ ui/tx/TxSubHeading.tsx | 51 +++-- ui/tx/assetFlows/TxAssetFlowsListItem.tsx | 46 ++++ ui/tx/assetFlows/TxAssetFlowsTableItem.tsx | 28 +++ .../components/NovesActionSnippet.tsx} | 55 ++--- .../NovesSubHeadingInterpretation.tsx | 65 ++++++ .../components/NovesTokenTooltipContent.tsx} | 6 +- .../components/NovesTokenTransferSnippet.tsx | 0 .../utils/generateFlowViewData.test.ts | 56 +++++ .../utils/generateFlowViewData.ts} | 20 +- .../utils/getDescriptionItems.test.ts | 15 ++ ui/tx/assetFlows/utils/getDescriptionItems.ts | 177 ++++++++++++++++ ui/tx/assetFlows/utils/getTokensData.test.ts | 32 +++ ui/tx/assetFlows/utils/getTokensData.ts | 78 +++++++ ui/txs/Noves/NovesUseFetchDescribe.tsx | 56 ----- ui/txs/TxType.tsx | 6 +- ui/txs/TxsContent.tsx | 11 +- ui/txs/TxsListItem.tsx | 13 +- ui/txs/TxsTable.tsx | 6 +- ui/txs/TxsTableItem.tsx | 13 +- 54 files changed, 1330 insertions(+), 1414 deletions(-) delete mode 100644 configs/app/features/noves.ts create mode 100644 mocks/noves/transaction.ts delete mode 100644 pages/api/noves/describe.ts delete mode 100644 pages/api/noves/history.ts delete mode 100644 pages/api/noves/translate.ts rename types/{novesApi.ts => api/noves.ts} (71%) create mode 100644 ui/address/AddressAccountHistory.tsx create mode 100644 ui/address/AddressAccountHistoryFilter.tsx create mode 100644 ui/address/accountHistory/AddressAccountHistoryListItem.tsx create mode 100644 ui/address/accountHistory/AddressAccountHistoryTableItem.tsx delete mode 100644 ui/address/noves/NovesAccountHistoryFilter.tsx delete mode 100644 ui/address/noves/NovesAddressAccountHistory.tsx delete mode 100644 ui/address/noves/NovesUseFetchHistory.tsx delete mode 100644 ui/address/noves/NovesUseFetchHistoryWithPages.tsx delete mode 100644 ui/address/noves/utils.ts create mode 100644 ui/shared/Noves/NovesFromTo.tsx delete mode 100644 ui/shared/Noves/NovesFromToComponent.tsx create mode 100644 ui/shared/Noves/utils.test.ts delete mode 100644 ui/tx/Noves/NovesTxAssetFlows.tsx delete mode 100644 ui/tx/Noves/NovesTxTitleSecondRow.tsx delete mode 100644 ui/tx/Noves/NovesUseFetchTranslate.tsx delete mode 100644 ui/tx/Noves/utils/NovesGetSplittedDescription.ts create mode 100644 ui/tx/TxAssetFlows.tsx create mode 100644 ui/tx/assetFlows/TxAssetFlowsListItem.tsx create mode 100644 ui/tx/assetFlows/TxAssetFlowsTableItem.tsx rename ui/tx/{Noves/components/NovesActionCard.tsx => assetFlows/components/NovesActionSnippet.tsx} (67%) create mode 100644 ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx rename ui/tx/{Noves/components/NovesTokensCard.tsx => assetFlows/components/NovesTokenTooltipContent.tsx} (87%) rename ui/tx/{Noves => assetFlows}/components/NovesTokenTransferSnippet.tsx (100%) create mode 100644 ui/tx/assetFlows/utils/generateFlowViewData.test.ts rename ui/tx/{Noves/utils/NovesGenerateFlowViewData.ts => assetFlows/utils/generateFlowViewData.ts} (84%) create mode 100644 ui/tx/assetFlows/utils/getDescriptionItems.test.ts create mode 100644 ui/tx/assetFlows/utils/getDescriptionItems.ts create mode 100644 ui/tx/assetFlows/utils/getTokensData.test.ts create mode 100644 ui/tx/assetFlows/utils/getTokensData.ts delete mode 100644 ui/txs/Noves/NovesUseFetchDescribe.tsx diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 1b72f90507..0879fb17d5 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -21,4 +21,3 @@ export { default as txInterpretation } from './txInterpretation'; export { default as web3Wallet } from './web3Wallet'; export { default as verifiedTokens } from './verifiedTokens'; export { default as zkEvmRollup } from './zkEvmRollup'; -export { default as noves } from './noves'; diff --git a/configs/app/features/noves.ts b/configs/app/features/noves.ts deleted file mode 100644 index 8c35f38051..0000000000 --- a/configs/app/features/noves.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Feature } from './types'; - -import { getEnvValue } from '../utils'; - -const novesEnabled = getEnvValue('NEXT_PUBLIC_NOVES_ENABLED') === 'true'; - -const title = 'Noves API'; - -const config: Feature<{ isEnabled: true }> = (() => { - if (novesEnabled) { - return Object.freeze({ - title, - isEnabled: true, - }); - } - - return Object.freeze({ - title, - isEnabled: false, - }); -})(); - -export default config; diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 60d224dad6..953ec7958e 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -43,6 +43,7 @@ import type { L2OutputRootsResponse } from 'types/api/l2OutputRoots'; import type { L2TxnBatchesResponse } from 'types/api/l2TxnBatches'; import type { L2WithdrawalsResponse } from 'types/api/l2Withdrawals'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; +import type { NovesAccountHistoryResponse, NovesDescribeResponse, NovesResponseData } from 'types/api/noves'; import type { RawTracesResponse } from 'types/api/rawTrace'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; @@ -542,6 +543,21 @@ export const RESOURCES = { filterFields: [], }, + // NOVES-FI + noves_transaction: { + path: '/api/v2/proxy/noves-fi/transactions/:hash', + pathParams: [ 'hash' as const ], + }, + noves_address_history: { + path: '/api/v2/proxy/noves-fi/addresses/:address/transactions', + pathParams: [ 'address' as const ], + filterFields: [], + }, + noves_describe_tx: { + path: '/api/v2/proxy/noves-fi/transactions/:hash/describe', + pathParams: [ 'hash' as const ], + }, + // CONFIGS config_backend_version: { path: '/api/v2/config/backend-version', @@ -613,7 +629,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | -'watchlist' | 'private_tags_address' | 'private_tags_tx'; +'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'noves_address_history'; export type PaginatedResponse = ResourcePayload; @@ -712,6 +728,9 @@ Q extends 'zkevm_l2_txn_batches_count' ? number : Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'config_backend_version' ? BackendVersionConfig : +Q extends 'noves_transaction' ? NovesResponseData : +Q extends 'noves_address_history' ? NovesAccountHistoryResponse : +Q extends 'noves_describe_tx' ? NovesDescribeResponse : never; /* eslint-enable @typescript-eslint/indent */ diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 7883dd665f..36e70e9fee 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -43,9 +43,6 @@ const OG_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Regular page', - '/api/noves/translate': 'Regular page', - '/api/noves/history': 'Regular page', - '/api/noves/describe': 'Regular page', '/api/media-type': 'Regular page', '/api/proxy': 'Regular page', '/api/csrf': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index da79ee00d3..175e118f13 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -46,9 +46,6 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, - '/api/noves/translate': DEFAULT_TEMPLATE, - '/api/noves/history': DEFAULT_TEMPLATE, - '/api/noves/describe': DEFAULT_TEMPLATE, '/api/media-type': DEFAULT_TEMPLATE, '/api/proxy': DEFAULT_TEMPLATE, '/api/csrf': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 12166aa36f..5a9e48eebb 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -41,9 +41,6 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': 'login', - '/api/noves/translate': 'node API media type', - '/api/noves/history': 'node API media type', - '/api/noves/describe': 'node API media type', '/api/media-type': 'node API media type', '/api/proxy': 'node API proxy', '/api/csrf': 'node API CSRF token', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index 058d0544ce..4ff5e22aa2 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -41,9 +41,6 @@ export const PAGE_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Login', - '/api/noves/translate': 'Node API: Media type', - '/api/noves/history': 'Node API: Media type', - '/api/noves/describe': 'Node API: Media type', '/api/media-type': 'Node API: Media type', '/api/proxy': 'Node API: Proxy', '/api/csrf': 'Node API: CSRF token', diff --git a/mocks/noves/transaction.ts b/mocks/noves/transaction.ts new file mode 100644 index 0000000000..406cea8478 --- /dev/null +++ b/mocks/noves/transaction.ts @@ -0,0 +1,103 @@ +import type { NovesResponseData } from 'types/api/noves'; + +import type { TokensData } from 'ui/tx/assetFlows/utils/getTokensData'; + +export const hash = '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53'; + +export const transaction: NovesResponseData = { + accountAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + chain: 'eth-goerli', + classificationData: { + description: 'Called function \'stake\' on contract 0xef326CdAdA59D3A740A76bB5f4F88Fb2.', + protocol: { + name: null, + }, + received: [], + sent: [ + { + action: 'sent', + actionFormatted: 'Sent', + amount: '3000', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + { + action: 'paidGas', + actionFormatted: 'Paid Gas', + amount: '0.000395521502109448', + from: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + to: { + address: null, + name: 'Validators', + }, + token: { + address: 'ETH', + decimals: 18, + name: 'ETH', + symbol: 'ETH', + }, + }, + ], + source: { + type: null, + }, + type: 'unclassified', + typeFormatted: 'Unclassified', + }, + rawTransactionData: { + blockNumber: 10388918, + fromAddress: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + gas: 275079, + gasPrice: 1500000008, + timestamp: 1705488588, + toAddress: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + transactionFee: { + amount: '395521502109448', + token: { + decimals: 18, + symbol: 'ETH', + }, + }, + transactionHash: '0x380400d04ebb4179a35b1d7fdef260776915f015e978f8587ef2704b843d4e53', + }, + txTypeVersion: 2, +}; + +export const tokenData: TokensData = { + nameList: [ 'PQR-Test', 'ETH' ], + symbolList: [ 'PQR' ], + idList: [], + byName: { + 'PQR-Test': { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + ETH: { name: 'ETH', symbol: undefined, address: 'ETH', id: undefined }, + }, + bySymbol: { + PQR: { + name: 'PQR-Test', + symbol: 'PQR', + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + id: undefined, + }, + undefined: { name: 'ETH', symbol: undefined, address: 'ETH', id: undefined }, + }, +}; diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index 3d53ee277d..c8b96cce4a 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -19,9 +19,6 @@ declare module "nextjs-routes" { | StaticRoute<"/api/csrf"> | StaticRoute<"/api/healthz"> | StaticRoute<"/api/media-type"> - | StaticRoute<"/api/noves/describe"> - | StaticRoute<"/api/noves/history"> - | StaticRoute<"/api/noves/translate"> | StaticRoute<"/api/proxy"> | StaticRoute<"/api-docs"> | StaticRoute<"/apps"> diff --git a/pages/api/noves/describe.ts b/pages/api/noves/describe.ts deleted file mode 100644 index 149195414c..0000000000 --- a/pages/api/noves/describe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import nodeFetch from 'node-fetch'; - -import { getEnvValue } from 'configs/app/utils'; - -const translateEnpoint = getEnvValue('NOVES_TRANSLATE_ENDPOINT') as string; -const translateApiKey = getEnvValue('NOVES_TRANSLATE_API_KEY') as string; -const translateSelectedChain = getEnvValue('NOVES_SELECTED_CHAIN') as string; - -const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { - if (nextReq.method !== 'POST') { - return nextRes.status(404).send({ - success: false, - message: 'Method not supported', - }); - } - const { txHash } = nextReq.body; - - const url = `${ translateEnpoint }/evm/${ translateSelectedChain }/describeTx/${ txHash }`; - const headers = { - apiKey: translateApiKey, - }; - - const apiRes = await nodeFetch(url, { - headers, - }); - - nextRes.status(apiRes.status).send(apiRes.body); -}; - -export default handler; diff --git a/pages/api/noves/history.ts b/pages/api/noves/history.ts deleted file mode 100644 index d7c6cdf5a8..0000000000 --- a/pages/api/noves/history.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import nodeFetch from 'node-fetch'; -import queryString from 'querystring'; - -import { getEnvValue } from 'configs/app/utils'; - -const translateEnpoint = getEnvValue('NOVES_TRANSLATE_ENDPOINT') as string; -const translateApiKey = getEnvValue('NOVES_TRANSLATE_API_KEY') as string; -const translateSelectedChain = getEnvValue('NOVES_SELECTED_CHAIN') as string; - -const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { - if (nextReq.method !== 'POST') { - return nextRes.status(404).send({ - success: false, - message: 'Method not supported', - }); - } - const { address } = nextReq.body; - const query = queryString.stringify(nextReq.query); - - const fetchParams = query ? query : `viewAsAccountAddress=${ address }`; - - const url = `${ translateEnpoint }/evm/${ translateSelectedChain }/txs/${ address }?${ fetchParams }`; - const headers = { - apiKey: translateApiKey, - }; - - const apiRes = await nodeFetch(url, { - headers, - }); - - nextRes.status(apiRes.status).send(apiRes.body); -}; - -export default handler; diff --git a/pages/api/noves/translate.ts b/pages/api/noves/translate.ts deleted file mode 100644 index 35cff795cc..0000000000 --- a/pages/api/noves/translate.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import nodeFetch from 'node-fetch'; - -import { getEnvValue } from 'configs/app/utils'; - -const translateEnpoint = getEnvValue('NOVES_TRANSLATE_ENDPOINT') as string; -const translateApiKey = getEnvValue('NOVES_TRANSLATE_API_KEY') as string; -const translateSelectedChain = getEnvValue('NOVES_SELECTED_CHAIN') as string; - -const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { - if (nextReq.method !== 'POST') { - return nextRes.status(404).send({ - success: false, - message: 'Method not supported', - }); - } - const { txHash } = nextReq.body; - - const url = `${ translateEnpoint }/evm/${ translateSelectedChain }/tx/${ txHash }`; - const headers = { - apiKey: translateApiKey, - }; - - const apiRes = await nodeFetch(url, { - headers, - }); - - nextRes.status(apiRes.status).send(apiRes.body); -}; - -export default handler; diff --git a/stubs/noves/Novestranslate.ts b/stubs/noves/Novestranslate.ts index 9f43a7a904..848ed6dab9 100644 --- a/stubs/noves/Novestranslate.ts +++ b/stubs/noves/Novestranslate.ts @@ -1,4 +1,4 @@ -import type { NovesResponseData, NovesClassificationData, NovesRawTransactionData } from 'types/novesApi'; +import type { NovesResponseData, NovesClassificationData, NovesRawTransactionData } from 'types/api/noves'; const NOVES_TRANSLATE_CLASSIFIED: NovesClassificationData = { description: 'Sent 0.04 ETH', diff --git a/types/novesApi.ts b/types/api/noves.ts similarity index 71% rename from types/novesApi.ts rename to types/api/noves.ts index 65f89fe9c8..fb72478bff 100644 --- a/types/novesApi.ts +++ b/types/api/noves.ts @@ -8,10 +8,13 @@ export interface NovesResponseData { export interface NovesClassificationData { type: string; - typeFormatted: string; + typeFormatted?: string; description: string; sent: Array; received: Array; + protocol?: { + name: string | null; + }; source: { type: string | null; }; @@ -20,7 +23,7 @@ export interface NovesClassificationData { export interface NovesSentReceived { action: string; - actionFormatted: string; + actionFormatted?: string; amount: string; to: NovesTo; from: NovesFrom; @@ -50,7 +53,7 @@ export interface NovesFrom { export interface NovesTo { name: string | null; - address: string; + address: string | null; } export interface NovesRawTransactionData { @@ -66,7 +69,11 @@ export interface NovesRawTransactionData { export interface NovesTransactionFee { amount: string; - currency: string; + currency?: string; + token?: { + decimals: number; + symbol: string; + }; } export interface NovesAccountHistoryResponse { @@ -74,15 +81,22 @@ export interface NovesAccountHistoryResponse { items: Array; pageNumber: number; pageSize: number; - nextPageUrl?: string; + next_page_params?: { + startBlock: null; + endBlock: string; + pageNumber: number; + pageSize: number; + ignoreTransactions: string; + viewAsAccountAddress: string; + }; } -export const NovesHistorySentReceivedFilterValues = [ 'received', 'sent' ] as const; +export const NovesHistoryFilterValues = [ 'received', 'sent' ] as const; -export type NovesHistorySentReceivedFilter = typeof NovesHistorySentReceivedFilterValues[number] | undefined; +export type NovesHistoryFilterValue = typeof NovesHistoryFilterValues[number] | undefined; export interface NovesHistoryFilters { - filter?: NovesHistorySentReceivedFilter; + filter?: NovesHistoryFilterValue; } export interface NovesDescribeResponse { diff --git a/types/client/txInterpretation.ts b/types/client/txInterpretation.ts index e264b267bc..23f55ed217 100644 --- a/types/client/txInterpretation.ts +++ b/types/client/txInterpretation.ts @@ -2,6 +2,7 @@ import type { ArrayElement } from 'types/utils'; export const PROVIDERS = [ 'blockscout', + 'noves', 'none', ] as const; diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx new file mode 100644 index 0000000000..a6a42daa53 --- /dev/null +++ b/ui/address/AddressAccountHistory.tsx @@ -0,0 +1,129 @@ +import { Box, Hide, Show, Table, + Tbody, Th, Thead, Tr } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { NovesHistoryFilterValue } from 'types/api/noves'; +import { NovesHistoryFilterValues } from 'types/api/noves'; + +import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { NOVES_TRANSLATE } from 'stubs/noves/novestranslate'; +import { generateListStub } from 'stubs/utils'; +import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAccountHistoryTableItem'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import { getFromToValue } from 'ui/shared/Noves/utils'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; + +import AddressAccountHistoryListItem from './accountHistory/AddressAccountHistoryListItem'; +import AccountHistoryFilter from './AddressAccountHistoryFilter'; + +const getFilterValue = (getFilterValueFromQuery).bind(null, NovesHistoryFilterValues); + +type Props = { + scrollRef?: React.RefObject; +} + +const AddressAccountHistory = ({ scrollRef }: Props) => { + const router = useRouter(); + + const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); + + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + + const { data, isError, pagination, isPlaceholderData } = useQueryWithPages({ + resourceName: 'noves_address_history', + pathParams: { address: currentAddress }, + scrollRef, + options: { + placeholderData: generateListStub<'noves_address_history'>(NOVES_TRANSLATE, 10, { hasNextPage: false, pageNumber: 1, pageSize: 10 }), + }, + }); + + const handleFilterChange = React.useCallback((val: string | Array) => { + + const newVal = getFilterValue(val); + setFilterValue(newVal); + }, [ ]); + + const actionBar = ( + + + + + + ); + + const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? getFromToValue(i, currentAddress) === filterValue : i); + + const content = ( + + + { filteredData?.map((item, i) => ( + + )) } + + + +
- + From 96e2def5c906dd2bf40fe6fecac38aa92f7061b8 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Sun, 31 Dec 2023 20:38:19 -0300 Subject: [PATCH 02/30] code refactored --- configs/app/features/index.ts | 1 + configs/app/features/noves.ts | 23 ++++ lib/metadata/getPageOgType.ts | 6 +- lib/metadata/templates/description.ts | 6 +- lib/metadata/templates/title.ts | 6 +- lib/mixpanel/getPageType.ts | 6 +- lib/utils/numberHelpers.ts | 44 ------- lib/utils/stringHelpers.ts | 41 ------ nextjs/nextjs-routes.d.ts | 6 +- pages/api/{ => noves}/describe.ts | 6 +- pages/api/{ => noves}/history.ts | 6 +- pages/api/{ => noves}/translate.ts | 6 +- stubs/history.ts | 10 -- stubs/noves/Novestranslate.ts | 43 +++++++ stubs/translate.ts | 12 -- stubs/translateClassified.ts | 22 ---- stubs/translateRaw.ts | 12 -- types/novesApi.ts | 91 ++++++++++++++ types/translateApi.ts | 89 ------------- ui/address/history/utils.ts | 13 -- .../NovesAccountHistoryFilter.tsx} | 9 +- .../NovesAddressAccountHistory.tsx} | 41 +++--- .../NovesUseFetchHistory.tsx} | 14 +-- .../NovesUseFetchHistoryWithPages.tsx} | 18 +-- ui/address/noves/utils.ts | 13 ++ ui/pages/Address.tsx | 4 +- ui/pages/Transaction.tsx | 81 ++---------- ui/shared/Noves/NovesFromToComponent.tsx | 68 ++++++++++ ui/shared/Noves/utils.ts | 88 +++++++++++++ ui/shared/accountHistory/FromToComponent.tsx | 117 ------------------ ui/shared/entities/address/AddressEntity.tsx | 7 +- ui/shared/entities/token/TokenEntity.tsx | 7 +- ui/shared/filters/FilterButton.tsx | 8 +- .../NovesTxAssetFlows.tsx} | 28 ++--- ui/tx/Noves/NovesTxTitleSecondRow.tsx | 80 ++++++++++++ .../tx/Noves/NovesUseFetchTranslate.tsx | 18 +-- .../components/NovesActionCard.tsx} | 28 ++--- .../components/NovesTokenTransferSnippet.tsx} | 4 +- .../components/NovesTokensCard.tsx} | 8 +- .../Noves/utils/NovesGenerateFlowViewData.ts | 102 +++++++++++++++ .../utils/NovesGetSplittedDescription.ts} | 104 +--------------- .../txs/Noves/NovesUseFetchDescribe.tsx | 16 +-- ui/txs/TxType.tsx | 3 +- ui/txs/TxsListItem.tsx | 4 +- ui/txs/TxsTableItem.tsx | 4 +- 45 files changed, 653 insertions(+), 670 deletions(-) create mode 100644 configs/app/features/noves.ts delete mode 100644 lib/utils/numberHelpers.ts delete mode 100644 lib/utils/stringHelpers.ts rename pages/api/{ => noves}/describe.ts (75%) rename pages/api/{ => noves}/history.ts (79%) rename pages/api/{ => noves}/translate.ts (75%) delete mode 100644 stubs/history.ts create mode 100644 stubs/noves/Novestranslate.ts delete mode 100644 stubs/translate.ts delete mode 100644 stubs/translateClassified.ts delete mode 100644 stubs/translateRaw.ts create mode 100644 types/novesApi.ts delete mode 100644 types/translateApi.ts delete mode 100644 ui/address/history/utils.ts rename ui/address/{history/AccountHistoryFilter.tsx => noves/NovesAccountHistoryFilter.tsx} (87%) rename ui/address/{AddressAccountHistory.tsx => noves/NovesAddressAccountHistory.tsx} (81%) rename ui/address/{history/useFetchHistory.tsx => noves/NovesUseFetchHistory.tsx} (73%) rename ui/address/{history/useFetchHistoryWithPages.tsx => noves/NovesUseFetchHistoryWithPages.tsx} (90%) create mode 100644 ui/address/noves/utils.ts create mode 100644 ui/shared/Noves/NovesFromToComponent.tsx create mode 100644 ui/shared/Noves/utils.ts delete mode 100644 ui/shared/accountHistory/FromToComponent.tsx rename ui/tx/{TxAssetFlows.tsx => Noves/NovesTxAssetFlows.tsx} (84%) create mode 100644 ui/tx/Noves/NovesTxTitleSecondRow.tsx rename lib/hooks/useFetchTranslate.tsx => ui/tx/Noves/NovesUseFetchTranslate.tsx (65%) rename ui/tx/{assetFlows/ActionCard.tsx => Noves/components/NovesActionCard.tsx} (77%) rename ui/tx/{assetFlows/TokenTransferSnippet.tsx => Noves/components/NovesTokenTransferSnippet.tsx} (88%) rename ui/tx/{assetFlows/TokensCard.tsx => Noves/components/NovesTokensCard.tsx} (85%) create mode 100644 ui/tx/Noves/utils/NovesGenerateFlowViewData.ts rename ui/tx/{assetFlows/utils/generateFlowViewData.ts => Noves/utils/NovesGetSplittedDescription.ts} (63%) rename lib/hooks/useFetchDescribe.tsx => ui/txs/Noves/NovesUseFetchDescribe.tsx (67%) diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index ad45f9a4a1..1a5ab78497 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -20,3 +20,4 @@ export { default as suave } from './suave'; export { default as web3Wallet } from './web3Wallet'; export { default as verifiedTokens } from './verifiedTokens'; export { default as zkEvmRollup } from './zkEvmRollup'; +export { default as noves } from './noves'; diff --git a/configs/app/features/noves.ts b/configs/app/features/noves.ts new file mode 100644 index 0000000000..8c35f38051 --- /dev/null +++ b/configs/app/features/noves.ts @@ -0,0 +1,23 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const novesEnabled = getEnvValue('NEXT_PUBLIC_NOVES_ENABLED') === 'true'; + +const title = 'Noves API'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (novesEnabled) { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 94806ec8e7..1bd48355a1 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -42,9 +42,9 @@ const OG_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Regular page', - '/api/translate': 'Regular page', - '/api/history': 'Regular page', - '/api/describe': 'Regular page', + '/api/noves/translate': 'Regular page', + '/api/noves/history': 'Regular page', + '/api/noves/describe': 'Regular page', '/api/media-type': 'Regular page', '/api/proxy': 'Regular page', '/api/csrf': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 3b8359c80d..712e138bf6 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -45,9 +45,9 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, - '/api/translate': DEFAULT_TEMPLATE, - '/api/history': DEFAULT_TEMPLATE, - '/api/describe': DEFAULT_TEMPLATE, + '/api/noves/translate': DEFAULT_TEMPLATE, + '/api/noves/history': DEFAULT_TEMPLATE, + '/api/noves/describe': DEFAULT_TEMPLATE, '/api/media-type': DEFAULT_TEMPLATE, '/api/proxy': DEFAULT_TEMPLATE, '/api/csrf': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 783c3da653..2acc470375 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -40,9 +40,9 @@ const TEMPLATE_MAP: Record = { // service routes, added only to make typescript happy '/login': 'login', - '/api/translate': 'node API media type', - '/api/history': 'node API media type', - '/api/describe': 'node API media type', + '/api/noves/translate': 'node API media type', + '/api/noves/history': 'node API media type', + '/api/noves/describe': 'node API media type', '/api/media-type': 'node API media type', '/api/proxy': 'node API proxy', '/api/csrf': 'node API CSRF token', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index b52df60220..2b85bcd1b9 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -40,9 +40,9 @@ export const PAGE_TYPE_DICT: Record = { // service routes, added only to make typescript happy '/login': 'Login', - '/api/translate': 'Node API: Media type', - '/api/history': 'Node API: Media type', - '/api/describe': 'Node API: Media type', + '/api/noves/translate': 'Node API: Media type', + '/api/noves/history': 'Node API: Media type', + '/api/noves/describe': 'Node API: Media type', '/api/media-type': 'Node API: Media type', '/api/proxy': 'Node API: Proxy', '/api/csrf': 'Node API: CSRF token', diff --git a/lib/utils/numberHelpers.ts b/lib/utils/numberHelpers.ts deleted file mode 100644 index 792f70b082..0000000000 --- a/lib/utils/numberHelpers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import _ from 'lodash'; - -export function formatNumberString(number: string, decimalPlaces: number) { - const [ whole, decimal ] = number.split('.'); - if (decimal) { - return `${ whole }.${ decimal.slice(0, decimalPlaces) }`; - } - return whole; -} - -export function formatNumberIsNeeded(number: string, decimalPlaces: number) { - // eslint-disable-next-line - const [whole, decimal] = number.split('.'); - if (decimal) { - return decimal.length > decimalPlaces; - } - return false; -} - -export function truncateMiddle(text: string, startLength: number, endLength: number): string { - if (text.length <= startLength + endLength + 3) { - return text; - } - return `${ text.substring(0, startLength) }...${ text.slice(-Math.abs(endLength)) }`; -} - -export function roundNumberIfNeeded(number: string, decimalPlaces: number) { - if (formatNumberIsNeeded(number, decimalPlaces)) { - const rounded = _.round(parseFloat(number), decimalPlaces); - - if (rounded === 0) { - const positiveNumbers = [ '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; - - const numberIndices = positiveNumbers.map((n => number.indexOf(n))).filter(n => n !== -1).sort((a, b) => a - b); - - const subStr = number.substring(0, numberIndices[0] + 1); - - return '~' + subStr; - } - - return '~' + rounded.toString(); - } - return number; -} diff --git a/lib/utils/stringHelpers.ts b/lib/utils/stringHelpers.ts deleted file mode 100644 index 6a37a105b8..0000000000 --- a/lib/utils/stringHelpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -export function camelCaseToSentence(camelCaseString: string | undefined) { - if (!camelCaseString) { - return ''; - } - - let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2'); - sentence = sentence.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'); - sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1); - sentence = capitalizeAcronyms(sentence); - return sentence; -} - -function capitalizeAcronyms(sentence: string) { - const acronymList = [ 'NFT' ]; // add more acronyms here if needed - - const words = sentence.split(' '); - - const capitalizedWords = words.map((word) => { - const acronym = word.toUpperCase(); - if (acronymList.includes(acronym)) { - return acronym.toUpperCase(); - } - return word; - }); - - return capitalizedWords.join(' '); -} - -export function truncateMiddle(string: string, startLength: number, endLength: number): string { - const text = string || ''; - - if (!text) { - return ''; - } - - if (text.length <= startLength + endLength + 3) { - return text; - } - - return `${ text.substring(0, startLength) }...${ text.slice(-Math.abs(endLength)) }`; -} diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index 1dbebe3ad5..dd579b42e5 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -17,12 +17,12 @@ declare module "nextjs-routes" { | DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }> | DynamicRoute<"/address/[hash]", { "hash": string }> | StaticRoute<"/api/csrf"> - | StaticRoute<"/api/describe"> | StaticRoute<"/api/healthz"> - | StaticRoute<"/api/history"> | StaticRoute<"/api/media-type"> + | StaticRoute<"/api/noves/describe"> + | StaticRoute<"/api/noves/history"> + | StaticRoute<"/api/noves/translate"> | StaticRoute<"/api/proxy"> - | StaticRoute<"/api/translate"> | StaticRoute<"/api-docs"> | StaticRoute<"/apps"> | DynamicRoute<"/apps/[id]", { "id": string }> diff --git a/pages/api/describe.ts b/pages/api/noves/describe.ts similarity index 75% rename from pages/api/describe.ts rename to pages/api/noves/describe.ts index b534f1adcc..149195414c 100644 --- a/pages/api/describe.ts +++ b/pages/api/noves/describe.ts @@ -3,9 +3,9 @@ import nodeFetch from 'node-fetch'; import { getEnvValue } from 'configs/app/utils'; -const translateEnpoint = getEnvValue('TRANSLATE_ENDPOINT') as string; -const translateApiKey = getEnvValue('TRANSLATE_API_KEY') as string; -const translateSelectedChain = getEnvValue('TRANSLATE_SELECTED_CHAIN') as string; +const translateEnpoint = getEnvValue('NOVES_TRANSLATE_ENDPOINT') as string; +const translateApiKey = getEnvValue('NOVES_TRANSLATE_API_KEY') as string; +const translateSelectedChain = getEnvValue('NOVES_SELECTED_CHAIN') as string; const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { if (nextReq.method !== 'POST') { diff --git a/pages/api/history.ts b/pages/api/noves/history.ts similarity index 79% rename from pages/api/history.ts rename to pages/api/noves/history.ts index 5a71c9457a..d7c6cdf5a8 100644 --- a/pages/api/history.ts +++ b/pages/api/noves/history.ts @@ -4,9 +4,9 @@ import queryString from 'querystring'; import { getEnvValue } from 'configs/app/utils'; -const translateEnpoint = getEnvValue('TRANSLATE_ENDPOINT') as string; -const translateApiKey = getEnvValue('TRANSLATE_API_KEY') as string; -const translateSelectedChain = getEnvValue('TRANSLATE_SELECTED_CHAIN') as string; +const translateEnpoint = getEnvValue('NOVES_TRANSLATE_ENDPOINT') as string; +const translateApiKey = getEnvValue('NOVES_TRANSLATE_API_KEY') as string; +const translateSelectedChain = getEnvValue('NOVES_SELECTED_CHAIN') as string; const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { if (nextReq.method !== 'POST') { diff --git a/pages/api/translate.ts b/pages/api/noves/translate.ts similarity index 75% rename from pages/api/translate.ts rename to pages/api/noves/translate.ts index edebf4ace7..35cff795cc 100644 --- a/pages/api/translate.ts +++ b/pages/api/noves/translate.ts @@ -3,9 +3,9 @@ import nodeFetch from 'node-fetch'; import { getEnvValue } from 'configs/app/utils'; -const translateEnpoint = getEnvValue('TRANSLATE_ENDPOINT') as string; -const translateApiKey = getEnvValue('TRANSLATE_API_KEY') as string; -const translateSelectedChain = getEnvValue('TRANSLATE_SELECTED_CHAIN') as string; +const translateEnpoint = getEnvValue('NOVES_TRANSLATE_ENDPOINT') as string; +const translateApiKey = getEnvValue('NOVES_TRANSLATE_API_KEY') as string; +const translateSelectedChain = getEnvValue('NOVES_SELECTED_CHAIN') as string; const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { if (nextReq.method !== 'POST') { diff --git a/stubs/history.ts b/stubs/history.ts deleted file mode 100644 index da99774bc1..0000000000 --- a/stubs/history.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { AccountHistoryResponse } from 'types/translateApi'; - -import { TRANSLATE } from './translate'; - -export const HISTORY: AccountHistoryResponse = { - hasNextPage: true, - items: [ TRANSLATE ], - pageNumber: 1, - pageSize: 10, -}; diff --git a/stubs/noves/Novestranslate.ts b/stubs/noves/Novestranslate.ts new file mode 100644 index 0000000000..9f43a7a904 --- /dev/null +++ b/stubs/noves/Novestranslate.ts @@ -0,0 +1,43 @@ +import type { NovesResponseData, NovesClassificationData, NovesRawTransactionData } from 'types/novesApi'; + +const NOVES_TRANSLATE_CLASSIFIED: NovesClassificationData = { + description: 'Sent 0.04 ETH', + received: [ { + action: 'Sent Token', + actionFormatted: 'Sent Token', + amount: '45', + from: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + to: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, + token: { + address: '', + name: 'ETH', + symbol: 'ETH', + decimals: 18, + }, + } ], + sent: [], + source: { + type: '', + }, + type: '0x2', + typeFormatted: 'Send NFT', +}; + +const NOVES_TRANSLATE_RAW: NovesRawTransactionData = { + blockNumber: 1, + fromAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + gas: 2, + gasPrice: 3, + timestamp: 20000, + toAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', + transactionFee: 2, + transactionHash: '0x128b79937a0eDE33258992c9668455f997f1aF24', +}; + +export const NOVES_TRANSLATE: NovesResponseData = { + accountAddress: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', + chain: 'base', + txTypeVersion: 2, + rawTransactionData: NOVES_TRANSLATE_RAW, + classificationData: NOVES_TRANSLATE_CLASSIFIED, +}; diff --git a/stubs/translate.ts b/stubs/translate.ts deleted file mode 100644 index 0c1d9c488c..0000000000 --- a/stubs/translate.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ResponseData } from 'types/translateApi'; - -import { TRANSLATE_CLASSIFIED } from './translateClassified'; -import { TRANSLATE_RAW } from './translateRaw'; - -export const TRANSLATE: ResponseData = { - accountAddress: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc', - chain: 'base', - txTypeVersion: 2, - rawTransactionData: TRANSLATE_RAW, - classificationData: TRANSLATE_CLASSIFIED, -}; diff --git a/stubs/translateClassified.ts b/stubs/translateClassified.ts deleted file mode 100644 index 494ed834e6..0000000000 --- a/stubs/translateClassified.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ClassificationData } from 'types/translateApi'; - -export const TRANSLATE_CLASSIFIED: ClassificationData = { - description: 'Sent 0.04 ETH', - received: [ { - action: 'Sent Token', - amount: '45', - from: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, - to: { name: '', address: '0xa0393A76b132526a70450273CafeceB45eea6dEE' }, - token: { - address: '', - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - } ], - sent: [], - source: { - type: '', - }, - type: '0x2', -}; diff --git a/stubs/translateRaw.ts b/stubs/translateRaw.ts deleted file mode 100644 index daba82675b..0000000000 --- a/stubs/translateRaw.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RawTransactionData } from 'types/translateApi'; - -export const TRANSLATE_RAW: RawTransactionData = { - blockNumber: 1, - fromAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', - gas: 2, - gasPrice: 3, - timestamp: 20000, - toAddress: '0xCFC123a23dfeD71bDAE054e487989d863C525C73', - transactionFee: 2, - transactionHash: '0x128b79937a0eDE33258992c9668455f997f1aF24', -}; diff --git a/types/novesApi.ts b/types/novesApi.ts new file mode 100644 index 0000000000..65f89fe9c8 --- /dev/null +++ b/types/novesApi.ts @@ -0,0 +1,91 @@ +export interface NovesResponseData { + txTypeVersion: number; + chain: string; + accountAddress: string; + classificationData: NovesClassificationData; + rawTransactionData: NovesRawTransactionData; +} + +export interface NovesClassificationData { + type: string; + typeFormatted: string; + description: string; + sent: Array; + received: Array; + source: { + type: string | null; + }; + message?: string; +} + +export interface NovesSentReceived { + action: string; + actionFormatted: string; + amount: string; + to: NovesTo; + from: NovesFrom; + token?: NovesToken; + nft?: NovesNft; +} + +export interface NovesToken { + symbol: string; + name: string; + decimals: number; + address: string; + id?: string; +} + +export interface NovesNft { + name: string; + id: string; + symbol: string; + address: string; +} + +export interface NovesFrom { + name: string | null; + address: string; +} + +export interface NovesTo { + name: string | null; + address: string; +} + +export interface NovesRawTransactionData { + transactionHash: string; + fromAddress: string; + toAddress: string; + blockNumber: number; + gas: number; + gasPrice: number; + transactionFee: NovesTransactionFee | number; + timestamp: number; +} + +export interface NovesTransactionFee { + amount: string; + currency: string; +} + +export interface NovesAccountHistoryResponse { + hasNextPage: boolean; + items: Array; + pageNumber: number; + pageSize: number; + nextPageUrl?: string; +} + +export const NovesHistorySentReceivedFilterValues = [ 'received', 'sent' ] as const; + +export type NovesHistorySentReceivedFilter = typeof NovesHistorySentReceivedFilterValues[number] | undefined; + +export interface NovesHistoryFilters { + filter?: NovesHistorySentReceivedFilter; +} + +export interface NovesDescribeResponse { + type: string; + description: string; +} diff --git a/types/translateApi.ts b/types/translateApi.ts deleted file mode 100644 index 61da855362..0000000000 --- a/types/translateApi.ts +++ /dev/null @@ -1,89 +0,0 @@ -export interface ResponseData { - txTypeVersion: number; - chain: string; - accountAddress: string; - classificationData: ClassificationData; - rawTransactionData: RawTransactionData; -} - -export interface ClassificationData { - type: string; - description: string; - sent: Array; - received: Array; - source: { - type: string | null; - }; - message?: string; -} - -export interface SentReceived { - action: string; - amount: string; - to: To; - from: From; - token?: Token; - nft?: Nft; -} - -export interface Token { - symbol: string; - name: string; - decimals: number; - address: string; - id?: string; -} - -export interface Nft { - name: string; - id: string; - symbol: string; - address: string; -} - -export interface From { - name: string | null; - address: string; -} - -export interface To { - name: string | null; - address: string; -} - -export interface RawTransactionData { - transactionHash: string; - fromAddress: string; - toAddress: string; - blockNumber: number; - gas: number; - gasPrice: number; - transactionFee: TransactionFee | number; - timestamp: number; -} - -export interface TransactionFee { - amount: string; - currency: string; -} - -export interface AccountHistoryResponse { - hasNextPage: boolean; - items: Array; - pageNumber: number; - pageSize: number; - nextPageUrl?: string; -} - -export const HistorySentReceivedFilterValues = [ 'received', 'sent' ] as const; - -export type HistorySentReceivedFilter = typeof HistorySentReceivedFilterValues[number] | undefined; - -export interface HistoryFilters { - filter?: HistorySentReceivedFilter; -} - -export interface DescribeResponse { - type: string; - description: string; -} diff --git a/ui/address/history/utils.ts b/ui/address/history/utils.ts deleted file mode 100644 index ec64971c1f..0000000000 --- a/ui/address/history/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AccountHistoryResponse } from 'types/translateApi'; -import type { ArrayElement } from 'types/utils'; - -export function generateHistoryStub( - stub: ArrayElement, - num = 50, - rest: Omit, -) { - return { - items: Array(num).fill(stub), - ...rest, - }; -} diff --git a/ui/address/history/AccountHistoryFilter.tsx b/ui/address/noves/NovesAccountHistoryFilter.tsx similarity index 87% rename from ui/address/history/AccountHistoryFilter.tsx rename to ui/address/noves/NovesAccountHistoryFilter.tsx index 322cb5bbc6..ef1879491e 100644 --- a/ui/address/history/AccountHistoryFilter.tsx +++ b/ui/address/noves/NovesAccountHistoryFilter.tsx @@ -8,7 +8,7 @@ import { } from '@chakra-ui/react'; import React from 'react'; -import type { HistorySentReceivedFilter } from 'types/translateApi'; +import type { NovesHistorySentReceivedFilter } from 'types/novesApi'; import Check from 'icons/check.svg'; import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; @@ -17,12 +17,12 @@ import FilterButton from 'ui/shared/filters/FilterButton'; interface Props { isActive: boolean; - defaultFilter: HistorySentReceivedFilter; + defaultFilter: NovesHistorySentReceivedFilter; onFilterChange: (nextValue: string | Array) => void; isLoading?: boolean; } -const AccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => { +const NovesAccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => { const { isOpen, onToggle } = useDisclosure(); const isInitialLoading = useIsInitialLoading(isLoading); @@ -41,7 +41,6 @@ const AccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoadi onClick={ onToggle } appliedFiltersNum={ isActive ? 1 : 0 } as="div" - border={ isOpen } /> @@ -85,4 +84,4 @@ const AccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoadi ); }; -export default React.memo(AccountHistoryFilter); +export default React.memo(NovesAccountHistoryFilter); diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/noves/NovesAddressAccountHistory.tsx similarity index 81% rename from ui/address/AddressAccountHistory.tsx rename to ui/address/noves/NovesAddressAccountHistory.tsx index 2556b090a1..a5645f6462 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/noves/NovesAddressAccountHistory.tsx @@ -5,46 +5,47 @@ import { AnimatePresence, motion } from 'framer-motion'; import { useRouter } from 'next/router'; import React from 'react'; -import type { HistorySentReceivedFilter } from 'types/translateApi'; -import { HistorySentReceivedFilterValues } from 'types/translateApi'; +import type { NovesHistorySentReceivedFilter } from 'types/novesApi'; +import { NovesHistorySentReceivedFilterValues } from 'types/novesApi'; import lightning from 'icons/lightning.svg'; import dayjs from 'lib/date/dayjs'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import { TRANSLATE } from 'stubs/translate'; -import { getFromTo } from 'ui/shared/accountHistory/FromToComponent'; +import { NOVES_TRANSLATE } from 'stubs/noves/Novestranslate'; import ActionBar from 'ui/shared/ActionBar'; import Icon from 'ui/shared/chakra/Icon'; import DataListDisplay from 'ui/shared/DataListDisplay'; import LinkInternal from 'ui/shared/LinkInternal'; +import NovesFromToComponent from 'ui/shared/Noves/NovesFromToComponent'; +import { NovesGetFromToValue } from 'ui/shared/Noves/utils'; import Pagination from 'ui/shared/pagination/Pagination'; -import AccountHistoryFilter from './history/AccountHistoryFilter'; -import useFetchHistoryWithPages from './history/useFetchHistoryWithPages'; -import { generateHistoryStub } from './history/utils'; +import NovesAccountHistoryFilter from './NovesAccountHistoryFilter'; +import useFetchHistoryWithPages from './NovesUseFetchHistoryWithPages'; +import { generateHistoryStub } from './utils'; dayjs.extend(utc); -const getFilterValue = (getFilterValueFromQuery).bind(null, HistorySentReceivedFilterValues); +const getFilterValue = (getFilterValueFromQuery).bind(null, NovesHistorySentReceivedFilterValues); type Props = { scrollRef?: React.RefObject; } -const AddressAccountHistory = ({ scrollRef }: Props) => { +const NovesAddressAccountHistory = ({ scrollRef }: Props) => { const router = useRouter(); const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); const addressColor = useColorModeValue('gray.500', 'whiteAlpha.600'); - const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); + const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); const { data, isError, pagination, isPlaceholderData } = useFetchHistoryWithPages({ address: currentAddress, scrollRef, options: { - placeholderData: generateHistoryStub(TRANSLATE, 10, { hasNextPage: true, pageSize: 10, pageNumber: 1 }), + placeholderData: generateHistoryStub(NOVES_TRANSLATE, 10, { hasNextPage: true, pageSize: 10, pageNumber: 1 }), }, }); @@ -56,7 +57,7 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { const actionBar = ( - { ); - const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? getFromTo(i, currentAddress, true) === filterValue : i); + const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? NovesGetFromToValue(i, currentAddress) === filterValue : i); const content = ( @@ -83,8 +84,8 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { display="flex" fontSize="xl" mr="5px" - color="#718096" - _dark={{ color: '#92a2bb' }} + color="gray.500" + _dark={{ color: 'gray.400' }} /> Action @@ -103,7 +104,7 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { - { getFromTo(tx, currentAddress) } + )) } @@ -149,8 +150,8 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { display="flex" fontSize="xl" mr="8px" - color="#718096" - _dark={{ color: '#92a2bb' }} + color="gray.500" + _dark={{ color: 'gray.400' }} /> @@ -162,7 +163,7 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { - { getFromTo(tx, currentAddress) } +
- + - { getActionFromTo(item, 50) } +
+ + + + + + + + + { filteredData?.map((item, i) => ( + + )) } + +
+ Age + + Action + + From/To +
+ + + ); + + return ( + <> + { /* should stay before tabs to scroll up with pagination */ } + + + + + ); +}; + +export default AddressAccountHistory; diff --git a/ui/address/AddressAccountHistoryFilter.tsx b/ui/address/AddressAccountHistoryFilter.tsx new file mode 100644 index 0000000000..d66519d635 --- /dev/null +++ b/ui/address/AddressAccountHistoryFilter.tsx @@ -0,0 +1,55 @@ +import { + Menu, + MenuButton, + MenuList, + MenuOptionGroup, + MenuItemOption, + useDisclosure, +} from '@chakra-ui/react'; +import React from 'react'; + +import type { NovesHistoryFilterValue } from 'types/api/noves'; + +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import FilterButton from 'ui/shared/filters/FilterButton'; + +interface Props { + isActive: boolean; + defaultFilter: NovesHistoryFilterValue; + onFilterChange: (nextValue: string | Array) => void; + isLoading?: boolean; +} + +const AccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => { + const { isOpen, onToggle } = useDisclosure(); + const isInitialLoading = useIsInitialLoading(isLoading); + + const onCloseMenu = React.useCallback(() => { + if (isOpen) { + onToggle(); + } + }, [ isOpen, onToggle ]); + + return ( + + + + + + + All + Received from + Sent to + + + + ); +}; + +export default React.memo(AccountHistoryFilter); diff --git a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx new file mode 100644 index 0000000000..143560ae2e --- /dev/null +++ b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx @@ -0,0 +1,60 @@ +import { Box, Flex, Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import dayjs from 'lib/date/dayjs'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/LinkInternal'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +type Props = { + isPlaceholderData: boolean; + tx: NovesResponseData; + currentAddress: string; +}; + +const AddressAccountHistoryListItem = (props: Props) => { + + return ( + + + + + + + + Action + + + + { dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() } + + + + + + { props.tx.classificationData.description } + + + + + + + + ); +}; + +export default React.memo(AddressAccountHistoryListItem); diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx new file mode 100644 index 0000000000..aab4cbf688 --- /dev/null +++ b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx @@ -0,0 +1,60 @@ +import { Td, Tr, Skeleton, Text, Box } from '@chakra-ui/react'; +import React from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import dayjs from 'lib/date/dayjs'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/LinkInternal'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +type Props = { + isPlaceholderData: boolean; + tx: NovesResponseData; + currentAddress: string; +}; + +const AddressAccountHistoryTableItem = (props: Props) => { + + return ( + + + + + { dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() } + + + + + + + + + + { props.tx.classificationData.description } + + + + + + + + + + + ); +}; + +export default React.memo(AddressAccountHistoryTableItem); diff --git a/ui/address/noves/NovesAccountHistoryFilter.tsx b/ui/address/noves/NovesAccountHistoryFilter.tsx deleted file mode 100644 index ef1879491e..0000000000 --- a/ui/address/noves/NovesAccountHistoryFilter.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { - Menu, - MenuButton, - MenuList, - MenuOptionGroup, - MenuItemOption, - useDisclosure, -} from '@chakra-ui/react'; -import React from 'react'; - -import type { NovesHistorySentReceivedFilter } from 'types/novesApi'; - -import Check from 'icons/check.svg'; -import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; -import Icon from 'ui/shared/chakra/Icon'; -import FilterButton from 'ui/shared/filters/FilterButton'; - -interface Props { - isActive: boolean; - defaultFilter: NovesHistorySentReceivedFilter; - onFilterChange: (nextValue: string | Array) => void; - isLoading?: boolean; -} - -const NovesAccountHistoryFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => { - const { isOpen, onToggle } = useDisclosure(); - const isInitialLoading = useIsInitialLoading(isLoading); - - const onCloseMenu = React.useCallback(() => { - if (isOpen) { - onToggle(); - } - }, [ isOpen, onToggle ]); - - return ( - - - - - - - - ) } - > - All - - - ) } - > - Received from - - - ) } - > - Sent to - - - - - ); -}; - -export default React.memo(NovesAccountHistoryFilter); diff --git a/ui/address/noves/NovesAddressAccountHistory.tsx b/ui/address/noves/NovesAddressAccountHistory.tsx deleted file mode 100644 index a5645f6462..0000000000 --- a/ui/address/noves/NovesAddressAccountHistory.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { Box, Hide, Show, Skeleton, StackDivider, Table, TableContainer, - Tbody, Td, Text, Th, Thead, Tr, VStack, useColorModeValue } from '@chakra-ui/react'; -import utc from 'dayjs/plugin/utc'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useRouter } from 'next/router'; -import React from 'react'; - -import type { NovesHistorySentReceivedFilter } from 'types/novesApi'; -import { NovesHistorySentReceivedFilterValues } from 'types/novesApi'; - -import lightning from 'icons/lightning.svg'; -import dayjs from 'lib/date/dayjs'; -import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import { NOVES_TRANSLATE } from 'stubs/noves/Novestranslate'; -import ActionBar from 'ui/shared/ActionBar'; -import Icon from 'ui/shared/chakra/Icon'; -import DataListDisplay from 'ui/shared/DataListDisplay'; -import LinkInternal from 'ui/shared/LinkInternal'; -import NovesFromToComponent from 'ui/shared/Noves/NovesFromToComponent'; -import { NovesGetFromToValue } from 'ui/shared/Noves/utils'; -import Pagination from 'ui/shared/pagination/Pagination'; - -import NovesAccountHistoryFilter from './NovesAccountHistoryFilter'; -import useFetchHistoryWithPages from './NovesUseFetchHistoryWithPages'; -import { generateHistoryStub } from './utils'; - -dayjs.extend(utc); - -const getFilterValue = (getFilterValueFromQuery).bind(null, NovesHistorySentReceivedFilterValues); - -type Props = { - scrollRef?: React.RefObject; -} - -const NovesAddressAccountHistory = ({ scrollRef }: Props) => { - const router = useRouter(); - - const currentAddress = getQueryParamString(router.query.hash).toLowerCase(); - - const addressColor = useColorModeValue('gray.500', 'whiteAlpha.600'); - - const [ filterValue, setFilterValue ] = React.useState(getFilterValue(router.query.filter)); - const { data, isError, pagination, isPlaceholderData } = useFetchHistoryWithPages({ - address: currentAddress, - scrollRef, - options: { - placeholderData: generateHistoryStub(NOVES_TRANSLATE, 10, { hasNextPage: true, pageSize: 10, pageNumber: 1 }), - }, - }); - - const handleFilterChange = React.useCallback((val: string | Array) => { - - const newVal = getFilterValue(val); - setFilterValue(newVal); - }, [ ]); - - const actionBar = ( - - - - - - ); - - const filteredData = isPlaceholderData ? data?.items : data?.items.filter(i => filterValue ? NovesGetFromToValue(i, currentAddress) === filterValue : i); - - const content = ( - - - }> - { filteredData?.map((tx, i) => ( - - - - - - - Action - - - - { dayjs(tx.rawTransactionData.timestamp * 1000).utc().fromNow() } - - - - - - - { tx.classificationData.description } - - - - - - - - )) } - - - - - - - - - - - - - - - - { filteredData?.map((tx, i) => ( - - - - - - )) } - - -
- Age - - Action - - From/To -
- - - { dayjs(tx.rawTransactionData.timestamp * 1000).utc().fromNow() } - - - - - - - - - { tx.classificationData.description } - - - - - - - - -
-
-
-
- ); - - return ( - <> - { /* should stay before tabs to scroll up with pagination */ } - - - - - ); -}; - -export default NovesAddressAccountHistory; diff --git a/ui/address/noves/NovesUseFetchHistory.tsx b/ui/address/noves/NovesUseFetchHistory.tsx deleted file mode 100644 index 6ae64586b3..0000000000 --- a/ui/address/noves/NovesUseFetchHistory.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; - -import type { NovesAccountHistoryResponse } from 'types/novesApi'; - -import config from 'configs/app'; -import type { Params as FetchParams } from 'lib/hooks/useFetch'; -import useFetch from 'lib/hooks/useFetch'; - -export interface ApiFetchParams { - queryParams?: Record | number | undefined>; - fetchParams?: Pick; -} - -export interface TranslateHistory { - payload?: { - address: string; - }; - status: Response['status']; - statusText: Response['statusText']; -} - -interface Params extends ApiFetchParams { - queryOptions?: Omit, 'queryKey' | 'queryFn'>; -} - -export default function NovesUseFetchHistory( - address: string | null, - page: number, - { queryOptions, queryParams }: Params = {}, -) { - const fetch = useFetch(); - - const url = new URL('/node-api/noves/history', config.app.baseUrl); - - queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { - // there are some pagination params that can be null or false for the next page - value !== undefined && value !== '' && url.searchParams.append(key, String(value)); - }); - - const body = { - address, - page, - }; - - return useQuery({ - queryKey: [ 'history', address, { ...queryParams }, body ], - queryFn: async() => { - // all errors and error typing is handled by react-query - // so error response will never go to the data - // that's why we are safe here to do type conversion "as Promise>" - if (!address) { - return undefined as unknown as NovesAccountHistoryResponse; - } - return fetch(url.toString(), { - method: 'POST', - body, - - }) as Promise; - }, - ...queryOptions, - }); -} diff --git a/ui/address/noves/NovesUseFetchHistoryWithPages.tsx b/ui/address/noves/NovesUseFetchHistoryWithPages.tsx deleted file mode 100644 index 7e42652771..0000000000 --- a/ui/address/noves/NovesUseFetchHistoryWithPages.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; -import { useQueryClient } from '@tanstack/react-query'; -import omit from 'lodash/omit'; -import { useRouter } from 'next/router'; -import queryString from 'querystring'; -import React, { useCallback } from 'react'; -import { animateScroll } from 'react-scroll'; - -import type { NovesAccountHistoryResponse, NovesHistoryFilters } from 'types/novesApi'; -import type { PaginationParams } from 'ui/shared/pagination/types'; - -import type { PaginatedResources, PaginationFilters, PaginationSorting } from 'lib/api/resources'; -import getQueryParamString from 'lib/router/getQueryParamString'; - -import type { TranslateHistory } from './NovesUseFetchHistory'; -import NovesUseFetchHistory from './NovesUseFetchHistory'; - -export interface Params { - address: string; - options?: Omit, 'queryKey' | 'queryFn'>; - filters?: NovesHistoryFilters; - sorting?: PaginationSorting; - scrollRef?: React.RefObject; -} - -type NextPageParams = Record; - -function getPaginationParamsFromQuery(queryString: string | Array | undefined) { - if (queryString) { - try { - return JSON.parse(decodeURIComponent(getQueryParamString(queryString))) as NextPageParams; - } catch (error) {} - } - - return {}; -} - -export type QueryWithPagesResult = -UseQueryResult & -{ - onFilterChange: (filters: PaginationFilters) => void; - pagination: PaginationParams; -} - -export default function NovesUseFetchHistoryWithPages({ - address, - filters, - options, - sorting, - scrollRef, -}: Params): QueryWithPagesResult { - const queryClient = useQueryClient(); - const router = useRouter(); - - const [ page, setPage ] = React.useState(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1); - const [ pageParams, setPageParams ] = React.useState>({ - [page]: getPaginationParamsFromQuery(router.query.next_page_params), - }); - const [ hasPages, setHasPages ] = React.useState(page > 1); - - const isMounted = React.useRef(false); - const canGoBackwards = React.useRef(!router.query.page); - const queryParams = React.useMemo(() => [ { ...pageParams[page], ...filters, ...sorting } ], [ pageParams, page, filters, sorting ])[0]; - - const scrollToTop = useCallback(() => { - scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 }); - }, [ scrollRef ]); - - const queryResult = NovesUseFetchHistory(address, page, { - queryParams, - queryOptions: { - staleTime: page === 1 ? 0 : Infinity, - ...options, - }, - }); - const { data } = queryResult; - - const queryKey = React.useMemo(() => [ 'history', address, page, { ...queryParams } ], [ address, page, queryParams ]); - - const onNextPageClick = useCallback(() => { - if (!data?.nextPageUrl) { - // we hide next page button if no next_page_params - return; - } - const pageQuery = data.nextPageUrl || ''; - const nextPageParams = queryString.parse(pageQuery.split('?').pop() || ''); - setPageParams((prev) => ({ - ...prev, - [page + 1]: nextPageParams as NextPageParams, - })); - setPage(prev => prev + 1); - - const nextPageQuery = { - ...router.query, - page: String(page + 1), - next_page_params: encodeURIComponent(JSON.stringify(nextPageParams)), - }; - - setHasPages(true); - scrollToTop(); - router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); - }, [ data?.nextPageUrl, page, router, scrollToTop ]); - - const onPrevPageClick = useCallback(() => { - // returning to the first page - // we dont have pagination params for the first page - let nextPageQuery: typeof router.query = { ...router.query }; - if (page === 2) { - nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]); - canGoBackwards.current = true; - } else { - nextPageQuery.next_page_params = encodeURIComponent(JSON.stringify(pageParams[page - 1])); - nextPageQuery.page = String(page - 1); - } - - scrollToTop(); - router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) - .then(() => { - setPage(prev => prev - 1); - page === 2 && queryClient.removeQueries({ queryKey }); - }); - }, [ pageParams, router, page, scrollToTop, queryClient, queryKey ]); - - const resetPage = useCallback(() => { - queryClient.removeQueries({ queryKey }); - - scrollToTop(); - const nextRouterQuery = omit(router.query, [ 'next_page_params', 'page' ]); - router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => { - queryClient.removeQueries({ queryKey }); - setPage(1); - setPageParams({}); - 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, type: 'inactive' }); - }, 100); - }); - }, [ queryClient, router, scrollToTop, queryKey ]); - - const onFilterChange = useCallback((newFilters: PaginationFilters | undefined) => { - const newQuery = omit(router.query, 'next_page_params', 'page'); - if (newFilters) { - Object.entries(newFilters).forEach(([ key, value ]) => { - if (value && value.length) { - newQuery[key] = Array.isArray(value) ? value.join(',') : (value || ''); - } - }); - } - scrollToTop(); - router.push( - { - pathname: router.pathname, - query: newQuery, - }, - undefined, - { shallow: true }, - ).then(() => { - setHasPages(false); - setPage(1); - setPageParams({}); - }); - }, [ router, scrollToTop ]); - - const hasNextPage = data?.hasNextPage ? data.hasNextPage : false; - - const pagination = { - page, - onNextPageClick, - onPrevPageClick, - resetPage, - hasPages, - hasNextPage, - canGoBackwards: canGoBackwards.current, - isLoading: queryResult.isPlaceholderData, - isVisible: hasPages || hasNextPage, - }; - - React.useEffect(() => { - if (page !== 1 && isMounted.current) { - queryClient.cancelQueries({ queryKey }); - setPage(1); - } - // hook should run only when address has changed - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ address ]); - - React.useEffect(() => { - window.setTimeout(() => { - isMounted.current = true; - }, 0); - }, []); - - return { ...queryResult, pagination, onFilterChange }; -} diff --git a/ui/address/noves/utils.ts b/ui/address/noves/utils.ts deleted file mode 100644 index 898d3929d9..0000000000 --- a/ui/address/noves/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NovesAccountHistoryResponse } from 'types/novesApi'; -import type { ArrayElement } from 'types/utils'; - -export function generateHistoryStub( - stub: ArrayElement, - num = 50, - rest: Omit, -) { - return { - items: Array(num).fill(stub), - ...rest, - }; -} diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index 9442b0c1e5..da41de033f 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -11,6 +11,7 @@ import useContractTabs from 'lib/hooks/useContractTabs'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; import getQueryParamString from 'lib/router/getQueryParamString'; import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address'; +import AddressAccountHistory from 'ui/address/AddressAccountHistory'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressContract from 'ui/address/AddressContract'; @@ -23,7 +24,6 @@ import AddressTxs from 'ui/address/AddressTxs'; import AddressWithdrawals from 'ui/address/AddressWithdrawals'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressQrCode from 'ui/address/details/AddressQrCode'; -import NovesAddressAccountHistory from 'ui/address/noves/NovesAddressAccountHistory'; import SolidityscanReport from 'ui/address/SolidityscanReport'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TextAd from 'ui/shared/ad/TextAd'; @@ -38,6 +38,8 @@ import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ]; +const feature = config.features.txInterpretation; + const AddressPageContent = () => { const router = useRouter(); const appProps = useAppContext(); @@ -73,12 +75,13 @@ const AddressPageContent = () => { count: addressTabsCountersQuery.data?.transactions_count, component: , }, - { - id: 'history', - title: 'Account history', - //count: , - component: , - }, + feature.isEnabled && feature.provider === 'noves' ? + { + id: 'account_history', + title: 'Account history', + component: , + } : + undefined, config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ? { id: 'withdrawals', diff --git a/ui/pages/Transaction.tsx b/ui/pages/Transaction.tsx index 95b7fe4baa..8c3b82b82b 100644 --- a/ui/pages/Transaction.tsx +++ b/ui/pages/Transaction.tsx @@ -7,7 +7,6 @@ import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/contexts/app'; import getQueryParamString from 'lib/router/getQueryParamString'; -import { NOVES_TRANSLATE } from 'stubs/noves/Novestranslate'; import { TX } from 'stubs/tx'; import TextAd from 'ui/shared/ad/TextAd'; import EntityTags from 'ui/shared/EntityTags'; @@ -15,10 +14,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; -import NovesTxAssetFlows from 'ui/tx/Noves/NovesTxAssetFlows'; -//import NovesTxTitleSecondRow from 'ui/tx/Noves/NovesTxTitleSecondRow'; -import NovesUseFetchTranslate from 'ui/tx/Noves/NovesUseFetchTranslate'; -import { NovesGetFlowCount } from 'ui/tx/Noves/utils/NovesGenerateFlowViewData'; +import TxAssetFlows from 'ui/tx/TxAssetFlows'; import TxDetails from 'ui/tx/TxDetails'; import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped'; import TxInternals from 'ui/tx/TxInternals'; @@ -28,20 +24,14 @@ import TxState from 'ui/tx/TxState'; import TxSubHeading from 'ui/tx/TxSubHeading'; import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; +const feature = config.features.txInterpretation; + const TransactionPageContent = () => { const router = useRouter(); const appProps = useAppContext(); const hash = getQueryParamString(router.query.hash); - const fetchTranslate = NovesUseFetchTranslate(hash, { - queryOptions: { - placeholderData: NOVES_TRANSLATE, - }, - }); - - const { data: translateData } = fetchTranslate; - const { data, isPlaceholderData } = useApiQuery('tx', { pathParams: { hash }, queryOptions: { @@ -52,8 +42,8 @@ const TransactionPageContent = () => { const tabs: Array = [ { id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: }, - config.features.noves.isEnabled ? - { id: 'asset_flows', title: 'Asset Flows', component: , count: NovesGetFlowCount(translateData) } : + feature.isEnabled && feature.provider === 'noves' ? + { id: 'asset_flows', title: 'Asset Flows', component: } : undefined, config.features.suave.isEnabled && data?.wrapped ? { id: 'wrapped', title: 'Regular tx details', component: } : @@ -87,7 +77,6 @@ const TransactionPageContent = () => { }; }, [ appProps.referrer ]); - //const titleSecondRow = ; const titleSecondRow = ; return ( diff --git a/ui/shared/Noves/NovesFromTo.tsx b/ui/shared/Noves/NovesFromTo.tsx new file mode 100644 index 0000000000..f2fd7153ba --- /dev/null +++ b/ui/shared/Noves/NovesFromTo.tsx @@ -0,0 +1,67 @@ +import { Box, Skeleton, Tag, TagLabel } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; + +import AddressEntity from '../entities/address/AddressEntity'; +import { getActionFromTo, getFromTo } from './utils'; + +interface Props { + isLoaded: boolean; + txData?: NovesResponseData; + currentAddress?: string; + item?: NovesFlowViewItem; +} + +const NovesFromTo: FC = ({ isLoaded, txData, currentAddress = '', item }) => { + const data = React.useMemo(() => { + if (txData) { + return getFromTo(txData, currentAddress); + } + if (item) { + return getActionFromTo(item); + } + + return { text: 'Sent to', address: '' }; + }, [ currentAddress, item, txData ]); + + const isSent = data.text.startsWith('Sent'); + + const addressObj = { hash: data.address || '', name: data.name || '' }; + + return ( + + + + + { data.text } + + Received from + + + + + + ); +}; + +export default NovesFromTo; diff --git a/ui/shared/Noves/NovesFromToComponent.tsx b/ui/shared/Noves/NovesFromToComponent.tsx deleted file mode 100644 index 0525845ba9..0000000000 --- a/ui/shared/Noves/NovesFromToComponent.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Box, Tag, TagLabel } from '@chakra-ui/react'; -import type { FC } from 'react'; -import React, { useEffect, useState } from 'react'; - -import type { NovesResponseData } from 'types/novesApi'; - -import type { NovesFlowViewItem } from 'ui/tx/Noves/utils/NovesGenerateFlowViewData'; - -import AddressEntity from '../entities/address/AddressEntity'; -import type { FromToData } from './utils'; -import { NovesGetActionFromTo, NovesGetFromTo } from './utils'; - -interface Props { - txData?: NovesResponseData; - currentAddress?: string; - item?: NovesFlowViewItem; -} - -const NovesFromToComponent: FC = ({ txData, currentAddress = '', item }) => { - const [ data, setData ] = useState({ text: 'Sent to', address: '' }); - - useEffect(() => { - let fromTo; - - if (txData) { - fromTo = NovesGetFromTo(txData, currentAddress); - setData(fromTo); - } else if (item) { - fromTo = NovesGetActionFromTo(item); - setData(fromTo); - } - }, [ currentAddress, item, txData ]); - - const isSent = data.text.startsWith('Sent'); - - const addressObj = { hash: data.address || '', name: data.name || '' }; - - return ( - - - - { data.text } - - Received from - - - - - - - ); -}; - -export default NovesFromToComponent; diff --git a/ui/shared/Noves/utils.test.ts b/ui/shared/Noves/utils.test.ts new file mode 100644 index 0000000000..3a80a7cd28 --- /dev/null +++ b/ui/shared/Noves/utils.test.ts @@ -0,0 +1,53 @@ +import * as transactionMock from 'mocks/noves/transaction'; +import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; + +import { getActionFromTo, getFromTo, getFromToValue } from './utils'; + +it('get data for FromTo component from transaction', async() => { + const result = getFromTo(transactionMock.transaction, transactionMock.transaction.accountAddress); + + expect(result).toEqual({ + text: 'Sent to', + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + }); +}); + +it('get what type of FromTo component will be', async() => { + const result = getFromToValue(transactionMock.transaction, transactionMock.transaction.accountAddress); + + expect(result).toEqual('sent'); +}); + +it('get data for FromTo component from flow item', async() => { + const item: NovesFlowViewItem = { + action: { + label: 'Sent', + amount: '3000', + flowDirection: 'toRight', + nft: undefined, + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + rightActor: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + leftActor: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', + }; + + const result = getActionFromTo(item); + + expect(result).toEqual({ + text: 'Sent to', + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }); +}); diff --git a/ui/shared/Noves/utils.ts b/ui/shared/Noves/utils.ts index 0017d6387d..9e0bf88677 100644 --- a/ui/shared/Noves/utils.ts +++ b/ui/shared/Noves/utils.ts @@ -1,6 +1,6 @@ -import type { NovesResponseData, NovesSentReceived } from 'types/novesApi'; +import type { NovesResponseData, NovesSentReceived } from 'types/api/noves'; -import type { NovesFlowViewItem } from 'ui/tx/Noves/utils/NovesGenerateFlowViewData'; +import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; export interface FromToData { text: string; @@ -8,7 +8,7 @@ export interface FromToData { name?: string | null; } -export const NovesGetFromTo = (txData: NovesResponseData, currentAddress: string): FromToData => { +export const getFromTo = (txData: NovesResponseData, currentAddress: string): FromToData => { const raw = txData.rawTransactionData; const sent = txData.classificationData.sent; let sentFound: Array = []; @@ -22,7 +22,7 @@ export const NovesGetFromTo = (txData: NovesResponseData, currentAddress: string let receivedFound: Array = []; if (received && received[0]) { receivedFound = received - .filter((received) => received.to.address.toLocaleLowerCase() === currentAddress) + .filter((received) => received.to.address?.toLocaleLowerCase() === currentAddress) .filter((received) => received.from.address); } @@ -37,14 +37,18 @@ export const NovesGetFromTo = (txData: NovesResponseData, currentAddress: string } } if (sentFound.length > receivedFound.length) { - return { text: 'Sent to', address: sentFound[0].to.address } ; + // already filtered if null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { text: 'Sent to', address: sentFound[0].to.address! } ; } else { return { text: 'Received from', address: receivedFound[0].from.address } ; } } if (sent && sentFound[0]) { - return { text: 'Sent to', address: sentFound[0].to.address } ; + // already filtered if null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return { text: 'Sent to', address: sentFound[0].to.address! } ; } if (received && receivedFound[0]) { @@ -70,19 +74,16 @@ export const NovesGetFromTo = (txData: NovesResponseData, currentAddress: string return { text: 'Sent to', address: currentAddress }; }; -export const NovesGetFromToValue = (txData: NovesResponseData, currentAddress: string) => { - const fromTo = NovesGetFromTo(txData, currentAddress); +export const getFromToValue = (txData: NovesResponseData, currentAddress: string) => { + const fromTo = getFromTo(txData, currentAddress); return fromTo.text.split(' ').shift()?.toLowerCase(); }; -export const NovesGetActionFromTo = (item: NovesFlowViewItem): FromToData => { - if (item.action.flowDirection === 'toRight') { - return { - text: 'Sent to', address: item.rightActor.address, name: item.rightActor.name, - }; - } +export const getActionFromTo = (item: NovesFlowViewItem): FromToData => { return { - text: 'Received from', address: item.rightActor.address, name: item.rightActor.name, + text: item.action.flowDirection === 'toRight' ? 'Sent to' : 'Received from', + address: item.rightActor.address, + name: item.rightActor.name, }; }; diff --git a/ui/tx/Noves/NovesTxAssetFlows.tsx b/ui/tx/Noves/NovesTxAssetFlows.tsx deleted file mode 100644 index fe76ca7f81..0000000000 --- a/ui/tx/Noves/NovesTxAssetFlows.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Box, Skeleton, Text, Show, Hide, VStack, StackDivider, Divider } from '@chakra-ui/react'; -import type { UseQueryResult } from '@tanstack/react-query'; -import _ from 'lodash'; -import React, { useState } from 'react'; - -import type { NovesResponseData } from 'types/novesApi'; -import type { PaginationParams } from 'ui/shared/pagination/types'; - -import lightning from 'icons/lightning.svg'; -import ActionBar from 'ui/shared/ActionBar'; -import Icon from 'ui/shared/chakra/Icon'; -import DataListDisplay from 'ui/shared/DataListDisplay'; -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import NovesFromToComponent from 'ui/shared/Noves/NovesFromToComponent'; -import Pagination from 'ui/shared/pagination/Pagination'; -import type { NovesTranslateError } from 'ui/tx/Noves/NovesUseFetchTranslate'; - -import NovesActionCard from './components/NovesActionCard'; -import { NovesGenerateFlowViewData } from './utils/NovesGenerateFlowViewData'; - -interface FlowViewProps { - data: UseQueryResult; -} - -export default function NovesTxAssetFlows(props: FlowViewProps) { - const { data: queryData, isPlaceholderData, isError } = props.data; - - const [ page, setPage ] = useState(1); - - const ViewData = queryData ? NovesGenerateFlowViewData(queryData) : []; - const chunkedViewData = _.chunk(ViewData, 10); - - const paginationProps: PaginationParams = { - onNextPageClick: () => setPage(page + 1), - onPrevPageClick: () => setPage(page - 1), - resetPage: () => setPage(1), - canGoBackwards: true, - isLoading: isPlaceholderData, - page: page, - hasNextPage: Boolean(chunkedViewData[page]), - hasPages: true, - isVisible: true, - }; - - const data = chunkedViewData [page - 1]; - - const actionBar = ( - - - - - Wallet - - - - - - - - - - - - ); - - const content = ( - -
- - - { data?.length && } - }> - { data?.map((item, i) => ( - - - - - - - Action - - - - - - - - - - - - )) } - - - - - - - - - - - - - - { data?.map((item, i) => ( - - - - - )) } - -
- Actions - - From/To -
- - - - - - - -
-
-
-
-
-
- ); - - return ( - - ); -} diff --git a/ui/tx/Noves/NovesTxTitleSecondRow.tsx b/ui/tx/Noves/NovesTxTitleSecondRow.tsx deleted file mode 100644 index b45ae0cec5..0000000000 --- a/ui/tx/Noves/NovesTxTitleSecondRow.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Box, Skeleton, Text } from '@chakra-ui/react'; -import type { UseQueryResult } from '@tanstack/react-query'; -import type { FC } from 'react'; -import React from 'react'; - -import type { NovesResponseData } from 'types/novesApi'; - -import lightning from 'icons/lightning.svg'; -import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; -import Icon from 'ui/shared/chakra/Icon'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; -import TxEntity from 'ui/shared/entities/tx/TxEntity'; -import NetworkExplorers from 'ui/shared/NetworkExplorers'; -import NovesTokenTransferSnippet from 'ui/tx/Noves/components/NovesTokenTransferSnippet'; -import type { NovesTranslateError } from 'ui/tx/Noves/NovesUseFetchTranslate'; -import { NovesGetSplittedDescription } from 'ui/tx/Noves/utils/NovesGetSplittedDescription'; - -interface Props { - fetchTranslate: UseQueryResult; - hash: string; - txTag: string | null | undefined; -} - -const NovesTxTitleSecondRow: FC = ({ fetchTranslate, hash, txTag }) => { - - const { data, isError, isPlaceholderData } = fetchTranslate; - - if (isPlaceholderData || isError || !data?.classificationData.description) { - return ( - - - { txTag && } - - - ); - } - - const description = NovesGetSplittedDescription(data); - - return ( - - - { description.map((item, i) => ( - <> - - { i === 0 && ( - - ) } - { item.text } - - { item.hasId && item.token ? ( - - ) : - item.token && ( - - ) } - - )) } - - - ); -}; - -export default NovesTxTitleSecondRow; diff --git a/ui/tx/Noves/NovesUseFetchTranslate.tsx b/ui/tx/Noves/NovesUseFetchTranslate.tsx deleted file mode 100644 index 7a46a13a94..0000000000 --- a/ui/tx/Noves/NovesUseFetchTranslate.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; - -import type { NovesResponseData } from 'types/novesApi'; - -import config from 'configs/app'; -import type { Params as FetchParams } from 'lib/hooks/useFetch'; - -import useFetch from '../../../lib/hooks/useFetch'; - -export interface ApiFetchParams { - queryParams?: Record | number | undefined>; - fetchParams?: Pick; -} - -export interface NovesTranslateError { - payload?: { - txHash: string; - }; - status: Response['status']; - statusText: Response['statusText']; -} - -interface Params extends ApiFetchParams { - queryOptions?: Omit, 'queryKey' | 'queryFn'>; -} - -export default function NovesUseFetchTranslate( - txHash: string | null, - { queryOptions }: Params = {}, -) { - const fetch = useFetch(); - - const url = new URL('/node-api/noves/translate', config.app.baseUrl); - - const body = { - txHash, - }; - - return useQuery({ - queryKey: [ 'translate', txHash, body ], - queryFn: async() => { - // all errors and error typing is handled by react-query - // so error response will never go to the data - // that's why we are safe here to do type conversion "as Promise>" - if (!txHash) { - return undefined as unknown as NovesResponseData; - } - return fetch(url.toString(), { - method: 'POST', - body, - }) as Promise; - }, - ...queryOptions, - }); -} diff --git a/ui/tx/Noves/utils/NovesGetSplittedDescription.ts b/ui/tx/Noves/utils/NovesGetSplittedDescription.ts deleted file mode 100644 index 2b1aaa48d6..0000000000 --- a/ui/tx/Noves/utils/NovesGetSplittedDescription.ts +++ /dev/null @@ -1,180 +0,0 @@ -import _ from 'lodash'; - -import type { NovesResponseData } from 'types/novesApi'; - -interface TokensData { - nameList: Array; - symbolList: Array; - idList: Array; - names: { - [x: string]: { - name: string | undefined; - symbol: string | undefined; - address: string | undefined; - id?: string | undefined; - }; - }; - symbols: { - [x: string]: { - name: string | undefined; - symbol: string | undefined; - address: string | undefined; - id: string | undefined; - }; - }; -} - -function getTokensData(data: NovesResponseData): TokensData { - const sent = data.classificationData.sent || []; - const received = data.classificationData.received || []; - - const txItems = [ ...sent, ...received ]; - - const tokens = txItems.map((item) => { - const name = item.nft?.name || item.token?.name; - const symbol = item.nft?.symbol || item.token?.symbol; - - const token = { - name: name, - symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol, - address: item.nft?.address || item.token?.address, - id: item.nft?.id || item.token?.id, - }; - - return token; - }); - - const tokensGroupByname = _.groupBy(tokens, 'name'); - const tokensGroupBySymbol = _.groupBy(tokens, 'symbol'); - const tokensGroupById = _.groupBy(tokens, 'id'); - - const mappedNames = _.mapValues(tokensGroupByname, (i) => { - return i[0]; - }); - - const mappedSymbols = _.mapValues(tokensGroupBySymbol, (i) => { - return i[0]; - }); - - const mappedIds = _.mapValues(tokensGroupById, (i) => { - return i[0]; - }); - - const nameList = _.keysIn(mappedNames).filter(i => i !== 'undefined'); - const symbolList = _.keysIn(mappedSymbols).filter(i => i !== 'undefined'); - const idList = _.keysIn(mappedIds).filter(i => i !== 'undefined'); - - return { - nameList, - symbolList, - idList, - names: mappedNames, - symbols: mappedSymbols, - }; -} - -export const NovesGetSplittedDescription = (translateData: NovesResponseData) => { - const description = translateData.classificationData.description; - const removeEndDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; - let parsedDescription = ' ' + removeEndDot; - const tokenData = getTokensData(translateData); - - const idsMatched = tokenData.idList.filter(id => parsedDescription.includes(`#${ id }`)); - const namesMatched = tokenData.nameList.filter(name => parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); - let symbolsMatched = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); - - symbolsMatched = symbolsMatched.filter(symbol => !namesMatched.includes(tokenData.symbols[symbol]?.name || '')); - - let indicesSorted: Array = []; - let namesMapped; - let symbolsMapped; - - if (idsMatched.length) { - namesMatched.forEach(name => { - const hasId = idsMatched.includes(tokenData.names[name].id || ''); - if (hasId) { - parsedDescription = parsedDescription.replaceAll(`#${ tokenData.names[name].id }`, ''); - } - }); - - symbolsMatched.forEach(name => { - const hasId = idsMatched.includes(tokenData.symbols[name].id || ''); - if (hasId) { - parsedDescription = parsedDescription.replaceAll(`#${ tokenData.symbols[name].id }`, ''); - } - }); - } - - if (namesMatched.length) { - namesMapped = namesMatched.map(name => { - const searchString = ` ${ name.toUpperCase() }`; - let hasId = false; - - if (idsMatched.length) { - hasId = idsMatched.includes(tokenData.names[name].id || ''); - } - - return { - name, - hasId, - indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, - token: tokenData.names[name], - }; - }); - - namesMapped.forEach(i => indicesSorted.push(...i.indices)); - } - - if (symbolsMatched.length) { - symbolsMapped = symbolsMatched.map(name => { - const searchString = ` ${ name.toUpperCase() }`; - let hasId = false; - - if (idsMatched.length) { - hasId = idsMatched.includes(tokenData.symbols[name].id || ''); - } - - return { - name, - hasId, - indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, - token: tokenData.symbols[name], - }; - }); - - symbolsMapped.forEach(i => indicesSorted.push(...i.indices)); - } - - indicesSorted = _.uniq(indicesSorted.sort((a, b) => a - b)); - - const tokenWithIndices = _.uniqBy(_.concat(namesMapped, symbolsMapped), 'name'); - - const descriptionSplitted = indicesSorted.map((a, i) => { - const item = tokenWithIndices.find(t => t?.indices.includes(a)); - - if (i === 0) { - return { - token: item?.token, - text: parsedDescription.substring(0, a), - hasId: item?.hasId, - }; - } else { - const startIndex = indicesSorted[i - 1] + (tokenWithIndices.find(t => t?.indices.includes(indicesSorted[i - 1]))?.name.length || 0); - return { - token: item?.token, - text: parsedDescription.substring(startIndex + 1, a), - hasId: item?.hasId, - }; - } - }); - - const lastIndex = indicesSorted[indicesSorted.length - 1]; - const startIndex = lastIndex + (tokenWithIndices.find(t => t?.indices.includes(lastIndex))?.name.length || 0); - const restString = parsedDescription.substring(startIndex + 1); - - if (restString) { - descriptionSplitted.push({ text: restString, token: undefined, hasId: false }); - } - - return descriptionSplitted; -}; diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/TxAssetFlows.tsx new file mode 100644 index 0000000000..5411e25026 --- /dev/null +++ b/ui/tx/TxAssetFlows.tsx @@ -0,0 +1,122 @@ +import { Table, Thead, Tbody, Tr, Th, TableContainer, Box, Skeleton, Text, Show, Hide, Divider } from '@chakra-ui/react'; +import _ from 'lodash'; +import React, { useState } from 'react'; + +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { NOVES_TRANSLATE } from 'stubs/noves/novestranslate'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import Pagination from 'ui/shared/pagination/Pagination'; + +import TxAssetFlowsListItem from './assetFlows/TxAssetFlowsListItem'; +import TxAssetFlowsTableItem from './assetFlows/TxAssetFlowsTableItem'; +import { generateFlowViewData } from './assetFlows/utils/generateFlowViewData'; + +interface FlowViewProps { + hash: string; +} + +export default function TxAssetFlows(props: FlowViewProps) { + + const { data: queryData, isPlaceholderData, isError } = useApiQuery('noves_transaction', { + pathParams: { hash: props.hash }, + queryOptions: { + enabled: Boolean(props.hash), + placeholderData: NOVES_TRANSLATE, + }, + }); + + const [ page, setPage ] = useState(1); + + const ViewData = queryData ? generateFlowViewData(queryData) : []; + const chunkedViewData = _.chunk(ViewData, 10); + + const paginationProps: PaginationParams = { + onNextPageClick: () => setPage(page + 1), + onPrevPageClick: () => setPage(page - 1), + resetPage: () => setPage(1), + canGoBackwards: true, + isLoading: isPlaceholderData, + page: page, + hasNextPage: Boolean(chunkedViewData[page]), + hasPages: true, + isVisible: true, + }; + + const data = chunkedViewData [page - 1]; + + const actionBar = ( + + + + + Wallet + + + + + + + + ); + + const content = ( + <> + + { data?.length && } + + { data?.map((item, i) => ( + + )) } + + + + + + + + + + + + + { data?.map((item, i) => ( + + )) } + +
+ Actions + + From/To +
+
+
+ + ); + + return ( + + ); +} diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 477aeddef6..12ca3a1463 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -3,6 +3,7 @@ import React from 'react'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; +import { NOVES_TRANSLATE } from 'stubs/noves/novestranslate'; import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TxEntity from 'ui/shared/entities/tx/TxEntity'; @@ -10,38 +11,64 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers'; import { TX_ACTIONS_BLOCK_ID } from 'ui/tx/details/txDetailsActions/TxDetailsActionsWrapper'; import TxInterpretation from 'ui/tx/interpretation/TxInterpretation'; +import NovesSubHeadingInterpretation from './assetFlows/components/NovesSubHeadingInterpretation'; + type Props = { hash?: string; hasTag: boolean; } +const feature = config.features.txInterpretation; + const TxSubHeading = ({ hash, hasTag }: Props) => { - const hasInterpretationFeature = config.features.txInterpretation.isEnabled; + const hasInterpretationFeature = feature.isEnabled; + const isNovesInterpretation = hasInterpretationFeature && feature.provider === 'noves'; const txInterpretationQuery = useApiQuery('tx_interpretation', { pathParams: { hash }, queryOptions: { - enabled: Boolean(hash) && hasInterpretationFeature, + enabled: Boolean(hash) && (hasInterpretationFeature && !isNovesInterpretation), placeholderData: TX_INTERPRETATION, }, }); - const hasInterpretation = hasInterpretationFeature && + const novesInterpretationQuery = useApiQuery('noves_transaction', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash) && isNovesInterpretation, + placeholderData: NOVES_TRANSLATE, + }, + }); + + const hasNovesInterpretation = isNovesInterpretation && + (novesInterpretationQuery.isPlaceholderData || Boolean(novesInterpretationQuery.data?.classificationData.description)); + + const hasInternalInterpretation = (hasInterpretationFeature && !isNovesInterpretation) && (txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length)); + const hasInterpretation = hasNovesInterpretation || hasInternalInterpretation; + return ( - { hasInterpretation && ( - - - { !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1 && + ) : + ( + + + { !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1 && all actions } - - ) } + + ) + } { !hasInterpretation && } { !hasTag && } diff --git a/ui/tx/assetFlows/TxAssetFlowsListItem.tsx b/ui/tx/assetFlows/TxAssetFlowsListItem.tsx new file mode 100644 index 0000000000..9a9b491248 --- /dev/null +++ b/ui/tx/assetFlows/TxAssetFlowsListItem.tsx @@ -0,0 +1,46 @@ +import { Box, Skeleton, Text } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +import NovesActionSnippet from './components/NovesActionSnippet'; +import type { NovesFlowViewItem } from './utils/generateFlowViewData'; + +type Props = { + isPlaceholderData: boolean; + item: NovesFlowViewItem; +}; + +const TxAssetFlowsListItem = (props: Props) => { + + return ( + + + + + + + + Action + + + + + + + + + + + + ); +}; + +export default React.memo(TxAssetFlowsListItem); diff --git a/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx b/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx new file mode 100644 index 0000000000..efe617edcf --- /dev/null +++ b/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx @@ -0,0 +1,28 @@ +import { Td, Tr } from '@chakra-ui/react'; +import React from 'react'; + +import NovesFromTo from 'ui/shared/Noves/NovesFromTo'; + +import NovesActionSnippet from './components/NovesActionSnippet'; +import type { NovesFlowViewItem } from './utils/generateFlowViewData'; + +type Props = { + isPlaceholderData: boolean; + item: NovesFlowViewItem; +}; + +const TxAssetFlowsTableItem = (props: Props) => { + + return ( + + + + + + + + + ); +}; + +export default React.memo(TxAssetFlowsTableItem); diff --git a/ui/tx/Noves/components/NovesActionCard.tsx b/ui/tx/assetFlows/components/NovesActionSnippet.tsx similarity index 67% rename from ui/tx/Noves/components/NovesActionCard.tsx rename to ui/tx/assetFlows/components/NovesActionSnippet.tsx index 773482574f..d9bbc6dd8f 100644 --- a/ui/tx/Noves/components/NovesActionCard.tsx +++ b/ui/tx/assetFlows/components/NovesActionSnippet.tsx @@ -1,22 +1,24 @@ -import { Box, Hide, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Show, Text, useColorModeValue } from '@chakra-ui/react'; +import { Box, Hide, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Show, Skeleton, Text, useColorModeValue } from '@chakra-ui/react'; import type { FC } from 'react'; import React from 'react'; -import lightning from 'icons/lightning.svg'; -import Icon from 'ui/shared/chakra/Icon'; import TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import IconSvg from 'ui/shared/IconSvg'; -import type { NovesAction, NovesFlowViewItem } from '../utils/NovesGenerateFlowViewData'; -import NovesTokensCard from './NovesTokensCard'; +import type { NovesFlowViewItem } from '../utils/generateFlowViewData'; +import NovesTokenTooltipContent from './NovesTokenTooltipContent'; interface Props { item: NovesFlowViewItem; + isLoaded: boolean; } -const NovesActionCard: FC = ({ item }) => { +const NovesActionSnippet: FC = ({ item, isLoaded }) => { const popoverBg = useColorModeValue('gray.700', 'gray.300'); - const getTokenData = (action: NovesAction) => { + const token = React.useMemo(() => { + const action = item.action; + const name = action.nft?.name || action.token?.name; const symbol = action.nft?.symbol || action.token?.symbol; @@ -27,10 +29,12 @@ const NovesActionCard: FC = ({ item }) => { }; return token; - }; + }, [ item.action ]); + + const validTokenAddress = token.address ? Boolean(token.address.match(/^0x[a-fA-F\d]{40}$/)) : false; return ( - <> + @@ -40,10 +44,10 @@ const NovesActionCard: FC = ({ item }) => { { item.action.amount } = ({ item }) => { > - @@ -78,10 +81,10 @@ const NovesActionCard: FC = ({ item }) => { { item.action.amount } = ({ item }) => { shadow="lg" width="fit-content" zIndex="modal" + padding={ 2 } > - - - - + - + ); }; -export default React.memo(NovesActionCard); +export default React.memo(NovesActionSnippet); diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx new file mode 100644 index 0000000000..2d77792e73 --- /dev/null +++ b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx @@ -0,0 +1,65 @@ +import { Box, Skeleton, Text } from '@chakra-ui/react'; +import type { FC } from 'react'; +import React, { Fragment } from 'react'; + +import type { NovesResponseData } from 'types/api/noves'; + +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import IconSvg from 'ui/shared/IconSvg'; +import { getDescriptionItems } from 'ui/tx/assetFlows/utils/getDescriptionItems'; + +import NovesTokenTransferSnippet from './NovesTokenTransferSnippet'; + +interface Props { + data: NovesResponseData | undefined; + isLoading: boolean; +} + +const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { + if (!data) { + return null; + } + + const description = getDescriptionItems(data); + + return ( + + + { description.map((item, i) => ( + + + { i === 0 && ( + + ) } + { item.text } + + { item.hasId && item.token ? ( + + ) : + item.token && ( + + ) } + + )) } + + + ); +}; + +export default NovesSubHeadingInterpretation; diff --git a/ui/tx/Noves/components/NovesTokensCard.tsx b/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx similarity index 87% rename from ui/tx/Noves/components/NovesTokensCard.tsx rename to ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx index c3c4818904..d92b5a5d6c 100644 --- a/ui/tx/Noves/components/NovesTokensCard.tsx +++ b/ui/tx/assetFlows/components/NovesTokenTooltipContent.tsx @@ -2,7 +2,7 @@ import { Box, Text, useColorModeValue } from '@chakra-ui/react'; import type { FC } from 'react'; import React from 'react'; -import type { NovesNft, NovesToken } from 'types/novesApi'; +import type { NovesNft, NovesToken } from 'types/api/noves'; import CopyToClipboard from 'ui/shared/CopyToClipboard'; @@ -11,7 +11,7 @@ interface Props { token: NovesToken | NovesNft | undefined; } -const NovesTokensCard: FC = ({ token, amount }) => { +const NovesTokenTooltipContent: FC = ({ token, amount }) => { const textColor = useColorModeValue('white', 'blackAlpha.900'); if (!token) { @@ -51,4 +51,4 @@ const NovesTokensCard: FC = ({ token, amount }) => { ); }; -export default React.memo(NovesTokensCard); +export default React.memo(NovesTokenTooltipContent); diff --git a/ui/tx/Noves/components/NovesTokenTransferSnippet.tsx b/ui/tx/assetFlows/components/NovesTokenTransferSnippet.tsx similarity index 100% rename from ui/tx/Noves/components/NovesTokenTransferSnippet.tsx rename to ui/tx/assetFlows/components/NovesTokenTransferSnippet.tsx diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.test.ts b/ui/tx/assetFlows/utils/generateFlowViewData.test.ts new file mode 100644 index 0000000000..58508ef8ee --- /dev/null +++ b/ui/tx/assetFlows/utils/generateFlowViewData.test.ts @@ -0,0 +1,56 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { generateFlowViewData } from './generateFlowViewData'; + +it('creates asset flows items', async() => { + const result = generateFlowViewData(transactionMock.transaction); + + expect(result).toEqual( + [ + { + action: { + label: 'Sent', + amount: '3000', + flowDirection: 'toRight', + token: { + address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', + decimals: 18, + name: 'PQR-Test', + symbol: 'PQR', + }, + }, + rightActor: { + address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', + name: null, + }, + leftActor: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', + }, + { + action: { + label: 'Paid Gas', + amount: '0.000395521502109448', + flowDirection: 'toRight', + token: { + address: 'ETH', + decimals: 18, + name: 'ETH', + symbol: 'ETH', + }, + }, + rightActor: { + address: '', + name: 'Validators', + }, + leftActor: { + address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', + name: 'This wallet', + }, + accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', + }, + ], + ); +}); diff --git a/ui/tx/Noves/utils/NovesGenerateFlowViewData.ts b/ui/tx/assetFlows/utils/generateFlowViewData.ts similarity index 84% rename from ui/tx/Noves/utils/NovesGenerateFlowViewData.ts rename to ui/tx/assetFlows/utils/generateFlowViewData.ts index dfed45f48d..b5d0825603 100644 --- a/ui/tx/Noves/utils/NovesGenerateFlowViewData.ts +++ b/ui/tx/assetFlows/utils/generateFlowViewData.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/novesApi'; +import type { NovesNft, NovesResponseData, NovesSentReceived, NovesToken } from 'types/api/noves'; export interface NovesAction { label: string; @@ -13,7 +13,7 @@ export interface NovesAction { export interface NovesFlowViewItem { action: NovesAction; rightActor: { - address: string; + address: string ; name: string | null; }; leftActor: { @@ -23,7 +23,7 @@ export interface NovesFlowViewItem { accountAddress: string; } -export function NovesGenerateFlowViewData(data: NovesResponseData): Array { +export function generateFlowViewData(data: NovesResponseData): Array { const perspectiveAddress = data.accountAddress.toLowerCase(); const sent = data.classificationData.sent || []; @@ -40,7 +40,7 @@ export function NovesGenerateFlowViewData(data: NovesResponseData): Array { const action = { - label: item.actionFormatted, + label: item.actionFormatted || item.action, amount: item.amount || undefined, flowDirection: getFlowDirection(item, perspectiveAddress), nft: item.nft || undefined, @@ -67,7 +67,7 @@ export function NovesGenerateFlowViewData(data: NovesResponseData): Array { - if (!data) { - return 0; - } - return NovesGenerateFlowViewData(data).length; -}; diff --git a/ui/tx/assetFlows/utils/getDescriptionItems.test.ts b/ui/tx/assetFlows/utils/getDescriptionItems.test.ts new file mode 100644 index 0000000000..5685b64d44 --- /dev/null +++ b/ui/tx/assetFlows/utils/getDescriptionItems.test.ts @@ -0,0 +1,15 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { getDescriptionItems } from './getDescriptionItems'; + +it('creates sub heading items to render', async() => { + const result = getDescriptionItems(transactionMock.transaction); + + expect(result).toEqual([ + { + text: ' Called function \'stake\' on contract 0xef326CdAdA59D3A740A76bB5f4F88Fb2', + token: undefined, + hasId: false, + }, + ]); +}); diff --git a/ui/tx/assetFlows/utils/getDescriptionItems.ts b/ui/tx/assetFlows/utils/getDescriptionItems.ts new file mode 100644 index 0000000000..7780eea66d --- /dev/null +++ b/ui/tx/assetFlows/utils/getDescriptionItems.ts @@ -0,0 +1,177 @@ +import _ from 'lodash'; + +import type { NovesResponseData } from 'types/api/noves'; + +import type { TokensData } from './getTokensData'; +import { getTokensData } from './getTokensData'; + +interface TokenWithIndices { + name: string; + hasId: boolean; + indices: Array; + token: { + name: string | undefined; + symbol: string | undefined; + address: string | undefined; + id?: string | undefined; + }; +} + +export interface DescriptionItems { + token: { + name: string | undefined; + symbol: string | undefined; + address: string | undefined; + id?: string | undefined; + } | undefined; + text: string; + hasId: boolean | undefined; +} + +export const getDescriptionItems = (translateData: NovesResponseData): Array => { + + // Remove final dot and add space at the start to avoid matching issues + const description = translateData.classificationData.description; + const removedFinalDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; + let parsedDescription = ' ' + removedFinalDot; + const tokenData = getTokensData(translateData); + + const idsMatched = tokenData.idList.filter(id => parsedDescription.includes(`#${ id }`)); + const tokensMatchedByName = tokenData.nameList.filter(name => parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); + let tokensMatchedBySymbol = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); + + // Filter symbols if they're already matched by name + tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); + + const indices: Array = []; + let tokensByName; + let tokensBySymbol; + + if (idsMatched.length) { + parsedDescription = removeIds(tokensMatchedByName, tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); + } + + if (tokensMatchedByName.length) { + tokensByName = parseTokensByName(tokensMatchedByName, idsMatched, tokenData, parsedDescription); + + tokensByName.forEach(i => indices.push(...i.indices)); + } + + if (tokensMatchedBySymbol.length) { + tokensBySymbol = parseTokensBySymbol(tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); + + tokensBySymbol.forEach(i => indices.push(...i.indices)); + } + + const indicesSorted = _.uniq(indices.sort((a, b) => a - b)); + + const tokensWithIndices = _.uniqBy(_.concat(tokensByName, tokensBySymbol), 'name'); + + return createDescriptionItems(indicesSorted, tokensWithIndices, parsedDescription); +}; + +const removeIds = ( + tokensMatchedByName: Array, + tokensMatchedBySymbol: Array, + idsMatched: Array, + tokenData: TokensData, + parsedDescription: string, +) => { + // Remove ids from the description since we already have that info in the token object + let description = parsedDescription; + + tokensMatchedByName.forEach(name => { + const hasId = idsMatched.includes(tokenData.byName[name].id || ''); + if (hasId) { + description = description.replaceAll(`#${ tokenData.byName[name].id }`, ''); + } + }); + + tokensMatchedBySymbol.forEach(name => { + const hasId = idsMatched.includes(tokenData.bySymbol[name].id || ''); + if (hasId) { + description = description.replaceAll(`#${ tokenData.bySymbol[name].id }`, ''); + } + }); + + return description; +}; + +const parseTokensByName = (tokensMatchedByName: Array, idsMatched: Array, tokenData: TokensData, parsedDescription: string) => { + // Find indices and create tokens object + + const tokensByName: Array = tokensMatchedByName.map(name => { + const searchString = ` ${ name.toUpperCase() }`; + let hasId = false; + + if (idsMatched.length) { + hasId = idsMatched.includes(tokenData.byName[name].id || ''); + } + + return { + name, + hasId, + indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, + token: tokenData.byName[name], + }; + }); + + return tokensByName; +}; + +const parseTokensBySymbol = (tokensMatchedBySymbol: Array, idsMatched: Array, tokenData: TokensData, parsedDescription: string) => { + // Find indices and create tokens object + + const tokensBySymbol: Array = tokensMatchedBySymbol.map(name => { + const searchString = ` ${ name.toUpperCase() }`; + let hasId = false; + + if (idsMatched.length) { + hasId = idsMatched.includes(tokenData.bySymbol[name].id || ''); + } + + return { + name, + hasId, + indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, + token: tokenData.bySymbol[name], + }; + }); + + return tokensBySymbol; +}; + +const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: Array, parsedDescription: string) => { + // Split the description and create array of objects to render + const descriptionItems = indicesSorted.map((endIndex, i) => { + const item = tokensWithIndices.find(t => t?.indices.includes(endIndex)); + + if (i === 0) { + return { + token: item?.token, + text: parsedDescription.substring(0, endIndex), + hasId: item?.hasId, + }; + } else { + const previousItem = tokensWithIndices.find(t => t?.indices.includes(indicesSorted[i - 1])); + // Add the length of the text of the previous token to remove it from the start + const startIndex = indicesSorted[i - 1] + (previousItem?.name.length || 0) + 1; + return { + token: item?.token, + text: parsedDescription.substring(startIndex, endIndex), + hasId: item?.hasId, + }; + } + }); + + const lastIndex = indicesSorted[indicesSorted.length - 1]; + const startIndex = lastIndex + (tokensWithIndices.find(t => t?.indices.includes(lastIndex))?.name.length || 0); + const restString = parsedDescription.substring(startIndex + 1); + + // Check if there is text left after the last token and push it to the array + if (restString) { + descriptionItems.push({ text: restString, token: undefined, hasId: false }); + } + + return descriptionItems; +}; diff --git a/ui/tx/assetFlows/utils/getTokensData.test.ts b/ui/tx/assetFlows/utils/getTokensData.test.ts new file mode 100644 index 0000000000..06e56ac32a --- /dev/null +++ b/ui/tx/assetFlows/utils/getTokensData.test.ts @@ -0,0 +1,32 @@ +import fetch from 'jest-fetch-mock'; +import { renderHook, wrapper, act } from 'jest/lib'; +import flushPromises from 'jest/utils/flushPromises'; +import useApiQuery from 'lib/api/useApiQuery'; +import * as transactionMock from 'mocks/noves/transaction'; + +import { getTokensData } from './getTokensData'; + +it('creates a tokens data object', async() => { + const params = { + pathParams: { + hash: transactionMock.hash, + }, + }; + + fetch.mockResponse(JSON.stringify(transactionMock.transaction)); + + const { result } = renderHook(() => useApiQuery('noves_transaction', params), { wrapper }); + await waitForApiResponse(); + + expect(result.current.data).toEqual(transactionMock.transaction); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const tokensResult = getTokensData(result.current.data!); + + expect(tokensResult).toEqual(transactionMock.tokenData); +}); + +async function waitForApiResponse() { + await flushPromises(); + await act(flushPromises); +} diff --git a/ui/tx/assetFlows/utils/getTokensData.ts b/ui/tx/assetFlows/utils/getTokensData.ts new file mode 100644 index 0000000000..6a0128ed14 --- /dev/null +++ b/ui/tx/assetFlows/utils/getTokensData.ts @@ -0,0 +1,78 @@ +import _ from 'lodash'; + +import type { NovesResponseData } from 'types/api/noves'; + +export interface TokensData { + nameList: Array; + symbolList: Array; + idList: Array; + byName: { + [x: string]: { + name: string | undefined; + symbol: string | undefined; + address: string | undefined; + id?: string | undefined; + }; + }; + bySymbol: { + [x: string]: { + name: string | undefined; + symbol: string | undefined; + address: string | undefined; + id: string | undefined; + }; + }; +} + +export function getTokensData(data: NovesResponseData): TokensData { + const sent = data.classificationData.sent || []; + const received = data.classificationData.received || []; + + const txItems = [ ...sent, ...received ]; + + // Extract all tokens data + const tokens = txItems.map((item) => { + const name = item.nft?.name || item.token?.name; + const symbol = item.nft?.symbol || item.token?.symbol; + + const token = { + name: name, + symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol, + address: item.nft?.address || item.token?.address, + id: item.nft?.id || item.token?.id, + }; + + return token; + }); + + // Group tokens by property into arrays + const tokensGroupByname = _.groupBy(tokens, 'name'); + const tokensGroupBySymbol = _.groupBy(tokens, 'symbol'); + const tokensGroupById = _.groupBy(tokens, 'id'); + + // Map properties to an object and remove duplicates + const mappedNames = _.mapValues(tokensGroupByname, (i) => { + return i[0]; + }); + + const mappedSymbols = _.mapValues(tokensGroupBySymbol, (i) => { + return i[0]; + }); + + const mappedIds = _.mapValues(tokensGroupById, (i) => { + return i[0]; + }); + + // Array of keys to match in string + const nameList = _.keysIn(mappedNames).filter(i => i !== 'undefined'); + const symbolList = _.keysIn(mappedSymbols).filter(i => i !== 'undefined'); + const idList = _.keysIn(mappedIds).filter(i => i !== 'undefined'); + + return { + nameList, + symbolList, + idList, + byName: mappedNames, + bySymbol: mappedSymbols, + }; +} diff --git a/ui/txs/Noves/NovesUseFetchDescribe.tsx b/ui/txs/Noves/NovesUseFetchDescribe.tsx deleted file mode 100644 index efb504b38c..0000000000 --- a/ui/txs/Noves/NovesUseFetchDescribe.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; - -import type { NovesDescribeResponse } from 'types/novesApi'; - -import config from 'configs/app'; -import type { Params as FetchParams } from 'lib/hooks/useFetch'; - -import useFetch from '../../../lib/hooks/useFetch'; - -export interface ApiFetchParams { - queryParams?: Record | number | undefined>; - fetchParams?: Pick; -} - -export interface TranslateError { - payload?: { - txHash: string; - }; - status: Response['status']; - statusText: Response['statusText']; -} - -interface Params extends ApiFetchParams { - queryOptions?: Omit, 'queryKey' | 'queryFn'>; -} - -export default function NovesUseFetchDescribe( - txHash: string | null, - { queryOptions }: Params = {}, -) { - const fetch = useFetch(); - - const url = new URL('/node-api/noves/describe', config.app.baseUrl); - - const body = { - txHash, - }; - - return useQuery({ - queryKey: [ 'describe', txHash, body ], - queryFn: async() => { - // all errors and error typing is handled by react-query - // so error response will never go to the data - // that's why we are safe here to do type conversion "as Promise>" - if (!txHash) { - return undefined as unknown as NovesDescribeResponse; - } - return fetch(url.toString(), { - method: 'POST', - body, - }) as Promise; - }, - ...queryOptions, - }); -} diff --git a/ui/txs/TxType.tsx b/ui/txs/TxType.tsx index 26e6c76213..462040040c 100644 --- a/ui/txs/TxType.tsx +++ b/ui/txs/TxType.tsx @@ -57,7 +57,11 @@ const TxType = ({ types, isLoading, translateLabel }: Props) => { if (translateLabel) { if (!filteredTypes.includes(translateLabel)) { - label = translateLabel; + return ( + + { translateLabel } + + ); } } diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index 5fb20bdad8..446d1af2c8 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -4,6 +4,7 @@ import React from 'react'; import type { AddressFromToFilter } from 'types/api/address'; import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; +import config from 'configs/app'; import useIsMobile from 'lib/hooks/useIsMobile'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import DataListDisplay from 'ui/shared/DataListDisplay'; @@ -32,7 +33,6 @@ type Props = { filterValue?: AddressFromToFilter; enableTimeIncrement?: boolean; top?: number; - translate?: boolean; items?: Array; isPlaceholderData: boolean; isError: boolean; @@ -40,6 +40,8 @@ type Props = { sort: TransactionsSortingValue | undefined; } +const feature = config.features.txInterpretation; + const TxsContent = ({ query, filter, @@ -51,7 +53,6 @@ const TxsContent = ({ currentAddress, enableTimeIncrement, top, - translate, items, isPlaceholderData, isError, @@ -65,6 +66,8 @@ const TxsContent = ({ setSorting(value); }, [ sort, setSorting ]); + const translateEnabled = feature.isEnabled && feature.provider === 'noves'; + const content = items ? ( <> @@ -85,7 +88,7 @@ const TxsContent = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isPlaceholderData } - translate={ translate } + translateEnabled={ translateEnabled } /> )) } @@ -103,7 +106,7 @@ const TxsContent = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isPlaceholderData } - translate={ translate } + translateEnabled={ translateEnabled } /> diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index 21315fe73b..92c1ab6a07 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -8,6 +8,7 @@ import React from 'react'; import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; import getValueWithUnit from 'lib/getValueWithUnit'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import { space } from 'lib/html-entities'; @@ -20,7 +21,6 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import TxStatus from 'ui/shared/statusTag/TxStatus'; import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; -import NovesUseFetchDescribe from 'ui/txs/Noves/NovesUseFetchDescribe'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxType from 'ui/txs/TxType'; @@ -30,13 +30,13 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - translate?: boolean; + translateEnabled?: boolean; } const TAG_WIDTH = 48; const ARROW_WIDTH = 24; -const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement, translate }: Props) => { +const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement, translateEnabled }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); @@ -44,7 +44,12 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - const { data: describeData } = NovesUseFetchDescribe(translate ? tx.hash : null); + const { data: describeData } = useApiQuery('noves_describe_tx', { + pathParams: { hash: tx.hash }, + queryOptions: { + enabled: Boolean(translateEnabled), + }, + }); return ( diff --git a/ui/txs/TxsTable.tsx b/ui/txs/TxsTable.tsx index da480dbdf7..5a89830497 100644 --- a/ui/txs/TxsTable.tsx +++ b/ui/txs/TxsTable.tsx @@ -23,7 +23,7 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - translate?: boolean; + translateEnabled?: boolean; } const TxsTable = ({ @@ -38,7 +38,7 @@ const TxsTable = ({ currentAddress, enableTimeIncrement, isLoading, - translate, + translateEnabled, }: Props) => { return ( @@ -95,7 +95,7 @@ const TxsTable = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isLoading } - translate={ translate } + translateEnabled={ translateEnabled } /> )) } diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index fe2da83698..3194edcb08 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -14,6 +14,7 @@ import React from 'react'; import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import Tag from 'ui/shared/chakra/Tag'; import CurrencyValue from 'ui/shared/CurrencyValue'; @@ -25,7 +26,6 @@ import InOutTag from 'ui/shared/InOutTag'; import TxStatus from 'ui/shared/statusTag/TxStatus'; import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; -import NovesUseFetchDescribe from 'ui/txs/Noves/NovesUseFetchDescribe'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxType from './TxType'; @@ -36,17 +36,22 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - translate?: boolean; + translateEnabled?: boolean; } -const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading, translate }: Props) => { +const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading, translateEnabled }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isIn = Boolean(currentAddress && currentAddress === dataTo?.hash); const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - const { data: describeData } = NovesUseFetchDescribe(translate ? tx.hash : null); + const { data: describeData } = useApiQuery('noves_describe_tx', { + pathParams: { hash: tx.hash }, + queryOptions: { + enabled: Boolean(translateEnabled), + }, + }); const addressFrom = ( Date: Fri, 2 Feb 2024 09:08:34 -0300 Subject: [PATCH 04/30] Code set up for new proxy 'describeTxs' --- types/api/noves.ts | 6 ++ ui/tx/assetFlows/utils/getTokensData.test.ts | 27 +----- ui/txs/TxsContent.tsx | 12 +-- ui/txs/TxsListItem.tsx | 24 ++++-- ui/txs/TxsTable.tsx | 8 +- ui/txs/TxsTableItem.tsx | 23 +++-- ui/txs/noves/useDescribeTxs.tsx | 89 ++++++++++++++++++++ 7 files changed, 138 insertions(+), 51 deletions(-) create mode 100644 ui/txs/noves/useDescribeTxs.tsx diff --git a/types/api/noves.ts b/types/api/noves.ts index fb72478bff..65d51bd51d 100644 --- a/types/api/noves.ts +++ b/types/api/noves.ts @@ -103,3 +103,9 @@ export interface NovesDescribeResponse { type: string; description: string; } + +export interface NovesDescribeTxsResponse { + txHash: string; + type: string; + description: string; +}[]; diff --git a/ui/tx/assetFlows/utils/getTokensData.test.ts b/ui/tx/assetFlows/utils/getTokensData.test.ts index 06e56ac32a..37124e000c 100644 --- a/ui/tx/assetFlows/utils/getTokensData.test.ts +++ b/ui/tx/assetFlows/utils/getTokensData.test.ts @@ -1,32 +1,9 @@ -import fetch from 'jest-fetch-mock'; -import { renderHook, wrapper, act } from 'jest/lib'; -import flushPromises from 'jest/utils/flushPromises'; -import useApiQuery from 'lib/api/useApiQuery'; import * as transactionMock from 'mocks/noves/transaction'; import { getTokensData } from './getTokensData'; it('creates a tokens data object', async() => { - const params = { - pathParams: { - hash: transactionMock.hash, - }, - }; + const result = getTokensData(transactionMock.transaction); - fetch.mockResponse(JSON.stringify(transactionMock.transaction)); - - const { result } = renderHook(() => useApiQuery('noves_transaction', params), { wrapper }); - await waitForApiResponse(); - - expect(result.current.data).toEqual(transactionMock.transaction); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const tokensResult = getTokensData(result.current.data!); - - expect(tokensResult).toEqual(transactionMock.tokenData); + expect(result).toEqual(transactionMock.tokenData); }); - -async function waitForApiResponse() { - await flushPromises(); - await act(flushPromises); -} diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index 446d1af2c8..fe9c5351df 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -12,6 +12,7 @@ import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPage import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import getNextSortValue from 'ui/shared/sort/getNextSortValue'; +import useDescribeTxs from './noves/useDescribeTxs'; import TxsHeaderMobile from './TxsHeaderMobile'; import TxsListItem from './TxsListItem'; import TxsTable from './TxsTable'; @@ -68,7 +69,10 @@ const TxsContent = ({ const translateEnabled = feature.isEnabled && feature.provider === 'noves'; - const content = items ? ( + // Same array of transactions as "items" with "translate" field added + const itemsWithTranslate = useDescribeTxs(items, translateEnabled); + + const content = itemsWithTranslate ? ( <> @@ -80,7 +84,7 @@ const TxsContent = ({ isLoading={ isPlaceholderData } /> ) } - { items.map((tx, index) => ( + { itemsWithTranslate.map((tx, index) => ( )) } diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index 92c1ab6a07..a0e62ff9dc 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -5,8 +5,6 @@ import { } from '@chakra-ui/react'; import React from 'react'; -import type { Transaction } from 'types/api/transaction'; - import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import getValueWithUnit from 'lib/getValueWithUnit'; @@ -24,19 +22,21 @@ import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxType from 'ui/txs/TxType'; +import type { TransactionWithTranslate } from './noves/useDescribeTxs'; + type Props = { - tx: Transaction; + tx: TransactionWithTranslate; showBlockInfo: boolean; currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - translateEnabled?: boolean; + // "translateEnabled" removed and replaced with "tx.translate.enabled" } const TAG_WIDTH = 48; const ARROW_WIDTH = 24; -const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement, translateEnabled }: Props) => { +const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); @@ -44,18 +44,26 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - const { data: describeData } = useApiQuery('noves_describe_tx', { + // This will be removed once the new proxy is ready + const { data: describeData, isLoading: isDescribeLoading } = useApiQuery('noves_describe_tx', { pathParams: { hash: tx.hash }, queryOptions: { - enabled: Boolean(translateEnabled), + enabled: tx.translate.enabled, }, }); + // return ( - + { + + /* Whit the data inside tx + + */ + } + diff --git a/ui/txs/TxsTable.tsx b/ui/txs/TxsTable.tsx index 5a89830497..60c40e559c 100644 --- a/ui/txs/TxsTable.tsx +++ b/ui/txs/TxsTable.tsx @@ -2,17 +2,18 @@ import { Link, Table, Tbody, Tr, Th, Show, Hide } from '@chakra-ui/react'; import { AnimatePresence } from 'framer-motion'; import React from 'react'; -import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; +import type { TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; import config from 'configs/app'; import IconSvg from 'ui/shared/IconSvg'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import TheadSticky from 'ui/shared/TheadSticky'; +import type { TransactionWithTranslate } from './noves/useDescribeTxs'; import TxsTableItem from './TxsTableItem'; type Props = { - txs: Array; + txs: Array; sort: (field: TransactionsSortingField) => () => void; sorting?: TransactionsSortingValue; top: number; @@ -23,7 +24,6 @@ type Props = { currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - translateEnabled?: boolean; } const TxsTable = ({ @@ -38,7 +38,6 @@ const TxsTable = ({ currentAddress, enableTimeIncrement, isLoading, - translateEnabled, }: Props) => { return (
@@ -95,7 +94,6 @@ const TxsTable = ({ currentAddress={ currentAddress } enableTimeIncrement={ enableTimeIncrement } isLoading={ isLoading } - translateEnabled={ translateEnabled } /> )) } diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index 3194edcb08..3f797eb1a1 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -11,8 +11,6 @@ import { import { motion } from 'framer-motion'; import React from 'react'; -import type { Transaction } from 'types/api/transaction'; - import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; @@ -28,30 +26,33 @@ import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; +import type { TransactionWithTranslate } from './noves/useDescribeTxs'; import TxType from './TxType'; type Props = { - tx: Transaction; + tx: TransactionWithTranslate; showBlockInfo: boolean; currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - translateEnabled?: boolean; + // "translateEnabled" removed and replaced with "tx.translate.enabled" } -const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading, translateEnabled }: Props) => { +const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isIn = Boolean(currentAddress && currentAddress === dataTo?.hash); const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - const { data: describeData } = useApiQuery('noves_describe_tx', { + // This will be removed once the new proxy is ready + const { data: describeData, isLoading: isDescribeLoading } = useApiQuery('noves_describe_tx', { pathParams: { hash: tx.hash }, queryOptions: { - enabled: Boolean(translateEnabled), + enabled: tx.translate.enabled, }, }); + // const addressFrom = ( - - - - - diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx index 2d77792e73..1cac830b3b 100644 --- a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx +++ b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx @@ -24,7 +24,7 @@ const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { return ( - + { description.map((item, i) => ( From fe5875f351277b10876132ffedb99a078d0eb5ee Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Sun, 3 Mar 2024 04:33:12 -0300 Subject: [PATCH 17/30] sub heading fix --- .../NovesSubHeadingInterpretation.tsx | 7 +++ ui/tx/assetFlows/utils/getDescriptionItems.ts | 49 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx index 1cac830b3b..e050ea7cbd 100644 --- a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx +++ b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx @@ -39,6 +39,13 @@ const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { ) } { item.text } + { + item.actionText && ( + + { item.actionText } + + ) + } { item.hasId && item.token ? ( ; - token: NovesTokenInfo; + token?: NovesTokenInfo; + type?: 'action'; } export interface DescriptionItems { token: NovesTokenInfo | undefined; text: string; hasId: boolean | undefined; + type?: string; + actionText?: string; } export const getDescriptionItems = (translateData: NovesResponseData): Array => { @@ -30,6 +33,9 @@ export const getDescriptionItems = (translateData: NovesResponseData): Array parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); let tokensMatchedBySymbol = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); + const actions = [ 'sent', 'Sent', 'Called function', 'called function', 'on contract' ]; + const actionsMatched = actions.filter(action => parsedDescription.includes(action)); + // Filter symbols if they're already matched by name tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); @@ -37,6 +43,8 @@ export const getDescriptionItems = (translateData: NovesResponseData): Array indices.push(...i.indices)); } + if (actionsMatched.length) { + tokensByAction = parseTokensByAction(actionsMatched, parsedDescription); + + tokensByAction.forEach(i => indices.push(...i.indices)); + } + const indicesSorted = _.uniq(indices.sort((a, b) => a - b)); - const tokensWithIndices = _.uniqBy(_.concat(tokensByName, tokensBySymbol), 'name'); + const tokensWithIndices = _.uniqBy(_.concat(tokensByName, tokensBySymbol, tokensByAction), 'name'); return createDescriptionItems(indicesSorted, tokensWithIndices, parsedDescription); }; @@ -131,27 +145,52 @@ const parseTokensBySymbol = (tokensMatchedBySymbol: Array, idsMatched: A return tokensBySymbol; }; +const parseTokensByAction = (actionsMatched: Array, parsedDescription: string) => { + const tokensBySymbol: Array = actionsMatched.map(action => { + return { + name: action, + indices: [ ...parsedDescription.matchAll(new RegExp(action, 'gi')) ].map(a => a.index) as unknown as Array, + hasId: false, + type: 'action', + }; + }); + + return tokensBySymbol; +}; + const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: Array, parsedDescription: string) => { // Split the description and create array of objects to render const descriptionItems = indicesSorted.map((endIndex, i) => { const item = tokensWithIndices.find(t => t?.indices.includes(endIndex)); + let token; + if (i === 0) { - return { + const isAction = item?.type === 'action'; + + token = { token: item?.token, text: parsedDescription.substring(0, endIndex), hasId: item?.hasId, + type: isAction ? 'action' : undefined, + actionText: isAction ? item.name : undefined, }; } else { const previousItem = tokensWithIndices.find(t => t?.indices.includes(indicesSorted[i - 1])); // Add the length of the text of the previous token to remove it from the start const startIndex = indicesSorted[i - 1] + (previousItem?.name.length || 0) + 1; - return { + const isAction = item?.type === 'action'; + + token = { token: item?.token, text: parsedDescription.substring(startIndex, endIndex), hasId: item?.hasId, + type: isAction ? 'action' : undefined, + actionText: isAction ? item.name : undefined, }; } + + return token; }); const lastIndex = indicesSorted[indicesSorted.length - 1]; @@ -160,7 +199,7 @@ const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: // Check if there is text left after the last token and push it to the array if (restString) { - descriptionItems.push({ text: restString, token: undefined, hasId: false }); + descriptionItems.push({ text: restString, token: undefined, hasId: false, type: undefined, actionText: undefined }); } return descriptionItems; From ad72727f3291fa7811b5b0dc65b4988a216494b0 Mon Sep 17 00:00:00 2001 From: francisco-noves Date: Tue, 5 Mar 2024 18:06:45 -0300 Subject: [PATCH 18/30] Removed pagination in account history --- ui/address/AddressAccountHistory.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index 5b7cf3aa7e..51ee241354 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -14,7 +14,6 @@ import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAcc import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import { getFromToValue } from 'ui/shared/Noves/utils'; -import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import TheadSticky from 'ui/shared/TheadSticky'; @@ -58,7 +57,6 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { isLoading={ pagination.isLoading } /> - ); From b0a21acac3430581a530521997b4d63a9527ddc9 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 6 Mar 2024 10:06:23 -0300 Subject: [PATCH 19/30] remove duplicate route --- nextjs/nextjs-routes.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index f3f568b30b..ffb30e5797 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -39,7 +39,6 @@ declare module "nextjs-routes" { | StaticRoute<"/login"> | StaticRoute<"/name-domains"> | DynamicRoute<"/name-domains/[name]", { "name": string }> - | StaticRoute<"/name-domains"> | DynamicRoute<"/op/[hash]", { "hash": string }> | StaticRoute<"/ops"> | StaticRoute<"/output-roots"> From 79e73164421b86ea7423c2761748c028b4351e72 Mon Sep 17 00:00:00 2001 From: francisco-noves Date: Thu, 7 Mar 2024 14:01:37 -0300 Subject: [PATCH 20/30] updated table theme and icon gap --- ui/address/AddressAccountHistory.tsx | 2 ++ ui/address/accountHistory/AddressAccountHistoryTableItem.tsx | 4 ++-- ui/tx/TxSubHeading.tsx | 2 +- ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index 51ee241354..5b7cf3aa7e 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -14,6 +14,7 @@ import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAcc import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import { getFromToValue } from 'ui/shared/Noves/utils'; +import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import TheadSticky from 'ui/shared/TheadSticky'; @@ -57,6 +58,7 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { isLoading={ pagination.isLoading } /> + ); diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx index 1b98d899aa..35b47a150f 100644 --- a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx +++ b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx @@ -24,14 +24,14 @@ const AddressAccountHistoryTableItem = (props: Props) => { return ( - - -
- + { + + /* Whit the data inside tx + + */ + } + diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx new file mode 100644 index 0000000000..e74b905400 --- /dev/null +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -0,0 +1,89 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import _ from 'lodash'; + +import type { NovesDescribeTxsResponse } from 'types/api/noves'; +import type { Transaction } from 'types/api/transaction'; + +import type { ResourceError } from 'lib/api/resources'; + +export interface DescribeTxs { + txHash: string; + type?: string; + description?: string; + isLoading: boolean; + enabled: boolean; +} + +export interface TransactionWithTranslate extends Transaction { + translate: DescribeTxs; +} + +export default function useDescribeTxs(items: Array | undefined, translateEnabled: boolean) { + const txsHash = items?.map(i => i.hash); + const txsChunk = _.chunk(txsHash || [], 10); + + const txsParams = Array(5).fill(undefined).map((_, i) => txsChunk[i]); + const txsQuerys = useFetchTxs(txsParams, translateEnabled); + + const isLoading = txsQuerys.some(query => query.isLoading || query.isPlaceholderData); + const queryData = txsQuerys.map(query => query.data ? query.data : []).flat(); + + const data: Array | undefined = items?.map(tx => { + const query = queryData.find(data => data.txHash === tx.hash); + + if (query) { + return { + ...tx, + translate: { + ...query, + isLoading: false, + enabled: translateEnabled, + }, + }; + } + + return { + ...tx, + translate: { + txHash: tx.hash, + isLoading, + enabled: translateEnabled, + }, + }; + }); + + // return same "items" array of Transaction with a new "translate" field. + + return data; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function useFetchTxs(txs: Array>, translateEnabled: boolean) { + const txsQuerys = []; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const body of txs) { + // loop to avoid writing 5 hook calls. + // txs will always have the same length so we always execute the same amount of hooks + + /* + ---- This will be available once the proxy for this endpoint is ready ---- + + const query = useApiQuery('noves_describe_txs', { + fetchParams: { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + }, + queryOptions: { + enabled: translateEnabled && Boolean(body), + }, + }); + + txsQuerys.push(query); + */ + + txsQuerys.push({} as UseQueryResult>); + } + + return txsQuerys; +} From 8c4a911d1066c85982720f1ecbe44696c4d25117 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Fri, 2 Feb 2024 09:25:51 -0300 Subject: [PATCH 05/30] minor fix --- ui/address/AddressAccountHistory.tsx | 2 +- ui/tx/TxAssetFlows.tsx | 2 +- ui/tx/TxSubHeading.tsx | 2 +- ui/txs/TxsListItem.tsx | 2 +- ui/txs/TxsTableItem.tsx | 2 +- ui/txs/noves/useDescribeTxs.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index a6a42daa53..33d8fa3638 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -8,7 +8,7 @@ import { NovesHistoryFilterValues } from 'types/api/noves'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import { NOVES_TRANSLATE } from 'stubs/noves/novestranslate'; +import { NOVES_TRANSLATE } from 'stubs/noves/novesTranslate'; import { generateListStub } from 'stubs/utils'; import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAccountHistoryTableItem'; import ActionBar from 'ui/shared/ActionBar'; diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/TxAssetFlows.tsx index 5411e25026..a5da240866 100644 --- a/ui/tx/TxAssetFlows.tsx +++ b/ui/tx/TxAssetFlows.tsx @@ -5,7 +5,7 @@ import React, { useState } from 'react'; import type { PaginationParams } from 'ui/shared/pagination/types'; import useApiQuery from 'lib/api/useApiQuery'; -import { NOVES_TRANSLATE } from 'stubs/noves/novestranslate'; +import { NOVES_TRANSLATE } from 'stubs/noves/novesTranslate'; import ActionBar from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 12ca3a1463..1bfdb8d5ba 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -3,7 +3,7 @@ import React from 'react'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; -import { NOVES_TRANSLATE } from 'stubs/noves/novestranslate'; +import { NOVES_TRANSLATE } from 'stubs/noves/novesTranslate'; import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TxEntity from 'ui/shared/entities/tx/TxEntity'; diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index a0e62ff9dc..b601bea207 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -48,7 +48,7 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI const { data: describeData, isLoading: isDescribeLoading } = useApiQuery('noves_describe_tx', { pathParams: { hash: tx.hash }, queryOptions: { - enabled: tx.translate.enabled, + enabled: Boolean(tx.translate?.enabled), }, }); // diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index 3f797eb1a1..d9a63db21a 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -49,7 +49,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, const { data: describeData, isLoading: isDescribeLoading } = useApiQuery('noves_describe_tx', { pathParams: { hash: tx.hash }, queryOptions: { - enabled: tx.translate.enabled, + enabled: Boolean(tx.translate?.enabled), }, }); // diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx index e74b905400..0937728713 100644 --- a/ui/txs/noves/useDescribeTxs.tsx +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -15,7 +15,7 @@ export interface DescribeTxs { } export interface TransactionWithTranslate extends Transaction { - translate: DescribeTxs; + translate?: DescribeTxs; } export default function useDescribeTxs(items: Array | undefined, translateEnabled: boolean) { From 6a7a1bab790c0b14c03d032c3a6d6d13a730eecd Mon Sep 17 00:00:00 2001 From: Juan Leandro Costa <83667708+juanlenoves@users.noreply.github.com> Date: Thu, 8 Feb 2024 10:35:33 -0300 Subject: [PATCH 06/30] Rename Novestranslate.ts to NovesTranslate.ts --- stubs/noves/{Novestranslate.ts => NovesTranslate.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename stubs/noves/{Novestranslate.ts => NovesTranslate.ts} (100%) diff --git a/stubs/noves/Novestranslate.ts b/stubs/noves/NovesTranslate.ts similarity index 100% rename from stubs/noves/Novestranslate.ts rename to stubs/noves/NovesTranslate.ts From e03304a43c3905207da1764fbe66dacf1f109e9d Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 8 Feb 2024 11:46:59 -0300 Subject: [PATCH 07/30] some quick stuff --- docs/ENVS.md | 2 +- ui/address/AddressAccountHistory.tsx | 2 +- ui/shared/Noves/NovesFromTo.tsx | 4 ++-- ui/tx/TxAssetFlows.tsx | 4 ++-- ui/tx/TxSubHeading.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/ENVS.md b/docs/ENVS.md index 88d82d217d..797b55c9ef 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -511,7 +511,7 @@ This feature is **enabled by default** with the `['metamask']` value. To switch | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER | `blockscout` \| `none` | Transaction interpretation provider that displays human readable transaction description | - | `none` | `blockscout` | +| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER | `blockscout` \| `noves` \| `none` | Transaction interpretation provider that displays human readable transaction description | - | `none` | `blockscout` |   diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index 33d8fa3638..064dcf6864 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -8,7 +8,7 @@ import { NovesHistoryFilterValues } from 'types/api/noves'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import { NOVES_TRANSLATE } from 'stubs/noves/novesTranslate'; +import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { generateListStub } from 'stubs/utils'; import AddressAccountHistoryTableItem from 'ui/address/accountHistory/AddressAccountHistoryTableItem'; import ActionBar from 'ui/shared/ActionBar'; diff --git a/ui/shared/Noves/NovesFromTo.tsx b/ui/shared/Noves/NovesFromTo.tsx index f2fd7153ba..92b3bebfa0 100644 --- a/ui/shared/Noves/NovesFromTo.tsx +++ b/ui/shared/Noves/NovesFromTo.tsx @@ -30,7 +30,7 @@ const NovesFromTo: FC = ({ isLoaded, txData, currentAddress = '', item }) const isSent = data.text.startsWith('Sent'); - const addressObj = { hash: data.address || '', name: data.name || '' }; + const address = { hash: data.address || '', name: data.name || '' }; return ( @@ -52,7 +52,7 @@ const NovesFromTo: FC = ({ isLoaded, txData, currentAddress = '', item }) diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 1bfdb8d5ba..4cf821eeaa 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -3,7 +3,7 @@ import React from 'react'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; -import { NOVES_TRANSLATE } from 'stubs/noves/novesTranslate'; +import { NOVES_TRANSLATE } from 'stubs/noves/NovesTranslate'; import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TxEntity from 'ui/shared/entities/tx/TxEntity'; From 0875d0097ec093f76a8cff5cc9d63020dbe9d00c Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 8 Feb 2024 22:14:50 -0300 Subject: [PATCH 08/30] partial fixes and commit for changing how useDescribeTxs and txsContent work (new GET endpoint) --- deploy/values/review/values.yaml.gotmpl | 1 + lib/api/resources.ts | 22 +++++++++++----- ui/txs/TxsContent.tsx | 8 +----- ui/txs/noves/useDescribeTxs.tsx | 34 ++++++++++++------------- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index d3bfc98002..d97f9186d9 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -76,6 +76,7 @@ frontend: NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" NEXT_PUBLIC_HAS_USER_OPS: true NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" + NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: noves envFromSecret: NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 4a0db00e56..be9d2d7bed 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -52,7 +52,7 @@ import type { L2OutputRootsResponse } from 'types/api/l2OutputRoots'; import type { L2TxnBatchesResponse } from 'types/api/l2TxnBatches'; import type { L2WithdrawalsResponse } from 'types/api/l2Withdrawals'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; -import type { NovesAccountHistoryResponse, NovesDescribeResponse, NovesResponseData } from 'types/api/noves'; +import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves'; import type { RawTracesResponse } from 'types/api/rawTrace'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; @@ -87,11 +87,17 @@ import type { ArrayElement } from 'types/utils'; import config from 'configs/app'; +export interface QueryParamArray { + key: string; + type: 'array'; +} + export interface ApiResource { path: ResourcePath; endpoint?: string; basePath?: string; pathParams?: Array; + queryParams?: Array; needAuth?: boolean; // for external APIs which require authentication } @@ -591,11 +597,13 @@ export const RESOURCES = { pathParams: [ 'address' as const ], filterFields: [], }, - noves_describe_tx: { - path: '/api/v2/proxy/noves-fi/transactions/:hash/describe', - pathParams: [ 'hash' as const ], + noves_describe_txs: { + path: '/api/v2/proxy/noves-fi/transactions', + queryParams: [ + 'viewAsAccountAddress' as const, + { key: 'hashes', type: 'array' } as const, + ], }, - // USER OPS user_ops: { path: '/api/v2/proxy/account-abstraction/operations', @@ -682,7 +690,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' | -'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'noves_address_history'; +'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'noves_address_history' | 'noves_describe_txs'; export type PaginatedResponse = ResourcePayload; @@ -783,7 +791,7 @@ Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'config_backend_version' ? BackendVersionConfig : Q extends 'noves_transaction' ? NovesResponseData : Q extends 'noves_address_history' ? NovesAccountHistoryResponse : -Q extends 'noves_describe_tx' ? NovesDescribeResponse : +Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse : Q extends 'addresses_lookup' ? EnsAddressLookupResponse : Q extends 'domain_info' ? EnsDomainDetailed : Q extends 'domain_events' ? EnsDomainEventsResponse : diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index a2eddc602e..53aa39d733 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -4,7 +4,6 @@ import React from 'react'; import type { AddressFromToFilter } from 'types/api/address'; import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; -import config from 'configs/app'; import useIsMobile from 'lib/hooks/useIsMobile'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import DataListDisplay from 'ui/shared/DataListDisplay'; @@ -40,8 +39,6 @@ type Props = { sort: TransactionsSortingValue | undefined; } -const feature = config.features.txInterpretation; - const TxsContent = ({ query, filter, @@ -66,10 +63,7 @@ const TxsContent = ({ setSorting(value); }, [ sort, setSorting ]); - const translateEnabled = feature.isEnabled && feature.provider === 'noves'; - - // Same array of transactions as "items" with "translate" field added - const itemsWithTranslate = useDescribeTxs(items, translateEnabled); + const itemsWithTranslate = useDescribeTxs(items, currentAddress); const content = itemsWithTranslate ? ( <> diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx index 0937728713..9e7dd21b1e 100644 --- a/ui/txs/noves/useDescribeTxs.tsx +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -4,7 +4,9 @@ import _ from 'lodash'; import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { Transaction } from 'types/api/transaction'; +import config from 'configs/app'; import type { ResourceError } from 'lib/api/resources'; +// import useApiQuery from 'lib/api/useApiQuery'; export interface DescribeTxs { txHash: string; @@ -18,12 +20,16 @@ export interface TransactionWithTranslate extends Transaction { translate?: DescribeTxs; } -export default function useDescribeTxs(items: Array | undefined, translateEnabled: boolean) { +const feature = config.features.txInterpretation; + +const translateEnabled = feature.isEnabled && feature.provider === 'noves'; + +export default function useDescribeTxs(items: Array | undefined, viewAsAccountAddress: string | undefined) { const txsHash = items?.map(i => i.hash); const txsChunk = _.chunk(txsHash || [], 10); const txsParams = Array(5).fill(undefined).map((_, i) => txsChunk[i]); - const txsQuerys = useFetchTxs(txsParams, translateEnabled); + const txsQuerys = useFetchTxs(txsParams, viewAsAccountAddress); const isLoading = txsQuerys.some(query => query.isLoading || query.isPlaceholderData); const queryData = txsQuerys.map(query => query.data ? query.data : []).flat(); @@ -58,7 +64,7 @@ export default function useDescribeTxs(items: Array | undefined, tr } // eslint-disable-next-line @typescript-eslint/no-unused-vars -function useFetchTxs(txs: Array>, translateEnabled: boolean) { +function useFetchTxs(txs: Array>, viewAsAccountAddress: string | undefined) { const txsQuerys = []; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -66,21 +72,15 @@ function useFetchTxs(txs: Array>, translateEnabled: boolean) { // loop to avoid writing 5 hook calls. // txs will always have the same length so we always execute the same amount of hooks - /* - ---- This will be available once the proxy for this endpoint is ready ---- - - const query = useApiQuery('noves_describe_txs', { - fetchParams: { - method: 'POST', - body: body ? JSON.stringify(body) : undefined, - }, - queryOptions: { - enabled: translateEnabled && Boolean(body), - }, - }); + // Need to fix implementation here to handle translateEnabled correctly, and verify the calls to describeTxs proxy are working correctly + // const query = useApiQuery('noves_describe_txs', { + // queryParams: { + // viewAsAccountAddress: viewAsAccountAddress, + // hashes: body, + // }, + // }); - txsQuerys.push(query); - */ + // txsQuerys.push(query); txsQuerys.push({} as UseQueryResult>); } From 67f98210e74515276067d876e7eb514c1477a4f1 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Mon, 12 Feb 2024 07:12:35 -0300 Subject: [PATCH 09/30] Pending PR fixes --- lib/api/resources.ts | 2 +- mocks/noves/transaction.ts | 4 +- types/api/noves.ts | 5 ++ types/api/transaction.ts | 3 + ui/shared/Noves/NovesFromTo.tsx | 4 +- ui/shared/Noves/utils.test.ts | 4 - ui/tx/TxAssetFlows.tsx | 8 +- ui/tx/TxSubHeading.tsx | 6 +- .../components/NovesTokenTransferSnippet.tsx | 11 +-- .../utils/generateFlowViewData.test.ts | 8 -- .../assetFlows/utils/generateFlowViewData.ts | 20 +---- ui/tx/assetFlows/utils/getDescriptionItems.ts | 16 +--- ui/tx/assetFlows/utils/getTokensData.ts | 34 ++++--- ui/txs/TxsContent.tsx | 2 +- ui/txs/TxsList.tsx | 5 +- ui/txs/TxsListItem.tsx | 25 +----- ui/txs/TxsTable.tsx | 5 +- ui/txs/TxsTableItem.tsx | 24 +---- ui/txs/noves/useDescribeTxs.tsx | 89 +++++++++---------- 19 files changed, 96 insertions(+), 179 deletions(-) diff --git a/lib/api/resources.ts b/lib/api/resources.ts index be9d2d7bed..10da552191 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -690,7 +690,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' | -'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'noves_address_history' | 'noves_describe_txs'; +'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'noves_address_history'; export type PaginatedResponse = ResourcePayload; diff --git a/mocks/noves/transaction.ts b/mocks/noves/transaction.ts index 406cea8478..88fcb330a9 100644 --- a/mocks/noves/transaction.ts +++ b/mocks/noves/transaction.ts @@ -89,7 +89,7 @@ export const tokenData: TokensData = { address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', id: undefined, }, - ETH: { name: 'ETH', symbol: undefined, address: 'ETH', id: undefined }, + ETH: { name: 'ETH', symbol: null, address: 'ETH', id: undefined }, }, bySymbol: { PQR: { @@ -98,6 +98,6 @@ export const tokenData: TokensData = { address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', id: undefined, }, - undefined: { name: 'ETH', symbol: undefined, address: 'ETH', id: undefined }, + 'null': { name: 'ETH', symbol: null, address: 'ETH', id: undefined }, }, }; diff --git a/types/api/noves.ts b/types/api/noves.ts index 65d51bd51d..b77d4ba6f9 100644 --- a/types/api/noves.ts +++ b/types/api/noves.ts @@ -109,3 +109,8 @@ export interface NovesDescribeTxsResponse { type: string; description: string; }[]; + +export interface NovesTxTranslation { + data?: NovesDescribeTxsResponse; + isLoading: boolean; +} diff --git a/types/api/transaction.ts b/types/api/transaction.ts index 79cb89b823..8b93d9d01a 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block'; import type { DecodedInput } from './decodedInput'; import type { Fee } from './fee'; import type { L2WithdrawalStatus } from './l2Withdrawals'; +import type { NovesTxTranslation } from './noves'; import type { TokenInfo } from './token'; import type { TokenTransfer } from './tokenTransfer'; import type { TxAction } from './txAction'; @@ -79,6 +80,8 @@ export type Transaction = { zkevm_batch_number?: number; zkevm_status?: typeof ZKEVM_L2_TX_STATUSES[number]; zkevm_sequence_hash?: string; + // Noves-fi + translation?: NovesTxTranslation; } export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ]; diff --git a/ui/shared/Noves/NovesFromTo.tsx b/ui/shared/Noves/NovesFromTo.tsx index 92b3bebfa0..7dbbf93f4f 100644 --- a/ui/shared/Noves/NovesFromTo.tsx +++ b/ui/shared/Noves/NovesFromTo.tsx @@ -42,13 +42,11 @@ const NovesFromTo: FC = ({ isLoaded, txData, currentAddress = '', item }) minW="max-content" > { data.text } - Received from { address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', name: null, }, - leftActor: { - address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', - name: 'This wallet', - }, accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', }; diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/TxAssetFlows.tsx index fcac25126f..8b80fba514 100644 --- a/ui/tx/TxAssetFlows.tsx +++ b/ui/tx/TxAssetFlows.tsx @@ -1,6 +1,6 @@ import { Table, Thead, Tbody, Tr, Th, TableContainer, Box, Skeleton, Text, Show, Hide, Divider } from '@chakra-ui/react'; import _ from 'lodash'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import type { PaginationParams } from 'ui/shared/pagination/types'; @@ -31,10 +31,10 @@ export default function TxAssetFlows(props: FlowViewProps) { const [ page, setPage ] = useState(1); - const ViewData = queryData ? generateFlowViewData(queryData) : []; + const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]); const chunkedViewData = _.chunk(ViewData, 10); - const paginationProps: PaginationParams = { + const paginationProps: PaginationParams = useMemo(() => ({ onNextPageClick: () => setPage(page + 1), onPrevPageClick: () => setPage(page - 1), resetPage: () => setPage(1), @@ -44,7 +44,7 @@ export default function TxAssetFlows(props: FlowViewProps) { hasNextPage: Boolean(chunkedViewData[page]), hasPages: Boolean(chunkedViewData[1]), isVisible: Boolean(chunkedViewData[1]), - }; + }), [ chunkedViewData, page, isPlaceholderData ]); const data = chunkedViewData [page - 1]; diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 4cf821eeaa..30e52c4422 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -50,13 +50,15 @@ const TxSubHeading = ({ hash, hasTag }: Props) => { return ( - { hasInterpretation && hasNovesInterpretation ? + { hasNovesInterpretation && ( - ) : + ) + } + { hasInternalInterpretation && ( ; tokenId: string; } diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.test.ts b/ui/tx/assetFlows/utils/generateFlowViewData.test.ts index 58508ef8ee..0cbb2770dd 100644 --- a/ui/tx/assetFlows/utils/generateFlowViewData.test.ts +++ b/ui/tx/assetFlows/utils/generateFlowViewData.test.ts @@ -23,10 +23,6 @@ it('creates asset flows items', async() => { address: '0xdD15D2650387Fb6FEDE27ae7392C402a393F8A37', name: null, }, - leftActor: { - address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', - name: 'This wallet', - }, accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', }, { @@ -45,10 +41,6 @@ it('creates asset flows items', async() => { address: '', name: 'Validators', }, - leftActor: { - address: '0xef6595A423c99f3f2821190A4d96fcE4DcD89a80', - name: 'This wallet', - }, accountAddress: '0xef6595a423c99f3f2821190a4d96fce4dcd89a80', }, ], diff --git a/ui/tx/assetFlows/utils/generateFlowViewData.ts b/ui/tx/assetFlows/utils/generateFlowViewData.ts index b5d0825603..9564c574d8 100644 --- a/ui/tx/assetFlows/utils/generateFlowViewData.ts +++ b/ui/tx/assetFlows/utils/generateFlowViewData.ts @@ -16,10 +16,6 @@ export interface NovesFlowViewItem { address: string ; name: string | null; }; - leftActor: { - address: string; - name: string | null; - }; accountAddress: string; } @@ -57,9 +53,7 @@ export function generateFlowViewData(data: NovesResponseData): Array; - token: { - name: string | undefined; - symbol: string | undefined; - address: string | undefined; - id?: string | undefined; - }; + token: NovesTokenInfo; } export interface DescriptionItems { - token: { - name: string | undefined; - symbol: string | undefined; - address: string | undefined; - id?: string | undefined; - } | undefined; + token: NovesTokenInfo | undefined; text: string; hasId: boolean | undefined; } diff --git a/ui/tx/assetFlows/utils/getTokensData.ts b/ui/tx/assetFlows/utils/getTokensData.ts index 6a0128ed14..75a1302a59 100644 --- a/ui/tx/assetFlows/utils/getTokensData.ts +++ b/ui/tx/assetFlows/utils/getTokensData.ts @@ -1,26 +1,21 @@ import _ from 'lodash'; import type { NovesResponseData } from 'types/api/noves'; +import type { TokenInfo } from 'types/api/token'; + +export interface NovesTokenInfo extends Pick { + id?: string | undefined; +} export interface TokensData { nameList: Array; symbolList: Array; idList: Array; byName: { - [x: string]: { - name: string | undefined; - symbol: string | undefined; - address: string | undefined; - id?: string | undefined; - }; + [x: string]: NovesTokenInfo; }; bySymbol: { - [x: string]: { - name: string | undefined; - symbol: string | undefined; - address: string | undefined; - id: string | undefined; - }; + [x: string]: NovesTokenInfo; }; } @@ -32,13 +27,13 @@ export function getTokensData(data: NovesResponseData): TokensData { // Extract all tokens data const tokens = txItems.map((item) => { - const name = item.nft?.name || item.token?.name; - const symbol = item.nft?.symbol || item.token?.symbol; + const name = item.nft?.name || item.token?.name || null; + const symbol = item.nft?.symbol || item.token?.symbol || null; const token = { name: name, - symbol: symbol?.toLowerCase() === name?.toLowerCase() ? undefined : symbol, - address: item.nft?.address || item.token?.address, + symbol: symbol?.toLowerCase() === name?.toLowerCase() ? null : symbol, + address: item.nft?.address || item.token?.address || '', id: item.nft?.id || item.token?.id, }; @@ -63,10 +58,11 @@ export function getTokensData(data: NovesResponseData): TokensData { return i[0]; }); + const filters = [ 'undefined', 'null' ]; // Array of keys to match in string - const nameList = _.keysIn(mappedNames).filter(i => i !== 'undefined'); - const symbolList = _.keysIn(mappedSymbols).filter(i => i !== 'undefined'); - const idList = _.keysIn(mappedIds).filter(i => i !== 'undefined'); + const nameList = _.keysIn(mappedNames).filter(i => !filters.includes(i)); + const symbolList = _.keysIn(mappedSymbols).filter(i => !filters.includes(i)); + const idList = _.keysIn(mappedIds).filter(i => !filters.includes(i)); return { nameList, diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index 53aa39d733..cfd9f81463 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -119,7 +119,7 @@ const TxsContent = ({ return ( ; + items: Array; } const TxsList = (props: Props) => { diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index 827a013113..dbfe38de95 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -5,8 +5,9 @@ import { } from '@chakra-ui/react'; import React from 'react'; +import type { Transaction } from 'types/api/transaction'; + import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; import getValueWithUnit from 'lib/getValueWithUnit'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import { space } from 'lib/html-entities'; @@ -21,15 +22,12 @@ import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxType from 'ui/txs/TxType'; -import type { TransactionWithTranslate } from './noves/useDescribeTxs'; - type Props = { - tx: TransactionWithTranslate; + tx: Transaction; showBlockInfo: boolean; currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - // "translateEnabled" removed and replaced with "tx.translate.enabled" } const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => { @@ -37,26 +35,11 @@ const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeI const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - // This will be removed once the new proxy is ready - const { data: describeData, isLoading: isDescribeLoading } = useApiQuery('noves_describe_tx', { - pathParams: { hash: tx.hash }, - queryOptions: { - enabled: Boolean(tx.translate?.enabled), - }, - }); - // - return ( - { - - /* Whit the data inside tx - - */ - } - + diff --git a/ui/txs/TxsTable.tsx b/ui/txs/TxsTable.tsx index 9c794a907b..f0ec839c14 100644 --- a/ui/txs/TxsTable.tsx +++ b/ui/txs/TxsTable.tsx @@ -2,7 +2,7 @@ import { Link, Table, Tbody, Tr, Th } from '@chakra-ui/react'; import { AnimatePresence } from 'framer-motion'; import React from 'react'; -import type { TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; +import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction'; import config from 'configs/app'; import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; @@ -12,11 +12,10 @@ import IconSvg from 'ui/shared/IconSvg'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import TheadSticky from 'ui/shared/TheadSticky'; -import type { TransactionWithTranslate } from './noves/useDescribeTxs'; import TxsTableItem from './TxsTableItem'; type Props = { - txs: Array; + txs: Array; sort: (field: TransactionsSortingField) => () => void; sorting?: TransactionsSortingValue; top: number; diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index 97e4245a5d..f78b00dd09 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -7,8 +7,9 @@ import { import { motion } from 'framer-motion'; import React from 'react'; +import type { Transaction } from 'types/api/transaction'; + import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import AddressFromTo from 'ui/shared/address/AddressFromTo'; import Tag from 'ui/shared/chakra/Tag'; @@ -20,31 +21,20 @@ import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; -import type { TransactionWithTranslate } from './noves/useDescribeTxs'; import TxType from './TxType'; type Props = { - tx: TransactionWithTranslate; + tx: Transaction; showBlockInfo: boolean; currentAddress?: string; enableTimeIncrement?: boolean; isLoading?: boolean; - // "translateEnabled" removed and replaced with "tx.translate.enabled" } const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading }: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement); - // This will be removed once the new proxy is ready - const { data: describeData, isLoading: isDescribeLoading } = useApiQuery('noves_describe_tx', { - pathParams: { hash: tx.hash }, - queryOptions: { - enabled: Boolean(tx.translate?.enabled), - }, - }); - // - return (
- { - - /* Whit the data inside tx - - */ - } - + diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx index 9e7dd21b1e..a73e267585 100644 --- a/ui/txs/noves/useDescribeTxs.tsx +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -1,89 +1,80 @@ -import type { UseQueryResult } from '@tanstack/react-query'; import _ from 'lodash'; +import { useMemo } from 'react'; -import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; -import type { ResourceError } from 'lib/api/resources'; -// import useApiQuery from 'lib/api/useApiQuery'; - -export interface DescribeTxs { - txHash: string; - type?: string; - description?: string; - isLoading: boolean; - enabled: boolean; -} - -export interface TransactionWithTranslate extends Transaction { - translate?: DescribeTxs; -} +import useApiQuery from 'lib/api/useApiQuery'; const feature = config.features.txInterpretation; const translateEnabled = feature.isEnabled && feature.provider === 'noves'; export default function useDescribeTxs(items: Array | undefined, viewAsAccountAddress: string | undefined) { - const txsHash = items?.map(i => i.hash); - const txsChunk = _.chunk(txsHash || [], 10); + const txsHash = _.uniq(items?.map(i => i.hash)); - const txsParams = Array(5).fill(undefined).map((_, i) => txsChunk[i]); - const txsQuerys = useFetchTxs(txsParams, viewAsAccountAddress); + const txsQueries = useFetchTxs(txsHash, viewAsAccountAddress); - const isLoading = txsQuerys.some(query => query.isLoading || query.isPlaceholderData); - const queryData = txsQuerys.map(query => query.data ? query.data : []).flat(); + const isLoading = useMemo(() => txsQueries.some(query => query.isLoading || query.isPlaceholderData), [ txsQueries ]); + const queryData = useMemo(() => txsQueries.map(query => query.data ? query.data : []).flat(), [ txsQueries ]); + + const data: Array | undefined = useMemo(() => items?.map(tx => { + if (!translateEnabled) { + // Can't return earlier because of hooks order + return tx; + } - const data: Array | undefined = items?.map(tx => { - const query = queryData.find(data => data.txHash === tx.hash); + const query = queryData.find(data => data.txHash.toLowerCase() === tx.hash.toLowerCase()); if (query) { return { ...tx, - translate: { - ...query, + translation: { + data: query, isLoading: false, - enabled: translateEnabled, }, }; } return { ...tx, - translate: { - txHash: tx.hash, + translation: { isLoading, - enabled: translateEnabled, }, }; - }); + }), [ items, queryData, isLoading ]); - // return same "items" array of Transaction with a new "translate" field. + // return same "items" array of Transaction with a new "translation" field. return data; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function useFetchTxs(txs: Array>, viewAsAccountAddress: string | undefined) { - const txsQuerys = []; +function useFetchTxs(txsHash: Array, viewAsAccountAddress: string | undefined) { + // we need to send 10 txs per call + const txsHashChunk = _.chunk(txsHash, 10); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const body of txs) { - // loop to avoid writing 5 hook calls. - // txs will always have the same length so we always execute the same amount of hooks + const txsQueries = []; - // Need to fix implementation here to handle translateEnabled correctly, and verify the calls to describeTxs proxy are working correctly - // const query = useApiQuery('noves_describe_txs', { - // queryParams: { - // viewAsAccountAddress: viewAsAccountAddress, - // hashes: body, - // }, - // }); + // loop to avoid writing 5 hook calls. + for (let index = 0; index < 5; index++) { - // txsQuerys.push(query); + const body = txsHashChunk[index]; + + // we always execute the same amount of hooks + + // eslint-disable-next-line react-hooks/rules-of-hooks + const query = useApiQuery('noves_describe_txs', { + queryParams: { + viewAsAccountAddress: viewAsAccountAddress, + hashes: body, + }, + queryOptions: { + enabled: translateEnabled && Boolean(body), + }, + }); - txsQuerys.push({} as UseQueryResult>); + txsQueries.push(query); } - return txsQuerys; + return txsQueries; } From ba04f7b69308f79c082dbd3f6211fd98597b394d Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 12 Feb 2024 11:15:13 -0300 Subject: [PATCH 10/30] tx asset flows pageSize of 50 --- ui/tx/TxAssetFlows.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/TxAssetFlows.tsx index 8b80fba514..333f02dcbd 100644 --- a/ui/tx/TxAssetFlows.tsx +++ b/ui/tx/TxAssetFlows.tsx @@ -32,7 +32,7 @@ export default function TxAssetFlows(props: FlowViewProps) { const [ page, setPage ] = useState(1); const ViewData = useMemo(() => (queryData ? generateFlowViewData(queryData) : []), [ queryData ]); - const chunkedViewData = _.chunk(ViewData, 10); + const chunkedViewData = _.chunk(ViewData, 50); const paginationProps: PaginationParams = useMemo(() => ({ onNextPageClick: () => setPage(page + 1), From d8573277a41e857ddc449c48d022d14d4dfbdb30 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Sat, 17 Feb 2024 09:51:46 -0300 Subject: [PATCH 11/30] PR comments fixes --- lib/api/resources.ts | 4 - ui/address/AddressAccountHistory.tsx | 15 +-- ui/tx/TxAssetFlows.tsx | 47 ++++---- .../components/NovesActionSnippet.tsx | 7 +- .../components/NovesTokenTooltipContent.tsx | 3 +- ui/txs/TxTranslationType.tsx | 32 ++++++ ui/txs/TxType.tsx | 15 +-- ui/txs/TxsContent.tsx | 10 +- ui/txs/TxsListItem.tsx | 7 +- ui/txs/TxsTableItem.tsx | 6 +- ui/txs/noves/useDescribeTxs.tsx | 107 ++++++++++-------- ui/txs/noves/utils.ts | 11 ++ 12 files changed, 155 insertions(+), 109 deletions(-) create mode 100644 ui/txs/TxTranslationType.tsx create mode 100644 ui/txs/noves/utils.ts diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 10da552191..01960e4fed 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -599,10 +599,6 @@ export const RESOURCES = { }, noves_describe_txs: { path: '/api/v2/proxy/noves-fi/transactions', - queryParams: [ - 'viewAsAccountAddress' as const, - { key: 'hashes', type: 'array' } as const, - ], }, // USER OPS user_ops: { diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index 064dcf6864..0d98b51678 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -1,5 +1,5 @@ import { Box, Hide, Show, Table, - Tbody, Th, Thead, Tr } from '@chakra-ui/react'; + Tbody, Th, Tr } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; @@ -16,6 +16,7 @@ import DataListDisplay from 'ui/shared/DataListDisplay'; import { getFromToValue } from 'ui/shared/Noves/utils'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import TheadSticky from 'ui/shared/TheadSticky'; import AddressAccountHistoryListItem from './accountHistory/AddressAccountHistoryListItem'; import AccountHistoryFilter from './AddressAccountHistoryFilter'; @@ -65,10 +66,10 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { const content = ( - + { filteredData?.map((item, i) => ( { )) } - + - + - + { filteredData?.map((item, i) => ( - + { data?.length && } { data?.map((item, i) => ( @@ -82,30 +83,28 @@ export default function TxAssetFlows(props: FlowViewProps) { )) } - - -
Age @@ -90,11 +91,11 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { From/To
- - - + + + + { data?.map((item, i) => ( + + )) } + +
+ + + + + - + - - - - { data?.map((item, i) => ( - - )) } - -
Actions - + From/To -
- +
); diff --git a/ui/tx/assetFlows/components/NovesActionSnippet.tsx b/ui/tx/assetFlows/components/NovesActionSnippet.tsx index d9bbc6dd8f..597b11d774 100644 --- a/ui/tx/assetFlows/components/NovesActionSnippet.tsx +++ b/ui/tx/assetFlows/components/NovesActionSnippet.tsx @@ -2,6 +2,7 @@ import { Box, Hide, Popover, PopoverArrow, PopoverContent, PopoverTrigger, Show, import type { FC } from 'react'; import React from 'react'; +import { HEX_REGEXP } from 'lib/regexp'; import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import IconSvg from 'ui/shared/IconSvg'; @@ -31,11 +32,11 @@ const NovesActionSnippet: FC = ({ item, isLoaded }) => { return token; }, [ item.action ]); - const validTokenAddress = token.address ? Boolean(token.address.match(/^0x[a-fA-F\d]{40}$/)) : false; + const validTokenAddress = token.address ? HEX_REGEXP.test(token.address) : false; return ( - + { item.action.label } @@ -54,7 +55,7 @@ const NovesActionSnippet: FC = ({ item, isLoaded }) => { /> - + = ({ token, amount }) => { } const showTokenName = token.symbol !== token.name; - const showTokenAddress = Boolean(token.address.match(/^0x[a-fA-F\d]{40}$/)); + const showTokenAddress = HEX_REGEXP.test(token.address); return ( diff --git a/ui/txs/TxTranslationType.tsx b/ui/txs/TxTranslationType.tsx new file mode 100644 index 0000000000..4086efb80f --- /dev/null +++ b/ui/txs/TxTranslationType.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import type { TransactionType } from 'types/api/transaction'; + +import Tag from 'ui/shared/chakra/Tag'; + +import { camelCaseToSentence } from './noves/utils'; +import TxType from './TxType'; + +export interface Props { + types: Array; + isLoading?: boolean; + translatationType: string | undefined; +} + +const TxTranslationType = ({ types, isLoading, translatationType }: Props) => { + + const filteredTypes = [ 'unclassified' ]; + + if (!translatationType || filteredTypes.includes(translatationType)) { + return ; + } + + return ( + + { camelCaseToSentence(translatationType) } + + ); + +}; + +export default TxTranslationType; diff --git a/ui/txs/TxType.tsx b/ui/txs/TxType.tsx index 462040040c..fe856096dc 100644 --- a/ui/txs/TxType.tsx +++ b/ui/txs/TxType.tsx @@ -7,16 +7,13 @@ import Tag from 'ui/shared/chakra/Tag'; export interface Props { types: Array; isLoading?: boolean; - translateLabel?: string; } const TYPES_ORDER = [ 'rootstock_remasc', 'rootstock_bridge', 'token_creation', 'contract_creation', 'token_transfer', 'contract_call', 'coin_transfer' ]; -const TxType = ({ types, isLoading, translateLabel }: Props) => { +const TxType = ({ types, isLoading }: Props) => { const typeToShow = types.sort((t1, t2) => TYPES_ORDER.indexOf(t1) - TYPES_ORDER.indexOf(t2))[0]; - const filteredTypes = [ 'unclassified' ]; - let label; let colorScheme; @@ -55,16 +52,6 @@ const TxType = ({ types, isLoading, translateLabel }: Props) => { } - if (translateLabel) { - if (!filteredTypes.includes(translateLabel)) { - return ( - - { translateLabel } - - ); - } - } - return ( { label } diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index cfd9f81463..44f687afc5 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -63,9 +63,9 @@ const TxsContent = ({ setSorting(value); }, [ sort, setSorting ]); - const itemsWithTranslate = useDescribeTxs(items, currentAddress); + const itemsWithTranslation = useDescribeTxs(items, currentAddress, query.isPlaceholderData); - const content = itemsWithTranslate ? ( + const content = itemsWithTranslation ? ( <> - + { tx.translation ? + : + + } diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index f78b00dd09..4c27c38693 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -21,6 +21,7 @@ import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import TxWatchListTags from 'ui/shared/tx/TxWatchListTags'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; +import TxTranslationType from './TxTranslationType'; import TxType from './TxType'; type Props = { @@ -61,7 +62,10 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
- + { tx.translation ? + : + + } diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx index a73e267585..d9081492cd 100644 --- a/ui/txs/noves/useDescribeTxs.tsx +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -1,26 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; import _ from 'lodash'; -import { useMemo } from 'react'; +import React from 'react'; +import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; -import useApiQuery from 'lib/api/useApiQuery'; +import useApiFetch from 'lib/api/useApiFetch'; const feature = config.features.txInterpretation; const translateEnabled = feature.isEnabled && feature.provider === 'noves'; -export default function useDescribeTxs(items: Array | undefined, viewAsAccountAddress: string | undefined) { - const txsHash = _.uniq(items?.map(i => i.hash)); - - const txsQueries = useFetchTxs(txsHash, viewAsAccountAddress); +export default function useDescribeTxs(items: Array | undefined, viewAsAccountAddress: string | undefined, isPlaceholderData: boolean) { + const apiFetch = useApiFetch(); - const isLoading = useMemo(() => txsQueries.some(query => query.isLoading || query.isPlaceholderData), [ txsQueries ]); - const queryData = useMemo(() => txsQueries.map(query => query.data ? query.data : []).flat(), [ txsQueries ]); + const txsHash = _.uniq(items?.map(i => i.hash)); + const txChunks = _.chunk(txsHash, 10); + + const queryKey = { + viewAsAccountAddress, + firstHash: txsHash[0] || '', + lastHash: txsHash[txsHash.length - 1] || '', + }; + + const describeQuery = useQuery({ + queryKey: [ 'noves_describe_txs', queryKey ], + queryFn: async() => { + const queries = txChunks.map((hashes) => { + if (hashes.length === 0) { + return Promise.resolve([]); + } + + return apiFetch('noves_describe_txs', { + queryParams: { + viewAsAccountAddress, + hashes, + }, + }) as Promise; + }); + + return Promise.all(queries); + }, + select: (data) => { + return data.flat(); + }, + enabled: translateEnabled && !isPlaceholderData, + }); + + const itemsWithTranslation = React.useMemo(() => items?.map(tx => { + const queryData = describeQuery.data; + const isLoading = describeQuery.isLoading; + + if (isLoading) { + return { + ...tx, + translation: { + isLoading, + }, + }; + } - const data: Array | undefined = useMemo(() => items?.map(tx => { - if (!translateEnabled) { - // Can't return earlier because of hooks order + if (!queryData || !translateEnabled) { return tx; } @@ -36,45 +77,13 @@ export default function useDescribeTxs(items: Array | undefined, vi }; } - return { - ...tx, - translation: { - isLoading, - }, - }; - }), [ items, queryData, isLoading ]); - - // return same "items" array of Transaction with a new "translation" field. - - return data; -} - -function useFetchTxs(txsHash: Array, viewAsAccountAddress: string | undefined) { - // we need to send 10 txs per call - const txsHashChunk = _.chunk(txsHash, 10); - - const txsQueries = []; + return tx; + }), [ items, describeQuery ]); - // loop to avoid writing 5 hook calls. - for (let index = 0; index < 5; index++) { - - const body = txsHashChunk[index]; - - // we always execute the same amount of hooks - - // eslint-disable-next-line react-hooks/rules-of-hooks - const query = useApiQuery('noves_describe_txs', { - queryParams: { - viewAsAccountAddress: viewAsAccountAddress, - hashes: body, - }, - queryOptions: { - enabled: translateEnabled && Boolean(body), - }, - }); - - txsQueries.push(query); + if (!translateEnabled || isPlaceholderData) { + return items; } - return txsQueries; + // return same "items" array of Transaction with a new "translation" field. + return itemsWithTranslation; } diff --git a/ui/txs/noves/utils.ts b/ui/txs/noves/utils.ts new file mode 100644 index 0000000000..c5a986b66a --- /dev/null +++ b/ui/txs/noves/utils.ts @@ -0,0 +1,11 @@ +export function camelCaseToSentence(camelCaseString: string | undefined) { + if (!camelCaseString) { + return ''; + } + + let sentence = camelCaseString.replace(/([a-z])([A-Z])/g, '$1 $2'); + sentence = sentence.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'); + sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1); + + return sentence; +} From 2dc925d72e38d5c0a25df76d8b433baf37083220 Mon Sep 17 00:00:00 2001 From: Juan Date: Sat, 17 Feb 2024 23:03:44 -0300 Subject: [PATCH 12/30] rename expected api endpoint for the describe_txs endpoint, more accurate and descriptive --- lib/api/resources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 01960e4fed..61ebcf4175 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -598,7 +598,7 @@ export const RESOURCES = { filterFields: [], }, noves_describe_txs: { - path: '/api/v2/proxy/noves-fi/transactions', + path: '/api/v2/proxy/noves-fi/describe-transactions', }, // USER OPS user_ops: { From 926ed8b552fc5b43b073193583870ab92f74fb18 Mon Sep 17 00:00:00 2001 From: Juan Date: Sun, 18 Feb 2024 17:17:00 -0300 Subject: [PATCH 13/30] one final re-name for api endpoint (make it clear it's an object vs an action) --- lib/api/resources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 61ebcf4175..d8f99573e5 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -598,7 +598,7 @@ export const RESOURCES = { filterFields: [], }, noves_describe_txs: { - path: '/api/v2/proxy/noves-fi/describe-transactions', + path: '/api/v2/proxy/noves-fi/transaction-descriptions', }, // USER OPS user_ops: { From 8d66a3eb5bd0b4f7702fa93612a14f1d91747e3d Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Tue, 20 Feb 2024 10:21:43 -0300 Subject: [PATCH 14/30] scrollRef fix --- ui/address/AddressAccountHistory.tsx | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/ui/address/AddressAccountHistory.tsx b/ui/address/AddressAccountHistory.tsx index 0d98b51678..5b7cf3aa7e 100644 --- a/ui/address/AddressAccountHistory.tsx +++ b/ui/address/AddressAccountHistory.tsx @@ -108,22 +108,17 @@ const AddressAccountHistory = ({ scrollRef }: Props) => { ); return ( - <> - { /* should stay before tabs to scroll up with pagination */ } - - - - + ); }; From 85b006acfabd115527426cc2d7a51b5e81142058 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Thu, 22 Feb 2024 21:37:08 -0300 Subject: [PATCH 15/30] build error fix --- lib/api/resources.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 526a7315e8..dc259df435 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -854,9 +854,6 @@ Q extends 'zkevm_l2_txn_batches_count' ? number : Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'config_backend_version' ? BackendVersionConfig : -Q extends 'noves_transaction' ? NovesResponseData : -Q extends 'noves_address_history' ? NovesAccountHistoryResponse : -Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse : Q extends 'addresses_lookup' ? EnsAddressLookupResponse : Q extends 'domain_info' ? EnsDomainDetailed : Q extends 'domain_events' ? EnsDomainEventsResponse : @@ -880,6 +877,9 @@ Q extends 'shibarium_deposits' ? ShibariumDepositsResponse : Q extends 'shibarium_withdrawals_count' ? number : Q extends 'shibarium_deposits_count' ? number : Q extends 'contract_security_audits' ? SmartContractSecurityAudits : +Q extends 'noves_transaction' ? NovesResponseData : +Q extends 'noves_address_history' ? NovesAccountHistoryResponse : +Q extends 'noves_describe_txs' ? NovesDescribeTxsResponse : never; /* eslint-enable @typescript-eslint/indent */ From b68a9249337a23798c5d0875a5ab049f762dd275 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Sun, 3 Mar 2024 03:36:31 -0300 Subject: [PATCH 16/30] design fixes --- types/api/noves.ts | 2 +- .../AddressAccountHistoryListItem.tsx | 10 ++++++++-- .../AddressAccountHistoryTableItem.tsx | 16 +++++++++++----- ui/shared/Noves/NovesFromTo.tsx | 15 ++++++--------- ui/shared/pagination/useQueryWithPages.ts | 11 +++++++++++ ui/tx/assetFlows/TxAssetFlowsTableItem.tsx | 4 ++-- .../components/NovesSubHeadingInterpretation.tsx | 2 +- 7 files changed, 40 insertions(+), 20 deletions(-) diff --git a/types/api/noves.ts b/types/api/noves.ts index b77d4ba6f9..0ddc6caf81 100644 --- a/types/api/noves.ts +++ b/types/api/noves.ts @@ -82,7 +82,7 @@ export interface NovesAccountHistoryResponse { pageNumber: number; pageSize: number; next_page_params?: { - startBlock: null; + startBlock: string; endBlock: string; pageNumber: number; pageSize: number; diff --git a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx index 143560ae2e..46cf969932 100644 --- a/ui/address/accountHistory/AddressAccountHistoryListItem.tsx +++ b/ui/address/accountHistory/AddressAccountHistoryListItem.tsx @@ -1,5 +1,5 @@ import { Box, Flex, Skeleton, Text } from '@chakra-ui/react'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { NovesResponseData } from 'types/api/noves'; @@ -17,6 +17,12 @@ type Props = { const AddressAccountHistoryListItem = (props: Props) => { + const parsedDescription = useMemo(() => { + const description = props.tx.classificationData.description; + + return description.endsWith('.') ? description.substring(0, description.length - 1) : description; + }, [ props.tx.classificationData.description ]); + return ( @@ -46,7 +52,7 @@ const AddressAccountHistoryListItem = (props: Props) => { whiteSpace="break-spaces" wordBreak="break-word" > - { props.tx.classificationData.description } + { parsedDescription } diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx index aab4cbf688..1b98d899aa 100644 --- a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx +++ b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx @@ -1,5 +1,5 @@ import { Td, Tr, Skeleton, Text, Box } from '@chakra-ui/react'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { NovesResponseData } from 'types/api/noves'; @@ -16,16 +16,22 @@ type Props = { const AddressAccountHistoryTableItem = (props: Props) => { + const parsedDescription = useMemo(() => { + const description = props.tx.classificationData.description; + + return description.endsWith('.') ? description.substring(0, description.length - 1) : description; + }, [ props.tx.classificationData.description ]); + return (
+ { dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() } + { whiteSpace="break-spaces" wordBreak="break-word" > - { props.tx.classificationData.description } + { parsedDescription } + diff --git a/ui/shared/Noves/NovesFromTo.tsx b/ui/shared/Noves/NovesFromTo.tsx index 7dbbf93f4f..db1ea47205 100644 --- a/ui/shared/Noves/NovesFromTo.tsx +++ b/ui/shared/Noves/NovesFromTo.tsx @@ -1,4 +1,4 @@ -import { Box, Skeleton, Tag, TagLabel } from '@chakra-ui/react'; +import { Box, Skeleton } from '@chakra-ui/react'; import type { FC } from 'react'; import React from 'react'; @@ -6,6 +6,7 @@ import type { NovesResponseData } from 'types/api/noves'; import type { NovesFlowViewItem } from 'ui/tx/assetFlows/utils/generateFlowViewData'; +import Tag from '../chakra/Tag'; import AddressEntity from '../entities/address/AddressEntity'; import { getActionFromTo, getFromTo } from './utils'; @@ -38,15 +39,10 @@ const NovesFromTo: FC = ({ isLoaded, txData, currentAddress = '', item }) - - { data.text } - + { data.text } = ({ isLoaded, txData, currentAddress = '', item }) fontWeight="500" noCopy={ !data.address } noLink={ !data.address } + noIcon={ address.name === 'Validators' } ml={ 2 } truncation="dynamic" /> diff --git a/ui/shared/pagination/useQueryWithPages.ts b/ui/shared/pagination/useQueryWithPages.ts index 41839200fa..4478f91883 100644 --- a/ui/shared/pagination/useQueryWithPages.ts +++ b/ui/shared/pagination/useQueryWithPages.ts @@ -39,6 +39,17 @@ function getNextPageParams(data: ResourcePayload v != null) + .reduce((acc, [ k, v ]) => ({ ...acc, [k]: v }), {}); + + } + return data.next_page_params; } diff --git a/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx b/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx index efe617edcf..0e2056b038 100644 --- a/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx +++ b/ui/tx/assetFlows/TxAssetFlowsTableItem.tsx @@ -15,10 +15,10 @@ const TxAssetFlowsTableItem = (props: Props) => { return (
+ +
+ { dayjs(props.tx.rawTransactionData.timestamp * 1000).fromNow() } + { { content } - { !hasTag && } + { !hasTag && } diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx index e050ea7cbd..7409f99af4 100644 --- a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx +++ b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx @@ -23,7 +23,7 @@ const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { const description = getDescriptionItems(data); return ( - + { description.map((item, i) => ( From 48cf473d5e151fb96f0121c4a255171da2af7ca9 Mon Sep 17 00:00:00 2001 From: francisco-noves Date: Thu, 7 Mar 2024 16:39:28 -0300 Subject: [PATCH 21/30] Removed wrong color in table --- ui/address/accountHistory/AddressAccountHistoryTableItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx index 35b47a150f..c3aa61a283 100644 --- a/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx +++ b/ui/address/accountHistory/AddressAccountHistoryTableItem.tsx @@ -54,7 +54,7 @@ const AddressAccountHistoryTableItem = (props: Props) => { + From 9aeb7d61e714ef9b2eb49b487e393520d88bd5b3 Mon Sep 17 00:00:00 2001 From: francisco-noves Date: Fri, 8 Mar 2024 14:08:27 -0300 Subject: [PATCH 22/30] removed null validation in page params --- ui/shared/pagination/useQueryWithPages.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ui/shared/pagination/useQueryWithPages.ts b/ui/shared/pagination/useQueryWithPages.ts index bf776f9d9a..fd293b5aa6 100644 --- a/ui/shared/pagination/useQueryWithPages.ts +++ b/ui/shared/pagination/useQueryWithPages.ts @@ -41,17 +41,6 @@ function getNextPageParams(data: ResourcePayload v != null) - .reduce((acc, [ k, v ]) => ({ ...acc, [k]: v }), {}); - - } - return data.next_page_params; } From 301ff00c37ed1249de67e09fac6184842db9b956 Mon Sep 17 00:00:00 2001 From: francisco-noves Date: Mon, 11 Mar 2024 15:12:05 -0300 Subject: [PATCH 23/30] updated margin --- ui/tx/TxSubHeading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 9fb070d679..657850b43a 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -105,7 +105,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { { content } - { !hasTag && } + { !hasTag && } From 674f102396d350fe3cdb22f3a7db160344b7598f Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Tue, 12 Mar 2024 10:17:51 -0300 Subject: [PATCH 24/30] margin fix --- ui/tx/TxSubHeading.tsx | 2 +- ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 657850b43a..cf1fedb210 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -105,7 +105,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { { content } - { !hasTag && } + { !hasTag && } diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx index 7409f99af4..7cbd42457c 100644 --- a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx +++ b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx @@ -23,8 +23,8 @@ const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { const description = getDescriptionItems(data); return ( - - + + { description.map((item, i) => ( From fd3dfa7c2475042379c5af3412998fd9c44a504b Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Wed, 13 Mar 2024 15:30:11 -0300 Subject: [PATCH 25/30] add icons to contracts --- .../NovesSubHeadingInterpretation.tsx | 13 ++++- .../utils/getDescriptionItems.test.ts | 21 +++++++- ui/tx/assetFlows/utils/getDescriptionItems.ts | 50 ++++++++++++++++--- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx index 7cbd42457c..6270ad2cd2 100644 --- a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx +++ b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx @@ -4,6 +4,7 @@ import React, { Fragment } from 'react'; import type { NovesResponseData } from 'types/api/noves'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import IconSvg from 'ui/shared/IconSvg'; import { getDescriptionItems } from 'ui/tx/assetFlows/utils/getDescriptionItems'; @@ -61,7 +62,17 @@ const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { fontSize="lg" w="fit-content" /> - ) } + ) + } + { + item.address && ( + + ) + } )) } diff --git a/ui/tx/assetFlows/utils/getDescriptionItems.test.ts b/ui/tx/assetFlows/utils/getDescriptionItems.test.ts index 5685b64d44..715a786f4c 100644 --- a/ui/tx/assetFlows/utils/getDescriptionItems.test.ts +++ b/ui/tx/assetFlows/utils/getDescriptionItems.test.ts @@ -7,9 +7,28 @@ it('creates sub heading items to render', async() => { expect(result).toEqual([ { - text: ' Called function \'stake\' on contract 0xef326CdAdA59D3A740A76bB5f4F88Fb2', token: undefined, + text: ' ', hasId: false, + type: 'action', + actionText: 'Called function', + address: undefined, + }, + { + token: undefined, + text: '\'stake\' ', + hasId: false, + type: 'action', + actionText: 'on contract', + address: undefined, + }, + { + token: undefined, + text: '', + hasId: false, + type: 'contract', + actionText: undefined, + address: '0xef326cdada59d3a740a76bb5f4f88fb2f1076164', }, ]); }); diff --git a/ui/tx/assetFlows/utils/getDescriptionItems.ts b/ui/tx/assetFlows/utils/getDescriptionItems.ts index d8a9a2e8b0..d252aea98d 100644 --- a/ui/tx/assetFlows/utils/getDescriptionItems.ts +++ b/ui/tx/assetFlows/utils/getDescriptionItems.ts @@ -10,7 +10,7 @@ interface TokenWithIndices { hasId: boolean; indices: Array; token?: NovesTokenInfo; - type?: 'action'; + type?: 'action' | 'contract'; } export interface DescriptionItems { @@ -19,8 +19,11 @@ export interface DescriptionItems { hasId: boolean | undefined; type?: string; actionText?: string; + address?: string; } +const CONTRACT_REGEXP = /(0x[\da-fA-F]{32}\b)/g; + export const getDescriptionItems = (translateData: NovesResponseData): Array => { // Remove final dot and add space at the start to avoid matching issues @@ -33,9 +36,11 @@ export const getDescriptionItems = (translateData: NovesResponseData): Array parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); let tokensMatchedBySymbol = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); - const actions = [ 'sent', 'Sent', 'Called function', 'called function', 'on contract' ]; + const actions = [ 'sent', 'Sent', 'Called function', 'called function', 'on contract', 'swap', 'Swap' ]; const actionsMatched = actions.filter(action => parsedDescription.includes(action)); + const contractMatched = parsedDescription.match(CONTRACT_REGEXP) || []; + // Filter symbols if they're already matched by name tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); @@ -44,6 +49,7 @@ export const getDescriptionItems = (translateData: NovesResponseData): Array indices.push(...i.indices)); } + if (contractMatched.length) { + tokensByContract = parseTokensByContract(contractMatched, parsedDescription, translateData); + + tokensByContract.forEach(i => indices.push(...i.indices)); + } + const indicesSorted = _.uniq(indices.sort((a, b) => a - b)); - const tokensWithIndices = _.uniqBy(_.concat(tokensByName, tokensBySymbol, tokensByAction), 'name'); + const tokensWithIndices = _.uniqBy(_.concat(tokensByName, tokensBySymbol, tokensByAction, tokensByContract), 'name'); return createDescriptionItems(indicesSorted, tokensWithIndices, parsedDescription); }; @@ -146,7 +158,7 @@ const parseTokensBySymbol = (tokensMatchedBySymbol: Array, idsMatched: A }; const parseTokensByAction = (actionsMatched: Array, parsedDescription: string) => { - const tokensBySymbol: Array = actionsMatched.map(action => { + const tokensByAction: Array = actionsMatched.map(action => { return { name: action, indices: [ ...parsedDescription.matchAll(new RegExp(action, 'gi')) ].map(a => a.index) as unknown as Array, @@ -155,7 +167,25 @@ const parseTokensByAction = (actionsMatched: Array, parsedDescription: s }; }); - return tokensBySymbol; + return tokensByAction; +}; + +const parseTokensByContract = (contractMatched: Array, parsedDescription: string, translateData: NovesResponseData) => { + const toAddress = translateData.rawTransactionData.toAddress.toLowerCase(); + const contractFiltered = contractMatched.filter(contract => toAddress.startsWith(contract.toLowerCase()))[0]; + + if (!contractFiltered) { + return []; + } + + const tokensByContract: Array = [ { + name: toAddress, + indices: [ ...parsedDescription.matchAll(new RegExp(contractFiltered, 'gi')) ].map(a => a.index) as unknown as Array, + hasId: false, + type: 'contract', + } ]; + + return tokensByContract; }; const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: Array, parsedDescription: string) => { @@ -167,26 +197,30 @@ const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: if (i === 0) { const isAction = item?.type === 'action'; + const isContract = item?.type === 'contract'; token = { token: item?.token, text: parsedDescription.substring(0, endIndex), hasId: item?.hasId, - type: isAction ? 'action' : undefined, + type: item?.type, actionText: isAction ? item.name : undefined, + address: isContract ? item.name : undefined, }; } else { const previousItem = tokensWithIndices.find(t => t?.indices.includes(indicesSorted[i - 1])); // Add the length of the text of the previous token to remove it from the start const startIndex = indicesSorted[i - 1] + (previousItem?.name.length || 0) + 1; const isAction = item?.type === 'action'; + const isContract = item?.type === 'contract'; token = { token: item?.token, text: parsedDescription.substring(startIndex, endIndex), hasId: item?.hasId, - type: isAction ? 'action' : undefined, + type: item?.type, actionText: isAction ? item.name : undefined, + address: isContract ? item.name : undefined, }; } @@ -199,7 +233,7 @@ const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: // Check if there is text left after the last token and push it to the array if (restString) { - descriptionItems.push({ text: restString, token: undefined, hasId: false, type: undefined, actionText: undefined }); + descriptionItems.push({ text: restString, token: undefined, hasId: false, type: undefined, actionText: undefined, address: undefined }); } return descriptionItems; From aa7bd8357171c73223a4c2f4482b406485b8dab7 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Wed, 20 Mar 2024 17:41:29 -0300 Subject: [PATCH 26/30] Sub-heading interpretation simplified --- types/api/noves.ts | 8 + ui/tx/TxSubHeading.tsx | 11 +- .../NovesSubHeadingInterpretation.tsx | 83 ------ .../components/NovesTokenTransferSnippet.tsx | 41 --- .../utils/createNovesSummaryObject.ts | 139 ++++++++++ ui/tx/assetFlows/utils/getAddressValues.ts | 76 ++++++ .../utils/getDescriptionItems.test.ts | 34 --- ui/tx/assetFlows/utils/getDescriptionItems.ts | 240 ------------------ ui/tx/assetFlows/utils/getTokensData.ts | 10 +- 9 files changed, 238 insertions(+), 404 deletions(-) delete mode 100644 ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx delete mode 100644 ui/tx/assetFlows/components/NovesTokenTransferSnippet.tsx create mode 100644 ui/tx/assetFlows/utils/createNovesSummaryObject.ts create mode 100644 ui/tx/assetFlows/utils/getAddressValues.ts delete mode 100644 ui/tx/assetFlows/utils/getDescriptionItems.test.ts delete mode 100644 ui/tx/assetFlows/utils/getDescriptionItems.ts diff --git a/types/api/noves.ts b/types/api/noves.ts index 0ddc6caf81..1d4b396716 100644 --- a/types/api/noves.ts +++ b/types/api/noves.ts @@ -12,6 +12,7 @@ export interface NovesClassificationData { description: string; sent: Array; received: Array; + approved?: Approved; protocol?: { name: string | null; }; @@ -21,6 +22,13 @@ export interface NovesClassificationData { message?: string; } +export interface Approved { + amount: string; + spender: string; + token?: NovesToken; + nft?: NovesNft; +} + export interface NovesSentReceived { action: string; actionFormatted?: string; diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 6b88cf700a..03d7a26ec8 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -11,7 +11,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation'; -import NovesSubHeadingInterpretation from './assetFlows/components/NovesSubHeadingInterpretation'; +import { createNovesSummaryObject } from './assetFlows/utils/createNovesSummaryObject'; import type { TxQuery } from './useTxQuery'; type Props = { @@ -52,11 +52,14 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { const hasViewAllInterpretationsLink = !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1; - if (hasNovesInterpretation) { + if (hasNovesInterpretation && novesInterpretationQuery.data) { + const novesSummary = createNovesSummaryObject(novesInterpretationQuery.data); + return ( - ); } else if (hasInternalInterpretation) { diff --git a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx b/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx deleted file mode 100644 index 6270ad2cd2..0000000000 --- a/ui/tx/assetFlows/components/NovesSubHeadingInterpretation.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Box, Skeleton, Text } from '@chakra-ui/react'; -import type { FC } from 'react'; -import React, { Fragment } from 'react'; - -import type { NovesResponseData } from 'types/api/noves'; - -import AddressEntity from 'ui/shared/entities/address/AddressEntity'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; -import IconSvg from 'ui/shared/IconSvg'; -import { getDescriptionItems } from 'ui/tx/assetFlows/utils/getDescriptionItems'; - -import NovesTokenTransferSnippet from './NovesTokenTransferSnippet'; - -interface Props { - data: NovesResponseData | undefined; - isLoading: boolean; -} - -const NovesSubHeadingInterpretation: FC = ({ data, isLoading }) => { - if (!data) { - return null; - } - - const description = getDescriptionItems(data); - - return ( - - - { description.map((item, i) => ( - - - { i === 0 && ( - - ) } - { item.text } - - { - item.actionText && ( - - { item.actionText } - - ) - } - { item.hasId && item.token ? ( - - ) : - item.token && ( - - ) - } - { - item.address && ( - - ) - } - - )) } - - - ); -}; - -export default NovesSubHeadingInterpretation; diff --git a/ui/tx/assetFlows/components/NovesTokenTransferSnippet.tsx b/ui/tx/assetFlows/components/NovesTokenTransferSnippet.tsx deleted file mode 100644 index 5333a2e310..0000000000 --- a/ui/tx/assetFlows/components/NovesTokenTransferSnippet.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import React from 'react'; - -import type { TokenInfo } from 'types/api/token'; - -import NftEntity from 'ui/shared/entities/nft/NftEntity'; -import TokenEntity from 'ui/shared/entities/token/TokenEntity'; - -interface Props { - token: Pick; - tokenId: string; -} - -const NovesTokenTransferSnippet = ({ token, tokenId }: Props) => { - return ( - - - - - ); -}; - -export default React.memo(NovesTokenTransferSnippet); diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.ts b/ui/tx/assetFlows/utils/createNovesSummaryObject.ts new file mode 100644 index 0000000000..c7a514e084 --- /dev/null +++ b/ui/tx/assetFlows/utils/createNovesSummaryObject.ts @@ -0,0 +1,139 @@ +import type { NovesResponseData } from 'types/api/noves'; +import type { TxInterpretationSummary } from 'types/api/txInterpretation'; + +import { createAddressValues } from './getAddressValues'; +import type { NovesTokenInfo, TokensData } from './getTokensData'; +import { getTokensData } from './getTokensData'; + +export interface SummaryAddress { + hash: string; + name?: string | null; + is_contract?: boolean; +} + +export interface SummaryValues { + match: string; + value: NovesTokenInfo | SummaryAddress; + type: 'token' | 'address'; +} + +interface NovesSummary { + summary_template: string; + summary_template_variables: {[x: string]: unknown}; +} + +export const createNovesSummaryObject = (translateData: NovesResponseData) => { + + // Remove final dot and add space at the start to avoid matching issues + const description = translateData.classificationData.description; + const removedFinalDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; + let parsedDescription = ' ' + removedFinalDot + ' '; + const tokenData = getTokensData(translateData); + + const idsMatched = tokenData.idList.filter(id => parsedDescription.includes(`#${ id }`)); + const tokensMatchedByName = tokenData.nameList.filter(name => parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); + let tokensMatchedBySymbol = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); + + // Filter symbols if they're already matched by name + tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); + + const summaryValues = []; + + if (idsMatched.length) { + parsedDescription = removeIds(tokensMatchedByName, tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); + } + + if (tokensMatchedByName.length) { + const values = createTokensSummaryValues(tokensMatchedByName, tokenData.byName); + summaryValues.push(...values); + } + + if (tokensMatchedBySymbol.length) { + const values = createTokensSummaryValues(tokensMatchedBySymbol, tokenData.bySymbol); + summaryValues.push(...values); + } + + const addressSummaryValues = createAddressValues(translateData, parsedDescription); + if (addressSummaryValues.length) { + summaryValues.push(...addressSummaryValues); + } + + return createSummaryTemplate(summaryValues, parsedDescription) as TxInterpretationSummary; +}; + +const removeIds = ( + tokensMatchedByName: Array, + tokensMatchedBySymbol: Array, + idsMatched: Array, + tokenData: TokensData, + parsedDescription: string, +) => { + // Remove ids from the description since we already have that info in the token object + let description = parsedDescription; + + tokensMatchedByName.forEach(name => { + const hasId = idsMatched.includes(tokenData.byName[name].id || ''); + if (hasId) { + description = description.replaceAll(`#${ tokenData.byName[name].id }`, ''); + } + }); + + tokensMatchedBySymbol.forEach(name => { + const hasId = idsMatched.includes(tokenData.bySymbol[name].id || ''); + if (hasId) { + description = description.replaceAll(`#${ tokenData.bySymbol[name].id }`, ''); + } + }); + + return description; +}; + +const createTokensSummaryValues = ( + matchedStrings: Array, + tokens: { + [x: string]: NovesTokenInfo; + }, +) => { + const summaryValues: Array = matchedStrings.map(match => ({ + match, + type: 'token', + value: tokens[match], + })); + + return summaryValues; +}; + +const createSummaryTemplate = (summaryValues: Array, parsedDescription: string) => { + let newDescription = parsedDescription; + + const result: NovesSummary = { + summary_template: newDescription, + summary_template_variables: {}, + }; + + if (!summaryValues[0]) { + return result; + } + + const createTemplate = (data: SummaryValues, index = 0) => { + newDescription = newDescription.replaceAll(new RegExp(` ${ data.match } `, 'gi'), `{${ data.match }}`); + + const variable = { + type: data.type, + value: data.value, + }; + + result.summary_template_variables[data.match] = variable; + + const nextValue = summaryValues[index + 1]; + if (nextValue) { + createTemplate(nextValue, index + 1); + } + }; + + createTemplate(summaryValues[0]); + + result.summary_template = newDescription; + + return result; +}; diff --git a/ui/tx/assetFlows/utils/getAddressValues.ts b/ui/tx/assetFlows/utils/getAddressValues.ts new file mode 100644 index 0000000000..9588a44392 --- /dev/null +++ b/ui/tx/assetFlows/utils/getAddressValues.ts @@ -0,0 +1,76 @@ +import type { NovesResponseData } from 'types/api/noves'; + +import type { SummaryAddress, SummaryValues } from './createNovesSummaryObject'; + +const ADDRESS_REGEXP = /(0x[\da-f]+\b)/gi; +const CONTRACT_REGEXP = /(contract 0x[\da-f]+\b)/gi; + +export const createAddressValues = (translateData: NovesResponseData, description: string) => { + const addressMatches = description.match(ADDRESS_REGEXP); + const contractMatches = description.match(CONTRACT_REGEXP); + + let descriptionAddresses: Array = addressMatches ? addressMatches : []; + let contractAddresses: Array = []; + + if (contractMatches?.length) { + contractAddresses = contractMatches.map(text => text.split(ADDRESS_REGEXP)[1]); + descriptionAddresses = addressMatches?.filter(address => !contractAddresses.includes(address)) || []; + } + + const addresses = extractAddresses(translateData); + + const descriptionSummaryValues = createAddressSummaryValues(descriptionAddresses, addresses); + const contractSummaryValues = createAddressSummaryValues(contractAddresses, addresses, true); + + const summaryValues = [ ...descriptionSummaryValues, ...contractSummaryValues ]; + + return summaryValues; +}; + +const createAddressSummaryValues = (descriptionAddresses: Array, addresses: Array, isContract = false) => { + const summaryValues: Array = descriptionAddresses.map(match => { + const address = addresses.find(address => address.hash.toUpperCase().startsWith(match.toUpperCase())); + + if (!address) { + return undefined; + } + + const value: SummaryValues = { + match: match, + type: 'address', + value: isContract ? { ...address, is_contract: true } : address, + }; + + return value; + }); + + return summaryValues.filter(value => value !== undefined) as Array; +}; + +function extractAddresses(data: NovesResponseData) { + const addressesSet: Set<{ hash: string | null; name?: string | null }> = new Set(); // Use a Set to store unique addresses + + addressesSet.add({ hash: data.rawTransactionData.fromAddress }); + addressesSet.add({ hash: data.rawTransactionData.toAddress }); + + if (data.classificationData.approved) { + addressesSet.add({ hash: data.classificationData.approved.spender }); + } + + if (data.txTypeVersion === 2) { + data.classificationData.sent.forEach((transaction) => { + addressesSet.add({ hash: transaction.from.address, name: transaction.from.name }); + addressesSet.add({ hash: transaction.to.address, name: transaction.to.name }); + }); + + data.classificationData.received.forEach((transaction) => { + addressesSet.add({ hash: transaction.from.address, name: transaction.from.name }); + addressesSet.add({ hash: transaction.to.address, name: transaction.to.name }); + }); + } + + const addresses = Array.from(addressesSet) as Array<{hash: string; name?: string}>; // Convert Set to an array + + // Remove empty and null values + return addresses.filter(address => address.hash !== null && address.hash !== ''); +} diff --git a/ui/tx/assetFlows/utils/getDescriptionItems.test.ts b/ui/tx/assetFlows/utils/getDescriptionItems.test.ts deleted file mode 100644 index 715a786f4c..0000000000 --- a/ui/tx/assetFlows/utils/getDescriptionItems.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as transactionMock from 'mocks/noves/transaction'; - -import { getDescriptionItems } from './getDescriptionItems'; - -it('creates sub heading items to render', async() => { - const result = getDescriptionItems(transactionMock.transaction); - - expect(result).toEqual([ - { - token: undefined, - text: ' ', - hasId: false, - type: 'action', - actionText: 'Called function', - address: undefined, - }, - { - token: undefined, - text: '\'stake\' ', - hasId: false, - type: 'action', - actionText: 'on contract', - address: undefined, - }, - { - token: undefined, - text: '', - hasId: false, - type: 'contract', - actionText: undefined, - address: '0xef326cdada59d3a740a76bb5f4f88fb2f1076164', - }, - ]); -}); diff --git a/ui/tx/assetFlows/utils/getDescriptionItems.ts b/ui/tx/assetFlows/utils/getDescriptionItems.ts deleted file mode 100644 index d252aea98d..0000000000 --- a/ui/tx/assetFlows/utils/getDescriptionItems.ts +++ /dev/null @@ -1,240 +0,0 @@ -import _ from 'lodash'; - -import type { NovesResponseData } from 'types/api/noves'; - -import type { NovesTokenInfo, TokensData } from './getTokensData'; -import { getTokensData } from './getTokensData'; - -interface TokenWithIndices { - name: string; - hasId: boolean; - indices: Array; - token?: NovesTokenInfo; - type?: 'action' | 'contract'; -} - -export interface DescriptionItems { - token: NovesTokenInfo | undefined; - text: string; - hasId: boolean | undefined; - type?: string; - actionText?: string; - address?: string; -} - -const CONTRACT_REGEXP = /(0x[\da-fA-F]{32}\b)/g; - -export const getDescriptionItems = (translateData: NovesResponseData): Array => { - - // Remove final dot and add space at the start to avoid matching issues - const description = translateData.classificationData.description; - const removedFinalDot = description.endsWith('.') ? description.slice(0, description.length - 1) : description; - let parsedDescription = ' ' + removedFinalDot; - const tokenData = getTokensData(translateData); - - const idsMatched = tokenData.idList.filter(id => parsedDescription.includes(`#${ id }`)); - const tokensMatchedByName = tokenData.nameList.filter(name => parsedDescription.toUpperCase().includes(` ${ name.toUpperCase() }`)); - let tokensMatchedBySymbol = tokenData.symbolList.filter(symbol => parsedDescription.toUpperCase().includes(` ${ symbol.toUpperCase() }`)); - - const actions = [ 'sent', 'Sent', 'Called function', 'called function', 'on contract', 'swap', 'Swap' ]; - const actionsMatched = actions.filter(action => parsedDescription.includes(action)); - - const contractMatched = parsedDescription.match(CONTRACT_REGEXP) || []; - - // Filter symbols if they're already matched by name - tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); - - const indices: Array = []; - let tokensByName; - let tokensBySymbol; - - let tokensByAction; - let tokensByContract; - - if (idsMatched.length) { - parsedDescription = removeIds(tokensMatchedByName, tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); - } - - if (tokensMatchedByName.length) { - tokensByName = parseTokensByName(tokensMatchedByName, idsMatched, tokenData, parsedDescription); - - tokensByName.forEach(i => indices.push(...i.indices)); - } - - if (tokensMatchedBySymbol.length) { - tokensBySymbol = parseTokensBySymbol(tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); - - tokensBySymbol.forEach(i => indices.push(...i.indices)); - } - - if (actionsMatched.length) { - tokensByAction = parseTokensByAction(actionsMatched, parsedDescription); - - tokensByAction.forEach(i => indices.push(...i.indices)); - } - - if (contractMatched.length) { - tokensByContract = parseTokensByContract(contractMatched, parsedDescription, translateData); - - tokensByContract.forEach(i => indices.push(...i.indices)); - } - - const indicesSorted = _.uniq(indices.sort((a, b) => a - b)); - - const tokensWithIndices = _.uniqBy(_.concat(tokensByName, tokensBySymbol, tokensByAction, tokensByContract), 'name'); - - return createDescriptionItems(indicesSorted, tokensWithIndices, parsedDescription); -}; - -const removeIds = ( - tokensMatchedByName: Array, - tokensMatchedBySymbol: Array, - idsMatched: Array, - tokenData: TokensData, - parsedDescription: string, -) => { - // Remove ids from the description since we already have that info in the token object - let description = parsedDescription; - - tokensMatchedByName.forEach(name => { - const hasId = idsMatched.includes(tokenData.byName[name].id || ''); - if (hasId) { - description = description.replaceAll(`#${ tokenData.byName[name].id }`, ''); - } - }); - - tokensMatchedBySymbol.forEach(name => { - const hasId = idsMatched.includes(tokenData.bySymbol[name].id || ''); - if (hasId) { - description = description.replaceAll(`#${ tokenData.bySymbol[name].id }`, ''); - } - }); - - return description; -}; - -const parseTokensByName = (tokensMatchedByName: Array, idsMatched: Array, tokenData: TokensData, parsedDescription: string) => { - // Find indices and create tokens object - - const tokensByName: Array = tokensMatchedByName.map(name => { - const searchString = ` ${ name.toUpperCase() }`; - let hasId = false; - - if (idsMatched.length) { - hasId = idsMatched.includes(tokenData.byName[name].id || ''); - } - - return { - name, - hasId, - indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, - token: tokenData.byName[name], - }; - }); - - return tokensByName; -}; - -const parseTokensBySymbol = (tokensMatchedBySymbol: Array, idsMatched: Array, tokenData: TokensData, parsedDescription: string) => { - // Find indices and create tokens object - - const tokensBySymbol: Array = tokensMatchedBySymbol.map(name => { - const searchString = ` ${ name.toUpperCase() }`; - let hasId = false; - - if (idsMatched.length) { - hasId = idsMatched.includes(tokenData.bySymbol[name].id || ''); - } - - return { - name, - hasId, - indices: [ ...parsedDescription.toUpperCase().matchAll(new RegExp(searchString, 'gi')) ].map(a => a.index) as unknown as Array, - token: tokenData.bySymbol[name], - }; - }); - - return tokensBySymbol; -}; - -const parseTokensByAction = (actionsMatched: Array, parsedDescription: string) => { - const tokensByAction: Array = actionsMatched.map(action => { - return { - name: action, - indices: [ ...parsedDescription.matchAll(new RegExp(action, 'gi')) ].map(a => a.index) as unknown as Array, - hasId: false, - type: 'action', - }; - }); - - return tokensByAction; -}; - -const parseTokensByContract = (contractMatched: Array, parsedDescription: string, translateData: NovesResponseData) => { - const toAddress = translateData.rawTransactionData.toAddress.toLowerCase(); - const contractFiltered = contractMatched.filter(contract => toAddress.startsWith(contract.toLowerCase()))[0]; - - if (!contractFiltered) { - return []; - } - - const tokensByContract: Array = [ { - name: toAddress, - indices: [ ...parsedDescription.matchAll(new RegExp(contractFiltered, 'gi')) ].map(a => a.index) as unknown as Array, - hasId: false, - type: 'contract', - } ]; - - return tokensByContract; -}; - -const createDescriptionItems = (indicesSorted: Array, tokensWithIndices: Array, parsedDescription: string) => { - // Split the description and create array of objects to render - const descriptionItems = indicesSorted.map((endIndex, i) => { - const item = tokensWithIndices.find(t => t?.indices.includes(endIndex)); - - let token; - - if (i === 0) { - const isAction = item?.type === 'action'; - const isContract = item?.type === 'contract'; - - token = { - token: item?.token, - text: parsedDescription.substring(0, endIndex), - hasId: item?.hasId, - type: item?.type, - actionText: isAction ? item.name : undefined, - address: isContract ? item.name : undefined, - }; - } else { - const previousItem = tokensWithIndices.find(t => t?.indices.includes(indicesSorted[i - 1])); - // Add the length of the text of the previous token to remove it from the start - const startIndex = indicesSorted[i - 1] + (previousItem?.name.length || 0) + 1; - const isAction = item?.type === 'action'; - const isContract = item?.type === 'contract'; - - token = { - token: item?.token, - text: parsedDescription.substring(startIndex, endIndex), - hasId: item?.hasId, - type: item?.type, - actionText: isAction ? item.name : undefined, - address: isContract ? item.name : undefined, - }; - } - - return token; - }); - - const lastIndex = indicesSorted[indicesSorted.length - 1]; - const startIndex = lastIndex + (tokensWithIndices.find(t => t?.indices.includes(lastIndex))?.name.length || 0); - const restString = parsedDescription.substring(startIndex + 1); - - // Check if there is text left after the last token and push it to the array - if (restString) { - descriptionItems.push({ text: restString, token: undefined, hasId: false, type: undefined, actionText: undefined, address: undefined }); - } - - return descriptionItems; -}; diff --git a/ui/tx/assetFlows/utils/getTokensData.ts b/ui/tx/assetFlows/utils/getTokensData.ts index 75a1302a59..2b14c9231c 100644 --- a/ui/tx/assetFlows/utils/getTokensData.ts +++ b/ui/tx/assetFlows/utils/getTokensData.ts @@ -3,6 +3,8 @@ import _ from 'lodash'; import type { NovesResponseData } from 'types/api/noves'; import type { TokenInfo } from 'types/api/token'; +import { HEX_REGEXP } from 'lib/regexp'; + export interface NovesTokenInfo extends Pick { id?: string | undefined; } @@ -22,18 +24,22 @@ export interface TokensData { export function getTokensData(data: NovesResponseData): TokensData { const sent = data.classificationData.sent || []; const received = data.classificationData.received || []; + const approved = data.classificationData.approved ? [ data.classificationData.approved ] : []; - const txItems = [ ...sent, ...received ]; + const txItems = [ ...sent, ...received, ...approved ]; // Extract all tokens data const tokens = txItems.map((item) => { const name = item.nft?.name || item.token?.name || null; const symbol = item.nft?.symbol || item.token?.symbol || null; + const address = item.nft?.address || item.token?.address || ''; + + const validTokenAddress = address ? HEX_REGEXP.test(address) : false; const token = { name: name, symbol: symbol?.toLowerCase() === name?.toLowerCase() ? null : symbol, - address: item.nft?.address || item.token?.address || '', + address: validTokenAddress ? address : '', id: item.nft?.id || item.token?.id, }; From fd90cf145ed710fbaab83f4cf5b55b9d8c136955 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Wed, 20 Mar 2024 17:57:01 -0300 Subject: [PATCH 27/30] token alignment fix --- ui/tx/assetFlows/components/NovesActionSnippet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tx/assetFlows/components/NovesActionSnippet.tsx b/ui/tx/assetFlows/components/NovesActionSnippet.tsx index 597b11d774..c1acc6b0db 100644 --- a/ui/tx/assetFlows/components/NovesActionSnippet.tsx +++ b/ui/tx/assetFlows/components/NovesActionSnippet.tsx @@ -67,7 +67,7 @@ const NovesActionSnippet: FC = ({ item, isLoaded }) => { flip={ false } > - + Date: Wed, 20 Mar 2024 19:28:24 -0300 Subject: [PATCH 28/30] tests added for new functions --- mocks/noves/transaction.ts | 4 ++-- .../utils/createNovesSummaryObject.test.ts | 20 +++++++++++++++++++ .../utils/createNovesSummaryObject.ts | 2 +- .../assetFlows/utils/getAddressValues.test.ts | 18 +++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts create mode 100644 ui/tx/assetFlows/utils/getAddressValues.test.ts diff --git a/mocks/noves/transaction.ts b/mocks/noves/transaction.ts index 88fcb330a9..6feb72a564 100644 --- a/mocks/noves/transaction.ts +++ b/mocks/noves/transaction.ts @@ -89,7 +89,7 @@ export const tokenData: TokensData = { address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', id: undefined, }, - ETH: { name: 'ETH', symbol: null, address: 'ETH', id: undefined }, + ETH: { name: 'ETH', symbol: null, address: '', id: undefined }, }, bySymbol: { PQR: { @@ -98,6 +98,6 @@ export const tokenData: TokensData = { address: '0x1bfe4298796198f8664b18a98640cec7c89b5baa', id: undefined, }, - 'null': { name: 'ETH', symbol: null, address: 'ETH', id: undefined }, + 'null': { name: 'ETH', symbol: null, address: '', id: undefined }, }, }; diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts b/ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts new file mode 100644 index 0000000000..35275d22bc --- /dev/null +++ b/ui/tx/assetFlows/utils/createNovesSummaryObject.test.ts @@ -0,0 +1,20 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { createNovesSummaryObject } from './createNovesSummaryObject'; + +it('creates interpretation summary object', async() => { + const result = createNovesSummaryObject(transactionMock.transaction); + + expect(result).toEqual({ + summary_template: ' Called function \'stake\' on contract{0xef326CdAdA59D3A740A76bB5f4F88Fb2}', + summary_template_variables: { + '0xef326CdAdA59D3A740A76bB5f4F88Fb2': { + type: 'address', + value: { + hash: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + is_contract: true, + }, + }, + }, + }); +}); diff --git a/ui/tx/assetFlows/utils/createNovesSummaryObject.ts b/ui/tx/assetFlows/utils/createNovesSummaryObject.ts index c7a514e084..81c2e956f0 100644 --- a/ui/tx/assetFlows/utils/createNovesSummaryObject.ts +++ b/ui/tx/assetFlows/utils/createNovesSummaryObject.ts @@ -37,7 +37,7 @@ export const createNovesSummaryObject = (translateData: NovesResponseData) => { // Filter symbols if they're already matched by name tokensMatchedBySymbol = tokensMatchedBySymbol.filter(symbol => !tokensMatchedByName.includes(tokenData.bySymbol[symbol]?.name || '')); - const summaryValues = []; + const summaryValues: Array = []; if (idsMatched.length) { parsedDescription = removeIds(tokensMatchedByName, tokensMatchedBySymbol, idsMatched, tokenData, parsedDescription); diff --git a/ui/tx/assetFlows/utils/getAddressValues.test.ts b/ui/tx/assetFlows/utils/getAddressValues.test.ts new file mode 100644 index 0000000000..f911253cce --- /dev/null +++ b/ui/tx/assetFlows/utils/getAddressValues.test.ts @@ -0,0 +1,18 @@ +import * as transactionMock from 'mocks/noves/transaction'; + +import { createAddressValues } from './getAddressValues'; + +it('creates addresses summary values', async() => { + const result = createAddressValues(transactionMock.transaction, transactionMock.transaction.classificationData.description); + + expect(result).toEqual([ + { + match: '0xef326CdAdA59D3A740A76bB5f4F88Fb2', + type: 'address', + value: { + hash: '0xef326CdAdA59D3A740A76bB5f4F88Fb2f1076164', + is_contract: true, + }, + }, + ]); +}); From 0c74939c7fc29c77552fa7f3c819021cc833b72f Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Wed, 20 Mar 2024 19:48:47 -0300 Subject: [PATCH 29/30] margin fix --- ui/tx/TxSubHeading.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/tx/TxSubHeading.tsx b/ui/tx/TxSubHeading.tsx index 03d7a26ec8..7ee1336512 100644 --- a/ui/tx/TxSubHeading.tsx +++ b/ui/tx/TxSubHeading.tsx @@ -60,6 +60,7 @@ const TxSubHeading = ({ hash, hasTag, txQuery }: Props) => { summary={ novesSummary } isLoading={ novesInterpretationQuery.isPlaceholderData } fontSize="lg" + mr={{ base: 0, lg: 6 }} /> ); } else if (hasInternalInterpretation) { From b18f25d39259b87311a7caeb5f2ee70e980a8104 Mon Sep 17 00:00:00 2001 From: NahuelNoves Date: Fri, 22 Mar 2024 14:38:06 -0300 Subject: [PATCH 30/30] remove divider on mobile asset flows --- ui/tx/TxAssetFlows.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/tx/TxAssetFlows.tsx b/ui/tx/TxAssetFlows.tsx index 6412d49128..931332151b 100644 --- a/ui/tx/TxAssetFlows.tsx +++ b/ui/tx/TxAssetFlows.tsx @@ -1,4 +1,4 @@ -import { Table, Tbody, Tr, Th, Box, Skeleton, Text, Show, Hide, Divider } from '@chakra-ui/react'; +import { Table, Tbody, Tr, Th, Box, Skeleton, Text, Show, Hide } from '@chakra-ui/react'; import _ from 'lodash'; import React, { useMemo, useState } from 'react'; @@ -72,8 +72,6 @@ export default function TxAssetFlows(props: FlowViewProps) { const content = ( <> - { data?.length && } - { data?.map((item, i) => (