diff --git a/.agents/skills/deploy-demo/SKILL.md b/.agents/skills/deploy-demo/SKILL.md index 71e87942fc1..a272401577d 100644 --- a/.agents/skills/deploy-demo/SKILL.md +++ b/.agents/skills/deploy-demo/SKILL.md @@ -47,7 +47,7 @@ _Note:_ In the command output, format all URLs as clickable Markdown links: `[Li **c. `build_image`** — whether to build & publish a fresh image (default `true`). - The image is preset-agnostic, so **re-pointing an existing demo to a different chain does not need a rebuild**. If the user asks to *redeploy / re-point / switch an existing demo to another preset* ("redeploy it for celo", "switch the demo to base without rebuilding", "re-point to gnosis"), pass `-f build_image=false` — this skips the multi-minute image build and just re-runs the deploy (~1–2 min), changing `ENVS_PRESET` so the pods roll. - For a first/normal deploy, skip the flag (default `true`, builds fresh). -- **Caveat:** `build_image=false` only works if an image for this branch + `variant` already exists (a prior successful build). If unsure, prefer building. Keep `variant` the same as the original deploy when re-pointing — the image tag and namespace are `-`. +- **Caveat:** `build_image=false` only works if an image for this branch already exists (a prior successful build). If unsure, prefer building. The image tag is `review-` — **variant-independent** (the image is both preset- and variant-agnostic). So an image built under either variant can be reused by the other: e.g. after a `review` build you can deploy `review-2` with `build_image=false`. Only the namespace/hostname (`-`) differ per variant, so the two demos coexist. ### 4. Trigger the workflow and capture the run ID diff --git a/.env.extra b/.env.extra index 943d499c57a..16e533486b0 100644 --- a/.env.extra +++ b/.env.extra @@ -7,3 +7,6 @@ # # This file IS committed — keep it empty on main. Do NOT put secrets here (use .env.secrets, # git-ignored). For purely-local, personal overrides use .env.local (git-ignored). + +NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED=true +NEXT_PUBLIC_INTERCHAIN_INDEXER_API_HOST=https://interchain-indexer.k8s-dev.blockscout.com diff --git a/.github/workflows/deploy-review.yml b/.github/workflows/deploy-review.yml index 95f3ded615c..d285eee6099 100644 --- a/.github/workflows/deploy-review.yml +++ b/.github/workflows/deploy-review.yml @@ -35,6 +35,7 @@ on: - filecoin - garnet - gnosis + - gnosis_chiado - immutable - mega_eth - multichain @@ -85,8 +86,11 @@ jobs: if: ${{ inputs.build_image }} uses: './.github/workflows/publish-image.yml' with: + # Variant-independent tag: the image is preset- AND variant-agnostic, + # so one image serves both the `review` and `review-2` demos. This lets a + # deploy for one variant reuse (build_image=false) an image built under the other. tags: | - type=raw,value=${{ inputs.variant }}-${{ needs.make_slug.outputs.REF_SLUG }} + type=raw,value=review-${{ needs.make_slug.outputs.REF_SLUG }} platforms: linux/amd64 secrets: inherit diff --git a/.vscode/tasks.json b/.vscode/tasks.json index edd58f60595..823a4db9874 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -327,6 +327,7 @@ "filecoin", "garnet", "gnosis", + "gnosis_chiado", "immutable", "mega_eth", "multichain", diff --git a/deploy/scripts/entrypoint.sh b/deploy/scripts/entrypoint.sh index 17e270e8b9f..5bb8947601e 100755 --- a/deploy/scripts/entrypoint.sh +++ b/deploy/scripts/entrypoint.sh @@ -5,7 +5,9 @@ export_envs_from_file() { local file="$1" [ -f "$file" ] || return 0 - while IFS='=' read -r name value; do + # `|| [ -n "$name" ]` keeps the final line even when the file has no trailing + # newline — otherwise `read` returns non-zero at EOF and silently drops it. + while IFS='=' read -r name value || [ -n "$name" ]; do if [[ -n "$name" && "$name" != \#* ]]; then export "$name"="$value" fi diff --git a/deploy/tools/envs-validator/schema_multichain.ts b/deploy/tools/envs-validator/schema_multichain.ts index 4b31a2591bc..72f2021ed6d 100644 --- a/deploy/tools/envs-validator/schema_multichain.ts +++ b/deploy/tools/envs-validator/schema_multichain.ts @@ -69,6 +69,7 @@ const schema = yup .concat(uiSchemas.footerSchema) .concat(uiSchemas.miscSchema) .concat(featuresSchemas.adsSchema) + .concat(featuresSchemas.crossChainTxsSchema) .concat(featuresSchemas.defiDropdownSchema) .concat(featuresSchemas.multichainButtonSchema) .concat(featuresSchemas.userOpsSchema) diff --git a/deploy/values/review-2/values.yaml.gotmpl b/deploy/values/review-2/values.yaml.gotmpl index 12c1aa1297e..4da42b81c59 100644 --- a/deploy/values/review-2/values.yaml.gotmpl +++ b/deploy/values/review-2/values.yaml.gotmpl @@ -23,7 +23,7 @@ frontend: replicaCount: 1 image: repository: ghcr.io/blockscout/frontend-private - tag: review-2-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} + tag: review-{{ requiredEnv "GITHUB_REF_NAME_SLUG" }} pullPolicy: Always ingress: enabled: true diff --git a/package.json b/package.json index 41dc17cabfe..a8f39ad7f79 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@blockscout/api-types": "0.0.1-beta.cf13c2de97", "@blockscout/bens-types": "1.7.1", "@blockscout/contracts-info-types": "1.5.2", - "@blockscout/interchain-indexer-types": "1.3.0", + "@blockscout/interchain-indexer-types": "1.5.0-beta3", "@blockscout/multichain-aggregator-types": "2.1.6", "@blockscout/points-types": "1.4.1", "@blockscout/stats-types": "2.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9d6142fa29..9089a7f3d98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,8 +34,8 @@ importers: specifier: 1.5.2 version: 1.5.2 '@blockscout/interchain-indexer-types': - specifier: 1.3.0 - version: 1.3.0 + specifier: 1.5.0-beta3 + version: 1.5.0-beta3 '@blockscout/multichain-aggregator-types': specifier: 2.1.6 version: 2.1.6 @@ -1330,8 +1330,8 @@ packages: '@blockscout/contracts-info-types@1.5.2': resolution: {integrity: sha512-hlkCzYKAsSzsNdWmmluLXsWEnt4Hnp1HsPNuXwU2uyYe9MS1jHGl3w/mxocpm8dAFg8FZC0riTQTlsxXOEHQmg==} - '@blockscout/interchain-indexer-types@1.3.0': - resolution: {integrity: sha512-S+nbsMJ4g10yjwf+BnJ5JL1jlvVHRGsCCpCEx57wQs5sw+odo22iVcZ2LmHNv8Vdh7Tn1+2tJonJQ0nKwadrig==} + '@blockscout/interchain-indexer-types@1.5.0-beta3': + resolution: {integrity: sha512-mAsyYMyebFsOdzLZK2DzV1nhbsf/SDIyZlPiVNWalqcMzeDwJUancf6cPU7DEtYhEWBVBcwd3jeXDUG6X5/u/w==} '@blockscout/multichain-aggregator-types@2.1.6': resolution: {integrity: sha512-qpotHqUXhp8NFZ9JioCn2B/gewWQ5stTV9QoHKnUbO/0HzXkLvJXdsOjqhkBlaVcOKEMFY08+T3t9O0oI0wQEA==} @@ -14472,7 +14472,7 @@ snapshots: '@blockscout/contracts-info-types@1.5.2': {} - '@blockscout/interchain-indexer-types@1.3.0': {} + '@blockscout/interchain-indexer-types@1.5.0-beta3': {} '@blockscout/multichain-aggregator-types@2.1.6': {} diff --git a/src/features/chain-stats/pages/details/ChainStatsDetails.tsx b/src/features/chain-stats/pages/details/ChainStatsDetails.tsx index cd765fef78a..b1182a2f896 100644 --- a/src/features/chain-stats/pages/details/ChainStatsDetails.tsx +++ b/src/features/chain-stats/pages/details/ChainStatsDetails.tsx @@ -13,6 +13,8 @@ import useChartQuery from 'src/features/chain-stats/hooks/useChartQuery'; import ChainStatsDetailsCrossChainTxs from 'src/features/cross-chain-txs/components/ChainStatsDetailsCrossChainTxs'; import useCrossChainChartQuery from 'src/features/cross-chain-txs/hooks/useCrossChainChartQuery'; import { CROSS_CHAIN_TXS_CHARTS } from 'src/features/cross-chain-txs/utils/chain-stats'; +import multichainConfig from 'src/features/multichain/chains-config'; +import useRoutedChainSelect from 'src/features/multichain/hooks/useRoutedChainSelect'; import config from 'src/config'; import isCustomAppError from 'src/shared/errors/is-custom-app-error'; @@ -47,10 +49,29 @@ const ChainStatsDetails = () => { counterPartyChainIdsFromQuery.length > 0 ? counterPartyChainIdsFromQuery : [ ALL_OPTION.value ], ); - const crossChainTxsChart = config.features.crossChainTxs.isEnabled ? CROSS_CHAIN_TXS_CHARTS.find((chart) => chart.id === id) : undefined; + const chainIdsWithCrossChain = React.useMemo(() => { + return multichainConfig()?.chains + .filter((chain) => chain.app_config.features.crossChainTxs.isEnabled) + .map((chain) => chain.id); + }, []); + + const chainSelectCrossChain = useRoutedChainSelect({ + chainIds: chainIdsWithCrossChain, + }); + + const chainData = React.useMemo(() => { + const chainId = chainSelectCrossChain.value?.[0]; + if (chainId) { + return multichainConfig()?.chains.find(({ id }) => id === chainId); + } + }, [ chainSelectCrossChain.value ]); + + const crossChainTxsChart = (chainData?.app_config ?? config).features.crossChainTxs.isEnabled ? + CROSS_CHAIN_TXS_CHARTS.find((chart) => chart.id === id) : + undefined; const queryBase = useChartQuery({ id, resolution, interval, enabled: !crossChainTxsChart }); - const queryCrossChain = useCrossChainChartQuery({ id, interval, counterPartyChainIds, enabled: Boolean(crossChainTxsChart) }); + const queryCrossChain = useCrossChainChartQuery({ id, interval, counterPartyChainIds, enabled: Boolean(crossChainTxsChart), chainData }); const query = crossChainTxsChart ? queryCrossChain : queryBase; const isInitialLoading = useIsInitialLoading(query.isPlaceholderData); @@ -108,6 +129,8 @@ const ChainStatsDetails = () => { ; isLoading: boolean; isError: boolean; isInitialLoading: boolean; @@ -34,6 +40,8 @@ interface Props { const ChainStatsDetailsCrossChainTxs = ({ chart, data, + baseChain, + baseChainSelectProps, isLoading, isError, isInitialLoading, @@ -50,44 +58,84 @@ const ChainStatsDetailsCrossChainTxs = ({ mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Share chart', Info: chart.id }); }, [ chart.id ]); - const chainsConfig = React.useMemo(() => { + const counterPartyChainsConfig = React.useMemo(() => { return chainsQuery.data?.items - .filter((chain) => chain.id !== config.chain.id) + .filter((chain) => chain.id !== (baseChain?.app_config ?? config).chain.id) .map((chain) => ({ id: chain.id, name: chain.name, logo: chain.logo, explorer_url: chain.explorer_url, })) || []; - }, [ chainsQuery.data ]); - - const chainSelect = ( - - ); + }, [ baseChain?.app_config, chainsQuery.data?.items ]); + + const baseChainIds = React.useMemo(() => { + return multichainConfig()?.chains + .filter((chain) => chain.app_config.features.crossChainTxs.isEnabled) + .map((chain) => chain.id); + }, []); const chainFilter = (() => { - const chainNameElement = { config.chain.name }; + const counterPartyChainSelect = ( + + ); + + const baseChainSelect = baseChainSelectProps ? ( + + ) : null; + + const chainName = { (baseChain?.app_config ?? config).chain.name }; + if (chart.id === 'outgoing-messages-paths') { + if (baseChainSelect) { + return ( + + { baseChainSelect } + to + { counterPartyChainSelect } + + ); + } + return ( - From { chainNameElement } to - { chainSelect } + + From { chainName } to + + { counterPartyChainSelect } ); } if (chart.id === 'incoming-messages-paths') { + if (baseChainSelect) { + return ( + + { counterPartyChainSelect } + to + { baseChainSelect } + + ); + } + return ( - { chainSelect } - to { chainNameElement } + { counterPartyChainSelect } + + to { chainName } + ); } @@ -101,7 +149,7 @@ const ChainStatsDetailsCrossChainTxs = ({ } return counterPartyChainIds.map((chainId) => { - return chainsConfig.find((chain) => chain.id === chainId)?.name || `Chain ${ chainId }`; + return counterPartyChainsConfig.find((chain) => chain.id === chainId)?.name || `Chain ${ chainId }`; }).join(', '); })(); @@ -130,11 +178,11 @@ const ChainStatsDetailsCrossChainTxs = ({ Source - { isOutgoing ? config.chain.name : counterPartyChainNames } + { isOutgoing ? (baseChain?.app_config ?? config).chain.name : counterPartyChainNames } Destination - { isOutgoing ? counterPartyChainNames : config.chain.name } + { isOutgoing ? counterPartyChainNames : (baseChain?.app_config ?? config).chain.name } Txns { isOutgoing ? 'sent' : 'received' } diff --git a/src/features/cross-chain-txs/components/ChartWidgetContainerCrossChain.tsx b/src/features/cross-chain-txs/components/ChartWidgetContainerCrossChain.tsx index 1e77e6cacc3..11f97dd73f6 100644 --- a/src/features/cross-chain-txs/components/ChartWidgetContainerCrossChain.tsx +++ b/src/features/cross-chain-txs/components/ChartWidgetContainerCrossChain.tsx @@ -6,6 +6,7 @@ import React from 'react'; import type { Props as ChartWidgetContainerProps } from 'src/features/chain-stats/components/ChartWidgetContainer'; import { getChartUrl } from 'src/features/chain-stats/utils/chart'; +import { useMultichainContext } from 'src/features/multichain/context'; import { SankeyChartWidget } from 'src/toolkit/components/charts/sankey/SankeyChartWidget'; @@ -18,7 +19,8 @@ interface Props extends Omit { const ChartWidgetContainerCrossChain = ({ id, interval, isLoading, onLoadingError, className, href, title, description }: Props) => { - const query = useCrossChainChartQuery({ id, interval, enabled: !isLoading }); + const chainData = useMultichainContext()?.chain; + const query = useCrossChainChartQuery({ id, interval, enabled: !isLoading, chainData }); React.useEffect(() => { if (query.isError) { diff --git a/src/features/cross-chain-txs/components/CrossChainTxsStatusTag.tsx b/src/features/cross-chain-txs/components/CrossChainTxsStatusTag.tsx index 8ee52d8fe47..15e8651f2f9 100644 --- a/src/features/cross-chain-txs/components/CrossChainTxsStatusTag.tsx +++ b/src/features/cross-chain-txs/components/CrossChainTxsStatusTag.tsx @@ -23,6 +23,8 @@ const CrossChainTxsStatusTag = ({ status: statusProp, mode = 'compact', ...rest return { status: 'error' as const, text: 'Failed' }; case MessageStatus.MESSAGE_STATUS_INITIATED: return { status: 'pending' as const, text: 'Initiated' }; + case MessageStatus.MESSAGE_STATUS_READY_TO_CLAIM: + return { status: 'pending' as const, text: 'Ready to claim' }; default: return { status: undefined, text: undefined }; } diff --git a/src/features/cross-chain-txs/components/txs/TransactionsCrossChainListItem.tsx b/src/features/cross-chain-txs/components/txs/TransactionsCrossChainListItem.tsx index eaa6a6fd89c..e0107229311 100644 --- a/src/features/cross-chain-txs/components/txs/TransactionsCrossChainListItem.tsx +++ b/src/features/cross-chain-txs/components/txs/TransactionsCrossChainListItem.tsx @@ -114,7 +114,7 @@ const TransactionsCrossChainListItem = ({ data, isLoading, rowGap = 3, currentAd { data.transfers.length } ) : ( - { data.transfers.length } + { data.transfers.length } ) } Sender diff --git a/src/features/cross-chain-txs/hooks/useCrossChainChartQuery.ts b/src/features/cross-chain-txs/hooks/useCrossChainChartQuery.ts index 0c17ad826a4..05d5a124e17 100644 --- a/src/features/cross-chain-txs/hooks/useCrossChainChartQuery.ts +++ b/src/features/cross-chain-txs/hooks/useCrossChainChartQuery.ts @@ -3,6 +3,7 @@ import { pickBy, uniqBy } from 'es-toolkit'; import type { ChartDataPayloadSankey, StatsIntervalIds } from 'src/features/chain-stats/types/client'; +import type { ClusterChainConfig } from 'src/features/multichain/types/client'; import useApiQuery from 'src/api/hooks/useApiQuery'; @@ -19,9 +20,10 @@ interface Props { interval: StatsIntervalIds; enabled?: boolean; counterPartyChainIds?: Array; + chainData?: ClusterChainConfig; } -export default function useCrossChainChartQuery({ id, interval, enabled = true, counterPartyChainIds }: Props) { +export default function useCrossChainChartQuery({ id, interval, enabled = true, counterPartyChainIds, chainData }: Props) { const chart = CROSS_CHAIN_TXS_CHARTS.find((chart) => chart.id === id) ?? CROSS_CHAIN_TXS_CHARTS[0]; const { start: startDate, end: endDate } = getDatesFromInterval(interval); @@ -29,7 +31,8 @@ export default function useCrossChainChartQuery({ id, interval, enabled = true, const resourceName = chart.resourceName; return useApiQuery(resourceName, { - pathParams: { chainId: config.chain.id }, + pathParams: { chainId: (chainData?.app_config ?? config).chain.id }, + chain: chainData, queryParams: pickBy({ from: startDate, to: endDate, diff --git a/src/features/cross-chain-txs/pages/address/AddressTxsCrossChain.tsx b/src/features/cross-chain-txs/pages/address/AddressTxsCrossChain.tsx index d0ae40487ee..010fc724dd6 100644 --- a/src/features/cross-chain-txs/pages/address/AddressTxsCrossChain.tsx +++ b/src/features/cross-chain-txs/pages/address/AddressTxsCrossChain.tsx @@ -10,7 +10,7 @@ import Pagination from 'src/shared/pagination/Pagination'; import TransactionsCrossChainContent from '../../components/txs/TransactionsCrossChainContent'; import type { Props } from '../../components/txs/TransactionsCrossChainContent'; -const AddressCrossChainTxs = ({ pagination, isLoading, ...rest }: Props) => { +const AddressTxsCrossChain = ({ pagination, isLoading, ...rest }: Props) => { const isMobile = useIsMobile(); return ( @@ -31,4 +31,4 @@ const AddressCrossChainTxs = ({ pagination, isLoading, ...rest }: Props) => { ); }; -export default React.memo(AddressCrossChainTxs); +export default React.memo(AddressTxsCrossChain); diff --git a/src/features/cross-chain-txs/pages/tx/TxCrossChainDetailsLifecycle.tsx b/src/features/cross-chain-txs/pages/tx/TxCrossChainDetailsLifecycle.tsx index c136fe20489..feae46b1cbb 100644 --- a/src/features/cross-chain-txs/pages/tx/TxCrossChainDetailsLifecycle.tsx +++ b/src/features/cross-chain-txs/pages/tx/TxCrossChainDetailsLifecycle.tsx @@ -22,7 +22,8 @@ const TxCrossChainDetailsLifecycle = ({ data, isLoading }: Props) => { const firstStepContent = (() => { if (!data.source_transaction_hash) { - return ; + const status = data.destination_transaction_hash ? 'success' : 'unfinalized'; + return ; } const isLast = isError && !data.destination_transaction_hash; diff --git a/src/features/cross-chain-txs/pages/tx/TxDetailsCrossChainTransfers.tsx b/src/features/cross-chain-txs/pages/tx/TxDetailsCrossChainTransfers.tsx index 6d3595a4db2..507eccd9340 100644 --- a/src/features/cross-chain-txs/pages/tx/TxDetailsCrossChainTransfers.tsx +++ b/src/features/cross-chain-txs/pages/tx/TxDetailsCrossChainTransfers.tsx @@ -3,6 +3,8 @@ import { route } from 'nextjs-routes'; import React from 'react'; +import { useMultichainContext } from 'src/features/multichain/context'; + import config from 'src/config'; import * as DetailedInfo from 'src/shared/detailed-info/DetailedInfo'; @@ -17,11 +19,16 @@ interface Props { const TxDetailsCrossChainTransfers = ({ hash, isLoading: isLoadingProp }: Props) => { - const { data, isPending } = useTxCrossChainTransfersQuery({ hash }); + const multichainContext = useMultichainContext(); + const chainConfig = multichainContext?.chain.app_config ?? config; + const { data, isPending } = useTxCrossChainTransfersQuery({ + hash, + enabled: chainConfig.features.crossChainTxs.isEnabled, + }); const isLoading = isLoadingProp || isPending; - if ((!isPending && (!data || !data.items.length)) || !config.features.crossChainTxs.isEnabled) { + if ((!isPending && (!data || !data.items.length)) || !chainConfig.features.crossChainTxs.isEnabled) { return null; } diff --git a/src/features/multichain/pages/address/MultichainAddressTokenTransfers.tsx b/src/features/multichain/pages/address/MultichainAddressTokenTransfers.tsx index 8093fe85a96..f9a225ab5aa 100644 --- a/src/features/multichain/pages/address/MultichainAddressTokenTransfers.tsx +++ b/src/features/multichain/pages/address/MultichainAddressTokenTransfers.tsx @@ -10,6 +10,8 @@ import type { TabItemRegular } from 'src/toolkit/components/AdaptiveTabs/types'; import { SocketProvider } from 'src/api/socket/context'; import getSocketUrl from 'src/api/socket/get-socket-url'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'src/shell/page/action-bar/ActionBar'; + import useAddressCountersQuery from 'src/slices/address/hooks/useAddressCountersQuery'; import AddressTokenTransfersLocal from 'src/slices/address/pages/details/token-transfers/AddressTokenTransfersLocal'; import useAddressTokenTransfersQuery from 'src/slices/address/pages/details/token-transfers/useAddressTokenTransfersQuery'; @@ -17,14 +19,19 @@ import TokenTransferFilter from 'src/slices/token-transfer/components/TokenTrans import { getTokenFilterValue } from 'src/slices/token/utils/list-utils'; import AddressAdvancedFilterLink from 'src/features/advanced-filter/components/AddressAdvancedFilterLink'; +import TokenTransfersCrossChainContent from 'src/features/cross-chain-txs/components/token-transfers/TokenTransfersCrossChainContent'; +import { INTERCHAIN_TRANSFER } from 'src/features/cross-chain-txs/stubs/messages'; import CsvExport from 'src/features/csv-export/components/CsvExport'; import multichainConfig from 'src/features/multichain/chains-config'; import ChainSelect from 'src/features/multichain/components/ChainSelect'; import ListCounterText from 'src/features/multichain/components/ListCounterText'; import { MultichainProvider } from 'src/features/multichain/context'; +import config from 'src/config'; import useIsMobile from 'src/shared/hooks/useIsMobile'; import Pagination from 'src/shared/pagination/Pagination'; +import useQueryWithPages from 'src/shared/pagination/useQueryWithPages'; +import { generateListStub } from 'src/shared/pagination/utils'; import getQueryParamString from 'src/shared/router/get-query-param-string'; import { EmptyState } from 'src/toolkit/chakra/empty-state'; @@ -62,6 +69,15 @@ const MultichainAddressTokenTransfers = ({ addressData, isLoading }: Props) => { const chainIds = React.useMemo(() => getAvailableChainIds(addressData), [ addressData ]); + const transfersQueryCrossChain = useQueryWithPages({ + resourceName: 'interchainIndexer:address_transfers', + pathParams: { hash }, + options: { + placeholderData: generateListStub<'interchainIndexer:address_transfers'>(INTERCHAIN_TRANSFER, 50, { next_page_params: undefined }), + enabled: !isLoading && !isLocalTab && config.features.crossChainTxs.isEnabled, + }, + }); + const transfersQueryLocal = useAddressTokenTransfersQuery({ currentAddress: hash, enabled: isLocalTab && !isLoading && chainIds.length > 0, @@ -164,14 +180,34 @@ const MultichainAddressTokenTransfers = ({ addressData, isLoading }: Props) => { ); } - return null; + if (isMobile) { + return null; + } + + return ; })(); const tabs: Array = [ { id: 'token_transfers_cross_chain', title: 'Cross-chain', - component: , + component: config.features.crossChainTxs.isEnabled ? ( + <> + { isMobile && !isLocalTab && transfersQueryCrossChain.pagination.isVisible && ( + + + + ) } + + + ) : , }, { id: [ 'token_transfers_local', 'token_transfers' ], diff --git a/src/features/multichain/pages/address/MultichainAddressTxs.tsx b/src/features/multichain/pages/address/MultichainAddressTxs.tsx index 2f8ef77705c..2a913070df7 100644 --- a/src/features/multichain/pages/address/MultichainAddressTxs.tsx +++ b/src/features/multichain/pages/address/MultichainAddressTxs.tsx @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LicenseRef-Blockscout -import { HStack } from '@chakra-ui/react'; +import { Flex, HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; @@ -10,21 +10,26 @@ import type { TabItemRegular } from 'src/toolkit/components/AdaptiveTabs/types'; import { SocketProvider } from 'src/api/socket/context'; import getSocketUrl from 'src/api/socket/get-socket-url'; -import { ACTION_BAR_HEIGHT_DESKTOP } from 'src/shell/page/action-bar/ActionBar'; +import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'src/shell/page/action-bar/ActionBar'; import useAddressCountersQuery from 'src/slices/address/hooks/useAddressCountersQuery'; import AddressTxsFilter from 'src/slices/address/pages/details/txs/AddressTxsFilter'; import useAddressTxsQuery from 'src/slices/address/pages/details/txs/useAddressTxsQuery'; import TxsWithApiSorting from 'src/slices/tx/pages/index/list/TxsWithApiSorting'; +import TransactionsCrossChainContent from 'src/features/cross-chain-txs/components/txs/TransactionsCrossChainContent'; +import { INTERCHAIN_MESSAGE } from 'src/features/cross-chain-txs/stubs/messages'; import CsvExport from 'src/features/csv-export/components/CsvExport'; import multichainConfig from 'src/features/multichain/chains-config'; import ChainSelect from 'src/features/multichain/components/ChainSelect'; import ListCounterText from 'src/features/multichain/components/ListCounterText'; import { MultichainProvider } from 'src/features/multichain/context'; +import config from 'src/config'; import useIsMobile from 'src/shared/hooks/useIsMobile'; import Pagination from 'src/shared/pagination/Pagination'; +import useQueryWithPages from 'src/shared/pagination/useQueryWithPages'; +import { generateListStub } from 'src/shared/pagination/utils'; import getQueryParamString from 'src/shared/router/get-query-param-string'; import { EmptyState } from 'src/toolkit/chakra/empty-state'; @@ -62,6 +67,15 @@ const MultichainAddressTxs = ({ addressData, isLoading }: Props) => { const chainIds = React.useMemo(() => getAvailableChainIds(addressData), [ addressData ]); + const txsQueryCrossChain = useQueryWithPages({ + resourceName: 'interchainIndexer:address_messages', + pathParams: { hash }, + options: { + placeholderData: generateListStub<'interchainIndexer:address_messages'>(INTERCHAIN_MESSAGE, 50, { next_page_params: undefined }), + enabled: !isLoading && !isLocalTab && config.features.crossChainTxs.isEnabled, + }, + }); + const txsQueryLocal = useAddressTxsQuery({ addressHash: hash, enabled: !isLoading && isLocalTab && chainIds.length > 0, @@ -124,35 +138,57 @@ const MultichainAddressTxs = ({ addressData, isLoading }: Props) => { return ( <> - - { txsLocalFilter } - { chainSelect } - - - { countersText } + + + { txsLocalFilter } + { chainSelect } + + + { countersText } + ); } - return null; + if (isMobile) { + return null; + } + + return ; })(); const tabs: Array = [ { id: 'txs_cross_chain', title: 'Cross-chain', - component: , + component: config.features.crossChainTxs.isEnabled ? ( + <> + { isMobile && !isLocalTab && txsQueryCrossChain.pagination.isVisible && ( + + + + ) } + + + ) : , }, { id: [ 'txs_local', 'txs' ], diff --git a/src/features/multichain/pages/home/LatestTxs.tsx b/src/features/multichain/pages/home/LatestTxs.tsx index 4e5140f9cb5..78008e6ff9c 100644 --- a/src/features/multichain/pages/home/LatestTxs.tsx +++ b/src/features/multichain/pages/home/LatestTxs.tsx @@ -7,11 +7,13 @@ import ChainSelect from 'src/features/multichain/components/ChainSelect'; import { MultichainProvider } from 'src/features/multichain/context'; import useRoutedChainSelect from 'src/features/multichain/hooks/useRoutedChainSelect'; +import config from 'src/config'; import getQueryParamString from 'src/shared/router/get-query-param-string'; import { EmptyState } from 'src/toolkit/chakra/empty-state'; import RoutedTabs from 'src/toolkit/components/RoutedTabs/RoutedTabs'; +import LatestTxsCrossChain from './LatestTxsCrossChain'; import LatestTxsLocal from './LatestTxsLocal'; const LatestTxs = () => { @@ -25,7 +27,7 @@ const LatestTxs = () => { { id: 'cross_chain_txs', title: 'Cross-chain txns', - component: , + component: config.features.crossChainTxs.isEnabled ? : , }, { id: 'txs_local', diff --git a/src/features/multichain/pages/home/LatestTxsCrossChain.tsx b/src/features/multichain/pages/home/LatestTxsCrossChain.tsx new file mode 100644 index 00000000000..482691fed31 --- /dev/null +++ b/src/features/multichain/pages/home/LatestTxsCrossChain.tsx @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LicenseRef-Blockscout + +import { Separator } from '@chakra-ui/react'; +import { route } from 'nextjs-routes'; +import React from 'react'; + +import useApiQuery from 'src/api/hooks/useApiQuery'; + +import TransactionsCrossChainContent from 'src/features/cross-chain-txs/components/txs/TransactionsCrossChainContent'; +import { INTERCHAIN_MESSAGE } from 'src/features/cross-chain-txs/stubs/messages'; + +import { generateListStub } from 'src/shared/pagination/utils'; + +import { Link } from 'src/toolkit/chakra/link'; + +const TXS_NUM = 3; + +const LatestTxsCrossChain = () => { + const { data, isPlaceholderData, isError } = useApiQuery('interchainIndexer:messages', { + queryOptions: { + placeholderData: generateListStub<'interchainIndexer:messages'>(INTERCHAIN_MESSAGE, TXS_NUM, { next_page_params: { page_token: 'token' } }), + }, + }); + + return ( + <> + + + + View all transactions + + + ); +}; + +export default React.memo(LatestTxsCrossChain); diff --git a/src/features/multichain/pages/token-transfers/MultichainTokenTransfers.tsx b/src/features/multichain/pages/token-transfers/MultichainTokenTransfers.tsx index 912ee94f70d..e2369430330 100644 --- a/src/features/multichain/pages/token-transfers/MultichainTokenTransfers.tsx +++ b/src/features/multichain/pages/token-transfers/MultichainTokenTransfers.tsx @@ -12,10 +12,12 @@ import useTokenTransfersQuery from 'src/slices/token-transfer/hooks/useTokenTran import TokenTypeFilter from 'src/slices/token/components/TokenTypeFilter'; import { getTokenFilterValue } from 'src/slices/token/utils/list-utils'; +import TokenTransfersCrossChain from 'src/features/cross-chain-txs/pages/token-transfers/TokenTransfersCrossChain'; import multichainConfig from 'src/features/multichain/chains-config'; import ChainSelect from 'src/features/multichain/components/ChainSelect'; import { MultichainProvider } from 'src/features/multichain/context'; +import config from 'src/config'; import PopoverFilter from 'src/shared/filters/PopoverFilter'; import useIsMobile from 'src/shared/hooks/useIsMobile'; import Pagination from 'src/shared/pagination/Pagination'; @@ -67,7 +69,7 @@ const MultichainTokenTransfers = () => { { id: 'index', title: 'Cross-chain', - component: , + component: config.features.crossChainTxs.isEnabled ? : , }, { id: 'local', diff --git a/src/features/multichain/pages/txs/MultichainTxs.tsx b/src/features/multichain/pages/txs/MultichainTxs.tsx index bdf4b5746f6..cd86a290a4f 100644 --- a/src/features/multichain/pages/txs/MultichainTxs.tsx +++ b/src/features/multichain/pages/txs/MultichainTxs.tsx @@ -7,10 +7,12 @@ import type { TabItemRegular } from 'src/toolkit/components/AdaptiveTabs/types'; import PageTitle from 'src/shell/page/title/PageTitle'; +import TransactionsCrossChain from 'src/features/cross-chain-txs/pages/txs/TransactionsCrossChain'; import ChainSelect from 'src/features/multichain/components/ChainSelect'; import { MultichainProvider } from 'src/features/multichain/context'; import useRoutedChainSelect from 'src/features/multichain/hooks/useRoutedChainSelect'; +import config from 'src/config'; import getQueryParamString from 'src/shared/router/get-query-param-string'; import { EmptyState } from 'src/toolkit/chakra/empty-state'; @@ -35,7 +37,7 @@ const MultichainTxs = () => { { id: 'txs_cross_chain', title: 'Cross-chain', - component: , + component: config.features.crossChainTxs.isEnabled ? : , }, { id: 'txs_local', diff --git a/src/server/getServerSideProps/main.ts b/src/server/getServerSideProps/main.ts index ebc332b22ce..b89541266cd 100644 --- a/src/server/getServerSideProps/main.ts +++ b/src/server/getServerSideProps/main.ts @@ -34,7 +34,7 @@ export const publicTagsSubmit = factory([ guards.publicTagsSubmit ]); export const pools = factory([ guards.pools ]); export const megaEth = factory([ guards.megaEth ]); export const zetaChainCCTX = factory([ guards.zetaChainCCTX ]); -export const crossChainTxs = factory([ guards.notMultichain, guards.crossChainTxs ]); +export const crossChainTxs = factory([ guards.crossChainTxs ]); // ROLLUPS export const rollup = factory([ guards.rollup ]); diff --git a/src/shared/entities/components.tsx b/src/shared/entities/components.tsx index 1d6c8e7e615..746dad45d45 100644 --- a/src/shared/entities/components.tsx +++ b/src/shared/entities/components.tsx @@ -104,6 +104,7 @@ type EntityIconProps = (ImageProps | SpriteIconProps) & Pick & EntityIconProps; diff --git a/src/shared/entities/utils.ts b/src/shared/entities/utils.ts index 8dd286262f5..31b1a4745ae 100644 --- a/src/shared/entities/utils.ts +++ b/src/shared/entities/utils.ts @@ -12,13 +12,13 @@ export function getIconProps(props: IconBaseProps, withShield: boolean = false) case 'subheading': { return { boxSize: props.boxSize ?? '20px', // for tables, lists, regular content and page subheadings - marginRight: props.marginRight ?? props.mr ?? (withShield ? '18px' : '8px'), + marginRight: withShield ? props.marginRightShield ?? '18px' : props.marginRight ?? props.mr ?? '8px', }; } case 'heading': { return { boxSize: props.boxSize ?? '30px', // for page headings - marginRight: props.marginRight ?? props.mr ?? (withShield ? '14px' : '8px'), + marginRight: withShield ? props.marginRightShield ?? '14px' : props.marginRight ?? props.mr ?? '8px', }; } } diff --git a/src/shared/values/entity/TokenValueInterchain.tsx b/src/shared/values/entity/TokenValueInterchain.tsx index 48e9e6adb16..f8074ebf933 100644 --- a/src/shared/values/entity/TokenValueInterchain.tsx +++ b/src/shared/values/entity/TokenValueInterchain.tsx @@ -37,10 +37,9 @@ const TokenValueInterchain = ({ token, tokenEntityProps, chain, ...rest }: Props chain={ chain } noCopy onlySymbol - flexShrink={ 0 } w="fit-content" ml={ 2 } - icon={{ marginRight: 1 }} + icon={{ marginRight: 1, marginRightShield: '14px' }} { ...tokenEntityProps } /> ); @@ -48,6 +47,7 @@ const TokenValueInterchain = ({ token, tokenEntityProps, chain, ...rest }: Props ); diff --git a/src/shell/navigation/useNavItems.tsx b/src/shell/navigation/useNavItems.tsx index 331cefbc5d4..f0fd5343863 100644 --- a/src/shell/navigation/useNavItems.tsx +++ b/src/shell/navigation/useNavItems.tsx @@ -293,7 +293,7 @@ export default function useNavItems(): ReturnType { icon: 'navigation/gas_tracker', isActive: pathname.startsWith('/gas-tracker'), }, - config.features.crossChainTxs.isEnabled && { + config.features.crossChainTxs.isEnabled && !config.features.multichain.isEnabled && { text: 'ICTT users', nextRoute: { pathname: '/ictt-users' as const }, icon: 'navigation/ictt_users', diff --git a/src/slices/address/components/entity/AddressEntity.tsx b/src/slices/address/components/entity/AddressEntity.tsx index fb0055d18b6..e201031ac72 100644 --- a/src/slices/address/components/entity/AddressEntity.tsx +++ b/src/slices/address/components/entity/AddressEntity.tsx @@ -17,6 +17,7 @@ import { distributeEntityProps, getContentProps, getIconProps } from 'src/shared import getChainTooltipText from 'src/shared/external-chains/get-chain-tooltip-text'; import { route } from 'src/shared/router/routes'; import type { IconName } from 'src/sprite/SpriteIcon'; +import SpriteIcon from 'src/sprite/SpriteIcon'; import { Skeleton } from 'src/toolkit/chakra/skeleton'; import { Tooltip } from 'src/toolkit/chakra/tooltip'; @@ -129,7 +130,12 @@ const Icon = (props: IconProps) => { size={ props.size ?? (props.variant === 'heading' ? 30 : 20) } hash={ getDisplayedAddress(props.address) } /> - { shield && } + { shield && ( + } : {}) } + /> + ) } { isDelegatedAddress && } diff --git a/src/slices/address/components/entity/AddressEntityInterchain.tsx b/src/slices/address/components/entity/AddressEntityInterchain.tsx index a3c855b4cca..7ded6297d7f 100644 --- a/src/slices/address/components/entity/AddressEntityInterchain.tsx +++ b/src/slices/address/components/entity/AddressEntityInterchain.tsx @@ -5,6 +5,8 @@ import React from 'react'; import type { ExternalChain } from 'src/shared/external-chains/types'; +import multichainConfig from 'src/features/multichain/chains-config'; + import config from 'src/config'; import type { EntityProps } from './AddressEntity'; @@ -20,9 +22,10 @@ const AddressEntityInterchain = ({ chain, currentAddress, ...props }: Props) => const isCurrentChain = chain?.id === config.chain.id; const isCurrentAddress = isCurrentChain && currentAddress?.toLowerCase() === props.address.hash.toLowerCase(); + const isMultichainAddress = multichainConfig()?.chains.some(({ id }) => id === chain?.id); - if (isCurrentChain) { - return ; + if (isCurrentChain || isMultichainAddress) { + return ; } return ; diff --git a/src/slices/token-transfer/pages/tx/TxTokenTransfer.tsx b/src/slices/token-transfer/pages/tx/TxTokenTransfer.tsx index 4f32952a53f..a583ca652af 100644 --- a/src/slices/token-transfer/pages/tx/TxTokenTransfer.tsx +++ b/src/slices/token-transfer/pages/tx/TxTokenTransfer.tsx @@ -43,6 +43,7 @@ const TxTokenTransfer = ({ txQuery, tokenTransferFilter, noCrossChain }: Props) const tab = getQueryParamString(router.query.tab); const areQueriesEnabled = !txQuery.isPlaceholderData && Boolean(txQuery.data?.status && txQuery.data?.hash); + const chainConfig = multichainContext?.chain?.app_config ?? config; const [ typeFilter, setTypeFilter ] = React.useState>( getTokenFilterValue(router.query.type, multichainContext?.chain?.app_config) || [], @@ -60,17 +61,17 @@ const TxTokenTransfer = ({ txQuery, tokenTransferFilter, noCrossChain }: Props) const crossChainQuery = useTxCrossChainTransfersQuery({ hash: String(txQuery.data?.hash), - enabled: areQueriesEnabled && !noCrossChain, + enabled: areQueriesEnabled && !noCrossChain && chainConfig.features.crossChainTxs.isEnabled, }); const hasCrossChainTab = - config.features.crossChainTxs.isEnabled && + chainConfig.features.crossChainTxs.isEnabled && Boolean(!crossChainQuery.isPlaceholderData && crossChainQuery.data?.items.length) && !noCrossChain; const isLocalTab = tab === 'token_transfers' || (tab !== 'token_transfers_cross_chain' && !hasCrossChainTab); const isTabsLoading = useIsInitialLoading( localQuery.isPlaceholderData || - (config.features.crossChainTxs.isEnabled && crossChainQuery.isPlaceholderData && !noCrossChain), + (chainConfig.features.crossChainTxs.isEnabled && crossChainQuery.isPlaceholderData && !noCrossChain), ); const tabs = [ diff --git a/src/slices/token/components/entity/TokenEntityExternal.tsx b/src/slices/token/components/entity/TokenEntityExternal.tsx index 7c4a1d8ba50..092a77c99a9 100644 --- a/src/slices/token/components/entity/TokenEntityExternal.tsx +++ b/src/slices/token/components/entity/TokenEntityExternal.tsx @@ -8,6 +8,8 @@ import type { ExternalChain } from 'src/shared/external-chains/types'; import type { EntityProps } from 'src/slices/token/components/entity/TokenEntity'; import TokenEntity from 'src/slices/token/components/entity/TokenEntity'; +import multichainConfig from 'src/features/multichain/chains-config'; + import { stripTrailingSlash } from 'src/toolkit/utils/url'; interface Props extends EntityProps, JsxStyleProps { @@ -38,6 +40,7 @@ const TokenEntityExternal = ({ chain, ...props }: Props) => { href={ href } noLink={ props.noLink || !href } link={{ external: true }} + chain={ multichainConfig() ? chain : undefined } /> ); }; diff --git a/src/slices/token/components/entity/TokenEntityInterchain.tsx b/src/slices/token/components/entity/TokenEntityInterchain.tsx index 3f04cf53459..8fe85401ab3 100644 --- a/src/slices/token/components/entity/TokenEntityInterchain.tsx +++ b/src/slices/token/components/entity/TokenEntityInterchain.tsx @@ -9,6 +9,8 @@ import type { EntityProps } from 'src/slices/token/components/entity/TokenEntity import TokenEntity from 'src/slices/token/components/entity/TokenEntity'; import TokenEntityExternal from 'src/slices/token/components/entity/TokenEntityExternal'; +import multichainConfig from 'src/features/multichain/chains-config'; + import config from 'src/config'; interface Props extends EntityProps, JsxStyleProps { @@ -18,9 +20,10 @@ interface Props extends EntityProps, JsxStyleProps { const TokenEntityInterchain = ({ chain, ...props }: Props) => { const isCurrentChain = chain?.id === config.chain.id; + const multichainChainInfo = multichainConfig()?.chains.find(({ id }) => id === chain?.id); - if (isCurrentChain) { - return ; + if (isCurrentChain || multichainChainInfo) { + return ; } return ; diff --git a/src/slices/tx/components/entity/TxEntityInterchain.tsx b/src/slices/tx/components/entity/TxEntityInterchain.tsx index a5db813bd00..c8f3c272e44 100644 --- a/src/slices/tx/components/entity/TxEntityInterchain.tsx +++ b/src/slices/tx/components/entity/TxEntityInterchain.tsx @@ -5,6 +5,8 @@ import React from 'react'; import type { ExternalChain } from 'src/shared/external-chains/types'; +import multichainConfig from 'src/features/multichain/chains-config'; + import config from 'src/config'; import type { EntityProps as TxEntityProps } from './TxEntity'; @@ -18,9 +20,10 @@ interface Props extends TxEntityProps, JsxStyleProps { const TxEntityInterchain = ({ chain, ...props }: Props) => { const isCurrentChain = chain?.id === config.chain.id; + const multichainChainInfo = multichainConfig()?.chains.find(({ id }) => id === chain?.id); - if (isCurrentChain) { - return ; + if (isCurrentChain || multichainChainInfo) { + return ; } return ; diff --git a/src/toolkit/pages/design-system/tabs/Values.pw.tsx b/src/toolkit/pages/design-system/tabs/Values.pw.tsx index ba38f244419..e71a6cdbeef 100644 --- a/src/toolkit/pages/design-system/tabs/Values.pw.tsx +++ b/src/toolkit/pages/design-system/tabs/Values.pw.tsx @@ -4,9 +4,11 @@ import { TabsRoot } from 'src/toolkit/chakra/tabs'; import { test, expect } from 'playwright/lib'; +import { TOKEN } from './Link'; import Values from './Values'; -test('default', async({ render }) => { +test('default', async({ render, mockAssetResponse }) => { + await mockAssetResponse(TOKEN.icon_url!, './playwright/mocks/duck.png'); const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/src/toolkit/pages/design-system/tabs/__screenshots__/Values.pw.tsx_default_default-1.png b/src/toolkit/pages/design-system/tabs/__screenshots__/Values.pw.tsx_default_default-1.png index d43fcfc0671..c04ecb52073 100644 Binary files a/src/toolkit/pages/design-system/tabs/__screenshots__/Values.pw.tsx_default_default-1.png and b/src/toolkit/pages/design-system/tabs/__screenshots__/Values.pw.tsx_default_default-1.png differ diff --git a/tools/dev-server/CONTEXT.md b/tools/dev-server/CONTEXT.md index 2762c83124f..887d6f309a9 100644 --- a/tools/dev-server/CONTEXT.md +++ b/tools/dev-server/CONTEXT.md @@ -52,7 +52,8 @@ lives here): (e.g. `HOMEPAGE_HERO_BANNER_CONFIG`, `MARKETPLACE_ESSENTIAL_DAPPS_CONFIG`). So `fetch.ts` emits raw values, exactly like the old committed presets. Because raw values are **not `source`-safe**, the container entrypoint reads `.env.tmp` via a `while IFS='=' read` loop, - never `source`. + never `source`. That loop uses `read … || [ -n "$name" ]` so a file with no trailing + newline doesn't silently drop its last variable (this bit us with `.env.extra`). - **dotenv-cli precedence: the FIRST `-e` file wins** (not the last). The run scripts therefore list env files **highest-priority-first**. - **`--omit-local-envs` is the dev/container switch.** Dev mode applies `localEnvs` (so APP_HOST diff --git a/tools/dev-server/registry.json b/tools/dev-server/registry.json index 868af6c93a0..49836417b79 100644 --- a/tools/dev-server/registry.json +++ b/tools/dev-server/registry.json @@ -10,6 +10,7 @@ "filecoin": "https://filecoin.blockscout.com", "garnet": "https://explorer.garnetchain.com", "gnosis": "https://gnosis.blockscout.com", + "gnosis_chiado": "https://gnosis-chiado.blockscout.com", "immutable": "https://explorer.immutable.com", "mega_eth": "https://megaeth.blockscout.com", "multichain": "https://explorer.blockscout.com",