diff --git a/package.json b/package.json index b661e67..7dc3395 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "@hyperlane-xyz/explorer", "description": "An interchain explorer for the Hyperlane protocol and network.", - "version": "3.7.0", + "version": "3.8.0", "author": "J M Rossy", "dependencies": { "@headlessui/react": "^1.7.17", - "@hyperlane-xyz/sdk": "3.7.0", - "@hyperlane-xyz/utils": "3.7.0", - "@hyperlane-xyz/widgets": "3.7.0", + "@hyperlane-xyz/sdk": "3.8.0", + "@hyperlane-xyz/utils": "3.8.0", + "@hyperlane-xyz/widgets": "3.8.0", "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6", "@rainbow-me/rainbowkit": "0.12.16", "@tanstack/react-query": "^4.24.10", diff --git a/src/features/api/getMessages.ts b/src/features/api/getMessages.ts index 404cf2d..6c3fd4c 100644 --- a/src/features/api/getMessages.ts +++ b/src/features/api/getMessages.ts @@ -1,13 +1,14 @@ import { Client } from '@urql/core'; import type { NextApiRequest } from 'next'; +import { MultiProvider } from '@hyperlane-xyz/sdk'; + import { API_GRAPHQL_QUERY_LIMIT } from '../../consts/api'; import { logger } from '../../utils/logger'; import { sanitizeString } from '../../utils/string'; import { MessageIdentifierType, buildMessageQuery } from '../messages/queries/build'; import { MessagesQueryResult } from '../messages/queries/fragments'; import { parseMessageQueryResult } from '../messages/queries/parse'; -import { SmartMultiProvider } from '../providers/SmartMultiProvider'; import { ApiHandlerResult, ApiMessage, toApiMessage } from './types'; import { failureResult, successResult } from './utils'; @@ -26,7 +27,7 @@ export async function handler( API_GRAPHQL_QUERY_LIMIT, ); const result = await client.query(query, variables).toPromise(); - const multiProvider = new SmartMultiProvider(); + const multiProvider = new MultiProvider(); const messages = parseMessageQueryResult(multiProvider, result.data); return successResult(messages.map(toApiMessage)); } diff --git a/src/features/api/getStatus.ts b/src/features/api/getStatus.ts index b2912e4..7389f94 100644 --- a/src/features/api/getStatus.ts +++ b/src/features/api/getStatus.ts @@ -1,13 +1,14 @@ import { Client } from '@urql/core'; import type { NextApiRequest } from 'next'; +import { MultiProvider } from '@hyperlane-xyz/sdk'; + import { API_GRAPHQL_QUERY_LIMIT } from '../../consts/api'; import { MessageStatus } from '../../types'; import { logger } from '../../utils/logger'; import { buildMessageQuery } from '../messages/queries/build'; import { MessagesStubQueryResult } from '../messages/queries/fragments'; import { parseMessageStubResult } from '../messages/queries/parse'; -import { SmartMultiProvider } from '../providers/SmartMultiProvider'; import { parseQueryParams } from './getMessages'; import { ApiHandlerResult } from './types'; @@ -34,7 +35,7 @@ export async function handler( ); const result = await client.query(query, variables).toPromise(); - const multiProvider = new SmartMultiProvider(); + const multiProvider = new MultiProvider(); const messages = parseMessageStubResult(multiProvider, result.data); return successResult(messages.map((m) => ({ id: m.msgId, status: m.status }))); diff --git a/src/features/api/searchMessages.ts b/src/features/api/searchMessages.ts index 5055db4..8645738 100644 --- a/src/features/api/searchMessages.ts +++ b/src/features/api/searchMessages.ts @@ -1,13 +1,14 @@ import { Client } from '@urql/core'; import type { NextApiRequest } from 'next'; +import { MultiProvider } from '@hyperlane-xyz/sdk'; + import { API_GRAPHQL_QUERY_LIMIT } from '../../consts/api'; import { logger } from '../../utils/logger'; import { sanitizeString } from '../../utils/string'; import { buildMessageSearchQuery } from '../messages/queries/build'; import { MessagesQueryResult } from '../messages/queries/fragments'; import { parseMessageQueryResult } from '../messages/queries/parse'; -import { SmartMultiProvider } from '../providers/SmartMultiProvider'; import { ApiHandlerResult, ApiMessage, toApiMessage } from './types'; import { failureResult, successResult } from './utils'; @@ -33,7 +34,7 @@ export async function handler( ); const result = await client.query(query, variables).toPromise(); - const multiProvider = new SmartMultiProvider(); + const multiProvider = new MultiProvider(); const messages = parseMessageQueryResult(multiProvider, result.data); return successResult(messages.map(toApiMessage)); diff --git a/src/features/chains/ConfigureChains.tsx b/src/features/chains/ConfigureChains.tsx index b654442..0c2747c 100644 --- a/src/features/chains/ConfigureChains.tsx +++ b/src/features/chains/ConfigureChains.tsx @@ -195,7 +195,6 @@ const customChainTextareaPlaceholder = `{ } ], "blocks": { "confirmations": 1, "estimateBlockTime": 13 }, "mailbox": "0x123...", - "interchainGasPaymaster": "0x123..." } `; diff --git a/src/features/chains/chainconfig.test.ts b/src/features/chains/chainconfig.test.ts index 043c19b..9527cc5 100644 --- a/src/features/chains/chainconfig.test.ts +++ b/src/features/chains/chainconfig.test.ts @@ -1,14 +1,17 @@ +import { ChainMetadata, ExplorerFamily } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + import { tryParseChainConfig } from './chainConfig'; -const validConfig = { +const validConfig: ChainMetadata<{ mailbox: Address }> = { chainId: 12345, name: 'mytestnet', - protocol: 'ethereum', + protocol: ProtocolType.Ethereum, rpcUrls: [{ http: 'https://fakerpc.com' }], blockExplorers: [ { name: 'FakeScan', - family: 'other', + family: ExplorerFamily.Other, url: 'https://fakeexplorer.com', apiUrl: 'https://fakeexplorer.com', }, diff --git a/src/features/messages/pi-queries/fetchPiChainMessages.test.ts b/src/features/messages/pi-queries/fetchPiChainMessages.test.ts index df039fb..ae8273a 100644 --- a/src/features/messages/pi-queries/fetchPiChainMessages.test.ts +++ b/src/features/messages/pi-queries/fetchPiChainMessages.test.ts @@ -1,8 +1,7 @@ -import { chainMetadata, hyperlaneEnvironments } from '@hyperlane-xyz/sdk'; +import { MultiProvider, chainMetadata, hyperlaneEnvironments } from '@hyperlane-xyz/sdk'; import { Message, MessageStatus } from '../../../types'; import { ChainConfig } from '../../chains/chainConfig'; -import { SmartMultiProvider } from '../../providers/SmartMultiProvider'; import { fetchMessagesFromPiChain } from './fetchPiChainMessages'; @@ -155,5 +154,5 @@ describe('fetchMessagesFromPiChain', () => { }); function createMP(config: ChainConfig) { - return new SmartMultiProvider({ ...chainMetadata, sepolia: config }); + return new MultiProvider({ ...chainMetadata, sepolia: config }); } diff --git a/src/features/messages/queries/build.ts b/src/features/messages/queries/build.ts index dae38ef..1e364ad 100644 --- a/src/features/messages/queries/build.ts +++ b/src/features/messages/queries/build.ts @@ -1,3 +1,5 @@ +import { isAddress } from '@hyperlane-xyz/utils'; + import { adjustToUtcTime } from '../../../utils/time'; import { stringToPostgresBytea } from './encoding'; @@ -60,21 +62,6 @@ export function buildMessageQuery( return { query, variables }; } -// TODO removing destination-based or clauses for now due to DB load -// Queries will need to be restructured into multiple requests -// https://github.com/hyperlane-xyz/hyperlane-explorer/issues/59 -// {destination_tx_hash: {_eq: $search}}, -// {destination_tx_sender: {_eq: $search}}, -const searchWhereClause = ` - {_or: [ - {msg_id: {_eq: $search}}, - {sender: {_eq: $search}}, - {recipient: {_eq: $search}}, - {origin_tx_hash: {_eq: $search}}, - {origin_tx_sender: {_eq: $search}}, - ]} -`; - export function buildMessageSearchQuery( searchInput: string, originFilter: string | null, @@ -97,25 +84,49 @@ export function buildMessageSearchQuery( startTime, endTime, }; + const whereClauses = buildSearchWhereClauses(searchInput); - const query = ` - query ($search: bytea, $originChains: [bigint!], $destinationChains: [bigint!], $startTime: timestamp, $endTime: timestamp) { - message_view( - where: { - _and: [ - ${originFilter ? '{origin_chain_id: {_in: $originChains}},' : ''} - ${destFilter ? '{destination_chain_id: {_in: $destinationChains}},' : ''} - ${startTimeFilter ? '{send_occurred_at: {_gte: $startTime}},' : ''} - ${endTimeFilter ? '{send_occurred_at: {_lte: $endTime}},' : ''} - ${hasInput ? searchWhereClause : ''} - ] - }, - order_by: {send_occurred_at: desc}, - limit: ${limit} - ) { - ${useStub ? messageStubFragment : messageDetailsFragment} - } - } - `; + // Due to DB performance issues, we cannot use an `_or` clause + // Instead, each where clause for the search will be its own query + const queries = whereClauses.map( + (whereClause, i) => + `q${i}: message_view( + where: { + _and: [ + ${originFilter ? '{origin_chain_id: {_in: $originChains}},' : ''} + ${destFilter ? '{destination_chain_id: {_in: $destinationChains}},' : ''} + ${startTimeFilter ? '{send_occurred_at: {_gte: $startTime}},' : ''} + ${endTimeFilter ? '{send_occurred_at: {_lte: $endTime}},' : ''} + ${whereClause} + ] + }, + order_by: {send_occurred_at: desc}, + limit: ${limit} + ) { + ${useStub ? messageStubFragment : messageDetailsFragment} + }`, + ); + + const query = `query ($search: bytea, $originChains: [bigint!], $destinationChains: [bigint!], $startTime: timestamp, $endTime: timestamp) { + ${queries.join('\n')} + }`; return { query, variables }; } + +function buildSearchWhereClauses(searchInput: string) { + if (!searchInput) return ['']; + if (isAddress(searchInput)) { + return [ + `{sender: {_eq: $search}}`, + `{recipient: {_eq: $search}}`, + `{origin_tx_sender: {_eq: $search}}`, + `{destination_tx_sender: {_eq: $search}}`, + ]; + } else { + return [ + `{msg_id: {_eq: $search}}`, + `{origin_tx_hash: {_eq: $search}}`, + `{destination_tx_hash: {_eq: $search}}`, + ]; + } +} diff --git a/src/features/messages/queries/fragments.ts b/src/features/messages/queries/fragments.ts index 7f9985c..677258d 100644 --- a/src/features/messages/queries/fragments.ts +++ b/src/features/messages/queries/fragments.ts @@ -121,10 +121,5 @@ export interface MessageEntry extends MessageStubEntry { num_payments: number; } -export interface MessagesStubQueryResult { - message_view: MessageStubEntry[]; -} - -export interface MessagesQueryResult { - message_view: MessageEntry[]; -} +export type MessagesStubQueryResult = Record; +export type MessagesQueryResult = Record; diff --git a/src/features/messages/queries/parse.ts b/src/features/messages/queries/parse.ts index aa773e3..38a008a 100644 --- a/src/features/messages/queries/parse.ts +++ b/src/features/messages/queries/parse.ts @@ -24,20 +24,24 @@ export function parseMessageStubResult( multiProvider: MultiProvider, data: MessagesStubQueryResult | undefined, ): MessageStub[] { - if (!data?.message_view?.length) return []; - return data.message_view + if (!data || !Object.keys(data).length) return []; + return Object.values(data) + .flat() .map((m) => parseMessageStub(multiProvider, m)) - .filter((m): m is MessageStub => !!m); + .filter((m): m is MessageStub => !!m) + .sort((a, b) => b.origin.timestamp - a.origin.timestamp); } export function parseMessageQueryResult( multiProvider: MultiProvider, data: MessagesQueryResult | undefined, ): Message[] { - if (!data?.message_view?.length) return []; - return data.message_view + if (!data || !Object.keys(data).length) return []; + return Object.values(data) + .flat() .map((m) => parseMessage(multiProvider, m)) - .filter((m): m is Message => !!m); + .filter((m): m is Message => !!m) + .sort((a, b) => b.origin.timestamp - a.origin.timestamp); } function parseMessageStub(multiProvider: MultiProvider, m: MessageStubEntry): MessageStub | null { diff --git a/src/features/providers/SmartMultiProvider.ts b/src/features/providers/SmartMultiProvider.ts deleted file mode 100644 index c586e15..0000000 --- a/src/features/providers/SmartMultiProvider.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - ChainMap, - ChainMetadata, - ChainName, - HyperlaneSmartProvider, - MultiProvider, - chainMetadata, -} from '@hyperlane-xyz/sdk'; - -import { logger } from '../../utils/logger'; -import type { ChainConfig } from '../chains/chainConfig'; - -export class SmartMultiProvider extends MultiProvider { - constructor(chainMetadata?: ChainMap, options?: any) { - super(chainMetadata, options); - } - // Override to use SmartProvider instead of FallbackProvider - override tryGetProvider(chainNameOrId: ChainName | number): HyperlaneSmartProvider | null { - const metadata = this.tryGetChainMetadata(chainNameOrId); - if (!metadata) return null; - const { name, rpcUrls, blockExplorers } = metadata; - - if (!this.providers[name] && (rpcUrls?.length || blockExplorers?.length)) { - this.providers[name] = new HyperlaneSmartProvider(name, rpcUrls, blockExplorers); - } - - return (this.providers[name] as HyperlaneSmartProvider) || null; - } -} - -export function buildSmartProvider(customChainConfigs: ChainMap) { - logger.debug('Building new SmartMultiProvider'); - return new SmartMultiProvider({ ...chainMetadata, ...customChainConfigs }); -} diff --git a/src/store.ts b/src/store.ts index 3c018c7..2ccf46a 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,10 +1,9 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk'; +import { ChainMap, MultiProvider, chainMetadata } from '@hyperlane-xyz/sdk'; import { ChainConfig } from './features/chains/chainConfig'; -import { buildSmartProvider } from './features/providers/SmartMultiProvider'; import { logger } from './utils/logger'; // Increment this when persist state has breaking changes @@ -26,9 +25,9 @@ export const useStore = create()( (set) => ({ chainConfigs: {}, setChainConfigs: (configs: ChainMap) => { - set({ chainConfigs: configs, multiProvider: buildSmartProvider(configs) }); + set({ chainConfigs: configs, multiProvider: buildMultiProvider(configs) }); }, - multiProvider: buildSmartProvider({}), + multiProvider: buildMultiProvider({}), setMultiProvider: (mp: MultiProvider) => { set({ multiProvider: mp }); }, @@ -46,10 +45,14 @@ export const useStore = create()( logger.error('Error during hydration', error); return; } - state.setMultiProvider(buildSmartProvider(state.chainConfigs)); + state.setMultiProvider(buildMultiProvider(state.chainConfigs)); logger.debug('Hydration finished'); }; }, }, ), ); + +function buildMultiProvider(customChainConfigs: ChainMap) { + return new MultiProvider({ ...chainMetadata, ...customChainConfigs }); +} diff --git a/yarn.lock b/yarn.lock index 15d0119..f934a45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1336,19 +1336,19 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/core@npm:3.7.0": - version: 3.7.0 - resolution: "@hyperlane-xyz/core@npm:3.7.0" +"@hyperlane-xyz/core@npm:3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/core@npm:3.8.0" dependencies: "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:3.7.0" + "@hyperlane-xyz/utils": "npm:3.8.0" "@openzeppelin/contracts": "npm:^4.9.3" "@openzeppelin/contracts-upgradeable": "npm:^v4.9.3" peerDependencies: "@ethersproject/abi": "*" "@ethersproject/providers": "*" "@types/sinon-chai": "*" - checksum: efa01d943dd5b67830bb7244291c8ba9849472e804dff589463de76d3c03e56bc8d62454b575a6621aa1b8b53cc0d1d3b752a83d34f4b328ecd85e1ff23230d5 + checksum: f0f614bd1a1d8a755d8522409473b5cb3042304450e3ffb8ac96cd2756ca27b9a6f0a243608ffddf70a31af3b1e8dba0138154615c41424b2fb2e5baca52c963 languageName: node linkType: hard @@ -1357,9 +1357,9 @@ __metadata: resolution: "@hyperlane-xyz/explorer@workspace:." dependencies: "@headlessui/react": "npm:^1.7.17" - "@hyperlane-xyz/sdk": "npm:3.7.0" - "@hyperlane-xyz/utils": "npm:3.7.0" - "@hyperlane-xyz/widgets": "npm:3.7.0" + "@hyperlane-xyz/sdk": "npm:3.8.0" + "@hyperlane-xyz/utils": "npm:3.8.0" + "@hyperlane-xyz/widgets": "npm:3.8.0" "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6" "@rainbow-me/rainbowkit": "npm:0.12.16" "@tanstack/react-query": "npm:^4.24.10" @@ -1397,14 +1397,14 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/sdk@npm:3.7.0": - version: 3.7.0 - resolution: "@hyperlane-xyz/sdk@npm:3.7.0" +"@hyperlane-xyz/sdk@npm:3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/sdk@npm:3.8.0" dependencies: "@cosmjs/cosmwasm-stargate": "npm:^0.31.3" "@cosmjs/stargate": "npm:^0.31.3" - "@hyperlane-xyz/core": "npm:3.7.0" - "@hyperlane-xyz/utils": "npm:3.7.0" + "@hyperlane-xyz/core": "npm:3.8.0" + "@hyperlane-xyz/utils": "npm:3.8.0" "@solana/spl-token": "npm:^0.3.8" "@solana/web3.js": "npm:^1.78.0" "@types/coingecko-api": "npm:^1.0.10" @@ -1421,30 +1421,30 @@ __metadata: peerDependencies: "@ethersproject/abi": "*" "@ethersproject/providers": "*" - checksum: b124a42f34502c4dad4127723d345158f592056d7e60e17d87c84bf81664ead20232ffaff66e6c21968dfd5693ba5122910fbcaa6b7db5b05fdd5d2051592835 + checksum: 5ca551b639a3a5a92266adbac9da973dd417e8a399797c2449b07af15c0f1f4659d1b98f4c1b834db999db476f66b832db4eac37efa1b9f50bc6c2530b2f98fd languageName: node linkType: hard -"@hyperlane-xyz/utils@npm:3.7.0": - version: 3.7.0 - resolution: "@hyperlane-xyz/utils@npm:3.7.0" +"@hyperlane-xyz/utils@npm:3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/utils@npm:3.8.0" dependencies: "@cosmjs/encoding": "npm:^0.31.3" "@solana/web3.js": "npm:^1.78.0" bignumber.js: "npm:^9.1.1" ethers: "npm:^5.7.2" - checksum: c76f36913c572702b9dfe22fd868db6fed01c0da9485319e33e8d00a6b8a1bfdcecb5f61c8a3fd8ccbef0b36809e8055db62d75d0c6759d5e079ee330586bcd1 + checksum: 9d313133d3cc0cdae605c96ffdcebb704a5951a75201bc23be8a2653fbad45e0a1fd4782f8d93ec0b5a01bddaa98999ef5f320cfbb6eef44e0de2176a8a4fb7b languageName: node linkType: hard -"@hyperlane-xyz/widgets@npm:3.7.0": - version: 3.7.0 - resolution: "@hyperlane-xyz/widgets@npm:3.7.0" +"@hyperlane-xyz/widgets@npm:3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/widgets@npm:3.8.0" peerDependencies: "@hyperlane-xyz/sdk": ^3.1 react: ^18 react-dom: ^18 - checksum: 5ddf7a7a5f599f1b340bbe4e981e0cb10dd8930cc616c566abb58210658acd2826386afd18e8789f8bb62fc19d14b69db87433adcc97c38ebda2c322cc7865a2 + checksum: 2a36a90d43250c86084b05580909f316f13ed37a9416feea4e20411cd16ca42d430c5746636abfc9eb948199f888cf2f02b1e4d2a92dbc7fa4eee24af0f13579 languageName: node linkType: hard