Skip to content

Commit

Permalink
1231 onboarding numeraire selection2 (#28)
Browse files Browse the repository at this point in the history
* use numeraire from local storage

* save multiple numeraires in local storage

* added NumerairesSlice

* add ChangeNumeraires service message

* numeraires persist

* add icons

* remove onboardNumeraires

* delete prices when numeraire is changed

* refactor & fix numeraires update bug

* add idb tests

* fix chain-id

* Update apps/extension/src/shared/components/numeraires-form.tsx

Co-authored-by: Jesse Pinho <[email protected]>

* Update apps/extension/src/shared/components/numeraires-form.tsx

Co-authored-by: Jesse Pinho <[email protected]>

* Update apps/extension/src/routes/popup/settings/settings.tsx

Co-authored-by: Jesse Pinho <[email protected]>

* Update apps/extension/src/routes/page/onboarding/set-numeraire.tsx

Co-authored-by: Jesse Pinho <[email protected]>

* Update apps/extension/src/routes/page/onboarding/set-numeraire.tsx

Co-authored-by: Jesse Pinho <[email protected]>

* fix lint

* add docs for clearSwapBasedPrices

* add chainId comment

---------

Co-authored-by: Jesse Pinho <[email protected]>
  • Loading branch information
Valentine1898 and jessepinho authored Jun 14, 2024
1 parent 236e650 commit 27d8552
Show file tree
Hide file tree
Showing 30 changed files with 343 additions and 22 deletions.
1 change: 1 addition & 0 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@penumbra-zone/bech32m": "workspace:*",
"@penumbra-zone/client": "workspace:*",
"@penumbra-zone/crypto-web": "workspace:*",
"@penumbra-zone/getters": "workspace:*",
"@penumbra-zone/perspective": "workspace:*",
"@penumbra-zone/protobuf": "workspace:*",
"@penumbra-zone/query": "workspace:*",
Expand Down
24 changes: 24 additions & 0 deletions apps/extension/src/icons/numeraires-gradient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const NumerairesGradientIcon = () => (
<svg width='80' height='80' viewBox='0 0 80 80' fill='none' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient
id='customGradient'
x1='0'
y1='0'
x2='80'
y2='80'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#8BE4D9' stopOpacity='0.7' />
<stop offset='0.522217' stopColor='#C8B880' stopOpacity='0.7' />
<stop offset='1' stopColor='#FF902F' stopOpacity='0.6' />
</linearGradient>
</defs>
<path
d='M61.3333 6C61.5979 6 61.8333 6.22386 61.8333 6.5V73.5C61.8333 73.7761 61.5979 74 61.3333 74C61.0688 74 60.8333 73.7761 60.8333 73.5V6.5C60.8333 6.22386 61.0688 6 61.3333 6ZM50.3333 18C50.5979 18 50.8333 18.2239 50.8333 18.5V73.5C50.8333 73.7761 50.5979 74 50.3333 74C50.0688 74 49.8333 73.7761 49.8333 73.5V18.5C49.8333 18.2239 50.0688 18 50.3333 18ZM71.3333 18C71.5979 18 71.8333 18.2239 71.8333 18.5V73.5C71.8333 73.7761 71.5979 74 71.3333 74C71.0688 74 70.8333 73.7761 70.8333 73.5V18.5C70.8333 18.2239 71.0688 18 71.3333 18ZM27.3333 21C27.5979 21 27.8333 21.2239 27.8333 21.5V73.5C27.8333 73.7761 27.5979 74 27.3333 74C27.0688 74 26.8333 73.7761 26.8333 73.5V21.5C26.8333 21.2239 27.0688 21 27.3333 21ZM7.33333 26C7.59788 26 7.83333 26.2239 7.83333 26.5V73.5C7.83333 73.7761 7.59788 74 7.33333 74C7.06878 74 6.83333 73.7761 6.83333 73.5V26.5C6.83333 26.2239 7.06878 26 7.33333 26ZM36.3333 26C36.5979 26 36.8333 26.2239 36.8333 26.5V73.5C36.8333 73.7761 36.5979 74 36.3333 74C36.0688 74 35.8333 73.7761 35.8333 73.5V26.5C35.8333 26.2239 36.0688 26 36.3333 26ZM16.3333 36C16.5979 36 16.8333 36.2239 16.8333 36.5V73.5C16.8333 73.7761 16.5979 74 16.3333 74C16.0688 74 15.8333 73.7761 15.8333 73.5V36.5C15.8333 36.2239 16.0688 36 16.3333 36Z'
fill='url(#customGradient)'
fillRule='evenodd'
clipRule='evenodd'
/>
</svg>
);
1 change: 1 addition & 0 deletions apps/extension/src/message/services.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum ServicesMessage {
ClearCache = 'ClearCache',
ChangeNumeraires = 'ChangeNumeraires',
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const SetDefaultFrontendPage = () => {

const onSubmit: FormEventHandler = (event): void => {
event.preventDefault();
navigate(PagePath.ONBOARDING_SUCCESS);
navigate(PagePath.SET_NUMERAIRES);
};

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 @@ -8,6 +8,7 @@ import { SetPassword } from './set-password';
import { pageIndexLoader } from '..';
import { SetGrpcEndpoint } from './set-grpc-endpoint';
import { SetDefaultFrontendPage } from './default-frontend';
import { SetNumerairesPage } from './set-numeraire';

export const onboardingRoutes = [
{
Expand Down Expand Up @@ -38,6 +39,10 @@ export const onboardingRoutes = [
path: PagePath.SET_DEFAULT_FRONTEND,
element: <SetDefaultFrontendPage />,
},
{
path: PagePath.SET_NUMERAIRES,
element: <SetNumerairesPage />,
},
{
path: PagePath.ONBOARDING_SUCCESS,
element: <OnboardingSuccess />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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
30 changes: 30 additions & 0 deletions apps/extension/src/routes/page/onboarding/set-numeraire.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Card, CardDescription, CardHeader, CardTitle } from '@penumbra-zone/ui/components/ui/card';
import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition';
import { usePageNav } from '../../../utils/navigate';
import { PagePath } from '../paths';
import { NumeraireForm } from '../../../shared/components/numeraires-form';

export const SetNumerairesPage = () => {
const navigate = usePageNav();

const onSuccess = (): void => {
navigate(PagePath.ONBOARDING_SUCCESS);
};

return (
<FadeTransition>
<Card className='w-[400px]' gradient>
<CardHeader>
<CardTitle>In which token denomination would you prefer to price assets?</CardTitle>
<CardDescription>
Prax does not use third-party price providers for privacy reasons. Instead, Prax indexes
asset prices locally by selected denomination.
</CardDescription>
</CardHeader>
<div className='mt-6'>
<NumeraireForm isOnboarding onSuccess={onSuccess} />
</div>
</Card>
</FadeTransition>
);
};
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 @@ -8,6 +8,7 @@ export enum PagePath {
SET_PASSWORD = '/welcome/set-password',
SET_GRPC_ENDPOINT = '/welcome/set-grpc-endpoint',
SET_DEFAULT_FRONTEND = '/welcome/set-default-frontend',
SET_NUMERAIRES = '/welcome/set-numeraires',
RESTORE_PASSWORD = '/restore-password',
RESTORE_PASSWORD_INDEX = '/restore-password/',
RESTORE_PASSWORD_SET_PASSWORD = '/restore-password/set-password',
Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/routes/popup/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export enum PopupPath {
SETTINGS_RECOVERY_PASSPHRASE = '/settings/recovery-passphrase',
SETTINGS_FULL_VIEWING_KEY = '/settings/full-viewing-key',
SETTINGS_SPEND_KEY = '/settings/spend-key',
SETTINGS_NUMERAIRES = '/settings/numeraires',
}
5 changes: 5 additions & 0 deletions apps/extension/src/routes/popup/settings/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SettingsRPC } from './settings-rpc';
import { SettingsSecurity } from './settings-security';
import { SettingsSpendKey } from './settings-spend-key';
import { SettingsDefaultFrontend } from './settings-default-frontend';
import { SettingsNumeraires } from './settings-numeraires';

export const settingsRoutes = [
{
Expand Down Expand Up @@ -56,4 +57,8 @@ export const settingsRoutes = [
path: PopupPath.SETTINGS_SPEND_KEY,
element: <SettingsSpendKey />,
},
{
path: PopupPath.SETTINGS_NUMERAIRES,
element: <SettingsNumeraires />,
},
];
18 changes: 18 additions & 0 deletions apps/extension/src/routes/popup/settings/settings-numeraires.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SettingsScreen } from './settings-screen';
import { NumerairesGradientIcon } from '../../../icons/numeraires-gradient';
import { usePopupNav } from '../../../utils/navigate';
import { PopupPath } from '../paths';
import { NumeraireForm } from '../../../shared/components/numeraires-form';

export const SettingsNumeraires = () => {
const navigate = usePopupNav();

const onSuccess = () => {
navigate(PopupPath.INDEX);
};
return (
<SettingsScreen title='Price denominations' IconComponent={NumerairesGradientIcon}>
<NumeraireForm onSuccess={onSuccess} />
</SettingsScreen>
);
};
6 changes: 6 additions & 0 deletions apps/extension/src/routes/popup/settings/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BarChartIcon,
DashboardIcon,
ExitIcon,
HomeIcon,
Expand Down Expand Up @@ -34,6 +35,11 @@ const links = [
icon: <Link1Icon className='size-5 text-muted-foreground' />,
href: PopupPath.SETTINGS_CONNECTED_SITES,
},
{
title: 'Price denomination',
icon: <BarChartIcon className='size-5 text-muted-foreground' />,
href: PopupPath.SETTINGS_NUMERAIRES,
},
{
title: 'Advanced',
icon: <DashboardIcon className='size-5 text-muted-foreground' />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { isValidUrl } from '../../utils/is-valid-url';

const useSaveGrpcEndpointSelector = (state: AllSlices) => ({
grpcEndpoint: state.network.grpcEndpoint,
chainId: state.network.chainId,
setGrpcEndpoint: state.network.setGRPCEndpoint,
setChainId: state.network.setChainId,
});

const getRpcsFromRegistry = () => {
Expand All @@ -25,10 +27,11 @@ export const useGrpcEndpointForm = () => {
const grpcEndpoints = useMemo(() => getRpcsFromRegistry(), []);

// Get the rpc set in storage (if present)
const { grpcEndpoint, setGrpcEndpoint } = useStoreShallow(useSaveGrpcEndpointSelector);
const { grpcEndpoint, chainId, setGrpcEndpoint, setChainId } = useStoreShallow(
useSaveGrpcEndpointSelector,
);

const [originalChainId, setOriginalChainId] = useState<string | undefined>();
const [chainId, setChainId] = useState<string>();
const [grpcEndpointInput, setGrpcEndpointInput] = useState<string>('');
const [rpcError, setRpcError] = useState<string>();
const [isSubmitButtonEnabled, setIsSubmitButtonEnabled] = useState(false);
Expand Down
96 changes: 96 additions & 0 deletions apps/extension/src/shared/components/numeraires-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { AllSlices, useStore } from '../../state';
import { useChainIdQuery } from '../../hooks/chain-id';
import { useMemo, useState } from 'react';
import { ServicesMessage } from '../../message/services';
import { SelectList } from '@penumbra-zone/ui/components/ui/select-list';
import { bech32mAssetId } from '@penumbra-zone/bech32m/passet';
import { getAssetId } from '@penumbra-zone/getters/metadata';
import { Button } from '@penumbra-zone/ui/components/ui/button';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

const getNumeraireFromRegistry = (chainId?: string): Metadata[] => {
if (!chainId) return [];
const registryClient = new ChainRegistryClient();
const registry = registryClient.get(chainId);
return registry.numeraires.map(n => registry.getMetadata(n));
};

const useNumerairesSelector = (state: AllSlices) => {
return {
selectedNumeraires: state.numeraires.selectedNumeraires,
selectNumeraire: state.numeraires.selectNumeraire,
saveNumeraires: state.numeraires.saveNumeraires,
networkChainId: state.network.chainId,
};
};

export const NumeraireForm = ({
isOnboarding,
onSuccess,
}: {
isOnboarding?: boolean;
onSuccess: () => void | Promise<void>;
}) => {
const { chainId } = useChainIdQuery();
const { selectedNumeraires, selectNumeraire, saveNumeraires, networkChainId } =
useStore(useNumerairesSelector);

// 'chainId' from 'useChainIdQuery' is not available during onboarding,
// this forces you to use two sources to guarantee 'chainId' for both settings and onboarding
const numeraires = useMemo(() => getNumeraireFromRegistry(chainId ?? networkChainId), [chainId]);

const [loading, setLoading] = useState(false);

const handleSubmit = () => {
setLoading(true);
void (async function () {
await saveNumeraires();
await chrome.runtime.sendMessage(ServicesMessage.ChangeNumeraires);
await onSuccess();
})();
};

return (
<div className='flex flex-col gap-2'>
<form className='flex flex-col gap-4' onSubmit={handleSubmit}>
<SelectList>
{numeraires.map(metadata => {
// Image default is "" and thus cannot do nullish-coalescing
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const icon = metadata.images[0]?.png || metadata.images[0]?.svg;
return (
<SelectList.Option
key={bech32mAssetId(getAssetId(metadata))}
value={getAssetId(metadata).toJsonString()}
label={metadata.symbol}
isSelected={selectedNumeraires.includes(getAssetId(metadata).toJsonString())}
onSelect={() => selectNumeraire(getAssetId(metadata).toJsonString())}
image={
!!icon && (
<img
src={icon}
className='size-full object-contain'
alt='rpc endpoint brand image'
/>
)
}
/>
);
})}

<Button
className='my-5'
key='save-button'
variant='gradient'
type='submit'
disabled={loading}
onClick={handleSubmit}
>
{isOnboarding ? 'Next' : loading ? 'Saving...' : 'Save'}
</Button>
</SelectList>
</form>
</div>
);
};
3 changes: 3 additions & 0 deletions apps/extension/src/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { createTxApprovalSlice, TxApprovalSlice } from './tx-approval';
import { createOriginApprovalSlice, OriginApprovalSlice } from './origin-approval';
import { ConnectedSitesSlice, createConnectedSitesSlice } from './connected-sites';
import { createDefaultFrontendSlice, DefaultFrontendSlice } from './default-frontend';
import { createNumerairesSlice, NumerairesSlice } from './numeraires';

export interface AllSlices {
wallets: WalletsSlice;
password: PasswordSlice;
seedPhrase: SeedPhraseSlice;
network: NetworkSlice;
numeraires: NumerairesSlice;
txApproval: TxApprovalSlice;
originApproval: OriginApprovalSlice;
connectedSites: ConnectedSitesSlice;
Expand All @@ -41,6 +43,7 @@ export const initializeStore = (
password: createPasswordSlice(session, local)(setState, getState, store),
seedPhrase: createSeedPhraseSlice(setState, getState, store),
network: createNetworkSlice(local)(setState, getState, store),
numeraires: createNumerairesSlice(local)(setState, getState, store),
connectedSites: createConnectedSitesSlice(local)(setState, getState, store),
txApproval: createTxApprovalSlice()(setState, getState, store),
originApproval: createOriginApprovalSlice()(setState, getState, store),
Expand Down
8 changes: 8 additions & 0 deletions apps/extension/src/state/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { AllSlices, SliceCreator } from '.';
export interface NetworkSlice {
grpcEndpoint: string | undefined;
fullSyncHeight?: number;
chainId?: string;
setGRPCEndpoint: (endpoint: string) => Promise<void>;
setChainId: (chainId: string) => void;
}

export const createNetworkSlice =
Expand All @@ -14,13 +16,19 @@ export const createNetworkSlice =
return {
grpcEndpoint: undefined,
fullSyncHeight: undefined,
chainId: undefined,
setGRPCEndpoint: async (endpoint: string) => {
set(state => {
state.network.grpcEndpoint = endpoint;
});

await local.set('grpcEndpoint', endpoint);
},
setChainId: (chainId: string) => {
set(state => {
state.network.chainId = chainId;
});
},
};
};

Expand Down
34 changes: 34 additions & 0 deletions apps/extension/src/state/numeraires.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { LocalStorageState } from '../storage/types';
import { AllSlices, SliceCreator } from '.';
import { ExtensionStorage } from '../storage/base';
import { Stringified } from '@penumbra-zone/types/jsonified';
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

export interface NumerairesSlice {
selectedNumeraires: Stringified<AssetId>[];
selectNumeraire: (numeraire: Stringified<AssetId>) => void;
saveNumeraires: () => Promise<void>;
}

export const createNumerairesSlice =
(local: ExtensionStorage<LocalStorageState>): SliceCreator<NumerairesSlice> =>
(set, get) => {
return {
selectedNumeraires: [],
selectNumeraire: (numeraire: Stringified<AssetId>) => {
set(state => {
const index = state.numeraires.selectedNumeraires.indexOf(numeraire);
if (index > -1) {
state.numeraires.selectedNumeraires.splice(index, 1);
} else {
state.numeraires.selectedNumeraires.push(numeraire);
}
});
},
saveNumeraires: async () => {
await local.set('numeraires', get().numeraires.selectedNumeraires);
},
};
};

export const numerairesSelector = (state: AllSlices) => state.numeraires;
Loading

0 comments on commit 27d8552

Please sign in to comment.