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

Start using Protobuf types #19

Draft
wants to merge 9 commits into
base: jessepinho/profile-screen
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions react-native-expo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,9 @@ Note that we use secure storage regardless of the sensitivity of the data. So, b
If you're developing a screen that sets data in secure storage but also sets temporary state data that shouldn't be persisted, the pattern we use is to create a separate slice for that screen that only contains the temporary data. For example, `<DefaultPaymentTokenScreen />` uses the `defaultPaymentTokenScreen` slice to maintain its screen state (such as, for example, the contents of the text field on the screen) and the `secureStore` slice to store the user's preferred payment token.

The slice can be consumed and updated just like any other slice (via `useAppSelector()` and `useAppDispatch()`), and can have its shape modified just like any other slice (in `store/secureStore.ts`). The persistence is accomplished via [`redux-persist`](https://github.com/rt2zz/redux-persist) and [redux-persist-expo-securestore](https://github.com/Cretezy/redux-persist-expo-securestore), using [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore/) under the hood. See `store/index.ts` to see how it's configured.

## Weird issues/gotchas

### Importing `@penumbra-zone/*` packages
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't think this is a solution at all, and this PR probably shouldn't be merged in its present state, but I'll leave that up to y'all to decide. I couldn't get the package imports to work without these metro.config changes (along with the tsconfig/package.json changes).


For some reason, although TypeScript and Storybook understand imports from `@penumbra-zone/...`, Expo often does not, and expects the import to come from e.g., inside `dist/`. This should be fixed; but in the meantime, `metro.config.cjs` temporarily patches these imports using an alias.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,33 @@ import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setSelectedAssetSymbol } from '@/store/portfolioScreen';
import { Sx, Text, View } from 'dripsy';
import useTransactionsForAsset from './useTransactionsForAsset';
import { getBalanceView } from '@penumbra-zone/getters/balances-response';
import { getSymbolFromValueView } from '@penumbra-zone/getters/value-view';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { createSelector } from 'reselect';
import { RootState } from '@/store';
import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view';

const balanceSelector = (selectedAssetSymbol?: string) =>
createSelector(
[(state: RootState) => state.balances.balancesResponses, () => selectedAssetSymbol],
balancesResponses =>
balancesResponses.find(
balancesResponse =>
getBalanceView.pipe(getSymbolFromValueView)(new BalancesResponse(balancesResponse)) ===
selectedAssetSymbol,
),
);

/**
* An action sheet for a given asset, with a list of relevant transactions and
* buttons for actions related to the asset (such as sending).
*/
export default function AssetActionSheet() {
const selectedAssetSymbol = useAppSelector(state => state.portfolioScreen.selectedAssetSymbol);
const balance = useAppSelector(state =>
state.balances.balances.find(balance => balance.assetSymbol === selectedAssetSymbol),
const balancesResponse = useAppSelector(balanceSelector(selectedAssetSymbol));
const balanceView = getBalanceView.optional(
balancesResponse ? new BalancesResponse(balancesResponse) : undefined,
);
const dispatch = useAppDispatch();
const transactions = useTransactionsForAsset(selectedAssetSymbol);
Expand All @@ -28,11 +46,12 @@ export default function AssetActionSheet() {
<AssetIcon />
</View>

<Text sx={sx.balance}>
{balance?.amount} {balance?.assetSymbol}
</Text>

<Text sx={sx.equivalentValue}>{balance?.equivalentValue} USDC</Text>
{balanceView && (
<Text sx={sx.balance}>
{getFormattedAmtFromValueView(balanceView)}
{selectedAssetSymbol}
</Text>
)}
</View>

<TransactionList transactions={transactions} showTitle />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Meta, StoryObj } from '@storybook/react';

import Balance from '.';
import balanceFactory from '@/factories/balance';
import balancesResponseFactory from '@/factories/balancesResponse';

const meta: Meta<typeof Balance> = {
component: Balance,
tags: ['autodocs'],
argTypes: {
balance: { control: false },
balancesResponse: { control: false },
},
};

export default meta;

export const Basic: StoryObj<typeof Balance> = {
args: {
balance: balanceFactory.build(),
balancesResponse: balancesResponseFactory.build(),
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,35 @@ import AssetIcon from '@/components/AssetIcon';
import ListItem from '@/components/ListItem';
import { useAppDispatch } from '@/store/hooks';
import { setSelectedAssetSymbol } from '@/store/portfolioScreen';
import IBalance from '@/types/Balance';
import { getBalanceView } from '@penumbra-zone/getters/balances-response';
import { getSymbolFromValueView, getDisplayDenomFromView } from '@penumbra-zone/getters/value-view';
import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { Sx, Text, View } from 'dripsy';

export interface BalanceProps {
balance: IBalance;
balancesResponse: BalancesResponse;
}

export default function Balance({ balance }: BalanceProps) {
export default function Balance({ balancesResponse }: BalanceProps) {
const dispatch = useAppDispatch();
const balanceView = getBalanceView(balancesResponse);
const symbol = getSymbolFromValueView(balanceView);
const displayDenom = getDisplayDenomFromView(balanceView);
const formattedAmount = getFormattedAmtFromValueView(balanceView);

return (
<ListItem
avatar={<AssetIcon />}
primaryText={balance.assetSymbol}
secondaryText={balance.assetName}
primaryText={symbol}
secondaryText={displayDenom}
suffix={
<View sx={sx.suffix}>
<Text variant='small'>{balance.amount}</Text>
<Text sx={sx.equivalentValue}>{balance.equivalentValue} USDC</Text>
<Text variant='small'>{formattedAmount}</Text>
{/* <Text sx={sx.equivalentValue}>{balancesResponse.equivalentValue} USDC</Text> */}
</View>
}
onPress={() => dispatch(setSelectedAssetSymbol(balance.assetSymbol))}
onPress={() => dispatch(setSelectedAssetSymbol(symbol))}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import List from '@/components/List';
import IBalance from '@/types/Balance';
import { useLingui } from '@lingui/react/macro';
import Balance from './Balance';
import AssetActionSheet from './AssetActionSheet';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { getSymbolFromValueView } from '@penumbra-zone/getters/value-view';

export interface BalanceListProps {
balances: IBalance[];
balancesResponses: BalancesResponse[];
}

/** Shows a list of the user's balances in every asset they hold. */
export default function BalanceList({ balances }: BalanceListProps) {
export default function BalanceList({ balancesResponses }: BalanceListProps) {
const { t } = useLingui();

return (
<>
<List title={t`Assets`}>
{balances.map(balance => (
<Balance key={balance.assetSymbol} balance={balance} />
{balancesResponses.map(balancesResponse => (
<Balance
key={getSymbolFromValueView(balancesResponse.balanceView)}
balancesResponse={balancesResponse}
/>
))}
</List>

Expand Down
20 changes: 14 additions & 6 deletions react-native-expo/components/PortfolioScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Sx, View } from 'dripsy';
import { ScrollView, Sx } from 'dripsy';
import BalanceAndActions from '../BalanceAndActions';
import BalanceList from './BalanceList';
import { useAppSelector } from '@/store/hooks';
import { createSelector } from 'reselect';
import { RootState } from '@/store';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';

const balancesResponsesSelector = createSelector(
[(state: RootState) => state.balances.balancesResponses],
balancesResponses =>
balancesResponses.map(balancesResponse => new BalancesResponse(balancesResponse)),
);

export default function PortfolioScreen() {
const balances = useAppSelector(state => state.balances.balances);
const balancesResponses = useAppSelector(balancesResponsesSelector);

return (
<View sx={sx.root}>
{/** @todo: Make this a `ScrollView`. */}
<ScrollView contentContainerSx={sx.root}>
<BalanceAndActions />

<BalanceList balances={balances} />
</View>
<BalanceList balancesResponses={balancesResponses} />
</ScrollView>
);
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { setSearchText } from '@/store/defaultPaymentTokenScreen';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setDefaultPaymentToken } from '@/store/secureStore';
import { Trans, useLingui } from '@lingui/react/macro';
import { Sx, Text, View } from 'dripsy';
import { ScrollView, Sx, Text } from 'dripsy';
import { Check, Search } from 'lucide-react-native';
import useFilteredAssets from './useFilteredAssets';
import TextInputIconStartAdornment from '@/components/TextInputIconStartAdornment';
Expand All @@ -20,7 +20,7 @@ export default function DefaultPaymentTokenScreen() {
const { t } = useLingui();

return (
<View sx={sx.root}>
<ScrollView contentContainerSx={sx.root}>
<Text variant='h4'>
<Trans>Default payment token</Trans>
</Text>
Expand Down Expand Up @@ -49,7 +49,7 @@ export default function DefaultPaymentTokenScreen() {
/>
))}
</List>
</View>
</ScrollView>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useAppSelector } from '@/store/hooks';
import ASSETS from './assets';
import { PENUMBRA_CHAIN_ID } from '@/utils/constants';
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { useMemo } from 'react';

/**
* @todo: Use remote client? (To avoid having to update the app just to get the
* latest metadatas.)
*/
const ALL_METADATAS = new ChainRegistryClient().bundled.get(PENUMBRA_CHAIN_ID).getAllAssets();

/**
* Returns asset types filtered by the search text from state.
*
Expand All @@ -12,18 +19,18 @@ import { useMemo } from 'react';
export default function useFilteredAssets() {
const searchText = useAppSelector(state => state.defaultPaymentTokenScreen.searchText);

const filteredAssets = useMemo(
() =>
ASSETS.filter(asset => {
const searchTextLowerCase = searchText.toLocaleLowerCase();
const filteredAssets = useMemo(() => {
if (!searchText.trim().length) return ALL_METADATAS;

return ALL_METADATAS.filter(metadata => {
const searchTextLowerCase = searchText.toLocaleLowerCase();

if (asset.name.toLocaleLowerCase().includes(searchTextLowerCase)) return true;
if (asset.symbol.toLocaleLowerCase().includes(searchTextLowerCase)) return true;
if (metadata.name.toLocaleLowerCase().includes(searchTextLowerCase)) return true;
if (metadata.symbol.toLocaleLowerCase().includes(searchTextLowerCase)) return true;

return false;
}),
[searchText],
);
return false;
});
}, [searchText]);

return filteredAssets;
}
7 changes: 5 additions & 2 deletions react-native-expo/components/ProfileScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import penumbraAddressFactory from '@/factories/penumbraAddress';
import addressFactory from '@/factories/address';
import Avatar from '../Avatar';
import { Sx, Text, View } from 'dripsy';
import Button from '../Button';
Expand All @@ -9,6 +9,9 @@ import AssetIcon from '../AssetIcon';
import ListItemChevronRightSuffix from '../ListItemChevronRightSuffix';
import { useRouter } from 'expo-router';
import { useAppSelector } from '@/store/hooks';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';

const mockAddress = bech32mAddress(addressFactory.build());

export default function ProfileScreen() {
const { t } = useLingui();
Expand All @@ -22,7 +25,7 @@ export default function ProfileScreen() {
<Avatar size='lg' />

<Text sx={sx.address} ellipsizeMode='middle' numberOfLines={1}>
{penumbraAddressFactory.build().value}
{mockAddress}
</Text>

<View sx={sx.buttons}>
Expand Down
8 changes: 4 additions & 4 deletions react-native-expo/components/TransactionList/index.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';

import TransactionList from '.';
import transactionFactory from '@/factories/transaction';
import penumbraAddressFactory from '@/factories/penumbraAddress';
import addressFactory from '@/factories/Address';

const meta: Meta<typeof TransactionList> = {
component: TransactionList,
Expand All @@ -20,18 +20,18 @@ export const Basic: StoryObj<typeof TransactionList> = {
showTitle: true,
transactions: [
transactionFactory.build({
senderAddress: penumbraAddressFactory.build().value,
senderAddress: addressFactory.build().value,
senderUsername: 'henry',
}),
transactionFactory.build({
type: 'send',
recipientAddress: penumbraAddressFactory.build().value,
recipientAddress: addressFactory.build().value,
recipientUsername: 'cate',
via: undefined,
}),
transactionFactory.build({
type: 'send',
recipientAddress: penumbraAddressFactory.build().value,
recipientAddress: addressFactory.build().value,
}),
],
},
Expand Down
13 changes: 13 additions & 0 deletions react-native-expo/factories/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PlainMessage } from '@bufbuild/protobuf';
import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import * as Factory from 'factory.ts';
import { generateRandomNumberBetween0And255 } from './helpers';

const addressFactory = Factory.makeFactory<PlainMessage<Address>>({
altBech32m: '',
inner: Factory.each(
() => new Uint8Array(Array(80).fill(null).map(generateRandomNumberBetween0And255)),
),
});

export default addressFactory;
12 changes: 12 additions & 0 deletions react-native-expo/factories/amount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Factory from 'factory.ts';
import { PlainMessage } from '@bufbuild/protobuf';
import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb';

const randomBigInt = () => BigInt(Math.round(Math.random() * 10 ** 7));

const amountFactory = Factory.makeFactory<PlainMessage<Amount>>({
hi: 0n,
lo: randomBigInt(),
});

export default amountFactory;
14 changes: 0 additions & 14 deletions react-native-expo/factories/balance.ts

This file was deleted.

Loading