Commit c2bc780d authored by tom's avatar tom

Naming systems integration (ENS)

Fixes #1376
parents 19bb92bc 942b5404
......@@ -40,7 +40,9 @@ jobs:
const tag = process.env.TAG;
const REGEXP = /^v[0-9]+.[0-9]+.[0-9]+-[a-z]+((\.|-)\d+)?$/i;
const match = tag.match(REGEXP);
return match && !match[1] ? 'true' : 'false';
const isInitial = match && !match[1] ? true : false;
core.info('is_initial flag value: ', isInitial);
return isInitial;
label_issues:
name: Add pre-release label to issues
......
......@@ -18,6 +18,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';
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;
......@@ -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
......@@ -439,6 +439,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<AdTextProviders>().oneOf(SUPPORTED_AD_TEXT_PROVIDERS),
NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(),
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
......
......@@ -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)
- [Name service integration](ENVS.md#name-service-integration)
- [Bridged tokens](ENVS.md#bridged-tokens)
......@@ -474,6 +475,14 @@ This feature is **enabled by default** with the `['metamask']` value. To switch
&nbsp;
### 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` |
&nbsp;
### Verified tokens info
| Variable | Type| Description | Compulsoriness | Default value | Example value |
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="currentColor" d="M10.833 7.5H17.5L9.167 20v-7.5H3.333l7.5-12.5v7.5ZM9.167 9.167v-3.15l-2.89 4.816h4.556v3.662l3.553-5.328h-5.22Z"/>
</svg>
......@@ -74,6 +74,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';
......@@ -283,6 +284,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: [],
......@@ -689,6 +694,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 :
......
......@@ -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 */
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: [],
ens_domain_name: null,
},
},
timestamp: {
type: 'timestamp',
value: '1687005431',
},
},
} ],
},
};
......@@ -54,6 +54,7 @@
| "graphQL"
| "info"
| "key"
| "lightning"
| "link"
| "lock"
| "minus"
......
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,
},
},
},
],
},
};
import type { AddressParam } from 'types/api/addressParams';
import type { TokenInfo } from 'types/api/token';
export interface TxInterpretationResponse {
data: {
summaries: Array<TxInterpretationSummary>;
};
}
export type TxInterpretationSummary = {
summary_template: string;
summary_template_variables: Record<string, TxInterpretationVariable>;
}
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;
}
......@@ -153,6 +153,7 @@ const MarketplaceAppCard = ({
<IconSvg
name="arrows/north-east"
marginLeft={ 1 }
boxSize={ 4 }
/>
</Link>
</Box>
......
......@@ -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<RoutedTab> = [
{ id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: <TxDetails/> },
{
id: 'index',
title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details',
component: <TxDetails/>,
},
config.features.suave.isEnabled && data?.wrapped ?
{ id: 'wrapped', title: 'Regular tx details', component: <TxDetailsWrapped data={ data.wrapped }/> } :
undefined,
......@@ -73,13 +75,7 @@ const TransactionPageContent = () => {
};
}, [ appProps.referrer ]);
const titleSecondRow = (
<>
<TxEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } mr={ 2 } fontFamily="heading"/>
{ !data?.tx_tag && <AccountActionsMenu mr={{ base: 0, lg: 3 }}/> }
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 3, lg: 'auto' }}/>
</>
);
const titleSecondRow = <TxSubHeading hash={ hash } hasTag={ Boolean(data?.tx_tag) }/>;
return (
<>
......
......@@ -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 key={ tag.label }>{ tag.display_name }</Tag>);
const actionsExist = data.actions && data.actions.length > 0;
const executionSuccessBadge = toAddress?.is_contract && data.result === 'success' ? (
<Tooltip label="Contract execution completed">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
......@@ -242,12 +240,7 @@ const TxDetails = () => {
<DetailsInfoItemDivider/>
{ actionsExist && (
<>
<TxDetailsActions actions={ data.actions }/>
<DetailsInfoItemDivider/>
</>
) }
<TxDetailsActions hash={ data.hash } actions={ data.actions } isTxDataLoading={ isPlaceholderData }/>
<DetailsInfoItem
title="From"
......
import { Box, Flex, Link } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import { TX_ACTIONS_BLOCK_ID } from 'ui/tx/details/txDetailsActions/TxDetailsActionsWrapper';
import TxInterpretation from 'ui/tx/interpretation/TxInterpretation';
type Props = {
hash?: string;
hasTag: boolean;
}
const TxSubHeading = ({ hash, hasTag }: Props) => {
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 (
<Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%">
{ hasInterpretationFeature && (
<Flex mr={{ base: 0, lg: 6 }} flexWrap="wrap" alignItems="center">
<TxInterpretation
summary={ txInterpretationQuery.data?.data.summaries[0] }
isLoading={ txInterpretationQuery.isPlaceholderData }
fontSize="lg"
/>
{ !txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1 &&
<Link ml={ 3 } href={ `#${ TX_ACTIONS_BLOCK_ID }` }>all actions</Link> }
</Flex>
) }
{ !hasInterpretation && <TxEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } mr={{ base: 0, lg: 2 }} fontFamily="heading"/> }
<Flex alignItems="center" justifyContent={{ base: 'start', lg: 'space-between' }} flexGrow={ 1 }>
{ !hasTag && <AccountActionsMenu mr={ 3 } mt={{ base: 3, lg: 0 }}/> }
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 0, lg: 'auto' }} mt={{ base: 3, lg: 0 }}/>
</Flex>
</Box>
);
};
export default TxSubHeading;
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<TxAction>;
hash?: string;
}
const TxDetailsActions = ({ isTxDataLoading, actions, hash }: Props) => {
if (config.features.txInterpretation.isEnabled) {
return <TxDetailsActionsInterpretation hash={ hash } isTxDataLoading={ isTxDataLoading }/>;
}
/* if tx interpretation is not configured, show tx actions from tx info */
if (actions && actions.length > 0) {
return <TxDetailsActionsRaw actions={ actions } isLoading={ isTxDataLoading }/>;
}
return null;
};
export default TxDetailsActions;
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 (
<>
<TxDetailsActionsWrapper isLoading={ isTxDataLoading || txInterpretationQuery.isPlaceholderData }>
{ actions.map((action, index: number) => (
<TxInterpretation
key={ index }
summary={ action }
isLoading={ isTxDataLoading || txInterpretationQuery.isPlaceholderData }
/>
),
) }
</TxDetailsActionsWrapper>
<DetailsInfoItemDivider/>
</>
);
};
export default TxDetailsActionsInterpretation;
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<TxAction>;
isLoading: boolean;
}
const TxDetailsActionsRaw = ({ actions, isLoading }: Props) => {
return (
<>
<TxDetailsActionsWrapper isLoading={ isLoading }>
{ actions.map((action, index: number) => <TxDetailsAction key={ index } action={ action }/>) }
</TxDetailsActionsWrapper>
<DetailsInfoItemDivider/>
</>
);
};
export default TxDetailsActionsRaw;
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<TxAction>;
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<HTMLDivElement>(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 }
>
<Flex
id={ TX_ACTIONS_BLOCK_ID }
flexDirection="column"
alignItems="stretch"
rowGap={ 5 }
......@@ -55,7 +56,7 @@ const TxDetailsActions = ({ actions }: Props) => {
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? 10 : 0 }
>
{ actions.map((action, index: number) => <TxDetailsAction key={ index } action={ action }/>) }
{ children }
</Flex>
</DetailsInfoItem>
);
......
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(
<TestApp>
<TxInterpretation summary={ txInterpretationMock.data.summaries[0] } isLoading={ false }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
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 (
<AddressEntity
address={ value }
truncation="constant"
sx={{ ':not(:first-child)': { marginLeft: 1 } }}
whiteSpace="initial"
onClick={ onAddressClick }
/>
);
}
case 'token':
return (
<TokenEntity
token={ value }
onlySymbol
noCopy
width="fit-content"
sx={{ ':not(:first-child)': { marginLeft: 1 } }}
mr={ 2 }
whiteSpace="initial"
onClick={ onTokenClick }
/>
);
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 <Text>{ numberString + ' ' }</Text>;
}
case 'timestamp':
// timestamp is in unix format
return <Text color="text_secondary">{ dayjs(Number(value) * 1000).format('llll') + ' ' }</Text>;
case 'string':
default: {
return <Text color="text_secondary">{ value.toString() + ' ' }</Text>;
}
}
};
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 (
<Skeleton display="flex" flexWrap="wrap" alignItems="center" isLoaded={ !isLoading } className={ className }>
<IconSvg name="lightning" boxSize={ 5 } color="text_secondary" mr={ 2 }/>
{ chunks.map((chunk, index) => {
return (
<Flex whiteSpace="pre" key={ chunk + index } fontWeight={ 500 }>
<Text color="text_secondary">{ chunk.trim() + (chunk.trim() && variablesNames[index] ? ' ' : '') }</Text>
{ index < variablesNames.length && (
variablesNames[index] === NATIVE_COIN_SYMBOL_VAR_NAME ?
<Text>{ config.chain.currency.symbol + ' ' }</Text> :
<TxInterpretationElementByType variable={ variables[variablesNames[index]] }/>
) }
</Flex>
);
}) }
</Skeleton>
);
};
export default chakra(TxInterpretation);
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 ', ' ', '' ]);
});
// 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);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment