Files
zeekayandHanzo Dev 2c14a255f9 fix(search): quick search answers with an envelope, not a list
Typing one character into the search box blanked every explorer in the estate.
The DOM collapsed to the error boundary — "Oops! Something went wrong" — with

  TypeError: e.data?.forEach is not a function

thrown from the itemsGroups useMemo in
ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx:95.

/search/quick is served in two shapes. Upstream Blockscout answers with a bare
list; eth.blockscout.com/api/v2/search/quick?q=1 returns []. Our backend
(luxfi/explorer 1.5.3) answers with the same paginated envelope as /search:

  api-explore.lux.network/search/quick?q=1
  HTTP 200  {"items":[{"block_number":1,"type":"block"}],"next_page_params":null}

The hanzo, zoo and pars backends return that byte-for-byte too, which is why a
single bundle offset crashes every hostname. The frontend declared the payload
as Array<SearchResultItem> and iterated it, so the envelope object met .forEach.

This is not the rate limiter. The explorer does draw 429s from api.lux.network,
but useFetch rejects on any non-ok response, so an error body never reaches
query.data — it stays undefined and `data?.forEach` short-circuits. Only a 200
carrying a non-array JSON body can produce this crash.

Proven against the live v1.1.13 bundle by rewriting that one response in flight
and changing nothing else: envelope, DOM 132 chars, crash; the same page fed a
bare list, DOM 2081 chars, no console errors, suggest panel renders "Blocks 1".

So the wire type now admits both shapes and unwrapItems is the single place that
collapses them, applied as the select of the quick_search query — the one point
where the resource enters the app. Every consumer downstream (suggest groups,
the desktop "show all results" count, mobile) is handed an array by
construction rather than each guarding for itself. A body in neither shape
yields an empty list, which the component already renders as "No results found",
so a future contract change degrades instead of blanking the page.

zetaChainCCTXQuery.data?.items.length is guarded the same way: the optional
chain stopped at data and left .items.length to throw "Cannot read properties
of undefined (reading 'length')" — the second error in the report — for any
response missing items.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 09:50:41 -07:00

349 lines
12 KiB
TypeScript

import type { ApiResource } from '../../types';
import type { AdvancedFilterParams, AdvancedFilterResponse, AdvancedFilterMethodsResponse } from 'types/api/advancedFilter';
import type {
ArbitrumL2TxnBatchesItem,
ArbitrumLatestDepositsResponse,
} from 'types/api/arbitrumL2';
import type { Blob } from 'types/api/blobs';
import type { Block } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
import type { BackendConfig, BackendVersionConfig, CeloConfig, ContractLanguagesConfig, CsvExportConfig } from 'types/api/configs';
import type { HotContractsFilters, HotContractsResponse, HotContractsSorting } from 'types/api/contracts';
import type { DepositsResponse, DepositsCounters } from 'types/api/deposits';
import type { CeloEpochDetails, CeloEpochElectionRewardDetailsResponse, CeloEpochListResponse } from 'types/api/epochs';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { NovesAccountHistoryResponse, NovesDescribeTxsResponse, NovesResponseData } from 'types/api/noves';
import type {
OptimisticL2DepositsItem,
} from 'types/api/optimisticL2';
import type { QuickSearchResponse, SearchRedirectResult, SearchResult, SearchResultFilters } from 'types/api/search';
import type { HomeStats } from 'types/api/stats';
import type {
Transaction,
} from 'types/api/transaction';
import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps';
import type {
ValidatorsStabilityCountersResponse,
ValidatorsStabilityFilters,
ValidatorsStabilityResponse,
ValidatorsStabilitySorting,
ValidatorsBlackfortCountersResponse,
ValidatorsBlackfortResponse,
ValidatorsBlackfortSorting,
ValidatorsZilliqaResponse,
ValidatorZilliqa,
} from 'types/api/validators';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
import type {
ZkEvmL2TxnBatchesItem,
} from 'types/api/zkEvmL2';
export const GENERAL_API_MISC_RESOURCES = {
// WITHDRAWALS
withdrawals: {
path: '/withdrawals',
filterFields: [],
paginated: true,
},
withdrawals_counters: {
path: '/withdrawals/counters',
},
// DEPOSITS
deposits: {
path: '/beacon/deposits',
filterFields: [],
paginated: true,
},
deposits_counters: {
path: '/beacon/deposits/count',
},
// APP STATS
stats: {
path: '/stats',
headers: {
'updated-gas-oracle': 'true',
},
},
stats_charts_txs: {
path: '/stats/charts/transactions',
},
stats_charts_market: {
path: '/stats/charts/market',
},
stats_charts_secondary_coin_price: {
path: '/stats/charts/secondary-coin-market',
},
stats_hot_contracts: {
path: '/stats/hot-smart-contracts',
paginated: true,
filterFields: [ 'scale' as const ],
},
// HOMEPAGE
homepage_blocks: {
path: '/main-page/blocks',
},
homepage_optimistic_deposits: {
path: '/main-page/optimism-deposits',
},
homepage_arbitrum_deposits: {
path: '/main-page/arbitrum/messages/to-rollup',
},
homepage_txs: {
path: '/main-page/transactions',
},
homepage_zkevm_l2_batches: {
path: '/main-page/zkevm/batches/confirmed',
},
homepage_arbitrum_l2_batches: {
path: '/main-page/arbitrum/batches/committed',
},
homepage_txs_watchlist: {
path: '/main-page/transactions/watchlist',
},
homepage_indexing_status: {
path: '/main-page/indexing-status',
},
homepage_zkevm_latest_batch: {
path: '/main-page/zkevm/batches/latest-number',
},
homepage_zksync_latest_batch: {
path: '/main-page/zksync/batches/latest-number',
},
homepage_arbitrum_latest_batch: {
path: '/main-page/arbitrum/batches/latest-number',
},
// SEARCH
quick_search: {
path: '/search/quick',
filterFields: [ 'q' ],
},
search: {
path: '/search',
filterFields: [ 'q' ],
paginated: true,
},
search_check_redirect: {
path: '/search/check-redirect',
},
// NOVES-FI
noves_transaction: {
path: '/proxy/3rdparty/noves-fi/transactions/:hash',
pathParams: [ 'hash' as const ],
},
noves_address_history: {
path: '/proxy/3rdparty/noves-fi/addresses/:address/transactions',
pathParams: [ 'address' as const ],
filterFields: [],
paginated: true,
},
noves_describe_txs: {
path: '/proxy/3rdparty/noves-fi/transaction-descriptions',
},
// USER OPS
user_ops: {
path: '/proxy/account-abstraction/operations',
filterFields: [ 'transaction_hash' as const, 'sender' as const ],
paginated: true,
},
user_op: {
path: '/proxy/account-abstraction/operations/:hash',
pathParams: [ 'hash' as const ],
},
user_ops_account: {
path: '/proxy/account-abstraction/accounts/:hash',
pathParams: [ 'hash' as const ],
},
user_op_interpretation: {
path: '/proxy/account-abstraction/operations/:hash/summary',
pathParams: [ 'hash' as const ],
},
// VALIDATORS
validators_stability: {
path: '/validators/stability',
filterFields: [ 'address_hash' as const, 'state_filter' as const ],
paginated: true,
},
validators_stability_counters: {
path: '/validators/stability/counters',
},
validators_blackfort: {
path: '/validators/blackfort',
filterFields: [],
paginated: true,
},
validators_blackfort_counters: {
path: '/validators/blackfort/counters',
},
validators_zilliqa: {
path: '/validators/zilliqa',
filterFields: [],
paginated: true,
},
validator_zilliqa: {
path: '/validators/zilliqa/:bls_public_key',
pathParams: [ 'bls_public_key' as const ],
filterFields: [],
},
// BLOBS
blob: {
path: '/blobs/:hash',
pathParams: [ 'hash' as const ],
},
// EPOCHS
epochs_celo: {
path: '/celo/epochs',
filterFields: [],
paginated: true,
},
epoch_celo: {
path: '/celo/epochs/:number',
pathParams: [ 'number' as const ],
},
epoch_celo_election_rewards: {
path: '/celo/epochs/:number/election-rewards/:reward_type',
pathParams: [ 'number' as const, 'reward_type' as const ],
filterFields: [],
paginated: true,
},
// ADVANCED FILTER
advanced_filter: {
path: '/advanced-filters',
filterFields: [
'transaction_types' as const,
'methods' as const,
'methods_names' as const /* frontend only */,
'age_from' as const,
'age_to' as const,
'age' as const /* frontend only */,
'from_address_hashes_to_include' as const,
'from_address_hashes_to_exclude' as const,
'to_address_hashes_to_include' as const,
'to_address_hashes_to_exclude' as const,
'address_relation' as const,
'amount_from' as const,
'amount_to' as const,
'token_contract_address_hashes_to_include' as const,
'token_contract_symbols_to_include' as const /* frontend only */,
'token_contract_address_hashes_to_exclude' as const,
'token_contract_symbols_to_exclude' as const /* frontend only */,
'block_number' as const,
'transaction_index' as const,
'internal_transaction_index' as const,
'token_transfer_index' as const,
],
paginated: true,
},
advanced_filter_methods: {
path: '/advanced-filters/methods',
filterFields: [ 'q' as const ],
},
advanced_filter_csv: {
path: '/advanced-filters/csv',
},
// CONFIGS
config_backend: {
path: '/config/backend',
},
config_backend_version: {
path: '/config/backend-version',
},
config_csv_export: {
path: '/config/csv-export',
},
config_celo: {
path: '/config/celo',
},
config_contract_languages: {
path: '/config/smart-contracts/languages',
},
// OTHER
api_v2_key: {
path: '/key',
},
} satisfies Record<string, ApiResource>;
export type GeneralApiMiscResourceName = `general:${ keyof typeof GENERAL_API_MISC_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiMiscResourcePayload<R extends GeneralApiMiscResourceName> =
R extends 'general:stats' ? HomeStats :
R extends 'general:stats_charts_txs' ? ChartTransactionResponse :
R extends 'general:stats_charts_market' ? ChartMarketResponse :
R extends 'general:stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse :
R extends 'general:stats_hot_contracts' ? HotContractsResponse :
R extends 'general:homepage_blocks' ? Array<Block> :
R extends 'general:homepage_txs' ? Array<Transaction> :
R extends 'general:homepage_txs_watchlist' ? Array<Transaction> :
R extends 'general:homepage_optimistic_deposits' ? Array<OptimisticL2DepositsItem> :
R extends 'general:homepage_arbitrum_deposits' ? ArbitrumLatestDepositsResponse :
R extends 'general:homepage_zkevm_l2_batches' ? { items: Array<ZkEvmL2TxnBatchesItem> } :
R extends 'general:homepage_arbitrum_l2_batches' ? { items: Array<ArbitrumL2TxnBatchesItem> } :
R extends 'general:homepage_indexing_status' ? IndexingStatus :
R extends 'general:homepage_zkevm_latest_batch' ? number :
R extends 'general:homepage_zksync_latest_batch' ? number :
R extends 'general:homepage_arbitrum_latest_batch' ? number :
R extends 'general:quick_search' ? QuickSearchResponse :
R extends 'general:search' ? SearchResult :
R extends 'general:search_check_redirect' ? SearchRedirectResult :
R extends 'general:config_backend' ? BackendConfig :
R extends 'general:config_backend_version' ? BackendVersionConfig :
R extends 'general:config_csv_export' ? CsvExportConfig :
R extends 'general:config_celo' ? CeloConfig :
R extends 'general:config_contract_languages' ? ContractLanguagesConfig :
R extends 'general:blob' ? Blob :
R extends 'general:validators_stability' ? ValidatorsStabilityResponse :
R extends 'general:validators_stability_counters' ? ValidatorsStabilityCountersResponse :
R extends 'general:validators_blackfort' ? ValidatorsBlackfortResponse :
R extends 'general:validators_blackfort_counters' ? ValidatorsBlackfortCountersResponse :
R extends 'general:validators_zilliqa' ? ValidatorsZilliqaResponse :
R extends 'general:validator_zilliqa' ? ValidatorZilliqa :
R extends 'general:epochs_celo' ? CeloEpochListResponse :
R extends 'general:epoch_celo' ? CeloEpochDetails :
R extends 'general:epoch_celo_election_rewards' ? CeloEpochElectionRewardDetailsResponse :
R extends 'general:user_ops' ? UserOpsResponse :
R extends 'general:user_op' ? UserOp :
R extends 'general:user_ops_account' ? UserOpsAccount :
R extends 'general:user_op_interpretation' ? TxInterpretationResponse :
R extends 'general:noves_transaction' ? NovesResponseData :
R extends 'general:noves_address_history' ? NovesAccountHistoryResponse :
R extends 'general:noves_describe_txs' ? NovesDescribeTxsResponse :
R extends 'general:withdrawals' ? WithdrawalsResponse :
R extends 'general:withdrawals_counters' ? WithdrawalsCounters :
R extends 'general:deposits' ? DepositsResponse :
R extends 'general:deposits_counters' ? DepositsCounters :
R extends 'general:advanced_filter' ? AdvancedFilterResponse :
R extends 'general:advanced_filter_methods' ? AdvancedFilterMethodsResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiMiscPaginationFilters<R extends GeneralApiMiscResourceName> =
R extends 'general:stats_hot_contracts' ? HotContractsFilters :
R extends 'general:search' ? SearchResultFilters :
R extends 'general:user_ops' ? UserOpsFilters :
R extends 'general:validators_stability' ? ValidatorsStabilityFilters :
R extends 'general:advanced_filter' ? AdvancedFilterParams :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiMiscPaginationSorting<R extends GeneralApiMiscResourceName> =
R extends 'general:stats_hot_contracts' ? HotContractsSorting :
R extends 'general:validators_stability' ? ValidatorsStabilitySorting :
R extends 'general:validators_blackfort' ? ValidatorsBlackfortSorting :
never;
/* eslint-enable @stylistic/indent */