diff --git a/src/components/cards/AssetsTableCard.tsx b/src/components/cards/AssetsTableCard.tsx index 4fa727ca2..dac314e22 100644 --- a/src/components/cards/AssetsTableCard.tsx +++ b/src/components/cards/AssetsTableCard.tsx @@ -191,6 +191,11 @@ function AssetRow({ const { data: trace } = useDenomTrace(denom); const { data: token, isValidating } = useToken(denom); const { data: nativeChain } = useNativeChain(); + const singleHopIbcCounterParty = + token?.traces && + token.traces.length === 1 && + token.traces.at(0)?.type === 'ibc' && + token.traces.at(0)?.counterparty; return token ? ( @@ -237,7 +242,7 @@ function AssetRow({ {token.chain.chain_id !== nativeChain.chain_id && ( // disable buttons if there is no known path to bridge them here -
+
{ + return from + ? relatedChainsClient + ?.getChainUtil(from.chain.chain_name) + .getAssetByDenom(getBaseDenom(from)) + : nativeChain && + to && + relatedChainsClient + ?.getChainUtil(nativeChain.chain_name) + .getAssetByDenom(to.base); + }, [from, nativeChain, relatedChainsClient, to]); + + // find the channel information on the from side for the bridge request + const { data: channelInfo } = useSingleHopChannelInfo( + chainFrom, + chainTo, + token + ); + + const { + data: chainClientStatusFrom, + isLoading: chainClientStatusFromIsLoading, + } = useSingleHopChannelStatus( + chainFrom, + useSingleHopChannelInfo(chainFrom, chainTo, token).data?.client_id + ); + const { data: chainClientStatusTo, isLoading: chainClientStatusToIsLoading } = + useSingleHopChannelStatus( + chainTo, + useSingleHopChannelInfo(chainTo, chainFrom, token).data?.client_id + ); const { wallet } = useWeb3(); const [{ isValidating: isValidatingBridgeTokens }, sendRequest] = useBridge( + // add chain source details chainFrom, + chainAddressFrom, + from ? getBaseDenom(from) : to?.base, + // add chain destination details chainTo ); const bridgeTokens = useCallback>( @@ -94,18 +131,28 @@ export default function BridgeCard({ const timeoutTimestamp = Long.fromNumber( Date.now() + defaultTimeout // calculate in ms then convert to nanoseconds ).multiply(1 / nanoseconds); - const ibcTransferInfo = ibcOpenTransfers?.find((transfer) => { - return transfer.chain.chain_id === chainTo.chain_id; - }); - if (!ibcTransferInfo) { + if (!channelInfo) { throw new Error( `IBC transfer path (${chainFrom.chain_id} -> ${chainTo.chain_id}) not found` ); } - const connectionLength = ibcTransferInfo.channel.connection_hops.length; - if (connectionLength !== 1) { + // future: can check both sides of the chain to see if they have IBC + // - send_enabled + // - receive_enabled + // by querying each chain with: /ibc/apps/transfer/v1/params + // (this may be redundant as we know there is an IBC connection already) + if (chainClientStatusFrom?.status !== 'Active') { throw new Error( - `Multi-hop IBC transfer paths not supported: ${connectionLength} connection hops` + `The connection source client is not active. Current status: ${ + chainClientStatusFrom?.status ?? 'unknown' + }` + ); + } + if (chainClientStatusTo?.status !== 'Active') { + throw new Error( + `The connection destination client is not active. Current status: ${ + chainClientStatusTo?.status ?? 'unknown' + }` ); } // bridging to native chain @@ -125,12 +172,12 @@ export default function BridgeCard({ try { await sendRequest({ - token: coin(amount, tokenDenom), + token: coin(amount, getBaseDenom(from)), timeout_timestamp: timeoutTimestamp, sender: chainAddressFrom, receiver: chainAddressTo, - source_port: ibcTransferInfo.channel.port_id, - source_channel: ibcTransferInfo.channel.channel_id, + source_port: channelInfo.port_id, + source_channel: channelInfo.channel_id, memo: '', timeout_height: { revision_height: Long.ZERO, @@ -153,23 +200,14 @@ export default function BridgeCard({ if (!baseAmount || !Number(baseAmount)) { throw new Error('Invalid Token Amount'); } - if (!ibcTransferInfo.channel.counterparty) { - throw new Error('No egress connection information found'); - } - // find the base IBC denom to match the base amount being sent - const tokenBaseDenom = getIbcDenom( - to.base, - ibcTransferInfo.channel.channel_id, - ibcTransferInfo.channel.port_id - ); try { await sendRequest({ - token: coin(baseAmount, tokenBaseDenom), + token: coin(baseAmount, to.base), timeout_timestamp: timeoutTimestamp, sender: chainAddressFrom, receiver: chainAddressTo, - source_port: ibcTransferInfo.channel.counterparty.port_id, - source_channel: ibcTransferInfo.channel.counterparty.channel_id, + source_port: channelInfo.port_id, + source_channel: channelInfo.channel_id, memo: '', timeout_height: { revision_height: Long.ZERO, @@ -185,56 +223,22 @@ export default function BridgeCard({ } }, [ + wallet, + from, + to, chainAddressFrom, chainAddressTo, chainFrom, chainTo, - onSuccess, - from, - ibcOpenTransfers, - sendRequest, - to, + channelInfo, + chainClientStatusFrom?.status, + chainClientStatusTo?.status, value, - wallet, + sendRequest, + onSuccess, ] ); - // find expected transfer time (1 block on source + 1 block on destination) - const { data: chainTimeFrom } = useRemoteChainBlockTime(chainFrom); - const { data: chainTimeTo } = useRemoteChainBlockTime(chainTo); - const chainTime = useMemo(() => { - if (chainTimeFrom !== undefined && chainTimeTo !== undefined) { - // default to 30s (in Nanoseconds) - const defaultMaxChainTime = '30000000000'; - const chainMsFrom = new BigNumber( - chainTimeFrom?.params?.max_expected_time_per_block?.toString() ?? - defaultMaxChainTime - ).multipliedBy(nanoseconds); - const chainMsTo = new BigNumber( - chainTimeTo?.params?.max_expected_time_per_block?.toString() ?? - defaultMaxChainTime - ).multipliedBy(nanoseconds); - const blockMinutes = chainMsFrom.plus(chainMsTo).dividedBy(minutes); - return `<${formatAmount(blockMinutes.toFixed(0), { - useGrouping: true, - })} minute${blockMinutes.isGreaterThan(1) ? 's' : ''}`; - } - }, [chainTimeFrom, chainTimeTo]); - - // find transfer fees - const { data: chainFeesFrom } = useRemoteChainFees(chainFrom); - const { data: chainFeesTo } = useRemoteChainFees(chainTo); - const chainFees = useMemo(() => { - if (chainFeesFrom !== undefined && chainFeesTo !== undefined) { - const one = new BigNumber(1); - const fee = new BigNumber(value) - .multipliedBy(one.plus(chainFeesFrom?.params?.fee_percentage ?? 0)) - .multipliedBy(one.plus(chainFeesTo?.params?.fee_percentage ?? 0)) - .minus(value); - return formatAmount(fee.toFixed(), { useGrouping: true }); - } - }, [value, chainFeesFrom, chainFeesTo]); - return ( chainFrom && chainTo && ( @@ -243,7 +247,7 @@ export default function BridgeCard({
@@ -402,38 +406,43 @@ export default function BridgeCard({
-
Estimated Time
-
- {from || to ? <>{chainTime ?? '...'} : null} -
-
-
-
Transfer Fee
+
Source chain status
- {Number(value) ? ( - <> - {chainFees ?? '...'} {(from || to)?.symbol} - - ) : null} + {chainClientStatusFrom?.status === 'Active' ? ( + {chainClientStatusFrom.status} + ) : !chainClientStatusFromIsLoading ? ( + + {chainClientStatusFrom?.status ?? 'Not connected'} + + ) : ( + 'Checking...' + )}
-
Total (est)
+
Destination chain status
- {Number(value) ? ( - <> - {formatAmount(value, { useGrouping: true })}{' '} - {(from || to)?.symbol} - - ) : null} + {chainClientStatusTo?.status === 'Active' ? ( + {chainClientStatusTo.status} + ) : !chainClientStatusToIsLoading ? ( + + {chainClientStatusTo?.status ?? 'Not connected'} + + ) : ( + 'Checking...' + )}
@@ -442,36 +451,43 @@ export default function BridgeCard({ ); } +// get an asset denom as known from its original chain +function getBaseDenom(asset: Asset): string { + return asset.traces?.at(0)?.counterparty.base_denom ?? asset.base; +} + function BridgeButton({ chainFrom, + chainFromAsset, chainTo, - token, value, + disabled, }: { chainFrom: Chain; + chainFromAsset?: Asset; chainTo: Chain; - token?: Token; value: string; + disabled: boolean; }) { const { data: chainAddressFrom } = useChainAddress(chainFrom); const { data: chainAddressTo } = useChainAddress(chainTo); const { data: bankBalanceAvailable } = useRemoteChainBankBalance( chainFrom, - token, + chainFromAsset?.base, chainAddressFrom ); const hasAvailableBalance = useMemo(() => { return new BigNumber(value || 0).isLessThanOrEqualTo( - token + chainFromAsset ? getDisplayDenomAmount( - token, + chainFromAsset, bankBalanceAvailable?.balance?.amount || 0 ) || 0 : 0 ); - }, [value, bankBalanceAvailable, token]); + }, [value, bankBalanceAvailable, chainFromAsset]); const errorMessage = useMemo(() => { switch (true) { @@ -483,10 +499,16 @@ function BridgeButton({ }, [hasAvailableBalance]); // return "incomplete" state - if (!token || !chainAddressFrom || !chainAddressTo || !Number(value)) { + if ( + disabled || + !chainFromAsset || + !chainAddressFrom || + !chainAddressTo || + !Number(value) + ) { return ( - ); } @@ -496,11 +518,15 @@ function BridgeButton({ type={errorMessage ? 'button' : 'submit'} onClick={() => undefined} className={[ - 'h3 p-4', + 'h4 p-4', errorMessage ? 'button-error' : 'button-primary', ].join(' ')} > - {errorMessage ? <>{errorMessage} : <>Bridge {token?.symbol}} + {errorMessage ? ( + <>{errorMessage} + ) : ( + <>Bridge {chainFromAsset?.symbol} + )} ); } @@ -523,7 +549,7 @@ function RemoteChainReserves({ const { data: chainEndpoint, isFetching: isFetchingChainEndpoint } = useRemoteChainRestEndpoint(chain); const { data: bankBalance, isFetching: isFetchingBankBalance } = - useRemoteChainBankBalance(chain, token, address); + useRemoteChainBankBalance(chain, token && getBaseDenom(token), address); if (chainEndpoint && address) { const bankBalanceAmount = bankBalance?.balance?.amount; diff --git a/src/lib/web3/hooks/useChains.ts b/src/lib/web3/hooks/useChains.ts index 5b036bb27..1fd14e560 100644 --- a/src/lib/web3/hooks/useChains.ts +++ b/src/lib/web3/hooks/useChains.ts @@ -1,41 +1,14 @@ import { Chain } from '@chain-registry/types'; import { useMemo } from 'react'; import { ibc } from '@duality-labs/neutronjs'; -import { QueryParamsResponse as QueryRouterParams } from '@duality-labs/neutronjs/types/codegen/packetforward/v1/query'; -import { Params as QueryConnectionParams } from '@duality-labs/neutronjs/types/codegen/ibc/core/connection/v1/connection'; -import { - QueryClientStatesRequest, - QueryClientStatesResponse, -} from '@duality-labs/neutronjs/types/codegen/ibc/core/client/v1/query'; -import { - QueryConnectionsRequest, - QueryConnectionsResponse, -} from '@duality-labs/neutronjs/types/codegen/ibc/core/connection/v1/query'; -import { - QueryChannelsRequest, - QueryChannelsResponse, -} from '@duality-labs/neutronjs/types/codegen/ibc/core/channel/v1/query'; import { QueryBalanceResponse } from '@duality-labs/neutronjs/types/codegen/cosmos/bank/v1beta1/query'; -import { State as ChannelState } from '@duality-labs/neutronjs/types/codegen/ibc/core/channel/v1/channel'; -import { State as ConnectionState } from '@duality-labs/neutronjs/types/codegen/ibc/core/connection/v1/connection'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { getChainInfo } from '../wallets/keplr'; -import { Token, getTokenId } from '../utils/tokens'; import { minutes } from '../../utils/time'; -import { useFetchAllPaginatedPages } from './useQueries'; -import { - useNativeChainClient, - useRelatedChainsClient, -} from './useDenomsFromRegistry'; -import { usePacketForwardRestClientPromise } from '../clients/restClients'; -import Long from 'long'; +import { useNativeChainClient } from './useDenomsFromRegistry'; import { SWRResponse } from 'swr'; -interface QueryConnectionParamsResponse { - params?: QueryConnectionParams; -} - const { REACT_APP__CHAIN = '', REACT_APP__CHAIN_NAME = '[chain_name]', @@ -108,238 +81,20 @@ export function useChainAddress(chain?: Chain): { return { data, isValidating: isFetching, error: error || undefined }; } -async function getIbcLcdClient( - restEndpoint?: string | null -): Promise | undefined> { - // get IBC LCD client - if (restEndpoint) { - return ibc.ClientFactory.createLCDClient({ restEndpoint }); - } -} - -function useIbcClientStates(chain: Chain | undefined) { - const { data: restEndpoint } = useRemoteChainRestEndpoint(chain); - const results = useInfiniteQuery({ - queryKey: ['ibc-client-states', restEndpoint], - queryFn: async ({ - pageParam: key, - }: { - pageParam?: Uint8Array; - }): Promise => { - // get IBC LCD client - const lcd = await getIbcLcdClient(restEndpoint); - // note: it appears that clients may appear in this list if they are of: - // - state: "STATE_OPEN", but with - // - status: "Expired" (this property must be queried individually: - // using GET/ibc/core/client/v1/client_status/07-tendermint-0) - // we ignore the status of the light clients here, but their status should - // be checked at the moment they are required for a transfer - const params: QueryClientStatesRequest = { - pagination: { - key, - count_total: !key, - }, - }; - return ( - lcd?.ibc.core.client.v1.clientStates(params) ?? { - client_states: [], - pagination: { - total: Long.ZERO, - }, - } - ); - }, - defaultPageParam: undefined, - getNextPageParam: (lastPage): Uint8Array | undefined => { - // don't pass an empty array as that will trigger another page to download - return lastPage?.pagination?.next_key?.length - ? lastPage.pagination.next_key - : undefined; - }, - refetchInterval: 5 * minutes, - refetchOnMount: false, - staleTime: Number.POSITIVE_INFINITY, - }); - - // fetch all pages - useFetchAllPaginatedPages(results); - - // combine all data pages - const data = useMemo( - () => results.data?.pages.flatMap((page) => page.client_states), - [results.data?.pages] - ); - - return { ...results, data }; -} - -function useIbcConnections(chain: Chain | undefined) { - const { data: restEndpoint } = useRemoteChainRestEndpoint(chain); - const results = useInfiniteQuery({ - queryKey: ['ibc-connections', restEndpoint], - queryFn: async ({ - pageParam: key, - }: { - pageParam?: Uint8Array; - }): Promise => { - // get IBC LCD client - const lcd = await getIbcLcdClient(restEndpoint); - const params: QueryConnectionsRequest = { - pagination: { - key, - count_total: !key, - }, - }; - return ( - lcd?.ibc.core.connection.v1.connections(params) ?? { - connections: [], - pagination: { total: Long.ZERO }, - height: { revision_height: Long.ZERO, revision_number: Long.ZERO }, - } - ); - }, - defaultPageParam: undefined, - getNextPageParam: (lastPage): Uint8Array | undefined => { - // don't pass an empty array as that will trigger another page to download - return lastPage?.pagination?.next_key?.length - ? lastPage.pagination.next_key - : undefined; - }, - refetchInterval: 5 * minutes, - refetchOnMount: false, - staleTime: Number.POSITIVE_INFINITY, - }); - - // fetch all pages - useFetchAllPaginatedPages(results); - - // combine all data pages - const data = useMemo( - () => results.data?.pages.flatMap((page) => page.connections), - [results.data?.pages] - ); - - return { ...results, data }; -} - -function useIbcChannels(chain: Chain | undefined) { - const { data: restEndpoint } = useRemoteChainRestEndpoint(chain); - const results = useInfiniteQuery({ - queryKey: ['ibc-channels', restEndpoint], - queryFn: async ({ - pageParam: key, - }: { - pageParam?: Uint8Array; - }): Promise => { - // get IBC LCD client - const lcd = await getIbcLcdClient(restEndpoint); - const params: QueryChannelsRequest = { - pagination: { - key, - count_total: !key, - }, - }; - return ( - lcd?.ibc.core.channel.v1.channels(params) ?? { - channels: [], - pagination: { total: Long.ZERO }, - height: { revision_height: Long.ZERO, revision_number: Long.ZERO }, - } - ); - }, - defaultPageParam: undefined, - getNextPageParam: (lastPage): Uint8Array | undefined => { - // don't pass an empty array as that will trigger another page to download - return lastPage?.pagination?.next_key?.length - ? lastPage.pagination.next_key - : undefined; - }, - refetchInterval: 5 * minutes, - refetchOnMount: false, - staleTime: Number.POSITIVE_INFINITY, - }); - - // fetch all pages - useFetchAllPaginatedPages(results); - - // combine all data pages - const data = useMemo( - () => results.data?.pages.flatMap((page) => page.channels), - [results.data?.pages] - ); - - return { ...results, data }; -} - -function filterConnectionsOpen( - connection: QueryConnectionsResponse['connections'][number] -): boolean { - return connection.state === (3 as ConnectionState.STATE_OPEN); +type CreateIbcRpcClient = typeof ibc.ClientFactory.createRPCQueryClient; +type IbcRpcClient = Awaited>; +export async function defaultRpcClientCheck(client: IbcRpcClient) { + await client.cosmos.base.tendermint.v1beta1.getNodeInfo(); } - -function filterChannelsOpen( - channel: QueryChannelsResponse['channels'][number] -): boolean { - return channel.state === (3 as ChannelState.STATE_OPEN); -} - -export function useIbcOpenTransfers(givenChain: Chain | undefined) { - const { data: nativeChain } = useNativeChain(); - const chain = givenChain ?? nativeChain; - const { data: clientStates } = useIbcClientStates(chain); - const { data: connections } = useIbcConnections(chain); - const { data: channels } = useIbcChannels(chain); - const { data: chainListClient } = useRelatedChainsClient(); - - return useMemo(() => { - // get openClients (all listed clients are assumed to be working) - const openClients = clientStates || []; - // get open connections - const openConnections = (connections || []).filter(filterConnectionsOpen); - // get open channels - const openChannels = (channels || []).filter(filterChannelsOpen); - - // note: we assume that if a client exists and its connections and channels - // are open, then the same open resources exist on the counterparty. - // this may not be true, but is good enough for some UI lists - return openClients.flatMap((clientState) => { - const chainID = - (clientState.client_state as unknown as { chain_id: string }) - ?.chain_id || undefined; - const chainList = chainListClient?.chains; - const chain = chainList?.find((chain) => chain.chain_id === chainID); - if (!chainID || !chain) return []; - return ( - openConnections - // filter to connections of the current client - .filter((c) => c.client_id === clientState.client_id) - .flatMap((connection) => { - return ( - openChannels - // filter to transfer channels that end at the current connection - .filter((ch) => ch.port_id === 'transfer') - .filter((ch) => ch.connection_hops.at(-1) === connection.id) - .map((channel) => { - return { - chain, - client: clientState, - connection, - channel, - }; - }) - ); - }) - ); - }); - }, [chainListClient, clientStates, connections, channels]); -} - -export function useRemoteChainRpcEndpoint(chain?: Chain) { +export function useRemoteChainRpcEndpoint( + chain?: Chain, + check?: (client: IbcRpcClient) => Promise +) { return useQuery({ - queryKey: ['cosmos-chain-rpc-endpoints', chain?.chain_id], + queryKey: ['cosmos-chain-rpc-endpoints', chain?.chain_id, !!check], queryFn: async (): Promise => { const rpcEndpoints = (chain?.apis?.rpc ?? []).map((rest) => rest.address); - if (rpcEndpoints.length > 0) { + if (check && rpcEndpoints.length > 0) { try { const rpcEndpoint = await Promise.race([ Promise.any( @@ -347,7 +102,7 @@ export function useRemoteChainRpcEndpoint(chain?: Chain) { const client = await ibc.ClientFactory.createRPCQueryClient({ rpcEndpoint, }); - await client.cosmos.base.tendermint.v1beta1.getNodeInfo(); + await check(client); return rpcEndpoint; }) ), @@ -355,7 +110,8 @@ export function useRemoteChainRpcEndpoint(chain?: Chain) { setTimeout(reject, 10000) ), ]); - return rpcEndpoint ?? null; + // remove trailing slash + return rpcEndpoint ? rpcEndpoint.replace(/\/$/, '') : null; } catch (e) { // all requests failed or the requests timed out return null; @@ -398,7 +154,8 @@ export function useRemoteChainRestEndpoint(chain?: Chain) { setTimeout(reject, 10000) ), ]); - return restEndpoint ?? null; + // remove trailing slash + return restEndpoint ? restEndpoint.replace(/\/$/, '') : null; } catch (e) { // all requests failed or the requests timed out return null; @@ -420,16 +177,10 @@ export function useRemoteChainRestEndpoint(chain?: Chain) { export function useRemoteChainBankBalance( chain: Chain | undefined, - token?: Token, - address?: string + denom?: string, // the denom on the queried chain + address?: string // the address on the queried chain ) { const { data: restEndpoint } = useRemoteChainRestEndpoint(chain); - // optionally find the IBC denom when querying the native chain - const denom = - restEndpoint === REACT_APP__REST_API - ? getTokenId(token) - : // query the base denom of any external chains - token?.base; return useQuery({ enabled: !!denom, queryKey: ['cosmos-chain-endpoints', restEndpoint, address], @@ -446,42 +197,3 @@ export function useRemoteChainBankBalance( refetchInterval: 5 * minutes, }); } - -export function useRemoteChainBlockTime(chain: Chain | undefined) { - const { data: restEndpoint } = useRemoteChainRestEndpoint(chain); - return useQuery({ - enabled: !!restEndpoint, - queryKey: ['cosmos-chain-block-time', restEndpoint], - queryFn: async (): Promise => { - if (restEndpoint) { - const client = await ibc.ClientFactory.createLCDClient({ - restEndpoint, - }); - try { - const params = await client.ibc.core.connection.v1.connectionParams(); - // fix return type to point to connection params and not client params - return params as unknown as QueryConnectionParamsResponse; - } catch (e) { - // many chains do not return this route, in which case: state empty - return {}; - } - } else { - return null; - } - }, - refetchInterval: 5 * minutes, - }); -} - -export function useRemoteChainFees(chain: Chain | undefined) { - const clientPromise = usePacketForwardRestClientPromise(); - const { data: restEndpoint } = useRemoteChainRestEndpoint(chain); - return useQuery({ - queryKey: ['cosmos-chain-fees', restEndpoint], - queryFn: async (): Promise => { - const client = await clientPromise; - return client.v1.params(); - }, - refetchInterval: 5 * minutes, - }); -} diff --git a/src/lib/web3/hooks/useDenomsFromRegistry.ts b/src/lib/web3/hooks/useDenomsFromRegistry.ts index f96888f7a..a078be34f 100644 --- a/src/lib/web3/hooks/useDenomsFromRegistry.ts +++ b/src/lib/web3/hooks/useDenomsFromRegistry.ts @@ -298,6 +298,9 @@ export function useRelatedChainsClient() { return createChainRegistryClient({ ...defaultClientOptions, chainNames: relatedChainNames, + // pass IBC name pairs related only to native chain so that the client + // doesn't try to fetch IBC data between other listed unrelated chains + ibcNamePairs, }); } ); @@ -341,12 +344,12 @@ function useDefaultAssetsClient() { } export function useChainUtil(): SWRResponse { - const swr = useDefaultAssetsClient(); + const { data, ...swr } = useDefaultAssetsClient(); // return just the chain utility instance // it is possible to get the original fetcher at chainUtil.chainInfo.fetcher return { ...swr, - data: swr.data?.getChainUtil(REACT_APP__CHAIN_NAME), + data: useMemo(() => data?.getChainUtil(REACT_APP__CHAIN_NAME), [data]), } as SWRResponse; } diff --git a/src/pages/Bridge/useBridge.ts b/src/pages/Bridge/useBridge.ts index 0c0e11fd8..5dce67614 100644 --- a/src/pages/Bridge/useBridge.ts +++ b/src/pages/Bridge/useBridge.ts @@ -9,10 +9,7 @@ import { } from '@duality-labs/neutronjs/types/codegen/ibc/applications/transfer/v1/tx'; import { GetTxsEventRequest } from '@duality-labs/neutronjs/types/codegen/cosmos/tx/v1beta1/service'; -import { - getCosmosRestClient, - getIbcRestClient, -} from '../../lib/web3/clients/restClients'; +import { getCosmosRestClient } from '../../lib/web3/clients/restClients'; import { IBCReceivePacketEvent, IBCSendPacketEvent, @@ -20,7 +17,6 @@ import { mapEventAttributes, } from '../../lib/web3/utils/events'; import { - useIbcOpenTransfers, useRemoteChainRestEndpoint, useRemoteChainRpcEndpoint, } from '../../lib/web3/hooks/useChains'; @@ -83,6 +79,8 @@ async function bridgeToken( export default function useBridge( chainFrom?: Chain, + chainFromAddress?: string, + chainFromDenom?: string, chainTo?: Chain ): [ { @@ -100,9 +98,18 @@ export default function useBridge( useRemoteChainRestEndpoint(chainFrom); const { data: restEndpointTo, refetch: refetchTo } = useRemoteChainRestEndpoint(chainTo); - const ibcOpenTransfers = useIbcOpenTransfers(chainFrom); const { data: rpcEndpointFrom, refetch: refetchRpc } = - useRemoteChainRpcEndpoint(chainFrom); + useRemoteChainRpcEndpoint( + chainFrom, + chainFromAddress && chainFromDenom + ? async (client) => { + await client.cosmos.bank.v1beta1.spendableBalanceByDenom({ + address: chainFromAddress, + denom: chainFromDenom, + }); + } + : undefined + ); const sendRequest = useCallback( async (request: MsgTransfer) => { @@ -132,12 +139,6 @@ export default function useBridge( if (!account || !account.address) { throw new Error('No wallet address'); } - const connection = ibcOpenTransfers.find(({ chain }) => { - return chain.chain_id === chainTo.chain_id; - })?.connection; - if (!connection) { - throw new Error('No connection between source and destination found'); - } const refetchOptions = { cancelRefetch: false }; const clientEndpointFrom = restEndpointFrom ?? (await refetchFrom(refetchOptions)).data; @@ -151,30 +152,6 @@ export default function useBridge( if (!clientEndpointTo) { throw new Error('No destination chain endpoint found'); } - const restClientFrom = await getIbcRestClient(clientEndpointFrom); - const restClientTo = await getIbcRestClient(clientEndpointTo); - // future: can check both sides of the chain to see if they have IBC - // - send_enabled - // - receive_enabled - // by querying each chain with: /ibc/apps/transfer/v1/params - // (this may be redundant as we know there is an IBC connection already) - const clientFromStatus = - await restClientFrom.core.client.v1.clientStatus({ - client_id: connection.counterparty.client_id, - }); - if (clientFromStatus.status !== 'Active') { - throw new Error( - `The connection source client is not active. Current status: ${clientFromStatus.status}` - ); - } - const clientToStatus = await restClientTo.core.client.v1.clientStatus({ - client_id: connection.client_id, - }); - if (clientToStatus.status !== 'Active') { - throw new Error( - `The connection destination client is not active. Current status: ${clientToStatus.status}` - ); - } const rpcClientEndpointFrom = rpcEndpointFrom ?? (await refetchRpc(refetchOptions)).data; if (!rpcClientEndpointFrom) { @@ -301,7 +278,6 @@ export default function useBridge( [ chainFrom, chainTo, - ibcOpenTransfers, refetchFrom, refetchRpc, refetchTo, diff --git a/src/pages/Bridge/useChannelInfo.ts b/src/pages/Bridge/useChannelInfo.ts new file mode 100644 index 000000000..5d8dfa03b --- /dev/null +++ b/src/pages/Bridge/useChannelInfo.ts @@ -0,0 +1,122 @@ +import { useMemo } from 'react'; +import { SWRResponse } from 'swr'; +import { useQuery } from '@tanstack/react-query'; +import { Asset, Chain, IBCInfo, IBCTrace } from '@chain-registry/types'; +import { QueryClientStatusResponse } from '@duality-labs/neutronjs/types/codegen/ibc/core/client/v1/query'; + +import { useRelatedChainsClient } from '../../lib/web3/hooks/useDenomsFromRegistry'; +import { useRemoteChainRestEndpoint } from '../../lib/web3/hooks/useChains'; +import { getIbcRestClient } from '../../lib/web3/clients/restClients'; + +type ChannelInfo = { + chain_name: string; + client_id: string; + connection_id: string; + channel_id: string; + port_id: string; + ordering: string; + version: string; + tags: object | undefined; +}; + +function getChannelSideInfo( + ibcInfo: IBCInfo, + channelId: string +): ChannelInfo | undefined { + const channel = ibcInfo.channels.find((channel) => { + return ( + channel.chain_1.channel_id === channelId || + channel.chain_2.channel_id === channelId + ); + }); + if (channel) { + // return the channel and connection props of the IBC chain side + const chainSide = + channel.chain_1.channel_id === channelId ? 'chain_1' : 'chain_2'; + return { + // take channel props + ordering: channel.ordering, + version: channel.version, + tags: channel.tags, + // take channel side props + ...channel[chainSide], + // take connection side props + ...ibcInfo[chainSide], + }; + } +} + +function getSingleHopIbcTrace(asset: Asset): IBCTrace | undefined { + if (asset.traces?.length === 1) { + const trace = asset.traces?.at(0); + if (trace && trace?.type === 'ibc') { + return trace as IBCTrace; + } + } +} + +function useConnectionInfo( + chain?: Chain, + counterChain?: Chain +): SWRResponse { + // find the channel information on the from side for the bridge request + const { data: relatedChainsClient, ...swr } = useRelatedChainsClient(); + const data = useMemo(() => { + // find matching ibcInfo from relatedChainsClient ibcData + const chainA = chain; + const chainB = counterChain; + return chainA?.chain_name && chainB?.chain_name + ? relatedChainsClient?.ibcData.find((connection) => { + return ( + (connection.chain_1.chain_name === chainA.chain_name && + connection.chain_2.chain_name === chainB.chain_name) || + (connection.chain_1.chain_name === chainB.chain_name && + connection.chain_2.chain_name === chainA.chain_name) + ); + }) + : undefined; + }, [chain, counterChain, relatedChainsClient]); + return { ...swr, data } as SWRResponse; +} + +export function useSingleHopChannelInfo( + chain?: Chain, + counterChain?: Chain, + token?: Asset +): SWRResponse { + // find the channel information on the from side for the bridge request + const { data: ibcInfo, ...swr } = useConnectionInfo(chain, counterChain); + const channelInfo = useMemo(() => { + const ibcTrace = token && getSingleHopIbcTrace(token); + if (ibcTrace) { + // return just the chain (not counterChain) channel info + if (ibcInfo && chain) { + return [ + getChannelSideInfo(ibcInfo, ibcTrace.chain.channel_id), + getChannelSideInfo(ibcInfo, ibcTrace.counterparty.channel_id), + ] + .filter((info) => info?.chain_name === chain.chain_name) + .at(0); + } + } + }, [chain, ibcInfo, token]); + + return { ...swr, data: channelInfo } as SWRResponse; +} + +export function useSingleHopChannelStatus(chain?: Chain, clientId?: string) { + const { data: restEndpoint, ...swr } = useRemoteChainRestEndpoint(chain); + const query = useQuery({ + queryKey: ['core.client.v1.clientStatus', restEndpoint, clientId], + queryFn: async (): Promise => { + const restClient = restEndpoint && (await getIbcRestClient(restEndpoint)); + return restClient && clientId + ? restClient.core.client.v1.clientStatus({ client_id: clientId }) + : null; + }, + refetchInterval: false, + refetchOnMount: false, + }); + + return restEndpoint === undefined ? { ...swr, data: undefined } : query; +}