Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sync: fresh and existing wallets skip trial decryption #164

Merged
merged 14 commits into from
Sep 4, 2024
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 });
};
};
33 changes: 31 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,25 @@ 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).toLocaleString()}
</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>
</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 +108,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 = () => {
grod220 marked this conversation as resolved.
Show resolved Hide resolved
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
1 change: 1 addition & 0 deletions apps/extension/src/storage/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const localDefaults: ExtensionStorageDefaults<LocalStorageState> = {
passwordKeyPrint: undefined,
frontendUrl: undefined,
numeraires: [],
walletCreationBlockHeight: undefined,
};

// Meant to be used for long-term persisted data. It is cleared when the extension is removed.
Expand Down
Loading