diff --git a/src/app/common/rpc.ts b/src/app/common/rpc.ts new file mode 100644 index 00000000..86da3b19 --- /dev/null +++ b/src/app/common/rpc.ts @@ -0,0 +1,2 @@ +export const BBN_RPC_URL = "https://rpc-dapp.devnet.babylonlabs.io"; +export const BBN_LCD_URL = "https://lcd-dapp.devnet.babylonlabs.io"; diff --git a/src/app/components/PersonalBalance/PersonalBalance.tsx b/src/app/components/PersonalBalance/PersonalBalance.tsx index 2a21ca72..a07371a9 100644 --- a/src/app/components/PersonalBalance/PersonalBalance.tsx +++ b/src/app/components/PersonalBalance/PersonalBalance.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; +import { useBbnQueryClient } from "@/app/hooks/client/query/useBbnQueryClient"; import { useRewardsService } from "@/app/hooks/services/useRewardsService"; import { ubbnToBbn } from "@/utils/bbn"; import { satoshiToBtc } from "@/utils/btc"; @@ -11,12 +12,9 @@ import { StatItem } from "../Stats/StatItem"; export function PersonalBalance() { const { getBalance: getBTCBalance, connected: btcConnected } = useBTCWallet(); - const { - signingStargateClient, - connected: cosmosConnected, - bech32Address, - } = useCosmosWallet(); + const { connected: cosmosConnected } = useCosmosWallet(); + const { getBalance } = useBbnQueryClient(); const { getRewards } = useRewardsService(); const [rewards, setRewards] = useState(); const [btcBalance, setBTCBalance] = useState(); @@ -33,17 +31,13 @@ export function PersonalBalance() { setBTCBalance(balance); }; const fetchCosmosBalance = async () => { - const balance = await signingStargateClient?.getBalance( - bech32Address, - "ubbn", - ); - const bbnAmount = Number(balance?.amount ?? 0); + const bbnAmount = await getBalance(); setCosmosBalance(bbnAmount); }; fetchRewards(); fetchBTCBalance(); fetchCosmosBalance(); - }, [getRewards, getBTCBalance, signingStargateClient, bech32Address]); + }, [getRewards, getBTCBalance, getBalance]); if (!btcConnected || !cosmosConnected) { return null; diff --git a/src/app/context/wallet/CosmosWalletProvider.tsx b/src/app/context/wallet/CosmosWalletProvider.tsx index cd6366d1..90b5d99b 100644 --- a/src/app/context/wallet/CosmosWalletProvider.tsx +++ b/src/app/context/wallet/CosmosWalletProvider.tsx @@ -31,8 +31,8 @@ interface CosmosWalletContextProps { const CosmosWalletContext = createContext({ bech32Address: "", connected: false, - disconnect: () => { }, - open: () => { }, + disconnect: () => {}, + open: () => {}, signingStargateClient: undefined, }); @@ -46,7 +46,7 @@ export const CosmosWalletProvider = ({ children }: PropsWithChildren) => { >(); const { showError, captureError } = useError(); - const { open = () => { }, connected } = useWalletConnect(); + const { open = () => {}, connected } = useWalletConnect(); const bbnConnector = useChainConnector("BBN"); const cosmosDisconnect = useCallback(() => { diff --git a/src/app/hooks/client/query/useBbnQuery.ts b/src/app/hooks/client/query/useBbnQuery.ts deleted file mode 100644 index 18cfce50..00000000 --- a/src/app/hooks/client/query/useBbnQuery.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { incentivequery } from "@babylonlabs-io/babylon-proto-ts"; -import { QueryClient, createProtobufRpcClient } from "@cosmjs/stargate"; -import { useCallback } from "react"; - -import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; - -/** - * Query service for Babylon which contains all the queries for - * interacting with Babylon RPC nodes - */ -export const useBbnQuery = () => { - const { queryClient, bech32Address } = useCosmosWallet(); - - const getRewards = useCallback(async (): Promise< - incentivequery.QueryRewardGaugesResponse | undefined - > => { - if (!queryClient || !bech32Address) { - return undefined; - } - const { incentive } = setupIncentiveExtension(queryClient); - - const req: incentivequery.QueryRewardGaugesRequest = - incentivequery.QueryRewardGaugesRequest.fromPartial({ - address: bech32Address, - }); - - return incentive.RewardGauges(req); - }, [queryClient, bech32Address]); - - return { - getRewards, - }; -}; - -// Extend the QueryClient with the Incentive module -const setupIncentiveExtension = ( - base: QueryClient, -): { - incentive: incentivequery.QueryClientImpl; -} => { - const rpc = createProtobufRpcClient(base); - const incentiveQueryClient = new incentivequery.QueryClientImpl(rpc); - return { incentive: incentiveQueryClient }; -}; diff --git a/src/app/hooks/client/query/useBbnQueryClient.ts b/src/app/hooks/client/query/useBbnQueryClient.ts new file mode 100644 index 00000000..29df6447 --- /dev/null +++ b/src/app/hooks/client/query/useBbnQueryClient.ts @@ -0,0 +1,76 @@ +import { incentivequery } from "@babylonlabs-io/babylon-proto-ts"; +import { + QueryClient, + createProtobufRpcClient, + setupBankExtension, +} from "@cosmjs/stargate"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { useCallback, useEffect, useState } from "react"; + +import { BBN_RPC_URL } from "@/app/common/rpc"; +import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; + +/** + * Query service for Babylon which contains all the queries for + * interacting with Babylon RPC nodes + */ +export const useBbnQueryClient = () => { + const { bech32Address } = useCosmosWallet(); + const [queryClient, setQueryClient] = useState(); + + useEffect(() => { + const initQueryClient = async () => { + const tmClient = await Tendermint34Client.connect(BBN_RPC_URL); + const queryClient = QueryClient.withExtensions(tmClient); + setQueryClient(queryClient); + }; + + initQueryClient(); + }, []); + + /** + * Gets the rewards from the user's account. + * @returns {Promise} - The rewards from the user's account. + */ + const getRewards = useCallback(async (): Promise< + incentivequery.QueryRewardGaugesResponse | undefined + > => { + if (!queryClient || !bech32Address) { + return undefined; + } + const { incentive } = setupIncentiveExtension(queryClient); + + const req: incentivequery.QueryRewardGaugesRequest = + incentivequery.QueryRewardGaugesRequest.fromPartial({ + address: bech32Address, + }); + + return incentive.RewardGauges(req); + }, [queryClient, bech32Address]); + + const getBalance = useCallback(async (): Promise => { + if (!queryClient || !bech32Address) { + return 0; + } + + const { bank } = setupBankExtension(queryClient); + const balance = await bank.balance(bech32Address, "ubbn"); + return Number(balance?.amount ?? 0); + }, [queryClient, bech32Address]); + + return { + getRewards, + getBalance, + }; +}; + +// Extend the QueryClient with the Incentive module +const setupIncentiveExtension = ( + base: QueryClient, +): { + incentive: incentivequery.QueryClientImpl; +} => { + const rpc = createProtobufRpcClient(base); + const incentiveQueryClient = new incentivequery.QueryClientImpl(rpc); + return { incentive: incentiveQueryClient }; +}; diff --git a/src/app/hooks/client/query/useBbnTransaction.ts b/src/app/hooks/client/query/useBbnTransaction.ts index 0f76ae94..e3e77942 100644 --- a/src/app/hooks/client/query/useBbnTransaction.ts +++ b/src/app/hooks/client/query/useBbnTransaction.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; -import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; +import { useSigningStargateClient } from "./useSigningStargateClient"; export interface BbnGasFee { amount: { denom: string; amount: string }[]; @@ -12,7 +12,7 @@ export interface BbnGasFee { * interacting with Babylon RPC nodes */ export const useBbnTransaction = () => { - const { signingStargateClient, bech32Address } = useCosmosWallet(); + const { simulate, signAndBroadcast } = useSigningStargateClient(); /** * Estimates the gas fee for a transaction. @@ -21,16 +21,7 @@ export const useBbnTransaction = () => { */ const estimateBbnGasFee = useCallback( async (msg: { typeUrl: string; value: T }): Promise => { - if (!signingStargateClient || !bech32Address) { - throw new Error("Wallet not connected"); - } - - // estimate gas - const gasEstimate = await signingStargateClient.simulate( - bech32Address, - [msg], - `estimate transaction fee for ${msg.typeUrl}`, - ); + const gasEstimate = await simulate(msg); // TODO: The gas calculation need to be improved // https://github.com/babylonlabs-io/simple-staking/issues/320 const gasWanted = Math.ceil(gasEstimate * 1.5); @@ -39,7 +30,7 @@ export const useBbnTransaction = () => { gas: gasWanted.toString(), }; }, - [signingStargateClient, bech32Address], + [simulate], ); /** @@ -48,34 +39,13 @@ export const useBbnTransaction = () => { * @returns {Promise} - The transaction hash and gas used. */ const sendBbnTx = useCallback( - async (msg: { - typeUrl: string; - value: T; - }): Promise<{ txHash: string; gasUsed: string }> => { - if (!signingStargateClient || !bech32Address) { - throw new Error("Wallet not connected"); - } - + async (msg: { typeUrl: string; value: T }) => { // estimate gas const fee = await estimateBbnGasFee(msg); - // sign it - const res = await signingStargateClient.signAndBroadcast( - bech32Address, - [msg], - fee, - ); - if (res.code !== 0) { - throw new Error( - `Failed to send ${msg.typeUrl} transaction, code: ${res.code}, txHash: ${res.transactionHash}`, - ); - } - return { - txHash: res.transactionHash, - gasUsed: res.gasUsed.toString(), - }; + await signAndBroadcast(msg, fee); }, - [signingStargateClient, bech32Address, estimateBbnGasFee], + [estimateBbnGasFee, signAndBroadcast], ); return { diff --git a/src/app/hooks/client/query/useSigningStargateClient.ts b/src/app/hooks/client/query/useSigningStargateClient.ts new file mode 100644 index 00000000..f8124259 --- /dev/null +++ b/src/app/hooks/client/query/useSigningStargateClient.ts @@ -0,0 +1,74 @@ +import { StdFee } from "@cosmjs/stargate"; +import { useCallback } from "react"; + +import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; + +/** + * Hook for signing and broadcasting transactions with the Cosmos wallet + */ +export const useSigningStargateClient = () => { + const { signingStargateClient, bech32Address } = useCosmosWallet(); + + /** + * Simulates a transaction to estimate the gas fee + * @param msg - The transaction message + * @returns The gas fee + */ + const simulate = useCallback( + (msg: { typeUrl: string; value: T }): Promise => { + if (!signingStargateClient || !bech32Address) { + throw new Error("Wallet not connected"); + } + + // estimate gas + return signingStargateClient.simulate( + bech32Address, + [msg], + `estimate transaction fee for ${msg.typeUrl}`, + ); + }, + [signingStargateClient, bech32Address], + ); + + /** + * Signs and broadcasts a transaction + * @param msg - The transaction message + * @param fee - The gas fee + * @returns The transaction hash and gas used + */ + const signAndBroadcast = useCallback( + async ( + msg: { + typeUrl: string; + value: T; + }, + fee: StdFee, + ): Promise<{ + txHash: string; + gasUsed: string; + }> => { + if (!signingStargateClient || !bech32Address) { + throw new Error("Wallet not connected"); + } + + const res = await signingStargateClient.signAndBroadcast( + bech32Address, + [msg], + fee, + ); + + if (res.code !== 0) { + throw new Error( + `Failed to send ${msg.typeUrl} transaction, code: ${res.code}, txHash: ${res.transactionHash}`, + ); + } + return { + txHash: res.transactionHash, + gasUsed: res.gasUsed.toString(), + }; + }, + [signingStargateClient, bech32Address], + ); + + return { simulate, signAndBroadcast }; +}; diff --git a/src/app/hooks/services/useRewardsService.tsx b/src/app/hooks/services/useRewardsService.ts similarity index 92% rename from src/app/hooks/services/useRewardsService.tsx rename to src/app/hooks/services/useRewardsService.ts index 355627a5..24306b17 100644 --- a/src/app/hooks/services/useRewardsService.tsx +++ b/src/app/hooks/services/useRewardsService.ts @@ -4,7 +4,7 @@ import { useCallback } from "react"; import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; import { BBN_REGISTRY_TYPE_URLS } from "@/utils/wallet/bbnRegistry"; -import { useBbnQuery } from "../client/query/useBbnQuery"; +import { useBbnQueryClient } from "../client/query/useBbnQueryClient"; import { useBbnTransaction } from "../client/query/useBbnTransaction"; const REWARD_GAUGE_KEY_BTC_DELEGATION = "btc_delegation"; @@ -16,7 +16,7 @@ export const useRewardsService = () => { signingStargateClient, } = useCosmosWallet(); - const { getRewards: getBbnRewards } = useBbnQuery(); + const { getRewards: getBbnRewards } = useBbnQueryClient(); const { estimateBbnGasFee, sendBbnTx } = useBbnTransaction(); /** @@ -72,8 +72,7 @@ export const useRewardsService = () => { const msg = createWithdrawRewardMsg(bech32Address); - const { txHash } = await sendBbnTx(msg); - console.log("successfully claimed rewards with txHash", txHash); + await sendBbnTx(msg); }, [bech32Address, signingStargateClient, sendBbnTx]); return { diff --git a/src/app/hooks/services/useTransactionService.tsx b/src/app/hooks/services/useTransactionService.ts similarity index 100% rename from src/app/hooks/services/useTransactionService.tsx rename to src/app/hooks/services/useTransactionService.ts diff --git a/src/config/wallet/babylon.ts b/src/config/wallet/babylon.ts index 6d8665ac..a7764621 100644 --- a/src/config/wallet/babylon.ts +++ b/src/config/wallet/babylon.ts @@ -1,12 +1,15 @@ // Temporary solution until we have a stable chain registry + +import { BBN_LCD_URL, BBN_RPC_URL } from "@/app/common/rpc"; + // The values here shall match from https://rpc.devnet.babylonlabs.io/genesis? export const bbnDevnet = { chainId: "devnet-7", chainName: "Babylon Devnet 7", chainSymbolImageUrl: "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/bbn-dev/chain.png", - rpc: "https://rpc-dapp.devnet.babylonlabs.io", - rest: "https://lcd-dapp.devnet.babylonlabs.io", + rpc: BBN_RPC_URL, + rest: BBN_LCD_URL, nodeProvider: { name: "Babylonlabs", email: "contact@babylonlabs.io",