Skip to content

Commit

Permalink
refactor latestBlockHeight fetchers
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Sep 5, 2024
1 parent bf69c9b commit 3279ada
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 83 deletions.
8 changes: 5 additions & 3 deletions apps/extension/src/hooks/full-sync-height.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 };
};
109 changes: 33 additions & 76 deletions apps/extension/src/hooks/latest-block-height.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,63 @@
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';
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<number> => {
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<number> => {
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<number> => {
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),
});
};
8 changes: 4 additions & 4 deletions apps/extension/src/routes/page/onboarding/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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);
};

Expand Down Expand Up @@ -77,7 +77,7 @@ export const GenerateSeedPhrase = () => {
<span className='font-bold text-gray-100'>
{Boolean(error) && <span className='text-red-500'>{String(error)}</span>}
{isLoading && 'Loading...'}
{latestBlockHeight && Number(latestBlockHeight)}
{!!latestBlockHeight && `${latestBlockHeight}`}
</span>
</p>
<p className='mt-2 text-sm text-gray-400'>
Expand Down

0 comments on commit 3279ada

Please sign in to comment.