Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-1035
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Feature } from './types';
|
||||
import type { Provider } from 'types/client/txInterpretation';
|
||||
import { PROVIDERS } from 'types/client/txInterpretation';
|
||||
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Transaction interpretation';
|
||||
|
||||
const provider: Provider = (() => {
|
||||
const value = getEnvValue('NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER');
|
||||
|
||||
if (value && PROVIDERS.includes(value as Provider)) {
|
||||
return value as Provider;
|
||||
}
|
||||
|
||||
return 'none';
|
||||
})();
|
||||
|
||||
const config: Feature<{ provider: Provider }> = (() => {
|
||||
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
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { MarketplaceAppOverview } from '../../../types/client/marketplace';
|
||||
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
|
||||
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
|
||||
import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token';
|
||||
import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation';
|
||||
import type { WalletType } from '../../../types/client/wallets';
|
||||
import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
|
||||
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
|
||||
@@ -438,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(TX_INTERPRETATION_PROVIDERS),
|
||||
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)
|
||||
- [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 |
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -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 :
|
||||
|
||||
@@ -170,7 +170,6 @@ export default function useNavItems(): ReturnType {
|
||||
{
|
||||
text: 'Verify contract',
|
||||
nextRoute: { pathname: '/contract-verification' as const },
|
||||
icon: 'verify-contract',
|
||||
isActive: pathname.startsWith('/contract-verification'),
|
||||
},
|
||||
...config.UI.sidebar.otherLinks,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -56,6 +56,7 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
|
||||
'Script error.',
|
||||
|
||||
// Relay and WalletConnect errors
|
||||
'The quota has been exceeded',
|
||||
'Attempt to connect to relay via',
|
||||
'WebSocket connection failed for URL: wss://relay.walletconnect.com',
|
||||
],
|
||||
@@ -67,9 +68,11 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
|
||||
// Woopra flakiness
|
||||
/eatdifferent\.com\.woopra-ns\.com/i,
|
||||
/static\.woopra\.com\/js\/woopra\.js/i,
|
||||
// Chrome extensions
|
||||
// Chrome and other extensions
|
||||
/extensions\//i,
|
||||
/^chrome:\/\//i,
|
||||
/^chrome-extension:\/\//i,
|
||||
/^moz-extension:\/\//i,
|
||||
// Other plugins
|
||||
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
|
||||
/webappstoolbarba\.texthelp\.com\//i,
|
||||
|
||||
@@ -14,7 +14,7 @@ export const read: Array<SmartContractReadMethod> = [
|
||||
method_id: '70a08231',
|
||||
name: 'FLASHLOAN_PREMIUM_TOTAL',
|
||||
outputs: [
|
||||
{ internalType: 'uint256', name: '', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
@@ -97,7 +97,7 @@ export const read: Array<SmartContractReadMethod> = [
|
||||
export const readResultSuccess: SmartContractQueryMethodReadSuccess = {
|
||||
is_error: false,
|
||||
result: {
|
||||
names: [ 'uint256' ],
|
||||
names: [ 'amount' ],
|
||||
output: [
|
||||
{ type: 'uint256', value: '42' },
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
} ],
|
||||
},
|
||||
};
|
||||
@@ -9,11 +9,11 @@ export function ad(): CspDev.DirectiveDescriptor {
|
||||
'connect-src': [
|
||||
'coinzilla.com',
|
||||
'*.coinzilla.com',
|
||||
'request-global.czilladx.com',
|
||||
'https://request-global.czilladx.com',
|
||||
'*.slise.xyz',
|
||||
],
|
||||
'frame-src': [
|
||||
'request-global.czilladx.com',
|
||||
'https://request-global.czilladx.com',
|
||||
],
|
||||
'script-src': [
|
||||
'coinzillatag.com',
|
||||
@@ -27,7 +27,7 @@ export function ad(): CspDev.DirectiveDescriptor {
|
||||
'cdn.coinzilla.io',
|
||||
],
|
||||
'font-src': [
|
||||
'request-global.czilladx.com',
|
||||
'https://request-global.czilladx.com',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { KEY_WORDS } from '../utils';
|
||||
const MAIN_DOMAINS = [
|
||||
`*.${ config.app.host }`,
|
||||
config.app.host,
|
||||
getFeaturePayload(config.features.sol2uml)?.api.endpoint,
|
||||
].filter(Boolean);
|
||||
|
||||
const getCspReportUrl = () => {
|
||||
@@ -113,6 +112,7 @@ export function app(): CspDev.DirectiveDescriptor {
|
||||
|
||||
'font-src': [
|
||||
KEY_WORDS.DATA,
|
||||
...MAIN_DOMAINS,
|
||||
],
|
||||
|
||||
'object-src': [
|
||||
|
||||
@@ -29,6 +29,9 @@ export const featureEnvs = {
|
||||
value: '[{"type":"omni","title":"OmniBridge","short_title":"OMNI"},{"type":"amb","title":"Arbitrary Message Bridge","short_title":"AMB"}]',
|
||||
},
|
||||
],
|
||||
txInterpretation: [
|
||||
{ name: 'NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER', value: 'blockscout' },
|
||||
],
|
||||
zkRollup: [
|
||||
{ name: 'NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK', value: 'true' },
|
||||
{ name: 'NEXT_PUBLIC_L1_BASE_URL', value: 'https://localhost:3101' },
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
| "graphQL"
|
||||
| "info"
|
||||
| "key"
|
||||
| "lightning"
|
||||
| "link"
|
||||
| "lock"
|
||||
| "minus"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -99,10 +99,10 @@ export interface SmartContractMethodOutput extends SmartContractMethodInput {
|
||||
export interface SmartContractQueryMethodReadSuccess {
|
||||
is_error: false;
|
||||
result: {
|
||||
names: Array<string>;
|
||||
names: Array<string | [ string, Array<string> ]>;
|
||||
output: Array<{
|
||||
type: string;
|
||||
value: string;
|
||||
value: string | Array<unknown>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ export type HomeStats = {
|
||||
}
|
||||
|
||||
export type GasPrices = {
|
||||
average: number;
|
||||
fast: number;
|
||||
slow: number;
|
||||
average: number | null;
|
||||
fast: number | null;
|
||||
slow: number | null;
|
||||
}
|
||||
|
||||
export type Counters = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ArrayElement } from 'types/utils';
|
||||
|
||||
export const PROVIDERS = [
|
||||
'blockscout',
|
||||
'none',
|
||||
] as const;
|
||||
|
||||
export type Provider = ArrayElement<typeof PROVIDERS>;
|
||||
@@ -84,6 +84,10 @@ const SolidityscanReport = ({ className, hash }: Props) => {
|
||||
const yetAnotherGrayColor = useColorModeValue('gray.400', 'gray.500');
|
||||
const popoverBgColor = useColorModeValue('white', 'gray.900');
|
||||
|
||||
const greatScoreColor = useColorModeValue('green.600', 'green.400');
|
||||
const averageScoreColor = useColorModeValue('purple.600', 'purple.400');
|
||||
const lowScoreColor = useColorModeValue('red.600', 'red.400');
|
||||
|
||||
if (isError || !score) {
|
||||
return null;
|
||||
}
|
||||
@@ -91,13 +95,13 @@ const SolidityscanReport = ({ className, hash }: Props) => {
|
||||
let scoreColor;
|
||||
let scoreLevel;
|
||||
if (score >= 80) {
|
||||
scoreColor = 'green.600';
|
||||
scoreColor = greatScoreColor;
|
||||
scoreLevel = 'GREAT';
|
||||
} else if (score >= 30) {
|
||||
scoreColor = 'orange.600';
|
||||
scoreColor = averageScoreColor;
|
||||
scoreLevel = 'AVERAGE';
|
||||
} else {
|
||||
scoreColor = 'red.600';
|
||||
scoreColor = lowScoreColor;
|
||||
scoreLevel = 'LOW';
|
||||
}
|
||||
|
||||
@@ -112,7 +116,6 @@ const SolidityscanReport = ({ className, hash }: Props) => {
|
||||
<Button
|
||||
className={ className }
|
||||
color={ scoreColor }
|
||||
borderColor={ scoreColor }
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, chakra, Flex, Text } from '@chakra-ui/react';
|
||||
import { Box, Button, chakra, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
@@ -166,7 +166,17 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
|
||||
{ 'outputs' in data && !isWrite && data.outputs.length > 0 && (
|
||||
<Flex mt={ 3 } fontSize="sm">
|
||||
<IconSvg name="arrows/down-right" boxSize={ 5 } mr={ 1 }/>
|
||||
<Text>{ data.outputs.map(({ type, name }) => [ name, type ].join(' ')).join(', ') }</Text>
|
||||
<p>
|
||||
{ data.outputs.map(({ type, name }, index) => {
|
||||
return (
|
||||
<>
|
||||
<chakra.span fontWeight={ 500 }>{ name } </chakra.span>
|
||||
<span>{ name ? `(${ type })` : type }</span>
|
||||
{ index < data.outputs.length - 1 && <span>, </span> }
|
||||
</>
|
||||
);
|
||||
}) }
|
||||
</p>
|
||||
</Flex>
|
||||
) }
|
||||
{ result && <ResultComponent item={ data } result={ result } onSettle={ handleTxSettle }/> }
|
||||
|
||||
@@ -99,3 +99,34 @@ test('success', async({ mount }) => {
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('complex success', async({ mount }) => {
|
||||
const result: ContractMethodReadResult = {
|
||||
is_error: false,
|
||||
result: {
|
||||
names: [
|
||||
[
|
||||
'data',
|
||||
[ 'totalSupply', 'owner', 'symbol' ],
|
||||
],
|
||||
'supports721',
|
||||
'page',
|
||||
],
|
||||
output: [
|
||||
{
|
||||
type: 'tuple[uint256,address,string]',
|
||||
value: [ 1000, '0xe150519ae293922cfe6217feba3add4726f5e851', 'AOC_INCUBATORS' ],
|
||||
},
|
||||
{ type: 'bool', value: 'true' },
|
||||
{ type: 'uint256[]', value: [ 1, 2, 3, 4, 5 ] },
|
||||
],
|
||||
},
|
||||
};
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -2,17 +2,55 @@ import { Alert, Box, chakra, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { ContractMethodReadResult } from './types';
|
||||
import type { SmartContractReadMethod } from 'types/api/contract';
|
||||
import type { SmartContractQueryMethodReadSuccess, SmartContractReadMethod } from 'types/api/contract';
|
||||
|
||||
import hexToUtf8 from 'lib/hexToUtf8';
|
||||
|
||||
const TUPLE_TYPE_REGEX = /\[(.+)\]/;
|
||||
|
||||
const ContractReadResultError = ({ children }: {children: React.ReactNode}) => {
|
||||
return (
|
||||
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word" whiteSpace="pre-wrap">
|
||||
{ children }
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
interface ItemProps {
|
||||
output: SmartContractQueryMethodReadSuccess['result']['output'][0];
|
||||
name: SmartContractQueryMethodReadSuccess['result']['names'][0];
|
||||
}
|
||||
|
||||
const ContractReadResultItem = ({ output, name }: ItemProps) => {
|
||||
if (Array.isArray(name)) {
|
||||
const [ structName, argNames ] = name;
|
||||
const argTypes = output.type.match(TUPLE_TYPE_REGEX)?.[1].split(',');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<chakra.span fontWeight={ 500 }> { structName }</chakra.span>
|
||||
<span> ({ output.type }) :</span>
|
||||
</p>
|
||||
{ argNames.map((argName, argIndex) => {
|
||||
return (
|
||||
<p key={ argName }>
|
||||
<chakra.span fontWeight={ 500 }> { argName }</chakra.span>
|
||||
<span>{ argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) }</span>
|
||||
</p>
|
||||
);
|
||||
}) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<span> </span>
|
||||
{ name && <chakra.span fontWeight={ 500 }>{ name } </chakra.span> }
|
||||
<span>({ output.type }) : { String(output.value) }</span>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -53,14 +91,12 @@ const ContractReadResult = ({ item, result, onSettle }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm">
|
||||
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm" whiteSpace="break-spaces" wordBreak="break-all">
|
||||
<p>
|
||||
[ <chakra.span fontWeight={ 600 }>{ 'name' in item ? item.name : '' }</chakra.span> method response ]
|
||||
[ <chakra.span fontWeight={ 500 }>{ 'name' in item ? item.name : '' }</chakra.span> method response ]
|
||||
</p>
|
||||
<p>[</p>
|
||||
{ result.result.output.map(({ type, value }, index) => (
|
||||
<chakra.p key={ index } whiteSpace="break-spaces" wordBreak="break-all"> { type }: { String(value) }</chakra.p>
|
||||
)) }
|
||||
{ result.result.output.map((output, index) => <ContractReadResultItem key={ index } output={ output } name={ result.result.names[index] }/>) }
|
||||
<p>]</p>
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 7.8 KiB |
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
import TruncatedValue from 'ui/shared/TruncatedValue';
|
||||
|
||||
import type { TokenEnhancedData } from '../utils/tokenUtils';
|
||||
@@ -47,11 +48,10 @@ const TokenSelectItem = ({ data }: Props) => {
|
||||
}
|
||||
})();
|
||||
|
||||
// TODO add filter param when token page is ready
|
||||
const url = route({ pathname: '/token/[hash]', query: { hash: data.token.address } });
|
||||
|
||||
return (
|
||||
<Flex
|
||||
<LinkInternal
|
||||
px={ 1 }
|
||||
py="10px"
|
||||
display="flex"
|
||||
@@ -62,9 +62,8 @@ const TokenSelectItem = ({ data }: Props) => {
|
||||
_hover={{
|
||||
bgColor: useColorModeValue('blue.50', 'gray.800'),
|
||||
}}
|
||||
color="initial"
|
||||
fontSize="sm"
|
||||
cursor="pointer"
|
||||
as="a"
|
||||
href={ url }
|
||||
>
|
||||
<Flex alignItems="center" w="100%" overflow="hidden">
|
||||
@@ -80,7 +79,7 @@ const TokenSelectItem = ({ data }: Props) => {
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%" whiteSpace="nowrap">
|
||||
{ secondRow }
|
||||
</Flex>
|
||||
</Flex>
|
||||
</LinkInternal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,18 +20,19 @@ const GraphQL = () => {
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const graphqlTheme = window.localStorage.getItem('graphiql:theme');
|
||||
|
||||
// colorModeState used as a key to re-render GraphiQL conponent after color mode change
|
||||
const [ colorModeState, setColorModeState ] = React.useState(colorMode);
|
||||
const [ colorModeState, setColorModeState ] = React.useState(graphqlTheme);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
const graphqlTheme = window.localStorage.getItem('graphiql:theme');
|
||||
if (graphqlTheme !== colorMode) {
|
||||
window.localStorage.setItem('graphiql:theme', colorMode);
|
||||
setColorModeState(colorMode);
|
||||
}
|
||||
}
|
||||
}, [ colorMode ]);
|
||||
}, [ colorMode, graphqlTheme ]);
|
||||
|
||||
if (!feature.isEnabled) {
|
||||
return null;
|
||||
|
||||
@@ -92,7 +92,7 @@ const Stats = () => {
|
||||
<StatsItem
|
||||
icon="gas"
|
||||
title="Gas tracker"
|
||||
value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` }
|
||||
value={ data.gas_prices.average !== null ? `${ Number(data.gas_prices.average).toLocaleString() } Gwei` : 'N/A' }
|
||||
_last={ isOdd ? lastItemTouchStyle : undefined }
|
||||
tooltipLabel={ gasLabel }
|
||||
isLoading={ isPlaceholderData }
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -11,11 +11,11 @@ const GasInfoTooltipContent = ({ gasPrices }: {gasPrices: GasPrices}) => {
|
||||
return (
|
||||
<Grid templateColumns="repeat(2, max-content)" rowGap={ 2 } columnGap={ 4 } padding={ 4 } fontSize="xs">
|
||||
<GridItem { ...nameStyleProps }>Slow</GridItem>
|
||||
<GridItem>{ `${ gasPrices.slow } Gwei` }</GridItem>
|
||||
<GridItem>{ gasPrices.slow !== null ? `${ gasPrices.slow } Gwei` : 'N/A' }</GridItem>
|
||||
<GridItem { ...nameStyleProps }>Average</GridItem>
|
||||
<GridItem>{ `${ gasPrices.average } Gwei` }</GridItem>
|
||||
<GridItem>{ gasPrices.average !== null ? `${ gasPrices.average } Gwei` : 'N/A' }</GridItem>
|
||||
<GridItem { ...nameStyleProps }>Fast</GridItem>
|
||||
<GridItem>{ `${ gasPrices.fast } Gwei` }</GridItem>
|
||||
<GridItem>{ gasPrices.fast !== null ? `${ gasPrices.fast } Gwei` : 'N/A' }</GridItem>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import NextLink from 'next/link';
|
||||
import type { LegacyRef } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
// NOTE! use this component only for links to pages that are completely implemented in new UI
|
||||
const LinkInternal = ({ isLoading, ...props }: LinkProps & { isLoading?: boolean }, ref: LegacyRef<HTMLAnchorElement>) => {
|
||||
if (isLoading) {
|
||||
return <Flex alignItems="center" { ...props as FlexProps }>{ props.children }</Flex>;
|
||||
|
||||
@@ -35,6 +35,7 @@ const ColorModeSwitch = () => {
|
||||
window.document.documentElement.style.setProperty(varName, hex);
|
||||
|
||||
cookies.set(cookies.NAMES.COLOR_MODE_HEX, hex);
|
||||
window.localStorage.setItem(cookies.NAMES.COLOR_MODE, nextTheme.colorMode);
|
||||
}, [ setColorMode ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -40,7 +40,7 @@ const TopBarStats = () => {
|
||||
</Skeleton>
|
||||
) }
|
||||
{ data?.coin_price && config.UI.homepage.showGasTracker && <TextSeparator color="divider"/> }
|
||||
{ data?.gas_prices && config.UI.homepage.showGasTracker && (
|
||||
{ data?.gas_prices && data.gas_prices.average !== null && config.UI.homepage.showGasTracker && (
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
<chakra.span color="text_secondary">Gas </chakra.span>
|
||||
<LightMode>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import { txInterpretation } from 'mocks/txs/txInterpretation';
|
||||
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
import * as configs from 'playwright/utils/configs';
|
||||
|
||||
import TxSubHeading from './TxSubHeading';
|
||||
|
||||
const hash = '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193';
|
||||
|
||||
const TX_INTERPRETATION_API_URL = buildApiUrl('tx_interpretation', { hash });
|
||||
|
||||
test('no interpretation +@mobile', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<TxSubHeading hash={ hash } hasTag={ false }/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
const bsInterpretationTest = test.extend({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: contextWithEnvs(configs.featureEnvs.txInterpretation) as any,
|
||||
});
|
||||
|
||||
bsInterpretationTest('with interpretation +@mobile +@dark-mode', async({ mount, page }) => {
|
||||
await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(txInterpretation),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<TxSubHeading hash={ hash } hasTag={ false }/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
bsInterpretationTest('no interpretation', async({ mount, page }) => {
|
||||
await page.route(TX_INTERPRETATION_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({ data: { summaries: [] } }),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<TxSubHeading hash={ hash } hasTag={ false }/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
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 || Boolean(txInterpretationQuery.data?.data.summaries.length));
|
||||
|
||||
return (
|
||||
<Box display={{ base: 'block', lg: 'flex' }} alignItems="center" w="100%">
|
||||
{ hasInterpretation && (
|
||||
<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;
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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<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;
|
||||
@@ -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) && !isTxDataLoading,
|
||||
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;
|
||||
@@ -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<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;
|
||||
@@ -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<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>
|
||||
);
|
||||
@@ -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 (
|
||||
<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);
|
||||
@@ -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 ', ' ', '' ]);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||