Skip to content

Commit

Permalink
feat: fixing ibc transfers gas estimate (#1567)
Browse files Browse the repository at this point in the history
* feat: fixing ibc shielded gas estimate

* feat: moving round of decimal places to gas fn

* feat: adding channel as required prop for gas estimation
  • Loading branch information
pedrorezende authored Jan 18, 2025
1 parent 4b83183 commit c231522
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 109 deletions.
2 changes: 1 addition & 1 deletion apps/namadillo/src/App/Ibc/IbcTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const IbcTransfer = (): JSX.Element => {
isShielded: shielded,
onChangeShielded: setShielded,
}}
gasConfig={gasConfig}
gasConfig={gasConfig.data}
changeFeeEnabled={false}
submittingText={currentProgress}
isSubmitting={transferStatus === "pending" || !!currentProgress}
Expand Down
2 changes: 0 additions & 2 deletions apps/namadillo/src/App/Transfer/TransferModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,6 @@ export const TransferModule = ({
return "NoSelectedAsset";
} else if (!source.amount || source.amount.eq(0)) {
return "NoAmount";
} else if (!gasConfig) {
return "NoTransactionFee";
} else if (
!availableAmountMinusFees ||
source.amount.gt(availableAmountMinusFees)
Expand Down
46 changes: 19 additions & 27 deletions apps/namadillo/src/atoms/integrations/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
assertIsDeliverTxSuccess,
calculateFee,
DeliverTxResponse,
MsgTransferEncodeObject,
SigningStargateClient,
StargateClient,
StdFee,
Expand All @@ -15,7 +16,6 @@ import { getIndexerApi } from "atoms/api";
import { queryForAck, queryForIbcTimeout } from "atoms/transactions";
import BigNumber from "bignumber.js";
import { getDefaultStore } from "jotai";
import { createIbcTransferMessage } from "lib/transactions";
import toml from "toml";
import {
AddressWithAsset,
Expand All @@ -25,7 +25,6 @@ import {
LocalnetToml,
TransferStep,
} from "types";
import { toBaseAmount } from "utils";
import { getKeplrWallet } from "utils/ibc";
import { getSdkInstance } from "utils/sdk";
import { rpcByChainAtom } from "./atoms";
Expand Down Expand Up @@ -105,35 +104,14 @@ export const createStargateClient = async (

export const getSignedMessage = async (
client: SigningStargateClient,
transferParams: IbcTransferParams,
maspCompatibleMemo: string = ""
transferMsg: MsgTransferEncodeObject,
gasConfig: GasConfig
): Promise<TxRaw> => {
const {
sourceAddress,
amount: displayAmount,
asset,
sourceChannelId,
gasConfig,
} = transferParams;

// cosmjs expects amounts to be represented in the base denom, so convert
const baseAmount = toBaseAmount(asset.asset, displayAmount);

const fee: StdFee = calculateFee(
gasConfig.gasLimit.toNumber(),
Math.ceil(gasConfig.gasLimit.toNumber()),
`${gasConfig.gasPrice.toString()}${gasConfig.gasToken}`
);

const transferMsg = createIbcTransferMessage(
sourceChannelId,
sourceAddress,
transferParams.destinationAddress,
baseAmount,
asset.originalAddress,
maspCompatibleMemo
);

return await client.sign(sourceAddress, [transferMsg], fee, "");
return await client.sign(transferMsg.value.sender!, [transferMsg], fee, "");
};

export const broadcastIbcTransaction = async (
Expand Down Expand Up @@ -281,3 +259,17 @@ export const fetchIbcChannelFromRegistry = async (
const channelInfo: IBCInfo = await (await fetch(queryUrl.toString())).json();
return getChannelFromIbcInfo(ibcChainName, channelInfo) || null;
};

export const simulateIbcTransferFee = async (
stargateClient: SigningStargateClient,
sourceAddress: string,
transferMsg: MsgTransferEncodeObject,
additionalPercentage: number = 0.05
): Promise<number> => {
const estimatedGas = await stargateClient.simulate(
sourceAddress!,
[transferMsg],
undefined
);
return estimatedGas * (1 + additionalPercentage);
};
Empty file added apps/namadillo/src/constants.ts
Empty file.
177 changes: 105 additions & 72 deletions apps/namadillo/src/hooks/useIbcTransaction.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { useMemo, useState } from "react";
import {
Address,
AddressWithAssetAndAmount,
ChainRegistryEntry,
GasConfig,
IbcTransferStage,
TransferStep,
TransferTransactionData,
} from "types";

type useIbcTransactionProps = {
sourceAddress?: string;
registry?: ChainRegistryEntry;
sourceChannel?: string;
shielded?: boolean;
destinationChannel?: Address;
selectedAsset?: AddressWithAssetAndAmount;
};

import { SigningStargateClient } from "@cosmjs/stargate";
import { QueryStatus } from "@tanstack/query-core";
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { TokenCurrency } from "App/Common/TokenCurrency";
import {
broadcastIbcTransactionAtom,
Expand All @@ -32,13 +14,35 @@ import {
dispatchToastNotificationAtom,
} from "atoms/notifications";
import BigNumber from "bignumber.js";
import { getIbcGasConfig } from "integrations/utils";
import invariant from "invariant";
import { useAtomValue, useSetAtom } from "jotai";
import { createTransferDataFromIbc } from "lib/transactions";
import {
createIbcTransferMessage,
createTransferDataFromIbc,
} from "lib/transactions";
import { useState } from "react";
import {
Address,
AddressWithAssetAndAmount,
ChainRegistryEntry,
GasConfig,
IbcTransferStage,
TransferStep,
TransferTransactionData,
} from "types";
import { toBaseAmount } from "utils";
import { sanitizeAddress } from "utils/address";
import { getKeplrWallet, sanitizeChannel } from "utils/ibc";
import { useSimulateIbcTransferFee } from "./useSimulateIbcTransferFee";

type useIbcTransactionProps = {
sourceAddress?: string;
registry?: ChainRegistryEntry;
sourceChannel?: string;
shielded?: boolean;
destinationChannel?: Address;
selectedAsset?: AddressWithAssetAndAmount;
};

type useIbcTransactionOutput = {
transferToNamada: (
Expand All @@ -48,7 +52,7 @@ type useIbcTransactionOutput = {
onUpdateStatus?: (status: string) => void
) => Promise<TransferTransactionData>;
transferStatus: "idle" | QueryStatus;
gasConfig: GasConfig | undefined;
gasConfig: UseQueryResult<GasConfig>;
};

export const useIbcTransaction = ({
Expand All @@ -62,6 +66,37 @@ export const useIbcTransaction = ({
const broadcastIbcTx = useAtomValue(broadcastIbcTransactionAtom);
const dispatchNotification = useSetAtom(dispatchToastNotificationAtom);
const [txHash, setTxHash] = useState<string | undefined>();
const [rpcUrl, setRpcUrl] = useState<string | undefined>();
const [stargateClient, setStargateClient] = useState<
SigningStargateClient | undefined
>();

// Avoid the same client being created twice for the same chain and provide
// a way to refetch the query in case it throws an error trying to connect to the RPC
useQuery({
queryKey: ["store-stargate-client", registry?.chain.chain_id],
enabled: Boolean(registry?.chain),
queryFn: async () => {
invariant(registry?.chain, "Error: Invalid chain");
setRpcUrl(undefined);
setStargateClient(undefined);
return await queryAndStoreRpc(registry.chain, async (rpc: string) => {
const client = await createStargateClient(rpc, registry.chain);
setStargateClient(client);
setRpcUrl(rpc);
return client;
});
},
});

const gasConfigQuery = useSimulateIbcTransferFee({
stargateClient,
registry,
selectedAsset,
isShieldedTransfer: shielded,
sourceAddress,
channel: sourceChannel,
});

const dispatchPendingTxNotification = (tx: TransferTransactionData): void => {
invariant(tx.hash, "Error: Transaction hash not provided");
Expand Down Expand Up @@ -90,13 +125,6 @@ export const useIbcTransaction = ({
});
};

const gasConfig = useMemo(() => {
if (typeof registry !== "undefined") {
return getIbcGasConfig(registry);
}
return undefined;
}, [registry]);

const getIbcTransferStage = (shielded: boolean): IbcTransferStage => {
return shielded ?
{ type: "IbcToShielded", currentStep: TransferStep.IbcToShielded }
Expand All @@ -116,12 +144,14 @@ export const useIbcTransaction = ({
invariant(selectedAsset, "Error: No asset is selected");
invariant(registry, "Error: Invalid chain");
invariant(sourceChannel, "Error: Invalid IBC source channel");
invariant(gasConfig, "Error: No transaction fee is set");
invariant(stargateClient, "Error: Stargate client not initialized");
invariant(rpcUrl, "Error: RPC URL not initialized");
invariant(
!shielded || destinationChannel,
"Error: Destination channel not provided"
);

// Set Keplr option to allow Namadillo to set the transaction fee
const baseKeplr = getKeplrWallet();
const savedKeplrOptions = baseKeplr.defaultOptions;
baseKeplr.defaultOptions = {
Expand All @@ -132,6 +162,8 @@ export const useIbcTransaction = ({

try {
const baseAmount = toBaseAmount(selectedAsset.asset, displayAmount);

// This step might require a bit of time
const { memo: maspCompatibleMemo, receiver: maspCompatibleReceiver } =
await (async () => {
onUpdateStatus?.("Generating MASP parameters...");
Expand All @@ -145,48 +177,49 @@ export const useIbcTransaction = ({
: { memo, receiver: destinationAddress };
})();

// Set Keplr option to allow Namadillo to set the transaction fee
const chainId = registry.chain.chain_id;

return await queryAndStoreRpc(registry.chain, async (rpc: string) => {
onUpdateStatus?.("Waiting for signature...");
const client = await createStargateClient(rpc, registry.chain);
const ibcTransferParams = {
signer: baseKeplr.getOfflineSigner(chainId),
chainId,
sourceAddress: sanitizeAddress(sourceAddress),
destinationAddress: sanitizeAddress(maspCompatibleReceiver),
amount: displayAmount,
asset: selectedAsset,
gasConfig,
sourceChannelId: sanitizeChannel(sourceChannel!),
destinationChannelId: sanitizeChannel(destinationChannel!) || "",
isShielded: !!shielded,
};

const signedMessage = await getSignedMessage(
client,
ibcTransferParams,
maspCompatibleMemo
);

onUpdateStatus?.("Broadcasting transaction...");
const txResponse = await broadcastIbcTx.mutateAsync({
client,
tx: signedMessage,
});

const tx = createTransferDataFromIbc(
txResponse,
rpc,
selectedAsset.asset,
chainId,
getIbcTransferStage(!!shielded)
);
dispatchPendingTxNotification(tx);
setTxHash(tx.hash);
return tx;
const transferMsg = createIbcTransferMessage(
sanitizeChannel(sourceChannel!),
sanitizeAddress(sourceAddress),
sanitizeAddress(maspCompatibleReceiver),
baseAmount,
selectedAsset.originalAddress,
maspCompatibleMemo
);

// In case the first estimate has failed for some reason, we try to refetch
const gasConfig = await (async () => {
if (!gasConfigQuery.data) {
onUpdateStatus?.("Estimating required gas...");
return (await gasConfigQuery.refetch()).data;
}
return gasConfigQuery.data;
})();
invariant(gasConfig, "Error: Failed to estimate gas usage");

onUpdateStatus?.("Waiting for signature...");
const signedMessage = await getSignedMessage(
stargateClient,
transferMsg,
gasConfig
);

onUpdateStatus?.("Broadcasting transaction...");
const txResponse = await broadcastIbcTx.mutateAsync({
client: stargateClient,
tx: signedMessage,
});

const tx = createTransferDataFromIbc(
txResponse,
rpcUrl || "",
selectedAsset.asset,
chainId,
getIbcTransferStage(!!shielded)
);
dispatchPendingTxNotification(tx);
setTxHash(tx.hash);
return tx;
} catch (err) {
dispatchErrorTxNotification(err);
throw err;
Expand All @@ -197,7 +230,7 @@ export const useIbcTransaction = ({

return {
transferToNamada,
gasConfig,
gasConfig: gasConfigQuery,
transferStatus: broadcastIbcTx.status,
};
};
Loading

0 comments on commit c231522

Please sign in to comment.