diff --git a/apps/extension/src/hooks/full-sync-height.ts b/apps/extension/src/hooks/full-sync-height.ts
index 65e14808..5724cceb 100644
--- a/apps/extension/src/hooks/full-sync-height.ts
+++ b/apps/extension/src/hooks/full-sync-height.ts
@@ -2,7 +2,7 @@ import { PopupLoaderData } from '../routes/popup/home';
import { useStore } from '../state';
import { networkSelector } from '../state/network';
import { useLoaderData } from 'react-router-dom';
-import { useLatestBlockHeight } from './latest-block-height';
+import { useRemoteLatestBlockHeight } from './latest-block-height';
const tryGetMax = (a?: number, b?: number): number | undefined => {
// Height can be 0n which is falsy, so should compare to undefined state
@@ -27,11 +27,13 @@ const useFullSyncHeight = (): number | undefined => {
export const useSyncProgress = () => {
const fullSyncHeight = useFullSyncHeight();
- const { data: queriedLatest, error } = useLatestBlockHeight();
+ const { data: queriedLatest, error } = useRemoteLatestBlockHeight();
// If we have a queried sync height and it's ahead of our block-height query,
// use the sync value instead
- const latestBlockHeight = queriedLatest ? tryGetMax(queriedLatest, fullSyncHeight) : undefined;
+ const latestBlockHeight = queriedLatest
+ ? tryGetMax(Number(queriedLatest), fullSyncHeight)
+ : undefined;
return { latestBlockHeight, fullSyncHeight, error };
};
diff --git a/apps/extension/src/hooks/latest-block-height.ts b/apps/extension/src/hooks/latest-block-height.ts
index b78de00d..0df03152 100644
--- a/apps/extension/src/hooks/latest-block-height.ts
+++ b/apps/extension/src/hooks/latest-block-height.ts
@@ -1,5 +1,4 @@
import { useQuery } from '@tanstack/react-query';
-import { sample } from 'lodash';
import { createPromiseClient } from '@connectrpc/connect';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { TendermintProxyService } from '@penumbra-zone/protobuf';
@@ -7,100 +6,58 @@ import { ChainRegistryClient } from '@penumbra-labs/registry';
import { useStore } from '../state';
import { networkSelector } from '../state/network';
-// Utility function to fetch the block height by randomly querying one of the RPC endpoints
-// from the chain registry, using a recursive callback to try another endpoint if the current
-// one fails. Additionally, this implements a timeout mechanism at the request level to avoid
-// hanging from stalled requests.
-const fetchBlockHeightWithFallback = async (endpoints: string[]): Promise => {
- if (endpoints.length === 0) {
- throw new Error('All RPC endpoints failed to fetch the block height.');
- }
-
- // Randomly select an RPC endpoint from the chain registry
- const randomGrpcEndpoint = sample(endpoints);
- if (!randomGrpcEndpoint) {
- throw new Error('No RPC endpoints found.');
- }
-
- try {
- return await fetchBlockHeightWithTimeout(randomGrpcEndpoint);
- } catch (e) {
- // Remove the current endpoint from the list and retry with remaining endpoints
- const remainingEndpoints = endpoints.filter(endpoint => endpoint !== randomGrpcEndpoint);
- return fetchBlockHeightWithFallback(remainingEndpoints);
- }
-};
-
-// Fetch the block height from a specific RPC endpoint with a request-level timeout that superceeds
-// the channel transport-level timeout to prevent hanging requests.
-export const fetchBlockHeightWithTimeout = async (
- grpcEndpoint: string,
- timeoutMs = 5000,
-): Promise => {
- return new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- reject(new Error('Request timed out'));
- }, timeoutMs);
-
+const fetchRemoteLatestBlockHeight = async (rpcUrls: string[], timeoutMs = 5000) => {
+ for (const baseUrl of rpcUrls) {
const tendermintClient = createPromiseClient(
TendermintProxyService,
- createGrpcWebTransport({ baseUrl: grpcEndpoint }),
+ createGrpcWebTransport({ baseUrl }),
);
- tendermintClient
- .getStatus({})
- .then(result => {
- if (!result.syncInfo) {
- reject(new Error('No syncInfo in getStatus result'));
- }
- clearTimeout(timeout);
- resolve(Number(result.syncInfo?.latestBlockHeight));
- })
- .catch(() => {
- clearTimeout(timeout);
- reject(new Error('RPC request timed out while fetching block height'));
- });
- });
-};
-
-// Fetch the block height from a specific RPC endpoint.
-export const fetchBlockHeight = async (grpcEndpoint: string): Promise => {
- const tendermintClient = createPromiseClient(
- TendermintProxyService,
- createGrpcWebTransport({ baseUrl: grpcEndpoint }),
- );
+ const latestBlockHeight = await tendermintClient
+ .getStatus({}, { signal: AbortSignal.timeout(timeoutMs) })
+ .then(
+ status => status.syncInfo?.latestBlockHeight,
+ () => undefined,
+ );
- const result = await tendermintClient.getStatus({});
- if (!result.syncInfo) {
- throw new Error('No syncInfo in getStatus result');
+ if (latestBlockHeight) {
+ return latestBlockHeight;
+ }
}
- return Number(result.syncInfo.latestBlockHeight);
+
+ throw new Error('Remote endpoint(s) failed to return block height.');
};
-export const useLatestBlockHeightWithFallback = () => {
+export const useRemoteLatestBlockHeightWithFallback = () => {
+ const { grpcEndpoint } = useStore(networkSelector);
+
return useQuery({
queryKey: ['latestBlockHeightWithFallback'],
- queryFn: async () => {
- const chainRegistryClient = new ChainRegistryClient();
- const { rpcs } = chainRegistryClient.bundled.globals();
- const suggestedEndpoints = rpcs.map(i => i.url);
- return await fetchBlockHeightWithFallback(suggestedEndpoints);
+ queryFn: () => {
+ const registryRpcUrls = new ChainRegistryClient().bundled
+ .globals()
+ .rpcs.map(({ url }) => url);
+
+ // random order fallbacks
+ const fallbackUrls = registryRpcUrls.sort(() => Math.random() - 0.5);
+
+ // but try any deliberately selected endpoint first
+ const rpcUrls = grpcEndpoint
+ ? [grpcEndpoint, ...fallbackUrls.filter(url => url !== grpcEndpoint)]
+ : fallbackUrls;
+
+ return fetchRemoteLatestBlockHeight(rpcUrls);
},
retry: false,
});
};
-export const useLatestBlockHeight = () => {
+export const useRemoteLatestBlockHeight = () => {
const { grpcEndpoint } = useStore(networkSelector);
return useQuery({
queryKey: ['latestBlockHeight'],
- queryFn: async () => {
- if (!grpcEndpoint) {
- return;
- }
- return await fetchBlockHeight(grpcEndpoint);
- },
+ queryFn: () => (grpcEndpoint ? fetchRemoteLatestBlockHeight([grpcEndpoint]) : undefined),
enabled: Boolean(grpcEndpoint),
});
};
diff --git a/apps/extension/src/routes/page/onboarding/generate.tsx b/apps/extension/src/routes/page/onboarding/generate.tsx
index 389a5902..d4c27bf8 100644
--- a/apps/extension/src/routes/page/onboarding/generate.tsx
+++ b/apps/extension/src/routes/page/onboarding/generate.tsx
@@ -14,7 +14,7 @@ import { generateSelector } from '../../../state/seed-phrase/generate';
import { usePageNav } from '../../../utils/navigate';
import { PagePath } from '../paths';
import { WordLengthToogles } from '../../../shared/containers/word-length-toogles';
-import { useLatestBlockHeightWithFallback } from '../../../hooks/latest-block-height';
+import { useRemoteLatestBlockHeightWithFallback } from '../../../hooks/latest-block-height';
import { localExtStorage } from '../../../storage/local';
export const GenerateSeedPhrase = () => {
@@ -23,10 +23,10 @@ export const GenerateSeedPhrase = () => {
const [count, { startCountdown }] = useCountdown({ countStart: 3 });
const [reveal, setReveal] = useState(false);
- const { data: latestBlockHeight, isLoading, error } = useLatestBlockHeightWithFallback();
+ const { data: latestBlockHeight, isLoading, error } = useRemoteLatestBlockHeightWithFallback();
const onSubmit = async () => {
- await localExtStorage.set('walletCreationBlockHeight', latestBlockHeight);
+ await localExtStorage.set('walletCreationBlockHeight', Number(latestBlockHeight));
navigate(PagePath.CONFIRM_BACKUP);
};
@@ -77,7 +77,7 @@ export const GenerateSeedPhrase = () => {
{Boolean(error) && {String(error)}}
{isLoading && 'Loading...'}
- {latestBlockHeight && Number(latestBlockHeight)}
+ {!!latestBlockHeight && `${latestBlockHeight}`}