Skip to content

Commit 8d6b99b

Browse files
Merge remote-tracking branch 'origin/main' into ezeth
2 parents 0468e80 + 6067416 commit 8d6b99b

File tree

11 files changed

+198
-65
lines changed

11 files changed

+198
-65
lines changed

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@hyperlane-xyz/warp-ui-template",
33
"description": "A web app template for building Hyperlane Warp Route UIs",
4-
"version": "8.5.0",
4+
"version": "12.1.0",
55
"author": "J M Rossy",
66
"dependencies": {
77
"@chakra-ui/next-js": "^2.4.2",
@@ -17,10 +17,10 @@
1717
"@emotion/react": "^11.13.3",
1818
"@emotion/styled": "^11.13.0",
1919
"@headlessui/react": "^2.2.0",
20-
"@hyperlane-xyz/registry": "12.1.0",
21-
"@hyperlane-xyz/sdk": "9.2.1",
22-
"@hyperlane-xyz/utils": "9.2.1",
23-
"@hyperlane-xyz/widgets": "9.2.1",
20+
"@hyperlane-xyz/registry": "13.9.0",
21+
"@hyperlane-xyz/sdk": "12.1.0",
22+
"@hyperlane-xyz/utils": "12.1.0",
23+
"@hyperlane-xyz/widgets": "12.1.0",
2424
"@interchain-ui/react": "^1.23.28",
2525
"@metamask/post-message-stream": "6.1.2",
2626
"@metamask/providers": "10.2.1",

src/components/icons/TokenIcon.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { IRegistry } from '@hyperlane-xyz/registry';
21
import { IToken } from '@hyperlane-xyz/sdk';
32
import { isHttpsUrl, isRelativeUrl } from '@hyperlane-xyz/utils';
43
import { Circle } from '@hyperlane-xyz/widgets';
54
import { useState } from 'react';
6-
import { useStore } from '../../features/store';
5+
import { links } from '../../consts/links';
76

87
interface Props {
98
token?: IToken | null;
@@ -16,8 +15,7 @@ export function TokenIcon({ token, size = 32 }: Props) {
1615
const fontSize = Math.floor(size / 2);
1716

1817
const [fallbackToText, setFallbackToText] = useState(false);
19-
const registry = useStore((s) => s.registry);
20-
const imageSrc = getImageSrc(registry, token);
18+
const imageSrc = getImageSrc(token);
2119
const bgColorSeed =
2220
token && (!imageSrc || fallbackToText)
2321
? (Buffer.from(token.addressOrDenom).at(0) || 0) % 5
@@ -40,11 +38,11 @@ export function TokenIcon({ token, size = 32 }: Props) {
4038
);
4139
}
4240

43-
function getImageSrc(registry: IRegistry, token?: IToken | null) {
41+
function getImageSrc(token?: IToken | null) {
4442
if (!token?.logoURI) return null;
4543
// If it's a valid, direct URL, return it
4644
if (isHttpsUrl(token.logoURI)) return token.logoURI;
4745
// Otherwise assume it's a relative URL to the registry base
48-
if (isRelativeUrl(token.logoURI)) return registry.getUri(token.logoURI);
46+
if (isRelativeUrl(token.logoURI)) return `${links.imgPath}${token.logoURI}`;
4947
return null;
5048
}

src/consts/chains.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import {
33
eclipsemainnetAddresses,
44
solanamainnet,
55
solanamainnetAddresses,
6+
sonicsvm,
7+
sonicsvmAddresses,
8+
soon,
9+
soonAddresses,
610
} from '@hyperlane-xyz/registry';
711
import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
812

@@ -15,12 +19,19 @@ export const chains: ChainMap<ChainMetadata & { mailbox?: Address }> = {
1519
...solanamainnet,
1620
// SVM chains require mailbox addresses for the token adapters
1721
mailbox: solanamainnetAddresses.mailbox,
18-
// Including a convenient rpc override because the Solana public RPC does not allow browser requests from localhost
1922
},
2023
eclipsemainnet: {
2124
...eclipsemainnet,
2225
mailbox: eclipsemainnetAddresses.mailbox,
2326
},
27+
soon: {
28+
...soon,
29+
mailbox: soonAddresses.mailbox,
30+
},
31+
sonicsvm: {
32+
...sonicsvm,
33+
mailbox: sonicsvmAddresses.mailbox,
34+
},
2435
// mycustomchain: {
2536
// protocol: ProtocolType.Ethereum,
2637
// chainId: 123123,

src/consts/links.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export const links = {
1313
privacyPolicy: 'https://hyperlane.xyz/privacy-policy',
1414
bounty:
1515
'https://github.com/search?q=org:hyperlane-xyz+label:bounty+is:open+is:issue&type=issues&s=&o=desc',
16+
imgPath: 'https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/main',
1617
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ChainMetadata, isRpcHealthy } from '@hyperlane-xyz/sdk';
2+
import { ProtocolType } from '@hyperlane-xyz/utils';
3+
import { beforeEach, describe, expect, test, vi } from 'vitest';
4+
import { checkRpcHealth } from './ChainConnectionWarning';
5+
6+
vi.mock('@hyperlane-xyz/sdk', async (importOriginal) => {
7+
const actual = (await importOriginal()) as { MultiProtocolProvider: any };
8+
return {
9+
...actual,
10+
MultiProtocolProvider: vi.fn().mockImplementation(() => ({
11+
getProvider: vi.fn(),
12+
})),
13+
isRpcHealthy: vi.fn(),
14+
};
15+
});
16+
17+
const mockRpcUrl = 'http://mock.test.rpc.com';
18+
19+
const mockEvmChainMetadata: ChainMetadata = {
20+
name: 'TestChain',
21+
protocol: ProtocolType.Ethereum,
22+
rpcUrls: [{ http: mockRpcUrl }, { http: mockRpcUrl }],
23+
chainId: 10000000,
24+
domainId: 10000000,
25+
};
26+
const mockSvmChainMetadata = {
27+
name: 'TestChain',
28+
protocol: ProtocolType.Sealevel,
29+
rpcUrls: [{ http: mockRpcUrl }, { http: mockRpcUrl }],
30+
chainId: 10000000,
31+
domainId: 10000000,
32+
};
33+
34+
describe('checkRpcHealth', () => {
35+
beforeEach(() => {
36+
vi.clearAllMocks();
37+
});
38+
39+
test('should call isRpcHealthy as many times as rpcUrls length when chain protocol is Ethereum', async () => {
40+
(isRpcHealthy as ReturnType<typeof vi.fn>).mockImplementation(() => Promise.resolve(true));
41+
await checkRpcHealth(mockEvmChainMetadata);
42+
expect(isRpcHealthy).toHaveBeenCalledTimes(mockEvmChainMetadata.rpcUrls.length);
43+
});
44+
45+
test('should call isRpcHealthy only once for non Ethereum chains', async () => {
46+
(isRpcHealthy as ReturnType<typeof vi.fn>).mockImplementation(() => Promise.resolve(true));
47+
await checkRpcHealth(mockSvmChainMetadata);
48+
expect(isRpcHealthy).toHaveBeenCalledTimes(1);
49+
});
50+
51+
test('should return true if at least one Ethereum RPC is healthy', async () => {
52+
(isRpcHealthy as ReturnType<typeof vi.fn>).mockImplementation((_, i) =>
53+
i === 1 ? Promise.resolve(true) : Promise.reject(),
54+
);
55+
const result = await checkRpcHealth(mockEvmChainMetadata);
56+
expect(result).toBe(true);
57+
});
58+
59+
test('should return false if no RPCs are healthy', async () => {
60+
(isRpcHealthy as ReturnType<typeof vi.fn>).mockImplementation(() => Promise.resolve(false));
61+
const result = await checkRpcHealth(mockEvmChainMetadata as any);
62+
expect(result).toBe(false);
63+
});
64+
});

src/features/chains/ChainConnectionWarning.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ChainMetadata, isRpcHealthy } from '@hyperlane-xyz/sdk';
2+
import { ProtocolType } from '@hyperlane-xyz/utils';
23
import { useQuery } from '@tanstack/react-query';
34
import { useState } from 'react';
45
import { FormWarningBanner } from '../../components/banner/FormWarningBanner';
@@ -25,7 +26,7 @@ export function ChainConnectionWarning({
2526
const isDestinationHealthy = await checkRpcHealth(destinationMetadata);
2627
return { isOriginHealthy, isDestinationHealthy };
2728
},
28-
refetchInterval: 5000,
29+
refetchInterval: 300000, // 5 minutes
2930
});
3031

3132
const unhealthyChain =
@@ -62,14 +63,21 @@ export function ChainConnectionWarning({
6263
);
6364
}
6465

65-
async function checkRpcHealth(chainMetadata: ChainMetadata) {
66+
export async function checkRpcHealth(chainMetadata: ChainMetadata) {
6667
try {
67-
// Note: this currently checks the health of only the first RPC,
68-
// which is what wallets and wallet libs (e.g. wagmi) will use
69-
const isHealthy = await isRpcHealthy(chainMetadata, 0);
70-
return isHealthy;
68+
// Note: this currently checks the health of only the first RPC for non EVM chains,
69+
// which is what wallets and wallet libs will use
70+
// for EVM chains it will use a fallback RPC, that is why we need to check if any RPC are healthy instead
71+
if (chainMetadata.protocol === ProtocolType.Ethereum) {
72+
const healthChecks = chainMetadata.rpcUrls.map((_, i) =>
73+
isRpcHealthy(chainMetadata, i).then((result) => (result ? true : Promise.reject())),
74+
);
75+
return await Promise.any(healthChecks);
76+
} else return await isRpcHealthy(chainMetadata, 0);
7177
} catch (error) {
72-
logger.warn('Error checking RPC health', error);
78+
if (error instanceof AggregateError)
79+
logger.warn(`No healthy RPCs found for ${chainMetadata.name}`);
80+
else logger.warn('Error checking RPC health', error);
7381
return false;
7482
}
7583
}

src/features/chains/metadata.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ import {
66
mergeChainMetadataMap,
77
RpcUrlSchema,
88
} from '@hyperlane-xyz/sdk';
9-
import { objFilter, objMap, promiseObjAll, tryParseJsonOrYaml } from '@hyperlane-xyz/utils';
9+
import {
10+
objFilter,
11+
objMap,
12+
promiseObjAll,
13+
ProtocolType,
14+
tryParseJsonOrYaml,
15+
} from '@hyperlane-xyz/utils';
1016
import { z } from 'zod';
1117
import { chains as ChainsTS } from '../../consts/chains.ts';
1218
import ChainsYaml from '../../consts/chains.yaml';
1319
import { config } from '../../consts/config.ts';
20+
import { links } from '../../consts/links.ts';
1421
import { logger } from '../../utils/logger.ts';
1522

1623
export async function assembleChainMetadata(
@@ -49,7 +56,7 @@ export async function assembleChainMetadata(
4956
registryChainMetadata,
5057
async (chainName, metadata): Promise<ChainMetadata> => ({
5158
...metadata,
52-
logoURI: (await registry.getChainLogoUri(chainName)) || undefined,
59+
logoURI: `${links.imgPath}/chains/${chainName}/logo.svg`,
5360
}),
5461
),
5562
);
@@ -63,13 +70,23 @@ export async function assembleChainMetadata(
6370
logger.warn('Invalid RPC overrides config', rpcOverrides.error);
6471
}
6572

66-
const chainMetadata = objMap(mergedChainMetadata, (chainName, metadata) => ({
67-
...metadata,
68-
rpcUrls:
73+
const chainMetadata = objMap(mergedChainMetadata, (chainName, metadata) => {
74+
const overridesUrl =
6975
rpcOverrides.success && rpcOverrides.data[chainName]
70-
? [rpcOverrides.data[chainName], ...metadata.rpcUrls]
71-
: metadata.rpcUrls,
72-
}));
76+
? rpcOverrides.data[chainName]
77+
: undefined;
78+
79+
if (!overridesUrl) return metadata;
80+
81+
// Only EVM supports fallback transport, so we are putting the override at the end
82+
const rpcUrls =
83+
metadata.protocol === ProtocolType.Ethereum
84+
? [...metadata.rpcUrls, overridesUrl]
85+
: [overridesUrl, ...metadata.rpcUrls];
86+
87+
return { ...metadata, rpcUrls };
88+
});
89+
7390
const chainMetadataWithOverrides = mergeChainMetadataMap(chainMetadata, storeMetadataOverrides);
7491
return { chainMetadata, chainMetadataWithOverrides };
7592
}

src/features/store.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GithubRegistry, IRegistry } from '@hyperlane-xyz/registry';
1+
import { chainAddresses, chainMetadata, IRegistry, PartialRegistry } from '@hyperlane-xyz/registry';
22
import {
33
ChainMap,
44
ChainMetadata,
@@ -10,7 +10,6 @@ import { objFilter } from '@hyperlane-xyz/utils';
1010
import { toast } from 'react-toastify';
1111
import { create } from 'zustand';
1212
import { persist } from 'zustand/middleware';
13-
import { config } from '../consts/config';
1413
import { logger } from '../utils/logger';
1514
import { assembleChainMetadata } from './chains/metadata';
1615
import { FinalTransferStatuses, TransferContext, TransferStatus } from './transfer/types';
@@ -88,10 +87,9 @@ export const useStore = create<AppState>()(
8887
set({ warpCoreConfigOverrides: overrides, multiProvider, warpCore });
8988
},
9089
multiProvider: new MultiProtocolProvider({}),
91-
registry: new GithubRegistry({
92-
uri: config.registryUrl,
93-
branch: config.registryBranch,
94-
proxyUrl: config.registryProxyUrl,
90+
registry: new PartialRegistry({
91+
chainAddresses: chainAddresses,
92+
chainMetadata: chainMetadata,
9593
}),
9694
warpCore: new WarpCore(new MultiProtocolProvider({}), []),
9795
setWarpContext: ({ registry, chainMetadata, multiProvider, warpCore }) => {

src/features/wallet/context/EvmWalletContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
walletConnectWallet,
1515
} from '@rainbow-me/rainbowkit/wallets';
1616
import { PropsWithChildren, useMemo } from 'react';
17-
import { createClient, http } from 'viem';
17+
import { createClient, fallback, http } from 'viem';
1818
import { WagmiProvider, createConfig } from 'wagmi';
1919
import { APP_NAME } from '../../../consts/app';
2020
import { config } from '../../../consts/config';
@@ -44,7 +44,7 @@ function initWagmi(multiProvider: MultiProtocolProvider) {
4444
chains: [chains[0], ...chains.splice(1)],
4545
connectors,
4646
client({ chain }) {
47-
const transport = http(chain.rpcUrls.default.http[0]);
47+
const transport = fallback(chain.rpcUrls.default.http.map((chainHttp) => http(chainHttp)));
4848
return createClient({ chain, transport });
4949
},
5050
});

vitest.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config';
33

44
export default defineConfig({
55
plugins: [tsconfigPaths()],
6+
assetsInclude: ['**/*.yaml'],
67
test: {},
78
});

0 commit comments

Comments
 (0)