diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx b/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx index 5b84ba9..04b2bab 100644 --- a/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx +++ b/react-native-expo/components/PortfolioScreen/BalanceList/AssetActionSheet/index.tsx @@ -5,6 +5,23 @@ 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 @@ -12,8 +29,9 @@ import useTransactionsForAsset from './useTransactionsForAsset'; */ 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); @@ -28,11 +46,12 @@ export default function AssetActionSheet() { - - {balance?.amount} {balance?.assetSymbol} - - - {balance?.equivalentValue} USDC + {balanceView && ( + + {getFormattedAmtFromValueView(balanceView)} + {selectedAssetSymbol} + + )} diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts index 4fb39db..919077d 100644 --- a/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts +++ b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.stories.ts @@ -1,13 +1,13 @@ import { Meta, StoryObj } from '@storybook/react'; import Balance from '.'; -import balanceFactory from '@/factories/balance'; +import balancesResponseFactory from '@/factories/balancesResponse'; const meta: Meta = { component: Balance, tags: ['autodocs'], argTypes: { - balance: { control: false }, + balancesResponse: { control: false }, }, }; @@ -15,6 +15,6 @@ export default meta; export const Basic: StoryObj = { args: { - balance: balanceFactory.build(), + balancesResponse: balancesResponseFactory.build(), }, }; diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx index 7739643..c8abeda 100644 --- a/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx +++ b/react-native-expo/components/PortfolioScreen/BalanceList/Balance/index.tsx @@ -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 ( } - primaryText={balance.assetSymbol} - secondaryText={balance.assetName} + primaryText={symbol} + secondaryText={displayDenom} suffix={ - {balance.amount} - {balance.equivalentValue} USDC + {formattedAmount} + {/* {balancesResponse.equivalentValue} USDC */} } - onPress={() => dispatch(setSelectedAssetSymbol(balance.assetSymbol))} + onPress={() => dispatch(setSelectedAssetSymbol(symbol))} /> ); } diff --git a/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx b/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx index fc06416..28f305f 100644 --- a/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx +++ b/react-native-expo/components/PortfolioScreen/BalanceList/index.tsx @@ -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 ( <> - {balances.map(balance => ( - + {balancesResponses.map(balancesResponse => ( + ))} diff --git a/react-native-expo/components/PortfolioScreen/index.tsx b/react-native-expo/components/PortfolioScreen/index.tsx index eedd2aa..1c96d8b 100644 --- a/react-native-expo/components/PortfolioScreen/index.tsx +++ b/react-native-expo/components/PortfolioScreen/index.tsx @@ -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 ( - - {/** @todo: Make this a `ScrollView`. */} + - - + + ); } diff --git a/react-native-expo/components/ProfileScreen/DefaultPaymentTokenScreen/useFilteredAssets.ts b/react-native-expo/components/ProfileScreen/DefaultPaymentTokenScreen/useFilteredAssets.ts index 1ea08d6..00cdda5 100644 --- a/react-native-expo/components/ProfileScreen/DefaultPaymentTokenScreen/useFilteredAssets.ts +++ b/react-native-expo/components/ProfileScreen/DefaultPaymentTokenScreen/useFilteredAssets.ts @@ -19,18 +19,18 @@ const ALL_METADATAS = new ChainRegistryClient().bundled.get(PENUMBRA_CHAIN_ID).g export default function useFilteredAssets() { const searchText = useAppSelector(state => state.defaultPaymentTokenScreen.searchText); - const filteredAssets = useMemo( - () => - ALL_METADATAS.filter(metadata => { - const searchTextLowerCase = searchText.toLocaleLowerCase(); + const filteredAssets = useMemo(() => { + if (!searchText.trim().length) return ALL_METADATAS; - if (metadata.name.toLocaleLowerCase().includes(searchTextLowerCase)) return true; - if (metadata.symbol.toLocaleLowerCase().includes(searchTextLowerCase)) return true; + return ALL_METADATAS.filter(metadata => { + const searchTextLowerCase = searchText.toLocaleLowerCase(); - return false; - }), - [searchText], - ); + if (metadata.name.toLocaleLowerCase().includes(searchTextLowerCase)) return true; + if (metadata.symbol.toLocaleLowerCase().includes(searchTextLowerCase)) return true; + + return false; + }); + }, [searchText]); return filteredAssets; } diff --git a/react-native-expo/factories/amount.ts b/react-native-expo/factories/amount.ts new file mode 100644 index 0000000..310ed22 --- /dev/null +++ b/react-native-expo/factories/amount.ts @@ -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>({ + hi: 0n, + lo: randomBigInt(), +}); + +export default amountFactory; diff --git a/react-native-expo/factories/balance.ts b/react-native-expo/factories/balance.ts deleted file mode 100644 index 8f25b4d..0000000 --- a/react-native-expo/factories/balance.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Balance from '@/types/Balance'; -import * as Factory from 'factory.ts'; -import ASSETS from './mockData/assets'; - -const randomDecimalWith6DecimalPlaces = () => Math.round(Math.random() * 10 ** 9) / 10 ** 3; - -const balanceFactory = Factory.makeFactory({ - amount: Factory.each(randomDecimalWith6DecimalPlaces), - equivalentValue: Factory.each(randomDecimalWith6DecimalPlaces), - assetSymbol: Factory.each(i => ASSETS[i % ASSETS.length].symbol), - assetName: Factory.each(i => ASSETS[i % ASSETS.length].name), -}); - -export default balanceFactory; diff --git a/react-native-expo/factories/balancesResponse.ts b/react-native-expo/factories/balancesResponse.ts new file mode 100644 index 0000000..672e12c --- /dev/null +++ b/react-native-expo/factories/balancesResponse.ts @@ -0,0 +1,23 @@ +import * as Factory from 'factory.ts'; +import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; +import { ChainRegistryClient } from '@penumbra-labs/registry'; +import { PENUMBRA_CHAIN_ID } from '@/utils/constants'; +import { PlainMessage } from '@bufbuild/protobuf'; +import amountFactory from './amount'; + +const METADATAS = new ChainRegistryClient().bundled.get(PENUMBRA_CHAIN_ID).getAllAssets(); + +const balancesResponseFactory = Factory.makeFactory>({ + balanceView: Factory.each(i => ({ + valueView: { + case: 'knownAssetId', + value: { + amount: amountFactory.build(), + equivalentValues: [], + metadata: METADATAS[i % METADATAS.length], + }, + }, + })), +}); + +export default balancesResponseFactory; diff --git a/react-native-expo/metro.config.cjs b/react-native-expo/metro.config.cjs index 52860ac..fa7e6dd 100644 --- a/react-native-expo/metro.config.cjs +++ b/react-native-expo/metro.config.cjs @@ -17,6 +17,20 @@ const ALIASES = { * @todo: Fix this! */ '@penumbra-zone/bech32m/penumbra': '@penumbra-zone/bech32m/dist/penumbra', + /** + * Same issue here + */ + '@penumbra-zone/bech32m/passet': '@penumbra-zone/bech32m/dist/passet', + '@penumbra-zone/protobuf/penumbra/view/v1/view_pb': + '@penumbra-zone/protobuf/dist/gen/penumbra/view/v1/view_pb', + '@penumbra-zone/protobuf/penumbra/core/component/stake/v1/stake_pb': + '@penumbra-zone/protobuf/dist/gen/penumbra/core/component/stake/v1/stake_pb', + '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb': + '@penumbra-zone/protobuf/dist/gen/penumbra/core/num/v1/num_pb', + '@penumbra-zone/getters/metadata': '@penumbra-zone/getters/dist/metadata.js', + '@penumbra-zone/getters/value-view': '@penumbra-zone/getters/dist/value-view.js', + '@penumbra-zone/types/value-view': '@penumbra-zone/types/dist/value-view.js', + '@penumbra-zone/getters/balances-response': '@penumbra-zone/getters/dist/balances-response.js', }; config.resolver.resolveRequest = (context, moduleName, platform) => { diff --git a/react-native-expo/package.json b/react-native-expo/package.json index 16a07bf..b74981f 100644 --- a/react-native-expo/package.json +++ b/react-native-expo/package.json @@ -67,7 +67,8 @@ "react-native-webview": "13.12.2", "react-redux": "^9.2.0", "redux-persist": "^6.0.0", - "redux-persist-expo-securestore": "^2.0.0" + "redux-persist-expo-securestore": "^2.0.0", + "reselect": "^5.1.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/react-native-expo/store/balances.ts b/react-native-expo/store/balances.ts index 81e2358..7941694 100644 --- a/react-native-expo/store/balances.ts +++ b/react-native-expo/store/balances.ts @@ -1,14 +1,19 @@ -import balanceFactory from '@/factories/balance'; -import Balance from '@/types/Balance'; +import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; +import balancesResponseFactory from '@/factories/balancesResponse'; import { createSlice } from '@reduxjs/toolkit'; +import { PlainMessage } from '@bufbuild/protobuf'; +import { ChainRegistryClient } from '@penumbra-labs/registry'; +import { PENUMBRA_CHAIN_ID } from '@/utils/constants'; + +const METADATAS = new ChainRegistryClient().bundled.get(PENUMBRA_CHAIN_ID).getAllAssets(); export interface BalancesState { - balances: Balance[]; + balancesResponses: PlainMessage[]; } const initialState: BalancesState = { /** @todo: Populate with real data */ - balances: balanceFactory.buildList(5), + balancesResponses: balancesResponseFactory.buildList(METADATAS.length), }; export const balancesSlice = createSlice({ @@ -17,6 +22,4 @@ export const balancesSlice = createSlice({ reducers: {}, }); -export const {} = balancesSlice.actions; - export default balancesSlice.reducer; diff --git a/react-native-expo/yarn.lock b/react-native-expo/yarn.lock index 13a8597..c3380af 100644 --- a/react-native-expo/yarn.lock +++ b/react-native-expo/yarn.lock @@ -9441,7 +9441,7 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reselect@^5.1.0: +reselect@^5.1.0, reselect@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==