diff --git a/configs/app/features/externalTxs.ts b/configs/app/features/externalTxs.ts new file mode 100644 index 0000000000..8d4191c208 --- /dev/null +++ b/configs/app/features/externalTxs.ts @@ -0,0 +1,32 @@ +import type { Feature } from './types'; + +import { getEnvValue, parseEnvJson } from '../utils'; + +type TxExternalTransactionsConfig = { + chain_name: string; + chain_logo_url: string; + explorer_url_template: string; +}; + +const externalTransactionsConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG')); + +const title = 'External transactions'; + +const config: Feature<{ chainName: string; chainLogoUrl: string; explorerUrlTemplate: string }> = (() => { + if (externalTransactionsConfig) { + return Object.freeze({ + title, + isEnabled: true, + chainName: externalTransactionsConfig.chain_name, + chainLogoUrl: externalTransactionsConfig.chain_logo_url, + explorerUrlTemplate: externalTransactionsConfig.explorer_url_template, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 2652c0b480..6cb6425f55 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -12,6 +12,7 @@ export { default as csvExport } from './csvExport'; export { default as dataAvailability } from './dataAvailability'; export { default as deFiDropdown } from './deFiDropdown'; export { default as easterEggBadge } from './easterEggBadge'; +export { default as externalTxs } from './externalTxs'; export { default as faultProofSystem } from './faultProofSystem'; export { default as gasTracker } from './gasTracker'; export { default as getGasButton } from './getGasButton'; diff --git a/docs/ENVS.md b/docs/ENVS.md index c96fd7a319..f64339cc5c 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -590,6 +590,14 @@ This feature is **enabled by default** with the `['metamask']` value. To switch   +### External transactions + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG | `{ chain_name: string; chain_logo_url: string; explorer_url_template: string; }` | Configuration of the external transactions links that should be added to the transaction details. | - | - | `{ chain_name: 'ethereum', chain_logo_url: 'https://example.com/logo.png', explorer_url_template: 'https://explorer.com/tx/{hash}' }` | v1.38.0+ | + +  + ### Verified tokens info | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | diff --git a/lib/api/resources.ts b/lib/api/resources.ts index b99d04846f..ab0763b7fa 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -514,6 +514,10 @@ export const RESOURCES = { path: '/api/v2/transactions/:hash/summary', pathParams: [ 'hash' as const ], }, + tx_external_transactions: { + path: '/api/v2/transactions/:hash/external_transactions', + pathParams: [ 'hash' as const ], + }, withdrawals: { path: '/api/v2/withdrawals', filterFields: [], @@ -1296,6 +1300,7 @@ Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_state_changes' ? TxStateChanges : Q extends 'tx_blobs' ? TxBlobs : Q extends 'tx_interpretation' ? TxInterpretationResponse : +Q extends 'tx_external_transactions' ? Array : Q extends 'addresses' ? AddressesResponse : Q extends 'addresses_metadata_search' ? AddressesMetadataSearchResult : Q extends 'address' ? Address : diff --git a/types/client/externalTxsConfig.ts b/types/client/externalTxsConfig.ts new file mode 100644 index 0000000000..7a81a5e1c2 --- /dev/null +++ b/types/client/externalTxsConfig.ts @@ -0,0 +1,5 @@ +export type TxExternalTxsConfig = { + chain_name: string; + chain_logo_url: string; + explorer_url_template: string; +}; diff --git a/ui/tx/TxExternalTxs.pw.tsx b/ui/tx/TxExternalTxs.pw.tsx new file mode 100644 index 0000000000..a43421e640 --- /dev/null +++ b/ui/tx/TxExternalTxs.pw.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { test, expect } from 'playwright/lib'; + +import TxExternalTxs from './TxExternalTxs'; + +const EXT_TX_HASH = '2uwpB95K9ae8yrpxxVXJ27ivvHXqrmy82jsamgNtdWJrYDGkCHsRwd2LKXubrQUzXMaojGxZmHZ85XVJN8EJ3LW8'; +const CONFIG = { + chain_name: 'Solana', + chain_logo_url: 'http://example.url', + explorer_url_template: 'https://scan.io/tx/{hash}', +}; + +test('base view', async({ page, render, mockEnvs, mockAssetResponse }) => { + await mockEnvs([ + [ 'NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG', JSON.stringify(CONFIG) ], + ]); + await mockAssetResponse(CONFIG.chain_logo_url, './playwright/mocks/image_s.jpg'); + await render(); + await page.getByText('13 Solana txs').hover(); + const popover = page.locator('.chakra-popover__content'); + await expect(popover).toBeVisible(); + await expect(popover).toHaveScreenshot(); +}); diff --git a/ui/tx/TxExternalTxs.tsx b/ui/tx/TxExternalTxs.tsx new file mode 100644 index 0000000000..a2eebcffbb --- /dev/null +++ b/ui/tx/TxExternalTxs.tsx @@ -0,0 +1,61 @@ +import { + PopoverTrigger, + PopoverBody, + PopoverContent, + Flex, + Link, + Image, +} from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import Popover from 'ui/shared/chakra/Popover'; +import TxEntity from 'ui/shared/entities/tx/TxEntity'; + +const externalTxFeature = config.features.externalTxs; + +interface Props { + data: Array; +} + +const TxExternalTxs: React.FC = ({ data }) => { + if (!externalTxFeature.isEnabled) { + return null; + } + + return ( + + + + { + { data.length } { externalTxFeature.chainName } tx{ data.length > 1 ? 's' : '' } + + + + + + { + { externalTxFeature.chainName } transaction{ data.length > 1 ? 's' : '' } + + + { data.map((txHash) => ( + + )) } + + + + + ); +}; + +export default TxExternalTxs; diff --git a/ui/tx/__screenshots__/TxExternalTxs.pw.tsx_default_base-view-1.png b/ui/tx/__screenshots__/TxExternalTxs.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..2ad8598a22 Binary files /dev/null and b/ui/tx/__screenshots__/TxExternalTxs.pw.tsx_default_base-view-1.png differ diff --git a/ui/tx/details/TxInfo.tsx b/ui/tx/details/TxInfo.tsx index af430c281e..1529d8d97f 100644 --- a/ui/tx/details/TxInfo.tsx +++ b/ui/tx/details/TxInfo.tsx @@ -24,6 +24,7 @@ import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2'; import { route } from 'nextjs-routes'; import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; import { WEI, WEI_IN_GWEI } from 'lib/consts'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import * as arbitrum from 'lib/rollups/arbitrum'; @@ -61,6 +62,7 @@ import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers'; import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus'; import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers'; +import TxExternalTxs from 'ui/tx/TxExternalTxs'; import TxSocketAlert from 'ui/tx/TxSocketAlert'; import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo'; @@ -74,9 +76,21 @@ interface Props { socketStatus?: 'close' | 'error'; } +const externalTxFeature = config.features.externalTxs; + const TxInfo = ({ data, isLoading, socketStatus }: Props) => { const [ isExpanded, setIsExpanded ] = React.useState(false); + const externalTxsQuery = useApiQuery('tx_external_transactions', { + pathParams: { + hash: data?.hash, + }, + queryOptions: { + enabled: externalTxFeature.isEnabled, + placeholderData: [ '1', '2', '3' ], + }, + }); + const handleCutClick = React.useCallback(() => { setIsExpanded((flag) => !flag); scroller.scrollTo('TxInfo__cutLink', { @@ -162,6 +176,13 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ) } + + { config.features.externalTxs.isEnabled && externalTxsQuery.data && externalTxsQuery.data.length > 0 && ( + + + + + ) }