Skip to content

Commit

Permalink
feat: add basic form for IBC transfers to Namada (#1106)
Browse files Browse the repository at this point in the history
This is a very rough first draft intended to be used as a starting point
for building more IBC features.
  • Loading branch information
emccorson committed Sep 18, 2024
1 parent 1c2c1aa commit 006a07e
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 117 deletions.
8 changes: 8 additions & 0 deletions apps/namadillo/src/App/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Router } from "@remix-run/router";
import { useAtomValue } from "jotai";
import {
Route,
Routes,
Expand All @@ -11,11 +12,14 @@ import { AccountOverview } from "./AccountOverview";
import { App } from "./App";
import { RouteErrorBoundary } from "./Common/RouteErrorBoundary";
import { Governance } from "./Governance";
import { Ibc } from "./Ibc";
import { SettingsPanel } from "./Settings/SettingsPanel";
import { Staking } from "./Staking";
import { SwitchAccountPanel } from "./SwitchAccount/SwitchAccountPanel";

import { applicationFeaturesAtom } from "atoms/settings";
import GovernanceRoutes from "./Governance/routes";
import IbcRoutes from "./Ibc/routes";
import SettingsRoutes from "./Settings/routes";
import { SignMessages } from "./SignMessages/SignMessages";
import MessageRoutes from "./SignMessages/routes";
Expand All @@ -28,6 +32,7 @@ import TransferRoutes from "./Transfer/routes";
export const MainRoutes = (): JSX.Element => {
const location = useLocation();
const state = location.state as { backgroundLocation?: Location };
const features = useAtomValue(applicationFeaturesAtom);

// Avoid animation being fired twice when navigating inside settings modal routes
const settingsAnimationKey =
Expand All @@ -46,6 +51,9 @@ export const MainRoutes = (): JSX.Element => {
element={<Governance />}
/>
<Route path={`${TransferRoutes.index()}/*`} element={<Transfer />} />
{features.ibcTransfersEnabled && (
<Route path={`${IbcRoutes.index()}/*`} element={<Ibc />} />
)}
</Route>
</Routes>
<Routes location={location} key={settingsAnimationKey}>
Expand Down
6 changes: 6 additions & 0 deletions apps/namadillo/src/App/Common/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SidebarMenuItem } from "App/Common/SidebarMenuItem";
import GovernanceRoutes from "App/Governance/routes";
import { MASPIcon } from "App/Icons/MASPIcon";
import { useAtomValue } from "jotai";
import { AiFillHome } from "react-icons/ai";
import { BsDiscord, BsTwitterX } from "react-icons/bs";
import { FaVoteYea } from "react-icons/fa";
Expand All @@ -9,9 +10,13 @@ import { IoSwapHorizontal } from "react-icons/io5";
import { TbVectorTriangle } from "react-icons/tb";
import { DISCORD_URL, TWITTER_URL } from "urls";

import IbcRoutes from "App/Ibc/routes";
import StakingRoutes from "App/Staking/routes";
import { applicationFeaturesAtom } from "atoms/settings";

export const Navigation = (): JSX.Element => {
const features = useAtomValue(applicationFeaturesAtom);

const menuItems: { label: string; icon: React.ReactNode; url?: string }[] = [
{
label: "Overview",
Expand Down Expand Up @@ -39,6 +44,7 @@ export const Navigation = (): JSX.Element => {
{
label: "IBC Transfer",
icon: <TbVectorTriangle />,
url: features.ibcTransfersEnabled ? IbcRoutes.index() : undefined,
},
{
label: "Transfer",
Expand Down
223 changes: 223 additions & 0 deletions apps/namadillo/src/App/Ibc/Ibc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
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 Ibc: React.FC = () => {
const [error, setError] = useState("");
const [address, setAddress] = useState("");
const [alias, setAlias] = useState("");
const [balances, setBalances] = useState<Coin[] | undefined>();
const [namadaAccounts, setNamadaAccounts] = useState<DerivedAccount[]>();
const [token, setToken] = useState("");
const [target, setTarget] = useState("");
const [amount, setAmount] = useState("");
const [channelId, setChannelId] = useState("");

const withErrorReporting =
(fn: () => Promise<void>): (() => Promise<void>) =>
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 (
<Panel title="IBC" className="mb-2 bg-[#999999] text-black">
{/* Error */}
<p className="text-[#ff0000]">{error}</p>

<hr />

{/* Keplr addresses */}
<h3>Keplr addresses</h3>
<button className={buttonStyles} onClick={getAddress}>
get address
</button>
<p>
{alias} {address}
</p>

<hr />

{/* Balances */}
<h3>Balances</h3>
<button className={buttonStyles} onClick={getBalances}>
get balances
</button>
{balances?.map(({ denom, amount }) => (
<div key={denom}>
<label>
<input
type="radio"
name="token"
value={denom}
checked={token === denom}
onChange={(e) => setToken(e.target.value)}
/>
{denom} {amount}
</label>
</div>
))}

<hr />

{/* Namada accounts */}
<h3>Namada accounts</h3>
<button className={buttonStyles} onClick={getNamadaAccounts}>
get namada accounts
</button>

{namadaAccounts?.map(({ alias, address }) => (
<div key={address}>
<label>
<input
type="radio"
name="target"
value={address}
checked={target === address}
onChange={(e) => setTarget(e.target.value)}
/>
{alias} {address}
</label>
</div>
))}

<hr />

{/* Amount to send */}
<h3>Amount to send</h3>
<input value={amount} onChange={(e) => setAmount(e.target.value)} />

<hr />

{/* Channel ID */}
<h3>Channel ID</h3>
<input value={channelId} onChange={(e) => setChannelId(e.target.value)} />

<hr />

{/* Submit IBC transfer */}
<h3>Submit IBC transfer</h3>
<button className={buttonStyles} onClick={submitIbcTransfer}>
submit IBC transfer
</button>
</Panel>
);
};

const queryBalances = async (owner: string): Promise<Coin[]> => {
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<string> => {
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<void> => {
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);
}
};
1 change: 1 addition & 0 deletions apps/namadillo/src/App/Ibc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Ibc } from "./Ibc";
5 changes: 5 additions & 0 deletions apps/namadillo/src/App/Ibc/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const index = (): string => `/ibc`;

export default {
index,
};
6 changes: 3 additions & 3 deletions packages/integrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
},
"dependencies": {
"@cosmjs/launchpad": "^0.27.1",
"@cosmjs/proto-signing": "^0.27.1",
"@cosmjs/stargate": "^0.29.5",
"@cosmjs/tendermint-rpc": "~0.29.5",
"@cosmjs/proto-signing": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@cosmjs/tendermint-rpc": "^0.32.4",
"@keplr-wallet/types": "^0.10.19",
"@metamask/providers": "^10.2.1",
"ethers": "^6.7.1"
Expand Down
Loading

0 comments on commit 006a07e

Please sign in to comment.