diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index ad45f9a4a1..0879fb17d5 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -17,6 +17,7 @@ export { default as sentry } from './sentry'; export { default as sol2uml } from './sol2uml'; export { default as stats } from './stats'; export { default as suave } from './suave'; +export { default as txInterpretation } from './txInterpretation'; export { default as web3Wallet } from './web3Wallet'; export { default as verifiedTokens } from './verifiedTokens'; export { default as zkEvmRollup } from './zkEvmRollup'; diff --git a/configs/app/features/txInterpretation.ts b/configs/app/features/txInterpretation.ts new file mode 100644 index 0000000000..88eb900dff --- /dev/null +++ b/configs/app/features/txInterpretation.ts @@ -0,0 +1,24 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'Transaction interpretation'; + +const provider = getEnvValue('NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER') || 'none'; + +const config: Feature<{ provider: string }> = (() => { + if (provider !== 'none') { + return Object.freeze({ + title, + provider, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/envs/.env.eth b/configs/envs/.env.eth index 71dc3fdd4d..75ef364321 100644 --- a/configs/envs/.env.eth +++ b/configs/envs/.env.eth @@ -42,6 +42,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout #meta NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 85966c74ed..676f2f2156 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -438,6 +438,7 @@ const schema = yup return isNoneSchema.isValidSync(data) || isArrayOfWalletsSchema.isValidSync(data); }), NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: yup.boolean(), + NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: yup.string().oneOf([ 'blockscout', 'none' ]), NEXT_PUBLIC_AD_TEXT_PROVIDER: yup.string().oneOf(SUPPORTED_AD_TEXT_PROVIDERS), NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), diff --git a/docs/ENVS.md b/docs/ENVS.md index 58b81b2c11..bee048088a 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -44,6 +44,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will - [Solidity to UML diagrams](ENVS.md#solidity-to-uml-diagrams) - [Blockchain statistics](ENVS.md#blockchain-statistics) - [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet) + - [Transaction interpretation](ENVS.md#transaction-interpretation) - [Verified tokens info](ENVS.md#verified-tokens-info) - [Bridged tokens](ENVS.md#bridged-tokens) - [Safe{Core} address tags](ENVS.md#safecore-address-tags) @@ -473,6 +474,14 @@ This feature is **enabled by default** with the `['metamask']` value. To switch   +### Transaction interpretation + +| 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` | + +  + ### Verified tokens info | Variable | Type| Description | Compulsoriness | Default value | Example value | diff --git a/icons/lightning.svg b/icons/lightning.svg new file mode 100644 index 0000000000..91b1ae92ca --- /dev/null +++ b/icons/lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 788a187a22..60d224dad6 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -65,6 +65,7 @@ import type { TransactionsResponseWatchlist, TransactionsSorting, } from 'types/api/transaction'; +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; import type { TTxsFilters } from 'types/api/txsFilters'; import type { TxStateChanges } from 'types/api/txStateChanges'; import type { VerifiedContractsSorting } from 'types/api/verifiedContracts'; @@ -246,6 +247,10 @@ export const RESOURCES = { pathParams: [ 'hash' as const ], filterFields: [], }, + tx_interpretation: { + path: '/api/v2/transactions/:hash/summary', + pathParams: [ 'hash' as const ], + }, withdrawals: { path: '/api/v2/withdrawals', filterFields: [], @@ -651,6 +656,7 @@ Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_state_changes' ? TxStateChanges : +Q extends 'tx_interpretation' ? TxInterpretationResponse : Q extends 'addresses' ? AddressesResponse : Q extends 'address' ? Address : Q extends 'address_counters' ? AddressCounters : diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index fae1b8e327..b886f32249 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -13,6 +13,7 @@ export enum EventTypes { CONTRACT_VERIFICATION = 'Contract verification', QR_CODE = 'QR code', PAGE_WIDGET = 'Page widget', + TX_INTERPRETATION_INTERACTION = 'Transaction interpratetion interaction' } /* eslint-disable @typescript-eslint/indent */ @@ -78,5 +79,8 @@ Type extends EventTypes.QR_CODE ? { Type extends EventTypes.PAGE_WIDGET ? { 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; } : +Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { + 'Type': 'Address click' | 'Token click'; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/mocks/txs/txInterpretation.ts b/mocks/txs/txInterpretation.ts new file mode 100644 index 0000000000..b351363a52 --- /dev/null +++ b/mocks/txs/txInterpretation.ts @@ -0,0 +1,45 @@ +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; + +export const txInterpretation: TxInterpretationResponse = { + data: { + summaries: [ { + summary_template: `{action_type} {amount} {token} to {to_address} on {timestamp}`, + summary_template_variables: { + action_type: { type: 'string', value: 'Transfer' }, + amount: { type: 'currency', value: '100' }, + token: { + type: 'token', + value: { + name: 'Duck', + type: 'ERC-20', + symbol: 'DUCK', + address: '0x486a3c5f34cDc4EF133f248f1C81168D78da52e8', + holders: '1152', + decimals: '18', + icon_url: null, + total_supply: '210000000000000000000000000', + exchange_rate: null, + circulating_market_cap: null, + }, + }, + to_address: { + type: 'address', + value: { + hash: '0x48c04ed5691981C42154C6167398f95e8f38a7fF', + implementation_name: null, + is_contract: false, + is_verified: false, + name: null, + private_tags: [], + public_tags: [], + watchlist_names: [], + }, + }, + timestamp: { + type: 'timestamp', + value: '1687005431', + }, + }, + } ], + }, +}; diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 8a6cb918a7..31e3f50b9d 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -52,6 +52,7 @@ | "graphQL" | "info" | "key" + | "lightning" | "link" | "lock" | "minus" diff --git a/stubs/txInterpretation.ts b/stubs/txInterpretation.ts new file mode 100644 index 0000000000..e54c2cddae --- /dev/null +++ b/stubs/txInterpretation.ts @@ -0,0 +1,34 @@ +import type { TxInterpretationResponse } from 'types/api/txInterpretation'; + +import { TOKEN_INFO_ERC_20 } from './token'; + +export const TX_INTERPRETATION: TxInterpretationResponse = { + data: { + summaries: [ + { + summary_template: '{action_type} {source_amount} Ether into {destination_amount} {destination_token}', + summary_template_variables: { + action_type: { type: 'string', value: 'Wrap' }, + source_amount: { type: 'currency', value: '0.7' }, + destination_amount: { type: 'currency', value: '0.7' }, + destination_token: { + type: 'token', + value: TOKEN_INFO_ERC_20, + }, + }, + }, + { + summary_template: '{action_type} {source_amount} Ether into {destination_amount} {destination_token}', + summary_template_variables: { + action_type: { type: 'string', value: 'Wrap' }, + source_amount: { type: 'currency', value: '0.7' }, + destination_amount: { type: 'currency', value: '0.7' }, + destination_token: { + type: 'token', + value: TOKEN_INFO_ERC_20, + }, + }, + }, + ], + }, +}; diff --git a/types/api/txInterpretation.ts b/types/api/txInterpretation.ts new file mode 100644 index 0000000000..be9393203f --- /dev/null +++ b/types/api/txInterpretation.ts @@ -0,0 +1,47 @@ +import type { AddressParam } from 'types/api/addressParams'; +import type { TokenInfo } from 'types/api/token'; + +export interface TxInterpretationResponse { + data: { + summaries: Array; + }; +} + +export type TxInterpretationSummary = { + summary_template: string; + summary_template_variables: Record; +} + +export type TxInterpretationVariable = + TxInterpretationVariableString | + TxInterpretationVariableCurrency | + TxInterpretationVariableTimestamp | + TxInterpretationVariableToken | + TxInterpretationVariableAddress; + +export type TxInterpretationVariableType = 'string' | 'currency' | 'timestamp' | 'token' | 'address'; + +export type TxInterpretationVariableString = { + type: 'string'; + value: string; +} + +export type TxInterpretationVariableCurrency = { + type: 'currency'; + value: string; +} + +export type TxInterpretationVariableTimestamp = { + type: 'timestamp'; + value: string; +} + +export type TxInterpretationVariableToken = { + type: 'token'; + value: TokenInfo; +} + +export type TxInterpretationVariableAddress = { + type: 'address'; + value: AddressParam; +} diff --git a/ui/pages/Transaction.tsx b/ui/pages/Transaction.tsx index 894006b550..57328ecc58 100644 --- a/ui/pages/Transaction.tsx +++ b/ui/pages/Transaction.tsx @@ -8,11 +8,8 @@ import useApiQuery from 'lib/api/useApiQuery'; import { useAppContext } from 'lib/contexts/app'; import getQueryParamString from 'lib/router/getQueryParamString'; 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 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'; @@ -23,6 +20,7 @@ import TxInternals from 'ui/tx/TxInternals'; import TxLogs from 'ui/tx/TxLogs'; import TxRawTrace from 'ui/tx/TxRawTrace'; import TxState from 'ui/tx/TxState'; +import TxSubHeading from 'ui/tx/TxSubHeading'; import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; const TransactionPageContent = () => { @@ -40,7 +38,11 @@ const TransactionPageContent = () => { }); const tabs: Array = [ - { id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: }, + { + id: 'index', + title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', + component: , + }, config.features.suave.isEnabled && data?.wrapped ? { id: 'wrapped', title: 'Regular tx details', component: } : undefined, @@ -73,13 +75,7 @@ const TransactionPageContent = () => { }; }, [ appProps.referrer ]); - const titleSecondRow = ( - <> - - { !data?.tx_tag && } - - - ); + const titleSecondRow = ; return ( <> diff --git a/ui/tx/TxDetails.tsx b/ui/tx/TxDetails.tsx index df1cec00ee..206c6190ab 100644 --- a/ui/tx/TxDetails.tsx +++ b/ui/tx/TxDetails.tsx @@ -44,7 +44,7 @@ import TextSeparator from 'ui/shared/TextSeparator'; import TxFeeStability from 'ui/shared/tx/TxFeeStability'; import Utilization from 'ui/shared/Utilization/Utilization'; import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps'; -import TxDetailsActions from 'ui/tx/details/TxDetailsActions'; +import TxDetailsActions from 'ui/tx/details/txDetailsActions/TxDetailsActions'; import TxDetailsFeePerGas from 'ui/tx/details/TxDetailsFeePerGas'; import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice'; import TxDetailsOther from 'ui/tx/details/TxDetailsOther'; @@ -98,8 +98,6 @@ const TxDetails = () => { ...toAddress?.watchlist_names || [], ].map((tag) => { tag.display_name }); - const actionsExist = data.actions && data.actions.length > 0; - const executionSuccessBadge = toAddress?.is_contract && data.result === 'success' ? ( @@ -242,12 +240,7 @@ const TxDetails = () => { - { actionsExist && ( - <> - - - - ) } + { + const hasInterpretationFeature = config.features.txInterpretation.isEnabled; + + const txInterpretationQuery = useApiQuery('tx_interpretation', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash) && hasInterpretationFeature, + placeholderData: TX_INTERPRETATION, + }, + }); + + const hasInterpretation = hasInterpretationFeature && + (txInterpretationQuery.isPlaceholderData || txInterpretationQuery.data?.data.summaries.length); + + return ( + + { hasInterpretationFeature && ( + + + { !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1 && + all actions } + + ) } + { !hasInterpretation && } + + { !hasTag && } + + + + ); +}; + +export default TxSubHeading; diff --git a/ui/tx/details/TxDetailsAction.tsx b/ui/tx/details/txDetailsActions/TxDetailsAction.tsx similarity index 100% rename from ui/tx/details/TxDetailsAction.tsx rename to ui/tx/details/txDetailsActions/TxDetailsAction.tsx diff --git a/ui/tx/details/txDetailsActions/TxDetailsActions.tsx b/ui/tx/details/txDetailsActions/TxDetailsActions.tsx new file mode 100644 index 0000000000..fc0622e2af --- /dev/null +++ b/ui/tx/details/txDetailsActions/TxDetailsActions.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import type { TxAction } from 'types/api/txAction'; + +import config from 'configs/app'; +import TxDetailsActionsInterpretation from 'ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation'; +import TxDetailsActionsRaw from 'ui/tx/details/txDetailsActions/TxDetailsActionsRaw'; + +type Props = { + isTxDataLoading: boolean; + actions?: Array; + hash?: string; +} + +const TxDetailsActions = ({ isTxDataLoading, actions, hash }: Props) => { + if (config.features.txInterpretation.isEnabled) { + return ; + } + + /* if tx interpretation is not configured, show tx actions from tx info */ + if (actions && actions.length > 0) { + return ; + } + + return null; +}; + +export default TxDetailsActions; diff --git a/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx b/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx new file mode 100644 index 0000000000..bd912f89fc --- /dev/null +++ b/ui/tx/details/txDetailsActions/TxDetailsActionsInterpretation.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { TX_INTERPRETATION } from 'stubs/txInterpretation'; +import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; +import TxInterpretation from 'ui/tx/interpretation/TxInterpretation'; + +import TxDetailsActionsWrapper from './TxDetailsActionsWrapper'; + +interface Props { + hash?: string; + isTxDataLoading: boolean; +} + +const TxDetailsActionsInterpretation = ({ hash, isTxDataLoading }: Props) => { + const txInterpretationQuery = useApiQuery('tx_interpretation', { + pathParams: { hash }, + queryOptions: { + enabled: Boolean(hash), + placeholderData: TX_INTERPRETATION, + refetchOnMount: false, + }, + }); + + const actions = txInterpretationQuery.data?.data.summaries; + + if (!actions || actions.length < 2) { + return null; + } + + return ( + <> + + { actions.map((action, index: number) => ( + + ), + ) } + + + + ); +}; + +export default TxDetailsActionsInterpretation; diff --git a/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx b/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx new file mode 100644 index 0000000000..a024347519 --- /dev/null +++ b/ui/tx/details/txDetailsActions/TxDetailsActionsRaw.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import type { TxAction } from 'types/api/txAction'; + +import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; + +import TxDetailsAction from './TxDetailsAction'; +import TxDetailsActionsWrapper from './TxDetailsActionsWrapper'; + +interface Props { + actions: Array; + isLoading: boolean; +} + +const TxDetailsActionsRaw = ({ actions, isLoading }: Props) => { + return ( + <> + + { actions.map((action, index: number) => ) } + + + + ); +}; + +export default TxDetailsActionsRaw; diff --git a/ui/tx/details/TxDetailsActions.tsx b/ui/tx/details/txDetailsActions/TxDetailsActionsWrapper.tsx similarity index 83% rename from ui/tx/details/TxDetailsActions.tsx rename to ui/tx/details/txDetailsActions/TxDetailsActionsWrapper.tsx index 200e0315fc..c9e963e803 100644 --- a/ui/tx/details/TxDetailsActions.tsx +++ b/ui/tx/details/txDetailsActions/TxDetailsActionsWrapper.tsx @@ -1,19 +1,18 @@ import { Flex, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; -import type { TxAction } from 'types/api/txAction'; - import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; -import TxDetailsAction from './TxDetailsAction'; - const SCROLL_GRADIENT_HEIGHT = 48; -interface Props { - actions: Array; +type Props = { + children: React.ReactNode; + isLoading?: boolean; } -const TxDetailsActions = ({ actions }: Props) => { +export const TX_ACTIONS_BLOCK_ID = 'tx-actions'; + +const TxDetailsActions = ({ children, isLoading }: Props) => { const containerRef = React.useRef(null); const [ hasScroll, setHasScroll ] = React.useState(false); @@ -34,8 +33,10 @@ const TxDetailsActions = ({ actions }: Props) => { hint="Highlighted events of the transaction" note={ hasScroll ? 'Scroll to see more' : undefined } position="relative" + isLoading={ isLoading } > { pr={ hasScroll ? 5 : 0 } pb={ hasScroll ? 10 : 0 } > - { actions.map((action, index: number) => ) } + { children } ); diff --git a/ui/tx/interpretation/TxInterpretation.pw.tsx b/ui/tx/interpretation/TxInterpretation.pw.tsx new file mode 100644 index 0000000000..e1305d13f1 --- /dev/null +++ b/ui/tx/interpretation/TxInterpretation.pw.tsx @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import { txInterpretation as txInterpretationMock } from 'mocks/txs/txInterpretation'; +import TestApp from 'playwright/TestApp'; + +import TxInterpretation from './TxInterpretation'; + +test('base view +@mobile +@dark-mode', async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/tx/interpretation/TxInterpretation.tsx b/ui/tx/interpretation/TxInterpretation.tsx new file mode 100644 index 0000000000..293d712977 --- /dev/null +++ b/ui/tx/interpretation/TxInterpretation.tsx @@ -0,0 +1,115 @@ +import { Skeleton, Flex, Text, chakra } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { TxInterpretationSummary, TxInterpretationVariable } from 'types/api/txInterpretation'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import * as mixpanel from 'lib/mixpanel/index'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import TokenEntity from 'ui/shared/entities/token/TokenEntity'; +import IconSvg from 'ui/shared/IconSvg'; + +import { extractVariables, getStringChunks, NATIVE_COIN_SYMBOL_VAR_NAME } from './utils'; + +type Props = { + summary?: TxInterpretationSummary; + isLoading?: boolean; + className?: string; +} + +const TxInterpretationElementByType = ({ variable }: { variable?: TxInterpretationVariable }) => { + const onAddressClick = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.TX_INTERPRETATION_INTERACTION, { Type: 'Address click' }); + }, []); + + const onTokenClick = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.TX_INTERPRETATION_INTERACTION, { Type: 'Token click' }); + }, []); + + if (!variable) { + return null; + } + + const { type, value } = variable; + switch (type) { + case 'address': { + return ( + + ); + } + case 'token': + return ( + + ); + case 'currency': { + let numberString = ''; + if (BigNumber(value).isLessThan(0.1)) { + numberString = BigNumber(value).toPrecision(2); + } else if (BigNumber(value).isLessThan(10000)) { + numberString = BigNumber(value).dp(2).toFormat(); + } else if (BigNumber(value).isLessThan(1000000)) { + numberString = BigNumber(value).dividedBy(1000).toFormat(2) + 'K'; + } else { + numberString = BigNumber(value).dividedBy(1000000).toFormat(2) + 'M'; + } + return { numberString + ' ' }; + } + case 'timestamp': + // timestamp is in unix format + return { dayjs(Number(value) * 1000).format('llll') + ' ' }; + case 'string': + default: { + return { value.toString() + ' ' }; + } + } +}; + +const TxInterpretation = ({ summary, isLoading, className }: Props) => { + if (!summary) { + return null; + } + + const template = summary.summary_template; + const variables = summary.summary_template_variables; + + const variablesNames = extractVariables(template); + + const chunks = getStringChunks(template); + + return ( + + + { chunks.map((chunk, index) => { + return ( + + { chunk.trim() + (chunk.trim() && variablesNames[index] ? ' ' : '') } + { index < variablesNames.length && ( + variablesNames[index] === NATIVE_COIN_SYMBOL_VAR_NAME ? + { config.chain.currency.symbol + ' ' } : + + ) } + + ); + }) } + + ); +}; + +export default chakra(TxInterpretation); diff --git a/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..2215fda332 Binary files /dev/null and b/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_default_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..d98c410d2a Binary files /dev/null and b/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_mobile_base-view-mobile-dark-mode-1.png new file mode 100644 index 0000000000..fd03fc242f Binary files /dev/null and b/ui/tx/interpretation/__screenshots__/TxInterpretation.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/tx/interpretation/utils.test.ts b/ui/tx/interpretation/utils.test.ts new file mode 100644 index 0000000000..c3089594b7 --- /dev/null +++ b/ui/tx/interpretation/utils.test.ts @@ -0,0 +1,13 @@ +import { extractVariables, getStringChunks } from './utils'; + +const template = '{action_type} {source_amount} {native} into {destination_amount} {destination_token}'; + +it('extracts variables names', () => { + const result = extractVariables(template); + expect(result).toEqual([ 'action_type', 'source_amount', 'native', 'destination_amount', 'destination_token' ]); +}); + +it('split string without capturing variables', () => { + const result = getStringChunks(template); + expect(result).toEqual([ '', ' ', ' ', ' into ', ' ', '' ]); +}); diff --git a/ui/tx/interpretation/utils.ts b/ui/tx/interpretation/utils.ts new file mode 100644 index 0000000000..c6ffbb6e42 --- /dev/null +++ b/ui/tx/interpretation/utils.ts @@ -0,0 +1,18 @@ +// we use that regex as a separator when splitting template and dont want to capture variables +// eslint-disable-next-line regexp/no-useless-non-capturing-group +export const VAR_REGEXP = /\{(?:[^}]+)\}/g; + +export const NATIVE_COIN_SYMBOL_VAR_NAME = 'native'; + +export function extractVariables(templateString: string) { + + const matches = templateString.match(VAR_REGEXP); + + const variablesNames = matches ? matches.map(match => match.slice(1, -1)) : []; + + return variablesNames; +} + +export function getStringChunks(template: string) { + return template.split(VAR_REGEXP); +}