Skip to content

Commit

Permalink
Simplify onboarding (#231)
Browse files Browse the repository at this point in the history
* Remove steps from onboarding

* add origin path

* remove 12 toggle

* clearing wallet height on chain id change

* Add better checks for remove in storage

* Review updates
  • Loading branch information
grod220 authored Nov 8, 2024
1 parent c888514 commit 2d0e9d1
Show file tree
Hide file tree
Showing 27 changed files with 271 additions and 496 deletions.
1 change: 0 additions & 1 deletion .github/ISSUE_TEMPLATE/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ Manual testing to confirm extension works with all flows. Can use mainnet rpc or
- Lock screen
- [ ] Entering correct password grants entry
- [ ] Entering incorrect password denies entry and gives warning
- [ ] Forgot password flow allows to reset wallet state
- [ ] Penumbra support link correctly links to discord
- [ ] Previous wallet version upgrade. Ways to test:
- In dev env, have previous version loaded (via `load unpacked`) and click `window -> extensions -> update` after re-building new code.
Expand Down
67 changes: 67 additions & 0 deletions apps/extension/src/hooks/latest-block-height.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { Code, ConnectError, createRouterTransport } from '@connectrpc/connect';
import { TendermintProxyService } from '@penumbra-zone/protobuf/penumbra/util/tendermint_proxy/v1/tendermint_proxy_connect';
import { fetchBlockHeightWithFallback } from './latest-block-height';
import { GetStatusResponse } from '@penumbra-zone/protobuf/penumbra/util/tendermint_proxy/v1/tendermint_proxy_pb';

const endpoints = ['rpc1.example.com', 'rpc2.example.com', 'rpc3.example.com'];

const getMock = (fn: () => GetStatusResponse) => {
return createRouterTransport(router => {
router.service(TendermintProxyService, {
getStatus() {
return fn();
},
});
});
};

describe('fetchBlockHeightWithFallback', () => {
it('should fetch block height successfully from the first endpoint', async () => {
const mockTransport = getMock(
() => new GetStatusResponse({ syncInfo: { latestBlockHeight: 800n } }),
);
const result = await fetchBlockHeightWithFallback(endpoints, mockTransport);
expect(result.blockHeight).toEqual(800);
expect(endpoints.includes(result.rpc)).toBeTruthy();
});

it('should fallback to the second endpoint if the first fails', async () => {
let called = false;
const mockTransport = getMock(() => {
if (!called) {
called = true;
throw new ConnectError('Error calling service', Code.Unknown);
}
return new GetStatusResponse({ syncInfo: { latestBlockHeight: 800n } });
});
const result = await fetchBlockHeightWithFallback(endpoints, mockTransport);
expect(result.blockHeight).toEqual(800);
expect(endpoints.includes(result.rpc)).toBeTruthy();
expect(called).toBeTruthy();
});

it('should fallback through all endpoints and throw an error if all fail', async () => {
let timesCalled = 0;
const mockTransport = getMock(() => {
timesCalled++;
throw new ConnectError('Error calling service', Code.Unknown);
});
await expect(() => fetchBlockHeightWithFallback(endpoints, mockTransport)).rejects.toThrow(
new Error('All RPC endpoints failed to fetch the block height.'),
);
expect(timesCalled).toEqual(3);
});

it('should throw an error immediately if the endpoints array is empty', async () => {
let timesCalled = 0;
const mockTransport = getMock(() => {
timesCalled++;
throw new ConnectError('Error calling service', Code.Unknown);
});
await expect(() => fetchBlockHeightWithFallback([], mockTransport)).rejects.toThrow(
new Error('All RPC endpoints failed to fetch the block height.'),
);
expect(timesCalled).toEqual(0);
});
});
37 changes: 13 additions & 24 deletions apps/extension/src/hooks/latest-block-height.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { sample } from 'lodash';
import { createPromiseClient } from '@connectrpc/connect';
import { createPromiseClient, Transport } 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> => {

export const fetchBlockHeightWithFallback = async (
endpoints: string[],
transport?: Transport, // Deps injection mostly for unit tests
): Promise<{ blockHeight: number; rpc: string }> => {
if (endpoints.length === 0) {
throw new Error('All RPC endpoints failed to fetch the block height.');
}
Expand All @@ -23,24 +26,23 @@ const fetchBlockHeightWithFallback = async (endpoints: string[]): Promise<number
}

try {
return await fetchBlockHeightWithTimeout(randomGrpcEndpoint);
const blockHeight = await fetchBlockHeightWithTimeout(randomGrpcEndpoint, transport);
return { blockHeight, rpc: 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);
return fetchBlockHeightWithFallback(remainingEndpoints, transport);
}
};

// Fetch the block height from a specific RPC endpoint with a request-level timeout that superceeds
// Fetch the block height from a specific RPC endpoint with a request-level timeout that supersedes
// the channel transport-level timeout to prevent hanging requests.
export const fetchBlockHeightWithTimeout = async (
grpcEndpoint: string,
timeoutMs = 5000,
transport = createGrpcWebTransport({ baseUrl: grpcEndpoint }),
timeoutMs = 3000,
): Promise<number> => {
const tendermintClient = createPromiseClient(
TendermintProxyService,
createGrpcWebTransport({ baseUrl: grpcEndpoint }),
);
const tendermintClient = createPromiseClient(TendermintProxyService, transport);

const result = await tendermintClient.getStatus({}, { signal: AbortSignal.timeout(timeoutMs) });
if (!result.syncInfo) {
Expand All @@ -63,19 +65,6 @@ export const fetchBlockHeight = async (grpcEndpoint: string): Promise<number> =>
return Number(result.syncInfo.latestBlockHeight);
};

export const useLatestBlockHeightWithFallback = () => {
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);
},
retry: false,
});
};

export const useLatestBlockHeight = () => {
const { grpcEndpoint } = useStore(networkSelector);

Expand Down
8 changes: 0 additions & 8 deletions apps/extension/src/routes/page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,10 @@ import { getDefaultFrontend } from '../../state/default-frontend';
// Will redirect to onboarding if necessary.
export const pageIndexLoader = async () => {
const wallets = await localExtStorage.get('wallets');
const grpcEndpoint = await localExtStorage.get('grpcEndpoint');
const frontendUrl = await localExtStorage.get('frontendUrl');

if (!wallets.length) {
return redirect(PagePath.WELCOME);
}
if (!grpcEndpoint) {
return redirect(PagePath.SET_GRPC_ENDPOINT);
}
if (!frontendUrl) {
return redirect(PagePath.SET_DEFAULT_FRONTEND);
}

return null;
};
Expand Down
5 changes: 3 additions & 2 deletions apps/extension/src/routes/page/onboarding/confirm-backup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { Input } from '@repo/ui/components/ui/input';
import { useStore } from '../../../state';
import { generateSelector } from '../../../state/seed-phrase/generate';
import { usePageNav } from '../../../utils/navigate';
import { PagePath } from '../paths';
import { navigateToPasswordPage } from './password/utils';
import { SEED_PHRASE_ORIGIN } from './password/types';

export const ConfirmBackup = () => {
const navigate = usePageNav();
Expand All @@ -38,7 +39,7 @@ export const ConfirmBackup = () => {
<Button
variant='gradient'
disabled={!userAttemptCorrect()}
onClick={() => navigate(PagePath.SET_PASSWORD)}
onClick={() => navigateToPasswordPage(navigate, SEED_PHRASE_ORIGIN.NEWLY_GENERATED)}
>
Next
</Button>
Expand Down

This file was deleted.

33 changes: 0 additions & 33 deletions apps/extension/src/routes/page/onboarding/default-frontend.tsx

This file was deleted.

45 changes: 4 additions & 41 deletions apps/extension/src/routes/page/onboarding/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,18 @@ import { useStore } from '../../../state';
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 { localExtStorage } from '../../../storage/local';

export const GenerateSeedPhrase = () => {
const navigate = usePageNav();
const { phrase, generateRandomSeedPhrase } = useStore(generateSelector);
const [count, { startCountdown }] = useCountdown({ countStart: 3 });
const [reveal, setReveal] = useState(false);

const { data: latestBlockHeight, isLoading, error } = useLatestBlockHeightWithFallback();

const onSubmit = async () => {
await localExtStorage.set('walletCreationBlockHeight', latestBlockHeight);
const onSubmit = () => {
navigate(PagePath.CONFIRM_BACKUP);
};

// On render, asynchronously generate a new seed phrase and initialize the wallet creation block height
// On render, asynchronously generate a new seed phrase
useEffect(() => {
if (!phrase.length) {
generateRandomSeedPhrase(SeedPhraseLength.TWELVE_WORDS);
Expand All @@ -47,7 +41,6 @@ export const GenerateSeedPhrase = () => {
</CardHeader>
<CardContent className='mt-6 grid gap-4'>
<div className={cn('grid gap-4', !reveal && 'blur')}>
<WordLengthToogles toogleClick={generateRandomSeedPhrase} phrase={phrase} />
<div
className={cn(
'grid gap-4 mt-2',
Expand All @@ -65,36 +58,11 @@ export const GenerateSeedPhrase = () => {
disabled={!reveal}
text={phrase.join(' ')}
label={<span className='font-bold text-muted-foreground'>Copy to clipboard</span>}
className='m-auto w-48'
className='m-auto'
isSuccessCopyText
/>
</div>

{reveal && (
<div className='mt-4 rounded-lg border border-gray-500 bg-gray-800 p-4 shadow-sm'>
<h4 className='text-center text-lg font-semibold text-gray-200'>Wallet Birthday</h4>
<p className='mt-2 text-center text-gray-300'>
<span className='font-bold text-gray-100'>
{Boolean(error) && <span className='text-red-500'>{String(error)}</span>}
{isLoading && 'Loading...'}
{latestBlockHeight && Number(latestBlockHeight)}
</span>
</p>
<p className='mt-2 text-sm text-gray-400'>
This is the block height at the time your wallet was created. Please save the block
height along with your recovery passphrase. It&apos;s not required, but will help
you restore your wallet quicker on a fresh Prax install next time.
</p>
<CopyToClipboard
disabled={!reveal}
text={Number(latestBlockHeight).toString()}
label={<span className='font-bold text-muted-foreground'>Copy to clipboard</span>}
className='m-auto mt-4 w-48'
isSuccessCopyText
/>
</div>
)}

<div className='mt-2 flex flex-col justify-center gap-4'>
<div className='flex flex-col gap-1'>
<p className='flex items-center gap-2 text-rust'>
Expand All @@ -117,12 +85,7 @@ export const GenerateSeedPhrase = () => {
</div>

{reveal ? (
<Button
className='mt-4'
variant='gradient'
onClick={() => void onSubmit()}
disabled={count !== 0}
>
<Button className='mt-4' variant='gradient' onClick={onSubmit} disabled={count !== 0}>
I have backed this up
</Button>
) : (
Expand Down
Loading

0 comments on commit 2d0e9d1

Please sign in to comment.