Skip to content

Commit

Permalink
feat: enable IBC withdrawals for all tokens
Browse files Browse the repository at this point in the history
Closes #1242

- Use base denom amounts when building IBC transfers with JS SDK
- Use unknown gas limit value for txs without an indexer types
- Use same gas limit and price for IBC withdrawal as rest of Namadillo
- Reveal public key if needed for IBC withdrawal. Fixes #1222
  • Loading branch information
emccorson committed Nov 12, 2024
1 parent 799e84c commit a54e752
Show file tree
Hide file tree
Showing 23 changed files with 486 additions and 271 deletions.
139 changes: 83 additions & 56 deletions apps/namadillo/src/App/Ibc/IbcWithdraw.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
import { Chain } from "@chain-registry/types";
import { WindowWithNamada } from "@namada/types";
import { mapUndefined } from "@namada/utils";
import {
OnSubmitTransferParams,
TransferModule,
} from "App/Transfer/TransferModule";
import { accountBalanceAtom, defaultAccountAtom } from "atoms/accounts";
import { chainAtom, chainParametersAtom } from "atoms/chain";
import { availableChainsAtom, chainRegistryAtom } from "atoms/integrations";
import BigNumber from "bignumber.js";
import { defaultAccountAtom } from "atoms/accounts";
import { namadaTransparentAssetsAtom } from "atoms/balance";
import { defaultGasConfigFamily } from "atoms/fees";
import {
availableChainsAtom,
chainRegistryAtom,
createIbcTxAtom,
} from "atoms/integrations";
import { useWalletManager } from "hooks/useWalletManager";
import { wallets } from "integrations";
import { KeplrWalletManager } from "integrations/Keplr";
import { useAtomValue } from "jotai";
import { useState } from "react";
import { broadcastTx } from "lib/query";
import { useEffect, useState } from "react";
import namadaChainRegistry from "registry/namada.json";
import { namadaAsset } from "registry/namadaAsset";
import { Address, AddressWithAssetAndAmountMap } from "types";
import { getSdkInstance } from "utils/sdk";
import { Address } from "types";
import { IbcTopHeader } from "./IbcTopHeader";

const defaultChainId = "cosmoshub-4";
const keplr = new KeplrWalletManager();
const namada = (window as WindowWithNamada).namada!;

export const IbcWithdraw: React.FC = () => {
const namadaAccount = useAtomValue(defaultAccountAtom);
const chainRegistry = useAtomValue(chainRegistryAtom);
const availableChains = useAtomValue(availableChainsAtom);
const namadaChainParams = useAtomValue(chainParametersAtom);
const namadaChain = useAtomValue(chainAtom);

const [selectedAssetAddress, setSelectedAssetAddress] = useState<Address>();

// TODO: remove hardcoding and display assets other than NAM
const availableAmount = useAtomValue(accountBalanceAtom).data;
const availableAssets: AddressWithAssetAndAmountMap =
availableAmount ?
{
[namadaAsset.address]: {
asset: namadaAsset,
originalAddress: namadaAsset.address,
amount: availableAmount,
},
}
: {};

const GAS_PRICE = BigNumber(0.000001); // 0.000001 NAM
const GAS_LIMIT = BigNumber(1_000_000);
const transactionFee = {
originalAddress: namadaAsset.address,
asset: namadaAsset,
amount: GAS_PRICE.multipliedBy(GAS_LIMIT),
};
const { data: availableAssets } = useAtomValue(namadaTransparentAssetsAtom);

const availableAmount = mapUndefined(
(address) => availableAssets?.[address]?.amount,
selectedAssetAddress
);

const { data: gasConfig } = useAtomValue(
defaultGasConfigFamily(["IbcTransfer"])
);

const transactionFee = mapUndefined(
({ gasLimit, gasPrice }) => ({
originalAddress: namadaAsset.address,
asset: namadaAsset,
amount: gasPrice.multipliedBy(gasLimit),
}),
gasConfig
);

const {
walletAddress: keplrAddress,
Expand All @@ -64,40 +63,68 @@ export const IbcWithdraw: React.FC = () => {
connectToChainId(chainId || defaultChainId);
};

const {
mutate: createIbcTx,
isSuccess,
isError,
error: ibcTxError,
data: ibcTxData,
} = useAtomValue(createIbcTxAtom);

// TODO: properly notify the user on error
useEffect(() => {
if (isError) {
console.error(ibcTxError);
}
}, [isError]);

useEffect(() => {
if (isSuccess) {
const { encodedTxData, signedTxs } = ibcTxData;
signedTxs.forEach((signedTx) =>
broadcastTx(
encodedTxData,
signedTx,
encodedTxData.meta?.props,
"IbcTransfer"
)
);
}
}, [isSuccess]);

const submitIbcTransfer = async ({
amount,
destinationAddress,
ibcOptions,
memo,
}: OnSubmitTransferParams): Promise<void> => {
const wrapperTxProps = {
token: namadaChain.data!.nativeTokenAddress,
feeAmount: GAS_PRICE,
gasLimit: GAS_LIMIT,
chainId: namadaChain.data!.chainId,
publicKey: namadaAccount.data!.publicKey,
};
const sdk = await getSdkInstance();
const tx = await sdk.tx.buildIbcTransfer(wrapperTxProps, {
source: namadaAccount.data!.address,
receiver: destinationAddress,
token: namadaChain.data!.nativeTokenAddress,
amount: amount!,
const selectedAsset = mapUndefined(
(address) => availableAssets?.[address],
selectedAssetAddress
);

if (typeof selectedAsset === "undefined") {
throw new Error("No selected asset");
}

const channelId = ibcOptions?.sourceChannel;
if (typeof channelId === "undefined") {
throw new Error("No channel ID is set");
}

if (typeof gasConfig === "undefined") {
throw new Error("No gas config");
}

createIbcTx({
destinationAddress,
token: selectedAsset,
amount,
portId: "transfer",
channelId: ibcOptions?.sourceChannel || "",
timeoutHeight: undefined,
timeoutSecOffset: undefined,
shieldingData: undefined,
channelId,
gasConfig,
memo,
});

const signedTxBytes = await namada.sign({
signer: namadaAccount.data!.address,
txs: [tx],
checksums: namadaChainParams.data!.checksums,
});

await sdk.rpc.broadcastTx(signedTxBytes![0], wrapperTxProps);
};

const onChangeChain = (chain: Chain): void => {
Expand Down
1 change: 1 addition & 0 deletions apps/namadillo/src/atoms/balance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./atoms";
28 changes: 2 additions & 26 deletions apps/namadillo/src/atoms/fees/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GasLimitTableInnerTxKindEnum as GasLimitTableIndexer } from "@anomaorg/namada-indexer-client";
import { defaultAccountAtom } from "atoms/accounts";
import { indexerApiAtom } from "atoms/api";
import { nativeTokenAddressAtom } from "atoms/chain";
Expand All @@ -8,33 +7,10 @@ import { atom } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
import { atomFamily } from "jotai/utils";
import { isPublicKeyRevealed } from "lib/query";
import { GasConfig, GasTable, TxKind } from "types";
import { GasConfig, GasTable } from "types";
import { TxKind } from "types/txKind";
import { fetchGasLimit, fetchMinimumGasPrice } from "./services";

// TODO: I think we should find a better solution for this
export const txKindFromIndexer = (
txKind: GasLimitTableIndexer
): TxKind | undefined => {
switch (txKind) {
case GasLimitTableIndexer.Bond:
return "Bond";
case GasLimitTableIndexer.Unbond:
return "Unbond";
case GasLimitTableIndexer.Redelegation:
return "Redelegate";
case GasLimitTableIndexer.Withdraw:
return "Withdraw";
case GasLimitTableIndexer.ClaimRewards:
return "ClaimRewards";
case GasLimitTableIndexer.VoteProposal:
return "VoteProposal";
case GasLimitTableIndexer.RevealPk:
return "RevealPk";
default:
return undefined;
}
};

export const gasCostTxKindAtom = atom<TxKind | undefined>(undefined);

export const gasLimitsAtom = atomWithQuery<GasTable>((get) => {
Expand Down
57 changes: 43 additions & 14 deletions apps/namadillo/src/atoms/fees/services.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
import { DefaultApi } from "@anomaorg/namada-indexer-client";
import {
DefaultApi,
GasLimitTableInnerTxKindEnum as GasLimitTableIndexer,
} from "@anomaorg/namada-indexer-client";
import { assertNever, mapUndefined } from "@namada/utils";
import BigNumber from "bignumber.js";
import invariant from "invariant";
import { GasTable } from "types";
import { txKindFromIndexer } from "./atoms";
import { TxKind, txKinds } from "types/txKind";

const txKindToIndexer = (txKind: TxKind): GasLimitTableIndexer => {
switch (txKind) {
case "Bond":
return GasLimitTableIndexer.Bond;
case "Unbond":
return GasLimitTableIndexer.Unbond;
case "Redelegate":
return GasLimitTableIndexer.Redelegation;
case "Withdraw":
return GasLimitTableIndexer.Withdraw;
case "ClaimRewards":
return GasLimitTableIndexer.ClaimRewards;
case "VoteProposal":
return GasLimitTableIndexer.VoteProposal;
case "RevealPk":
return GasLimitTableIndexer.RevealPk;
case "Unknown":
case "IbcTransfer": // TODO: don't use unknown once indexer is updated
return GasLimitTableIndexer.Unknown;
default:
return assertNever(txKind);
}
};

export const fetchGasLimit = async (api: DefaultApi): Promise<GasTable> => {
const gasTableResponse = await api.apiV1GasTokenGet("native");
const gasTable = gasTableResponse.data.reduce(
(acc, { gasLimit, txKind: indexerTxKind }) => {
const txKind = txKindFromIndexer(indexerTxKind);
if (txKind) {
const perKind = acc[txKind] || {};
acc[txKind] = { ...perKind, native: new BigNumber(gasLimit) };
}
return acc;
},
{} as GasTable
);

return gasTable;
return txKinds.reduce((acc, txKind) => {
const indexerTxKind = txKindToIndexer(txKind);
const gasLimit = gasTableResponse.data.find(
(entry) => entry.txKind === indexerTxKind
)?.gasLimit;
const maybeBigNumber = mapUndefined(BigNumber, gasLimit);

if (typeof maybeBigNumber === "undefined" || maybeBigNumber.isNaN()) {
throw new Error("Couldn't decode gas table");
} else {
return { ...acc, [txKind]: { native: maybeBigNumber } };
}
}, {} as GasTable);
};

export const fetchMinimumGasPrice = async (
Expand Down
58 changes: 56 additions & 2 deletions apps/namadillo/src/atoms/integrations/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { AssetList, Chain } from "@chain-registry/types";
import { ExtensionKey } from "@namada/types";
import { ExtensionKey, IbcTransferProps } from "@namada/types";
import { defaultAccountAtom } from "atoms/accounts";
import { chainAtom } from "atoms/chain";
import { settingsAtom } from "atoms/settings";
import { queryDependentFn } from "atoms/utils";
import BigNumber from "bignumber.js";
import { atom } from "jotai";
import { atomWithMutation, atomWithQuery } from "jotai-tanstack-query";
import { atomFamily, atomWithStorage } from "jotai/utils";
import { ChainId, ChainRegistryEntry } from "types";
import { TransactionPair } from "lib/query";
import {
AddressWithAsset,
ChainId,
ChainRegistryEntry,
GasConfig,
} from "types";
import {
createIbcTx,
getKnownChains,
ibcAddressToDenomTrace,
mapCoinsToAssets,
Expand Down Expand Up @@ -107,3 +117,47 @@ export const availableAssetsAtom = atom((get) => {
const settings = get(settingsAtom);
return getKnownChains(settings.enableTestnets).map(({ assets }) => assets);
});

type CreateIbcTxArgs = {
destinationAddress: string;
token: AddressWithAsset;
amount: BigNumber;
portId: string;
channelId: string;
memo?: string;
gasConfig: GasConfig;
};

export const createIbcTxAtom = atomWithMutation((get) => {
const account = get(defaultAccountAtom);
const chain = get(chainAtom);

return {
enabled: account.isSuccess && chain.isSuccess,
mutationKey: ["create-ibc-tx"],
mutationFn: async ({
destinationAddress,
token,
amount,
portId,
channelId,
memo,
gasConfig,
}: CreateIbcTxArgs): Promise<TransactionPair<IbcTransferProps>> => {
if (typeof account.data === "undefined") {
throw new Error("no account");
}
return createIbcTx(
account.data,
destinationAddress,
token,
amount,
portId,
channelId,
gasConfig,
chain.data!,
memo
);
},
};
});
Loading

0 comments on commit a54e752

Please sign in to comment.