Skip to content

Commit

Permalink
sync: fresh and existing wallets skip trial decryption (#164)
Browse files Browse the repository at this point in the history
* onboarding metadata for quick sync

* wallet birthday

* linting

* refactor to use zustand store for in-memory state management

* proper error propogation to caller in order to display in ui

* address more gabe's comments

* change type to support undefined

* ui formatting for wallet birthday

* add changeset

* [pairing] draft updates

* combine zustand slices

* [pairing] use react-query

* controlled form

* ui styling

---------

Co-authored-by: Gabe Rodriguez <grod220@gmail.com>
  • Loading branch information
TalDerei and grod220 authored Sep 4, 2024
1 parent 7343e9d commit b14f093
Show file tree
Hide file tree
Showing 23 changed files with 631 additions and 447 deletions.
6 changes: 6 additions & 0 deletions .changeset/cuddly-worms-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@repo/context': major
'chrome-extension': major
---

fresh and existing wallets skip trial decryption
16 changes: 8 additions & 8 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@
"@connectrpc/connect-web": "^1.4.0",
"@penumbra-labs/registry": "11.1.0",
"@penumbra-zone/bech32m": "^7.0.0",
"@penumbra-zone/client": "^18.0.1",
"@penumbra-zone/crypto-web": "^22.0.0",
"@penumbra-zone/client": "^18.1.0",
"@penumbra-zone/crypto-web": "^23.0.0",
"@penumbra-zone/getters": "^16.0.0",
"@penumbra-zone/keys": "^4.2.1",
"@penumbra-zone/perspective": "^28.0.0",
"@penumbra-zone/perspective": "^29.0.0",
"@penumbra-zone/protobuf": "^6.0.0",
"@penumbra-zone/query": "^29.0.0",
"@penumbra-zone/services": "^32.0.0",
"@penumbra-zone/storage": "^28.0.0",
"@penumbra-zone/query": "^30.0.0",
"@penumbra-zone/services": "^33.0.0",
"@penumbra-zone/storage": "^29.0.0",
"@penumbra-zone/transport-chrome": "^8.0.1",
"@penumbra-zone/transport-dom": "^7.5.0",
"@penumbra-zone/types": "^21.0.0",
"@penumbra-zone/wasm": "^26.2.0",
"@penumbra-zone/types": "^22.0.0",
"@penumbra-zone/wasm": "^27.0.0",
"@radix-ui/react-icons": "^1.3.0",
"@repo/context": "workspace:*",
"@repo/ui": "workspace:*",
Expand Down
24 changes: 2 additions & 22 deletions apps/extension/src/hooks/full-sync-height.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { PopupLoaderData } from '../routes/popup/home';
import { useStore } from '../state';
import { networkSelector } from '../state/network';
import { useLoaderData } from 'react-router-dom';
import { TendermintProxyService } from '@penumbra-zone/protobuf';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { createPromiseClient } from '@connectrpc/connect';
import { useLatestBlockHeight } 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 @@ -30,24 +27,7 @@ const useFullSyncHeight = (): number | undefined => {

export const useSyncProgress = () => {
const fullSyncHeight = useFullSyncHeight();
const { grpcEndpoint } = useStore(networkSelector);

const { data: queriedLatest, error } = useQuery({
queryKey: ['latestBlockHeight'],
queryFn: async () => {
if (!grpcEndpoint) {
return;
}
const tendermintClient = createPromiseClient(
TendermintProxyService,
createGrpcWebTransport({ baseUrl: grpcEndpoint }),
);
const blockHeight = (await tendermintClient.getStatus({}).catch(() => undefined))?.syncInfo
?.latestBlockHeight;
return Number(blockHeight);
},
enabled: Boolean(grpcEndpoint),
});
const { data: queriedLatest, error } = useLatestBlockHeight();

// If we have a queried sync height and it's ahead of our block-height query,
// use the sync value instead
Expand Down
74 changes: 74 additions & 0 deletions apps/extension/src/hooks/latest-block-height.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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 fetchBlockHeight(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 timeout to prevent hanging requests.
export const fetchBlockHeight = async (grpcEndpoint: string): Promise<number> => {
const tendermintClient = createPromiseClient(
TendermintProxyService,
createGrpcWebTransport({ baseUrl: grpcEndpoint, defaultTimeoutMs: 2000 }),
);

const result = await tendermintClient.getStatus({});
if (!result.syncInfo) {
throw new Error('No syncInfo in getStatus result');
}
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);

return useQuery({
queryKey: ['latestBlockHeight'],
queryFn: async () => {
if (!grpcEndpoint) {
return;
}
return await fetchBlockHeight(grpcEndpoint);
},
enabled: Boolean(grpcEndpoint),
});
};
3 changes: 1 addition & 2 deletions apps/extension/src/hooks/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { walletsSelector } from '../state/wallets';

// Saves hashed password, uses that hash to encrypt the seed phrase
// and then saves that to session + local storage
export const useOnboardingSave = () => {
export const useAddWallet = () => {
const { setPassword } = useStore(passwordSelector);
const { phrase: generatedPhrase } = useStore(generateSelector);
const { phrase: importedPhrase } = useStore(importSelector);
Expand All @@ -16,7 +16,6 @@ export const useOnboardingSave = () => {
// Determine which routes it came through to get here
const seedPhrase = generatedPhrase.length ? generatedPhrase : importedPhrase;
await setPassword(plaintextPassword);

await addWallet({ label: 'Wallet #1', seedPhrase });
};
};
40 changes: 38 additions & 2 deletions apps/extension/src/routes/page/onboarding/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@ 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);

// On render, generate a new seed phrase
const { data: latestBlockHeight, isLoading, error } = useLatestBlockHeightWithFallback();

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

// On render, asynchronously generate a new seed phrase and initialize the wallet creation block height
useEffect(() => {
if (!phrase.length) {
generateRandomSeedPhrase(SeedPhraseLength.TWELVE_WORDS);
Expand Down Expand Up @@ -60,6 +69,32 @@ export const GenerateSeedPhrase = () => {
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 @@ -80,11 +115,12 @@ export const GenerateSeedPhrase = () => {
</p>
</div>
</div>

{reveal ? (
<Button
className='mt-4'
variant='gradient'
onClick={() => navigate(PagePath.CONFIRM_BACKUP)}
onClick={() => void onSubmit()}
disabled={count !== 0}
>
I have backed this up
Expand Down
62 changes: 62 additions & 0 deletions apps/extension/src/routes/page/onboarding/height.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BackIcon } from '@repo/ui/components/ui/icons/back-icon';
import { Button } from '@repo/ui/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@repo/ui/components/ui/card';
import { FadeTransition } from '@repo/ui/components/ui/fade-transition';
import { usePageNav } from '../../../utils/navigate';
import { PagePath } from '../paths';
import { FormEvent, useState } from 'react';
import { Input } from '@repo/ui/components/ui/input';
import { localExtStorage } from '../../../storage/local';

export const ImportWalletCreationHeight = () => {
const navigate = usePageNav();
const [blockHeight, setBlockHeight] = useState<number>();

const handleSubmit = (event: FormEvent) => {
event.preventDefault();

void (async () => {
await localExtStorage.set('walletCreationBlockHeight', blockHeight ? blockHeight : 0);
navigate(PagePath.SET_PASSWORD);
})();
};

return (
<FadeTransition>
<BackIcon className='float-left mb-4' onClick={() => navigate(-1)} />
<Card className='w-[600px] p-8' gradient>
<CardHeader className='items-center text-center'>
<CardTitle className='text-xl font-semibold'>
Enter your wallet&apos;s birthday (Optional)
</CardTitle>
<CardDescription className='mt-2 text-sm'>
This is the block height at the time your wallet was created. Providing your
wallet&apos;s block creation height can help speed up the synchronization process, but
it&apos;s not required. If you don&apos;t have this information, you can safely skip
this step.
</CardDescription>
</CardHeader>
<CardContent className='mt-8'>
<form className='grid gap-6' onSubmit={handleSubmit}>
<Input
type='number'
placeholder='Enter block height'
value={blockHeight ? blockHeight : ''} // prevents uncontrolled form react err
onChange={e => setBlockHeight(Number(e.target.value))}
className='rounded-md border border-gray-700 p-3 text-[16px] font-normal leading-[24px]'
/>
<Button className='mt-6 w-full' variant='gradient' onClick={handleSubmit}>
Continue
</Button>
</form>
</CardContent>
</Card>
</FadeTransition>
);
};
2 changes: 1 addition & 1 deletion apps/extension/src/routes/page/onboarding/import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const ImportSeedPhrase = () => {

const handleSubmit = (event: MouseEvent | FormEvent) => {
event.preventDefault();
navigate(PagePath.SET_PASSWORD);
navigate(PagePath.IMPORT_WALLET_CREATION_HEIGHT);
};

return (
Expand Down
5 changes: 5 additions & 0 deletions apps/extension/src/routes/page/onboarding/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { pageIndexLoader } from '..';
import { SetGrpcEndpoint } from './set-grpc-endpoint';
import { SetDefaultFrontendPage } from './default-frontend';
import { SetNumerairesPage } from './set-numeraire';
import { ImportWalletCreationHeight } from './height';

export const onboardingRoutes = [
{
Expand All @@ -27,6 +28,10 @@ export const onboardingRoutes = [
path: PagePath.IMPORT_SEED_PHRASE,
element: <ImportSeedPhrase />,
},
{
path: PagePath.IMPORT_WALLET_CREATION_HEIGHT,
element: <ImportWalletCreationHeight />,
},
{
path: PagePath.SET_PASSWORD,
element: <SetPassword />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const SetGrpcEndpoint = () => {
<Card className='w-[400px]' gradient>
<CardHeader>
<CardTitle>Select your preferred RPC endpoint</CardTitle>

<CardDescription>
The requests you make may reveal your intentions about transactions you wish to make, so
select an RPC node that you trust. If you&apos;re unsure which one to choose, leave this
Expand Down
6 changes: 3 additions & 3 deletions apps/extension/src/routes/page/onboarding/set-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ import {
CardTitle,
} from '@repo/ui/components/ui/card';
import { FadeTransition } from '@repo/ui/components/ui/fade-transition';
import { useOnboardingSave } from '../../../hooks/onboarding';
import { useAddWallet } from '../../../hooks/onboarding';
import { usePageNav } from '../../../utils/navigate';
import { PagePath } from '../paths';
import { PasswordInput } from '../../../shared/components/password-input';

export const SetPassword = () => {
const navigate = usePageNav();
const onboardingSave = useOnboardingSave();
const addWallet = useAddWallet();
const [password, setPassword] = useState('');
const [confirmation, setConfirmation] = useState('');

const handleSubmit = (event: FormEvent) => {
event.preventDefault();

void (async () => {
await onboardingSave(password);
await addWallet(password);
navigate(PagePath.SET_GRPC_ENDPOINT);
})();
};
Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/routes/page/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum PagePath {
GENERATE_SEED_PHRASE = '/welcome/generate',
CONFIRM_BACKUP = '/welcome/confirm-backup',
IMPORT_SEED_PHRASE = '/welcome/import',
IMPORT_WALLET_CREATION_HEIGHT = '/welcome/set-wallet-creation-height',
ONBOARDING_SUCCESS = '/welcome/success',
SET_PASSWORD = '/welcome/set-password',
SET_GRPC_ENDPOINT = '/welcome/set-grpc-endpoint',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ import {
CardTitle,
} from '@repo/ui/components/ui/card';
import { FadeTransition } from '@repo/ui/components/ui/fade-transition';
import { useOnboardingSave } from '../../../hooks/onboarding';
import { useAddWallet } from '../../../hooks/onboarding';
import { usePageNav } from '../../../utils/navigate';
import { PagePath } from '../paths';
import { PasswordInput } from '../../../shared/components/password-input';

export const SetPassword = () => {
const navigate = usePageNav();
const finalOnboardingSave = useOnboardingSave();
const addWallet = useAddWallet();
const [password, setPassword] = useState('');
const [confirmation, setConfirmation] = useState('');

const handleSubmit = (event: FormEvent) => {
event.preventDefault();
void (async function () {
await finalOnboardingSave(password);
await addWallet(password);
navigate(PagePath.ONBOARDING_SUCCESS);
})();
};
Expand Down
Loading

0 comments on commit b14f093

Please sign in to comment.