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 (
);
};
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: