diff --git a/apps/namadillo/src/App/Ibc/Ibc.tsx b/apps/namadillo/src/App/Ibc/Ibc.tsx index e3be5b628..b6ef52911 100644 --- a/apps/namadillo/src/App/Ibc/Ibc.tsx +++ b/apps/namadillo/src/App/Ibc/Ibc.tsx @@ -1,223 +1,40 @@ -import { Coin } from "@cosmjs/launchpad"; -import { coin, coins } from "@cosmjs/proto-signing"; -import { - QueryClient, - SigningStargateClient, - StargateClient, - setupIbcExtension, -} from "@cosmjs/stargate"; -import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; -import { Window as KeplrWindow } from "@keplr-wallet/types"; import { Panel } from "@namada/components"; -import { DerivedAccount, WindowWithNamada } from "@namada/types"; +import { assertNever } from "@namada/utils"; import { useState } from "react"; -const keplr = (window as KeplrWindow).keplr!; -const namada = (window as WindowWithNamada).namada!; +import { IbcFromNamada } from "./IbcFromNamada"; +import { IbcToNamada } from "./IbcToNamada"; -const chain = "theta-testnet-001"; -const rpc = "https://rpc-t.cosmos.nodestake.top"; - -const buttonStyles = "bg-white my-2 p-2 block"; +const tabs = ["cosmos->namada", "namada->cosmos"] as const; +type Tab = (typeof tabs)[number]; export const Ibc: React.FC = () => { - const [error, setError] = useState(""); - const [address, setAddress] = useState(""); - const [alias, setAlias] = useState(""); - const [balances, setBalances] = useState(); - const [namadaAccounts, setNamadaAccounts] = useState(); - const [token, setToken] = useState(""); - const [target, setTarget] = useState(""); - const [amount, setAmount] = useState(""); - const [channelId, setChannelId] = useState(""); - - const withErrorReporting = - (fn: () => Promise): (() => Promise) => - async () => { - try { - await fn(); - setError(""); - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - setError(e instanceof Error ? e.message : "unknown error"); - } - }; - - const getAddress = withErrorReporting(async () => { - const key = await keplr.getKey(chain); - setAddress(key.bech32Address); - setAlias(key.name); - }); + const [selectedTab, setSelectedTab] = useState(tabs[0]); - const getBalances = withErrorReporting(async () => { - setBalances(undefined); - const balances = await queryBalances(address); - setBalances(balances); - }); - - const getNamadaAccounts = withErrorReporting(async () => { - const accounts = await namada.accounts(); - setNamadaAccounts(accounts); - }); - - const submitIbcTransfer = withErrorReporting(async () => - submitBridgeTransfer(rpc, chain, address, target, token, amount, channelId) - ); + const page = + selectedTab === "cosmos->namada" ? + : selectedTab === "namada->cosmos" ? + : assertNever(selectedTab); return ( - - {/* Error */} -

{error}

- -
- - {/* Keplr addresses */} -

Keplr addresses

- -

- {alias} {address} -

- -
- - {/* Balances */} -

Balances

- - {balances?.map(({ denom, amount }) => ( -
- -
- ))} - -
- - {/* Namada accounts */} -

Namada accounts

- - - {namadaAccounts?.map(({ alias, address }) => ( -
- -
- ))} - -
- - {/* Amount to send */} -

Amount to send

- setAmount(e.target.value)} /> - -
- - {/* Channel ID */} -

Channel ID

- setChannelId(e.target.value)} /> - -
- - {/* Submit IBC transfer */} -

Submit IBC transfer

- -
- ); -}; - -const queryBalances = async (owner: string): Promise => { - const client = await StargateClient.connect(rpc); - const balances = (await client.getAllBalances(owner)) || []; - - await Promise.all( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - balances.map(async (coin: any) => { - // any becuse of annoying readonly - if (coin.denom.startsWith("ibc/")) { - coin.denom = await ibcAddressToDenom(coin.denom); - } - }) - ); - - return [...balances]; -}; - -const ibcAddressToDenom = async (address: string): Promise => { - const tmClient = await Tendermint34Client.connect(rpc); - const queryClient = new QueryClient(tmClient); - const ibcExtension = setupIbcExtension(queryClient); - - const ibcHash = address.replace("ibc/", ""); - const { denomTrace } = await ibcExtension.ibc.transfer.denomTrace(ibcHash); - const baseDenom = denomTrace?.baseDenom; - - if (typeof baseDenom === "undefined") { - throw new Error("couldn't get denom from ibc address"); - } - - return baseDenom; -}; - -const submitBridgeTransfer = async ( - rpc: string, - sourceChainId: string, - source: string, - target: string, - token: string, - amount: string, - channelId: string -): Promise => { - const client = await SigningStargateClient.connectWithSigner( - rpc, - keplr.getOfflineSigner(sourceChainId), - { - broadcastPollIntervalMs: 300, - broadcastTimeoutMs: 8_000, - } + <> + + {tabs.map((tab) => ( +
+ +
+ ))} +
+ + {page} + ); - - const fee = { - amount: coins("0", token), - gas: "222000", - }; - - const response = await client.sendIbcTokens( - source, - target, - coin(amount, token), - "transfer", - channelId, - undefined, // timeout height - Math.floor(Date.now() / 1000) + 60, // timeout timestamp - fee, - `${sourceChainId}->Namada` - ); - - if (response.code !== 0) { - throw new Error(response.code + " " + response.rawLog); - } }; diff --git a/apps/namadillo/src/App/Ibc/IbcFromNamada.tsx b/apps/namadillo/src/App/Ibc/IbcFromNamada.tsx new file mode 100644 index 000000000..83c869840 --- /dev/null +++ b/apps/namadillo/src/App/Ibc/IbcFromNamada.tsx @@ -0,0 +1,162 @@ +import { Key as KeplrKey, Window as KeplrWindow } from "@keplr-wallet/types"; +import { Panel } from "@namada/components"; +import { WindowWithNamada } from "@namada/types"; +import BigNumber from "bignumber.js"; +import { getSdkInstance } from "hooks"; +import { useAtomValue } from "jotai"; +import { useEffect, useState } from "react"; + +import { NamCurrency } from "App/Common/NamCurrency"; +import { NamInput } from "App/Common/NamInput"; +import { accountBalanceAtom, defaultAccountAtom } from "atoms/accounts"; +import { chainAtom, chainParametersAtom } from "atoms/chain"; +import { getFirstError } from "atoms/utils"; + +const keplr = (window as KeplrWindow).keplr!; +const namada = (window as WindowWithNamada).namada!; + +const keplrChain = "theta-testnet-001"; + +const buttonStyles = "bg-white my-2 p-2 block"; + +export const IbcFromNamada: React.FC = () => { + const [error, setError] = useState(""); + const [keplrAccount, setKeplrAccount] = useState(); + const [amount, setAmount] = useState(); + const [channel, setChannel] = useState(""); + + const namadaAccount = useAtomValue(defaultAccountAtom); + const balance = useAtomValue(accountBalanceAtom); + const namadaChain = useAtomValue(chainAtom); + const namadaChainParams = useAtomValue(chainParametersAtom); + + useEffect(() => { + const error = getFirstError( + namadaAccount, + balance, + namadaChain, + namadaChainParams + ); + setError(error ? error.message : ""); + }, [ + namadaAccount.isError, + balance.isError, + namadaChain.isError, + namadaChainParams.isError, + ]); + + const withErrorReporting = + (fn: () => Promise): (() => Promise) => + async () => { + try { + await fn(); + setError(""); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e); + setError(e instanceof Error ? e.message : "unknown error"); + } + }; + + const getKeplrAccount = withErrorReporting(async () => { + const key = await keplr.getKey(keplrChain); + setKeplrAccount(key); + }); + + const submitIbcTransfer = withErrorReporting(async () => { + const wrapperTxProps = { + token: namadaChain.data!.nativeTokenAddress, + feeAmount: BigNumber(0), + gasLimit: BigNumber(20_000), + chainId: namadaChain.data!.chainId, + publicKey: namadaAccount.data!.publicKey, + memo: `Namada->${keplrChain}`, + }; + + const sdk = await getSdkInstance(); + const tx = await sdk.tx.buildIbcTransfer(wrapperTxProps, { + source: namadaAccount.data!.address, + receiver: keplrAccount!.bech32Address, + token: namadaChain.data!.nativeTokenAddress, + amount: amount!, + portId: "transfer", + channelId: channel, + timeoutHeight: undefined, + timeoutSecOffset: undefined, + memo: `Namada->${keplrChain}`, // how is this different from the wrapper memo? + shieldingData: undefined, + }); + + const signedTxBytes = await namada.sign({ + signer: namadaAccount.data!.address, + txs: [tx], + checksums: namadaChainParams.data!.checksums, + }); + + await sdk.rpc.broadcastTx(signedTxBytes![0], wrapperTxProps); + }); + + return ( + + {/* Error */} +

{error}

+ +
+ + {/* Namada account */} +

Namada account

+ {namadaAccount.isSuccess && typeof namadaAccount.data !== "undefined" && ( +

+ {namadaAccount.data.alias} {namadaAccount.data.address} +

+ )} + +
+ + {/* Balance */} +

Balance

+ {balance.isSuccess && } + +
+ + {/* Keplr address */} +

Keplr address

+ + + {keplrAccount && ( +

+ {keplrAccount.name} {keplrAccount.bech32Address} +

+ )} + +
+ + {/* Amount */} +

Amount to send

+ setAmount(e.target.value)} /> + +
+ + {/* Channel */} +

Channel

+ setChannel(e.target.value)} + /> + +
+ + {/* Submit IBC transfer */} +

Submit IBC transfer

+ +
+ ); +}; diff --git a/apps/namadillo/src/App/Ibc/IbcToNamada.tsx b/apps/namadillo/src/App/Ibc/IbcToNamada.tsx new file mode 100644 index 000000000..0ce1f1f32 --- /dev/null +++ b/apps/namadillo/src/App/Ibc/IbcToNamada.tsx @@ -0,0 +1,226 @@ +import { Coin } from "@cosmjs/launchpad"; +import { coin, coins } from "@cosmjs/proto-signing"; +import { + QueryClient, + SigningStargateClient, + StargateClient, + setupIbcExtension, +} from "@cosmjs/stargate"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { Window as KeplrWindow } from "@keplr-wallet/types"; +import { Panel } from "@namada/components"; +import { DerivedAccount, WindowWithNamada } from "@namada/types"; +import { useState } from "react"; + +const keplr = (window as KeplrWindow).keplr!; +const namada = (window as WindowWithNamada).namada!; + +const chain = "theta-testnet-001"; +const rpc = "https://rpc-t.cosmos.nodestake.top"; + +const buttonStyles = "bg-white my-2 p-2 block"; + +export const IbcToNamada: React.FC = () => { + const [error, setError] = useState(""); + const [address, setAddress] = useState(""); + const [alias, setAlias] = useState(""); + const [balances, setBalances] = useState(); + const [namadaAccounts, setNamadaAccounts] = useState(); + const [token, setToken] = useState(""); + const [target, setTarget] = useState(""); + const [amount, setAmount] = useState(""); + const [channelId, setChannelId] = useState(""); + + const withErrorReporting = + (fn: () => Promise): (() => Promise) => + async () => { + try { + await fn(); + setError(""); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e); + setError(e instanceof Error ? e.message : "unknown error"); + } + }; + + const getAddress = withErrorReporting(async () => { + const key = await keplr.getKey(chain); + setAddress(key.bech32Address); + setAlias(key.name); + }); + + const getBalances = withErrorReporting(async () => { + setBalances(undefined); + const balances = await queryBalances(address); + setBalances(balances); + }); + + const getNamadaAccounts = withErrorReporting(async () => { + const accounts = await namada.accounts(); + setNamadaAccounts(accounts); + }); + + const submitIbcTransfer = withErrorReporting(async () => + submitBridgeTransfer(rpc, chain, address, target, token, amount, channelId) + ); + + return ( + + {/* Error */} +

{error}

+ +
+ + {/* Keplr addresses */} +

Keplr addresses

+ +

+ {alias} {address} +

+ +
+ + {/* Balances */} +

Balances

+ + {balances?.map(({ denom, amount }) => ( +
+ +
+ ))} + +
+ + {/* Namada accounts */} +

Namada accounts

+ + + {namadaAccounts?.map(({ alias, address }) => ( +
+ +
+ ))} + +
+ + {/* Amount to send */} +

Amount to send

+ setAmount(e.target.value)} /> + +
+ + {/* Channel ID */} +

Channel ID

+ setChannelId(e.target.value)} /> + +
+ + {/* Submit IBC transfer */} +

Submit IBC transfer

+ +
+ ); +}; + +const queryBalances = async (owner: string): Promise => { + const client = await StargateClient.connect(rpc); + const balances = (await client.getAllBalances(owner)) || []; + + await Promise.all( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + balances.map(async (coin: any) => { + // any becuse of annoying readonly + if (coin.denom.startsWith("ibc/")) { + coin.denom = await ibcAddressToDenom(coin.denom); + } + }) + ); + + return [...balances]; +}; + +const ibcAddressToDenom = async (address: string): Promise => { + const tmClient = await Tendermint34Client.connect(rpc); + const queryClient = new QueryClient(tmClient); + const ibcExtension = setupIbcExtension(queryClient); + + const ibcHash = address.replace("ibc/", ""); + const { denomTrace } = await ibcExtension.ibc.transfer.denomTrace(ibcHash); + const baseDenom = denomTrace?.baseDenom; + + if (typeof baseDenom === "undefined") { + throw new Error("couldn't get denom from ibc address"); + } + + return baseDenom; +}; + +const submitBridgeTransfer = async ( + rpc: string, + sourceChainId: string, + source: string, + target: string, + token: string, + amount: string, + channelId: string +): Promise => { + const client = await SigningStargateClient.connectWithSigner( + rpc, + keplr.getOfflineSigner(sourceChainId), + { + broadcastPollIntervalMs: 300, + broadcastTimeoutMs: 8_000, + } + ); + + const fee = { + amount: coins("0", token), + gas: "222000", + }; + + const response = await client.sendIbcTokens( + source, + target, + coin(amount, token), + "transfer", + channelId, + undefined, // timeout height + Math.floor(Date.now() / 1000) + 60, // timeout timestamp + fee, + `${sourceChainId}->Namada` + ); + + if (response.code !== 0) { + throw new Error(response.code + " " + response.rawLog); + } +};