From 499c0e5baa1ec0ec2e04dd4257e80b741e854d8d Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Thu, 2 May 2024 07:47:14 +0200 Subject: [PATCH] See interchain balances [part 3] (#1021) * Balance hooks * Working balances --- apps/extension/package.json | 2 +- apps/minifront/package.json | 2 +- .../src/components/ibc/ibc-in/asset-utils.tsx | 29 +++++ .../components/ibc/ibc-in/assets-table.tsx | 77 ++++++++++++ .../components/ibc/ibc-in/chain-dropdown.tsx | 2 +- .../ibc/ibc-in/cosmos-wallet-connector.tsx | 11 +- .../src/components/ibc/ibc-in/hooks.ts | 114 ++++++++++++++++++ .../src/components/ibc/ibc-in/ibc-in-form.tsx | 18 ++- .../components/ibc/ibc-in/interchain-ui.tsx | 7 +- .../ibc/ibc-in/wallet-connect-button.tsx | 3 +- apps/minifront/src/components/ibc/layout.tsx | 4 +- apps/minifront/src/state/ibc-in.ts | 2 + pnpm-lock.yaml | 29 +++-- 13 files changed, 268 insertions(+), 32 deletions(-) create mode 100644 apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx create mode 100644 apps/minifront/src/components/ibc/ibc-in/assets-table.tsx create mode 100644 apps/minifront/src/components/ibc/ibc-in/hooks.ts diff --git a/apps/extension/package.json b/apps/extension/package.json index 4948c70fe4..c91df8f070 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -35,7 +35,7 @@ "@penumbra-zone/types": "workspace:*", "@penumbra-zone/ui": "workspace:*", "@penumbra-zone/wasm": "workspace:*", - "@tanstack/react-query": "^5.28.9", + "@tanstack/react-query": "4.36.1", "buffer": "^6.0.3", "exponential-backoff": "^3.1.1", "framer-motion": "^11.0.22", diff --git a/apps/minifront/package.json b/apps/minifront/package.json index c030db0eb0..d5ca9c186a 100644 --- a/apps/minifront/package.json +++ b/apps/minifront/package.json @@ -34,7 +34,7 @@ "@penumbra-zone/types": "workspace:*", "@penumbra-zone/ui": "workspace:*", "@radix-ui/react-icons": "^1.3.0", - "@tanstack/react-query": "^5.28.9", + "@tanstack/react-query": "4.36.1", "bech32": "^2.0.0", "bignumber.js": "^9.1.2", "chain-registry": "^1.45.5", diff --git a/apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx b/apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx new file mode 100644 index 0000000000..c3775e72b9 --- /dev/null +++ b/apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx @@ -0,0 +1,29 @@ +import { assets as cosmosAssetList } from 'chain-registry'; +import { Coin } from 'osmo-query'; +import { Asset } from '@chain-registry/types'; +import { BigNumber } from 'bignumber.js'; + +// Searches for corresponding denom in asset registry and returns the metadata +export const augmentToAsset = (coin: Coin, chainName: string): Asset => { + const match = cosmosAssetList + .find(({ chain_name }) => chain_name === chainName) + ?.assets.find(asset => asset.base === coin.denom); + + return match ? match : fallbackAsset(coin); +}; + +const fallbackAsset = (coin: Coin): Asset => { + return { + base: coin.denom, + denom_units: [{ denom: coin.denom, exponent: 0 }], + display: coin.denom, + name: coin.denom, + symbol: coin.denom, + }; +}; + +// Helps us convert from say 41000000uosmo to the more readable 41osmo +export const rawToDisplayAmount = (asset: Asset, amount: string) => { + const displayUnit = asset.denom_units.find(({ denom }) => denom === asset.display)?.exponent ?? 0; + return new BigNumber(amount).shiftedBy(-displayUnit).toString(); +}; diff --git a/apps/minifront/src/components/ibc/ibc-in/assets-table.tsx b/apps/minifront/src/components/ibc/ibc-in/assets-table.tsx new file mode 100644 index 0000000000..4a2d0c11a6 --- /dev/null +++ b/apps/minifront/src/components/ibc/ibc-in/assets-table.tsx @@ -0,0 +1,77 @@ +import { useChainConnector, useCosmosChainBalances } from './hooks'; +import { useStore } from '../../../state'; +import { ibcInSelector } from '../../../state/ibc-in'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@penumbra-zone/ui/components/ui/table'; +import { Avatar, AvatarImage } from '@penumbra-zone/ui/components/ui/avatar'; +import { Identicon } from '@penumbra-zone/ui/components/ui/identicon'; +import { LineWave } from 'react-loader-spinner'; + +export const AssetsTable = () => { + const { address } = useChainConnector(); + const { selectedChain } = useStore(ibcInSelector); + const { data, isLoading, error } = useCosmosChainBalances(); + + // User has not connected their wallet yet + if (!address || !selectedChain) return <>; + + if (isLoading) { + return ( +
+ Loading balances... + +
+ ); + } + + if (error) { + return
{String(error)}
; + } + + return ( +
+
+ Balances on {selectedChain.label} +
+ + + + Denom + Amount + + + + {data?.length === 0 && noBalancesRow()} + {data?.map(b => { + return ( + + + + + + + {b.displayDenom} + + {b.displayAmount} + + ); + })} + +
+
+ ); +}; + +const noBalancesRow = () => { + return ( + + No balances + + ); +}; diff --git a/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx b/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx index 8d20ef047c..bd4c17c9ac 100644 --- a/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx +++ b/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx @@ -63,7 +63,7 @@ export const ChainDropdown = () => { {selected?.label} ) : ( - 'Select a chain' + 'Shield assets from' )} diff --git a/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx b/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx index 810e67de36..4002462fa6 100644 --- a/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx +++ b/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx @@ -1,16 +1,9 @@ import { useStore } from '../../../state'; import { ibcInSelector } from '../../../state/ibc-in'; -import { useChain, useManager } from '@cosmos-kit/react'; import { WalletStatus } from '@cosmos-kit/core'; import { WalletAddrCard } from './wallet-addr-card'; import { ConnectWalletButton } from './wallet-connect-button'; - -export const useChainConnector = () => { - const { selectedChain } = useStore(ibcInSelector); - const { chainRecords } = useManager(); - const defaultChain = chainRecords[0]!.name; - return useChain(selectedChain?.chainName ?? defaultChain); -}; +import { useChainConnector } from './hooks'; export const CosmosWalletConnector = () => { const { selectedChain } = useStore(ibcInSelector); @@ -18,10 +11,10 @@ export const CosmosWalletConnector = () => { return (
- {address && selectedChain && }
+ {address && selectedChain && } {(status === WalletStatus.Rejected || status === WalletStatus.Error) && (
{message}
)} diff --git a/apps/minifront/src/components/ibc/ibc-in/hooks.ts b/apps/minifront/src/components/ibc/ibc-in/hooks.ts new file mode 100644 index 0000000000..a840888b20 --- /dev/null +++ b/apps/minifront/src/components/ibc/ibc-in/hooks.ts @@ -0,0 +1,114 @@ +import { useStore } from '../../../state'; +import { ibcInSelector } from '../../../state/ibc-in'; +import { useChain, useManager } from '@cosmos-kit/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import { ProtobufRpcClient } from '@cosmjs/stargate'; +import { Coin, createRpcQueryHooks, useRpcClient, useRpcEndpoint } from 'osmo-query'; +import { augmentToAsset, rawToDisplayAmount } from './asset-utils'; + +// This is sad, but osmo-query's custom hooks require calling .toJSON() on all fields. +// This will throw an error for bigint, so needs to be added to the prototype. +declare global { + interface BigInt { + toJSON(): string; + } +} + +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + +export const useChainConnector = () => { + const { selectedChain } = useStore(ibcInSelector); + const { chainRecords } = useManager(); + const defaultChain = chainRecords[0]!.name; + return useChain(selectedChain?.chainName ?? defaultChain); +}; + +const useCosmosQueryHooks = () => { + const { address, getRpcEndpoint, chain } = useChainConnector(); + + const rpcEndpointQuery = useRpcEndpoint({ + getter: getRpcEndpoint, + options: { + enabled: !!address, + staleTime: Infinity, + queryKey: ['rpc endpoint', address, chain.chain_name], + // Needed for osmo-query's internal caching + queryKeyHashFn: queryKey => { + return JSON.stringify([...queryKey, chain.chain_name]); + }, + }, + }) as UseQueryResult; + + const rpcClientQuery = useRpcClient({ + rpcEndpoint: rpcEndpointQuery.data ?? '', + options: { + enabled: !!address && !!rpcEndpointQuery.data, + staleTime: Infinity, + queryKey: ['rpc client', address, rpcEndpointQuery.data, chain.chain_name], + // Needed for osmo-query's internal caching + queryKeyHashFn: queryKey => { + return JSON.stringify([...queryKey, chain.chain_name]); + }, + }, + }) as UseQueryResult; + + const { cosmos: cosmosQuery, osmosis: osmosisQuery } = createRpcQueryHooks({ + rpc: rpcClientQuery.data, + }); + + const isReady = !!address && !!rpcClientQuery.data; + const isFetching = rpcEndpointQuery.isFetching || rpcClientQuery.isFetching; + + return { cosmosQuery, osmosisQuery, isReady, isFetching, address, chain }; +}; + +interface BalancesResponse { + balances: Coin[]; + pagination: { nexKey: Uint8Array; total: bigint }; +} + +interface CosmosAssetBalance { + raw: Coin; + displayDenom: string; + displayAmount: string; + icon?: string; +} + +interface UseCosmosChainBalancesRes { + data?: CosmosAssetBalance[]; + isLoading: boolean; + error: unknown; +} + +export const useCosmosChainBalances = (): UseCosmosChainBalancesRes => { + const { address, cosmosQuery, isReady, chain } = useCosmosQueryHooks(); + + const { data, isLoading, error } = cosmosQuery.bank.v1beta1.useAllBalances({ + request: { + address: address ?? '', + pagination: { + offset: 0n, + limit: 100n, + key: new Uint8Array(), + countTotal: true, + reverse: false, + }, + }, + options: { + enabled: isReady, + }, + }) as UseQueryResult; + + const augmentedAssets = data?.balances.map(coin => { + const asset = augmentToAsset(coin, chain.chain_name); + return { + raw: coin, + displayDenom: asset.display, + displayAmount: rawToDisplayAmount(asset, coin.amount), + icon: asset.logo_URIs?.svg ?? asset.logo_URIs?.png, + }; + }); + return { data: augmentedAssets, isLoading, error }; +}; diff --git a/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx b/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx index 7a185f9f08..68576a36c8 100644 --- a/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx +++ b/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx @@ -1,8 +1,12 @@ import { Button } from '@penumbra-zone/ui/components/ui/button'; import { LockClosedIcon } from '@radix-ui/react-icons'; import { InterchainUi } from './interchain-ui'; +import { useStore } from '../../../state'; +import { ibcInSelector } from '../../../state/ibc-in'; export const IbcInForm = () => { + const { ready } = useStore(ibcInSelector); + return (
{ }} > - + {ready && ( + + )} ); }; diff --git a/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx b/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx index 97a21dbce5..694ae6e82f 100644 --- a/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx +++ b/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx @@ -2,9 +2,13 @@ import { IbcChainProvider } from './chain-provider'; import { useRegistry } from '../../../fetchers/registry'; import { ChainDropdown } from './chain-dropdown'; import { CosmosWalletConnector } from './cosmos-wallet-connector'; +import { useStore } from '../../../state'; +import { ibcInSelector } from '../../../state/ibc-in'; +import { AssetsTable } from './assets-table'; export const InterchainUi = () => { const { data, isLoading, error } = useRegistry(); + const { selectedChain } = useStore(ibcInSelector); if (isLoading) return
Loading registry...
; if (error) return
Error trying to load registry!
; @@ -16,7 +20,8 @@ export const InterchainUi = () => {
- + {selectedChain && } + ); }; diff --git a/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx b/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx index a4e51d4855..477bb83c66 100644 --- a/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx +++ b/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx @@ -4,7 +4,8 @@ import { WalletIcon } from '@penumbra-zone/ui/components/ui/icons/wallet'; import { MouseEventHandler } from 'react'; import { useStore } from '../../../state'; import { ibcInSelector } from '../../../state/ibc-in'; -import { useChainConnector } from './cosmos-wallet-connector'; + +import { useChainConnector } from './hooks'; export const ConnectWalletButton = () => { const { connect, openView, status } = useChainConnector(); diff --git a/apps/minifront/src/components/ibc/layout.tsx b/apps/minifront/src/components/ibc/layout.tsx index 1e7bbdcd50..728628471f 100644 --- a/apps/minifront/src/components/ibc/layout.tsx +++ b/apps/minifront/src/components/ibc/layout.tsx @@ -13,7 +13,7 @@ export const IbcLayout = () => { direction='right' // Negative calculated margin giving lint issue /* eslint-disable-next-line tailwindcss/enforces-negative-arbitrary-values */ - className='invisible absolute -top-44 right-0 z-0 -mr-[calc(30vw-3px)] size-[30vw] text-stone-300 md:visible' + className='invisible absolute -top-32 right-0 z-0 -mr-80 size-80 text-stone-300 md:visible' /> @@ -22,7 +22,7 @@ export const IbcLayout = () => { direction='left' // Negative calculated margin giving lint issue /* eslint-disable-next-line tailwindcss/enforces-negative-arbitrary-values */ - className='invisible absolute -bottom-44 left-0 z-0 my-auto -ml-[calc(30vw-3px)] size-[30vw] text-stone-700 md:visible' + className='invisible absolute -bottom-32 left-0 z-0 my-auto -ml-80 size-80 text-stone-700 md:visible' /> diff --git a/apps/minifront/src/state/ibc-in.ts b/apps/minifront/src/state/ibc-in.ts index 91f3113f50..14c47fedff 100644 --- a/apps/minifront/src/state/ibc-in.ts +++ b/apps/minifront/src/state/ibc-in.ts @@ -4,10 +4,12 @@ import { ChainInfo } from '../components/ibc/ibc-in/chain-dropdown'; export interface IbcInSlice { selectedChain?: ChainInfo; setSelectedChain: (chain?: ChainInfo) => void; + ready: boolean; } export const createIbcInSlice = (): SliceCreator => set => { return { + ready: false, selectedChain: undefined, setSelectedChain: chain => { set(state => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65644311c4..88cb67838b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,8 +226,8 @@ importers: specifier: workspace:* version: link:../../packages/wasm '@tanstack/react-query': - specifier: ^5.28.9 - version: 5.32.0(react@18.3.1) + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1)(react@18.3.1) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -413,8 +413,8 @@ importers: specifier: ^1.3.0 version: 1.3.0(react@18.3.1) '@tanstack/react-query': - specifier: ^5.28.9 - version: 5.32.0(react@18.3.1) + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1)(react@18.3.1) bech32: specifier: ^2.0.0 version: 2.0.0 @@ -10174,17 +10174,26 @@ packages: '@swc/counter': 0.1.3 dev: true - /@tanstack/query-core@5.32.0: - resolution: {integrity: sha512-Z3flEgCat55DRXU5UMwYU1U+DgFZKA3iufyOKs+II7iRAo0uXkeU7PH5e6sOH1CGEag0IpKmZxlUFpCg6roSKw==} + /@tanstack/query-core@4.36.1: + resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} dev: false - /@tanstack/react-query@5.32.0(react@18.3.1): - resolution: {integrity: sha512-+E3UudQtarnx9A6xhpgMZapyF+aJfNBGFMgI459FnduEZqT/9KhOWnMOneZahLRt52yzskSA0AuOyLkXHK0yBA==} + /@tanstack/react-query@4.36.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} peerDependencies: - react: ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true dependencies: - '@tanstack/query-core': 5.32.0 + '@tanstack/query-core': 4.36.1 react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.2.0(react@18.3.1) dev: false /@terra-money/feather.js@1.2.1: