Skip to content

Commit

Permalink
See interchain balances [part 3] (#1021)
Browse files Browse the repository at this point in the history
* Balance hooks

* Working balances
  • Loading branch information
grod220 authored May 2, 2024
1 parent 926dc12 commit 499c0e5
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 32 deletions.
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/minifront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx
Original file line number Diff line number Diff line change
@@ -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();
};
77 changes: 77 additions & 0 deletions apps/minifront/src/components/ibc/ibc-in/assets-table.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='flex justify-center text-stone-700'>
<span className='text-purple-700'>Loading balances...</span>
<LineWave visible={true} height='70' width='70' color='#7e22ce' wrapperClass='-mt-9' />
</div>
);
}

if (error) {
return <div className='flex justify-center italic text-red-700'>{String(error)}</div>;
}

return (
<div className='text-stone-700'>
<div className='flex justify-center italic text-stone-400'>
Balances on {selectedChain.label}
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead className='w-[100px]'>Denom</TableHead>
<TableHead className='text-right'>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.length === 0 && noBalancesRow()}
{data?.map(b => {
return (
<TableRow key={b.displayDenom}>
<TableCell className='flex gap-2'>
<Avatar className='size-6'>
<AvatarImage src={b.icon} />
<Identicon uniqueIdentifier={b.displayDenom} type='gradient' size={22} />
</Avatar>
<span className='max-w-[200px] truncate'>{b.displayDenom}</span>
</TableCell>
<TableCell className='text-right'>{b.displayAmount}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
};

const noBalancesRow = () => {
return (
<TableRow>
<TableCell className='italic'>No balances</TableCell>
</TableRow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const ChainDropdown = () => {
<span className='mt-0.5'>{selected?.label}</span>
</div>
) : (
'Select a chain'
'Shield assets from'
)}
<ChevronsUpDown className='ml-2 size-4 shrink-0 opacity-50' />
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
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);
const { username, address, status, message } = useChainConnector();

return (
<div className='flex flex-col items-center justify-center gap-4'>
{address && selectedChain && <WalletAddrCard username={username} address={address} />}
<div className='w-52'>
<ConnectWalletButton />
</div>
{address && selectedChain && <WalletAddrCard username={username} address={address} />}
{(status === WalletStatus.Rejected || status === WalletStatus.Error) && (
<div className='text-purple-500'>{message}</div>
)}
Expand Down
114 changes: 114 additions & 0 deletions apps/minifront/src/components/ibc/ibc-in/hooks.ts
Original file line number Diff line number Diff line change
@@ -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<string>;

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<ProtobufRpcClient>;

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<BalancesResponse>;

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 };
};
18 changes: 12 additions & 6 deletions apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<form
className='flex w-full flex-col gap-4 md:w-[340px] xl:w-[450px]'
Expand All @@ -11,12 +15,14 @@ export const IbcInForm = () => {
}}
>
<InterchainUi />
<Button type='submit' variant='onLight' disabled>
<div className='flex items-center gap-2'>
<LockClosedIcon />
<span className='-mb-1'>Shield Assets</span>
</div>
</Button>
{ready && (
<Button type='submit' variant='onLight' disabled>
<div className='flex items-center gap-2'>
<LockClosedIcon />
<span className='-mb-1'>Shield Assets</span>
</div>
</Button>
)}
</form>
);
};
7 changes: 6 additions & 1 deletion apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div>Loading registry...</div>;
if (error) return <div>Error trying to load registry!</div>;
Expand All @@ -16,7 +20,8 @@ export const InterchainUi = () => {
<div className='-mt-4 flex justify-center'>
<ChainDropdown />
</div>
<CosmosWalletConnector />
{selectedChain && <CosmosWalletConnector />}
<AssetsTable />
</IbcChainProvider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions apps/minifront/src/components/ibc/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
/>
<IbcInForm />
</Card>
Expand All @@ -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'
/>
<IbcOutForm />
</Card>
Expand Down
2 changes: 2 additions & 0 deletions apps/minifront/src/state/ibc-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IbcInSlice> => set => {
return {
ready: false,
selectedChain: undefined,
setSelectedChain: chain => {
set(state => {
Expand Down
Loading

0 comments on commit 499c0e5

Please sign in to comment.