Skip to content

Commit

Permalink
add transaction interpretations
Browse files Browse the repository at this point in the history
  • Loading branch information
ArminaAiren committed Dec 21, 2023
1 parent 0f257b4 commit 4d0720b
Show file tree
Hide file tree
Showing 18 changed files with 352 additions and 6 deletions.
1 change: 1 addition & 0 deletions configs/app/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
21 changes: 21 additions & 0 deletions configs/app/features/txInterpretation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Feature } from './types';

import { getEnvValue } from '../utils';

const title = 'Transaction interpretation';

const config: Feature<{ isEnabled: true }> = (() => {
if (getEnvValue('NEXT_PUBLIC_TRANSACTION_INTERPRETATION_ENABLED') === 'true') {
return Object.freeze({
title,
isEnabled: true,
});
}

return Object.freeze({
title,
isEnabled: false,
});
})();

export default config;
1 change: 1 addition & 0 deletions configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -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_ENABLED=true

#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_ENABLED: yup.boolean(),
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(),
Expand Down
11 changes: 11 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -473,6 +474,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch

&nbsp;

### Transaction interpretation

<!-- Nikita will provide feature description -->

| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_TRANSACTION_INTERPRETATION_ENABLED | `boolean`| Set to `true` to enable transaction interpretation | - | - | `true` |

&nbsp;

### Verified tokens info

| Variable | Type| Description | Compulsoriness | Default value | Example value |
Expand Down
3 changes: 3 additions & 0 deletions icons/action.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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 :
Expand Down
45 changes: 45 additions & 0 deletions mocks/txs/txInterpretation.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
} ],
},
};
29 changes: 29 additions & 0 deletions stubs/tx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Transaction } from 'types/api/transaction';
import type { TxInterpretationResponse } from 'types/api/txInterpretation';

import { ADDRESS_PARAMS } from './addressParams';

Expand Down Expand Up @@ -59,3 +60,31 @@ export const TX_ZKEVM_L2: Transaction = {
};

export const TX_RAW_TRACE: RawTracesResponse = [];

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: {
name: 'Museion',
type: 'ERC-20',
symbol: 'MUSA',
address: '0x486a3c5f34cDc4EF133f248f1C81168D78da52e8',
holders: '1152',
decimals: '18',
icon_url: null,
total_supply: '210000000000000000000000000',
exchange_rate: null,
circulating_market_cap: null,
},
},
},
} ],
},
};
47 changes: 47 additions & 0 deletions types/api/txInterpretation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;
}
32 changes: 26 additions & 6 deletions ui/pages/Transaction.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';

Expand All @@ -7,7 +8,7 @@ import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import { TX, TX_INTERPRETATION } 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';
Expand All @@ -17,6 +18,8 @@ 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 TxInterpretation from 'ui/tx/interpretation/TxInterpretation';
import { checkTemplate as checkInterpretationTemplate } from 'ui/tx/interpretation/utils';
import TxDetails from 'ui/tx/TxDetails';
import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped';
import TxInternals from 'ui/tx/TxInternals';
Expand All @@ -39,6 +42,16 @@ const TransactionPageContent = () => {
},
});

const hasInterpretationFeature = config.features.txInterpretation.isEnabled;

const txInterpretationQuery = useApiQuery('tx_interpretation', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash) && hasInterpretationFeature,
placeholderData: TX_INTERPRETATION,
},
});

const tabs: Array<RoutedTab> = [
{ id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: <TxDetails/> },
config.features.suave.isEnabled && data?.wrapped ?
Expand Down Expand Up @@ -73,12 +86,19 @@ const TransactionPageContent = () => {
};
}, [ appProps.referrer ]);

const hasInterpretation =
hasInterpretationFeature && (txInterpretationQuery.isPlaceholderData ||
(txInterpretationQuery.data?.data.summaries[0] && checkInterpretationTemplate(txInterpretationQuery.data.data.summaries[0])));

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' }}/>
</>
<Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%">
{ hasInterpretationFeature && <TxInterpretation query={ txInterpretationQuery } mr={{ base: 0, lg: 6 }}/> }
{ !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 }>
{ !data?.tx_tag && <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>
);

return (
Expand Down
21 changes: 21 additions & 0 deletions ui/tx/interpretation/TxInterpretation.pw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';

import type { TxInterpretationResponse } from 'types/api/txInterpretation';

import type { ResourceError } from 'lib/api/resources';
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 query={{ data: txInterpretationMock } as UseQueryResult<TxInterpretationResponse, ResourceError>}/>
</TestApp>,
);

await expect(component).toHaveScreenshot();
});
66 changes: 66 additions & 0 deletions ui/tx/interpretation/TxInterpretation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Skeleton, Text, Icon, chakra } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import React from 'react';

import type { TxInterpretationResponse, TxInterpretationVariable } from 'types/api/txInterpretation';

import actionIcon from 'icons/action.svg';
import type { ResourceError } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';

import { extractVariables, getStringChunks } from './utils';

type Props = {
query: UseQueryResult<TxInterpretationResponse, ResourceError>;
className?: string;
}

const TxInterpretationElementByType = ({ type, value }: TxInterpretationVariable) => {
switch (type) {
case 'address':
return <AddressEntity address={ value } truncation="constant" sx={{ ':not(:first-child)': { marginLeft: 1 } }}/>;
case 'token':
return <TokenEntity token={ value } onlySymbol width="fit-content" sx={{ ':not(:first-child)': { marginLeft: 1 } }}/>;
case 'currency':
return <Text>{ BigNumber(value).toFormat() }</Text>;
case 'timestamp':
// timestamp is in unix format
return <Text>{ dayjs(Number(value) * 1000).format('llll') }</Text>;
case 'string':
default: {
return <Text>{ value.toString() }</Text>;
}
}
};

const TxInterpretation = ({ query, className }: Props) => {
if (!query.data?.data.summaries[0]) {
return null;
}

const template = query.data.data.summaries[0].summary_template;
const variables = query.data.data.summaries[0].summary_template_variables;

const variablesNames = extractVariables(template);

const chunks = getStringChunks(template);

return (
<Skeleton display="flex" flexWrap="wrap" alignItems="center" isLoaded={ !query.isPlaceholderData } className={ className }>
<Icon as={ actionIcon } boxSize={ 5 } color="text_secondary" mr={ 2 }/>
{ chunks.map((chunk, index) => {
return (
<>
<Text whiteSpace="pre">{ chunk }</Text>
{ index < chunks.length - 1 && <TxInterpretationElementByType { ...variables[variablesNames[index]] }/> }
</>
);
}) }
</Skeleton>
);
};

export default chakra(TxInterpretation);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4d0720b

Please sign in to comment.