Skip to content

Commit a47e2f7

Browse files
committed
feat: add basic form for IBC transfers to Namada (#1106)
This is a very rough first draft intended to be used as a starting point for building more IBC features.
1 parent 3741648 commit a47e2f7

File tree

7 files changed

+346
-117
lines changed

7 files changed

+346
-117
lines changed

apps/namadillo/src/App/AppRoutes.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Router } from "@remix-run/router";
2+
import { useAtomValue } from "jotai";
23
import {
34
Route,
45
Routes,
@@ -11,11 +12,14 @@ import { AccountOverview } from "./AccountOverview";
1112
import { App } from "./App";
1213
import { RouteErrorBoundary } from "./Common/RouteErrorBoundary";
1314
import { Governance } from "./Governance";
15+
import { Ibc } from "./Ibc";
1416
import { SettingsPanel } from "./Settings/SettingsPanel";
1517
import { Staking } from "./Staking";
1618
import { SwitchAccountPanel } from "./SwitchAccount/SwitchAccountPanel";
1719

20+
import { applicationFeaturesAtom } from "atoms/settings";
1821
import GovernanceRoutes from "./Governance/routes";
22+
import IbcRoutes from "./Ibc/routes";
1923
import SettingsRoutes from "./Settings/routes";
2024
import { SignMessages } from "./SignMessages/SignMessages";
2125
import MessageRoutes from "./SignMessages/routes";
@@ -28,6 +32,7 @@ import TransferRoutes from "./Transfer/routes";
2832
export const MainRoutes = (): JSX.Element => {
2933
const location = useLocation();
3034
const state = location.state as { backgroundLocation?: Location };
35+
const features = useAtomValue(applicationFeaturesAtom);
3136

3237
// Avoid animation being fired twice when navigating inside settings modal routes
3338
const settingsAnimationKey =
@@ -46,6 +51,9 @@ export const MainRoutes = (): JSX.Element => {
4651
element={<Governance />}
4752
/>
4853
<Route path={`${TransferRoutes.index()}/*`} element={<Transfer />} />
54+
{features.ibcTransfersEnabled && (
55+
<Route path={`${IbcRoutes.index()}/*`} element={<Ibc />} />
56+
)}
4957
</Route>
5058
</Routes>
5159
<Routes location={location} key={settingsAnimationKey}>

apps/namadillo/src/App/Common/Navigation.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SidebarMenuItem } from "App/Common/SidebarMenuItem";
22
import GovernanceRoutes from "App/Governance/routes";
33
import { MASPIcon } from "App/Icons/MASPIcon";
4+
import { useAtomValue } from "jotai";
45
import { AiFillHome } from "react-icons/ai";
56
import { BsDiscord, BsTwitterX } from "react-icons/bs";
67
import { FaVoteYea } from "react-icons/fa";
@@ -9,9 +10,13 @@ import { IoSwapHorizontal } from "react-icons/io5";
910
import { TbVectorTriangle } from "react-icons/tb";
1011
import { DISCORD_URL, TWITTER_URL } from "urls";
1112

13+
import IbcRoutes from "App/Ibc/routes";
1214
import StakingRoutes from "App/Staking/routes";
15+
import { applicationFeaturesAtom } from "atoms/settings";
1316

1417
export const Navigation = (): JSX.Element => {
18+
const features = useAtomValue(applicationFeaturesAtom);
19+
1520
const menuItems: { label: string; icon: React.ReactNode; url?: string }[] = [
1621
{
1722
label: "Overview",
@@ -39,6 +44,7 @@ export const Navigation = (): JSX.Element => {
3944
{
4045
label: "IBC Transfer",
4146
icon: <TbVectorTriangle />,
47+
url: features.ibcTransfersEnabled ? IbcRoutes.index() : undefined,
4248
},
4349
{
4450
label: "Transfer",

apps/namadillo/src/App/Ibc/Ibc.tsx

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { Coin } from "@cosmjs/launchpad";
2+
import { coin, coins } from "@cosmjs/proto-signing";
3+
import {
4+
QueryClient,
5+
SigningStargateClient,
6+
StargateClient,
7+
setupIbcExtension,
8+
} from "@cosmjs/stargate";
9+
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
10+
import { Window as KeplrWindow } from "@keplr-wallet/types";
11+
import { Panel } from "@namada/components";
12+
import { DerivedAccount, WindowWithNamada } from "@namada/types";
13+
import { useState } from "react";
14+
15+
const keplr = (window as KeplrWindow).keplr!;
16+
const namada = (window as WindowWithNamada).namada!;
17+
18+
const chain = "theta-testnet-001";
19+
const rpc = "https://rpc-t.cosmos.nodestake.top";
20+
21+
const buttonStyles = "bg-white my-2 p-2 block";
22+
23+
export const Ibc: React.FC = () => {
24+
const [error, setError] = useState("");
25+
const [address, setAddress] = useState("");
26+
const [alias, setAlias] = useState("");
27+
const [balances, setBalances] = useState<Coin[] | undefined>();
28+
const [namadaAccounts, setNamadaAccounts] = useState<DerivedAccount[]>();
29+
const [token, setToken] = useState("");
30+
const [target, setTarget] = useState("");
31+
const [amount, setAmount] = useState("");
32+
const [channelId, setChannelId] = useState("");
33+
34+
const withErrorReporting =
35+
(fn: () => Promise<void>): (() => Promise<void>) =>
36+
async () => {
37+
try {
38+
await fn();
39+
setError("");
40+
} catch (e) {
41+
// eslint-disable-next-line no-console
42+
console.log(e);
43+
setError(e instanceof Error ? e.message : "unknown error");
44+
}
45+
};
46+
47+
const getAddress = withErrorReporting(async () => {
48+
const key = await keplr.getKey(chain);
49+
setAddress(key.bech32Address);
50+
setAlias(key.name);
51+
});
52+
53+
const getBalances = withErrorReporting(async () => {
54+
setBalances(undefined);
55+
const balances = await queryBalances(address);
56+
setBalances(balances);
57+
});
58+
59+
const getNamadaAccounts = withErrorReporting(async () => {
60+
const accounts = await namada.accounts();
61+
setNamadaAccounts(accounts);
62+
});
63+
64+
const submitIbcTransfer = withErrorReporting(async () =>
65+
submitBridgeTransfer(rpc, chain, address, target, token, amount, channelId)
66+
);
67+
68+
return (
69+
<Panel title="IBC" className="mb-2 bg-[#999999] text-black">
70+
{/* Error */}
71+
<p className="text-[#ff0000]">{error}</p>
72+
73+
<hr />
74+
75+
{/* Keplr addresses */}
76+
<h3>Keplr addresses</h3>
77+
<button className={buttonStyles} onClick={getAddress}>
78+
get address
79+
</button>
80+
<p>
81+
{alias} {address}
82+
</p>
83+
84+
<hr />
85+
86+
{/* Balances */}
87+
<h3>Balances</h3>
88+
<button className={buttonStyles} onClick={getBalances}>
89+
get balances
90+
</button>
91+
{balances?.map(({ denom, amount }) => (
92+
<div key={denom}>
93+
<label>
94+
<input
95+
type="radio"
96+
name="token"
97+
value={denom}
98+
checked={token === denom}
99+
onChange={(e) => setToken(e.target.value)}
100+
/>
101+
{denom} {amount}
102+
</label>
103+
</div>
104+
))}
105+
106+
<hr />
107+
108+
{/* Namada accounts */}
109+
<h3>Namada accounts</h3>
110+
<button className={buttonStyles} onClick={getNamadaAccounts}>
111+
get namada accounts
112+
</button>
113+
114+
{namadaAccounts?.map(({ alias, address }) => (
115+
<div key={address}>
116+
<label>
117+
<input
118+
type="radio"
119+
name="target"
120+
value={address}
121+
checked={target === address}
122+
onChange={(e) => setTarget(e.target.value)}
123+
/>
124+
{alias} {address}
125+
</label>
126+
</div>
127+
))}
128+
129+
<hr />
130+
131+
{/* Amount to send */}
132+
<h3>Amount to send</h3>
133+
<input value={amount} onChange={(e) => setAmount(e.target.value)} />
134+
135+
<hr />
136+
137+
{/* Channel ID */}
138+
<h3>Channel ID</h3>
139+
<input value={channelId} onChange={(e) => setChannelId(e.target.value)} />
140+
141+
<hr />
142+
143+
{/* Submit IBC transfer */}
144+
<h3>Submit IBC transfer</h3>
145+
<button className={buttonStyles} onClick={submitIbcTransfer}>
146+
submit IBC transfer
147+
</button>
148+
</Panel>
149+
);
150+
};
151+
152+
const queryBalances = async (owner: string): Promise<Coin[]> => {
153+
const client = await StargateClient.connect(rpc);
154+
const balances = (await client.getAllBalances(owner)) || [];
155+
156+
await Promise.all(
157+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
158+
balances.map(async (coin: any) => {
159+
// any becuse of annoying readonly
160+
if (coin.denom.startsWith("ibc/")) {
161+
coin.denom = await ibcAddressToDenom(coin.denom);
162+
}
163+
})
164+
);
165+
166+
return [...balances];
167+
};
168+
169+
const ibcAddressToDenom = async (address: string): Promise<string> => {
170+
const tmClient = await Tendermint34Client.connect(rpc);
171+
const queryClient = new QueryClient(tmClient);
172+
const ibcExtension = setupIbcExtension(queryClient);
173+
174+
const ibcHash = address.replace("ibc/", "");
175+
const { denomTrace } = await ibcExtension.ibc.transfer.denomTrace(ibcHash);
176+
const baseDenom = denomTrace?.baseDenom;
177+
178+
if (typeof baseDenom === "undefined") {
179+
throw new Error("couldn't get denom from ibc address");
180+
}
181+
182+
return baseDenom;
183+
};
184+
185+
const submitBridgeTransfer = async (
186+
rpc: string,
187+
sourceChainId: string,
188+
source: string,
189+
target: string,
190+
token: string,
191+
amount: string,
192+
channelId: string
193+
): Promise<void> => {
194+
const client = await SigningStargateClient.connectWithSigner(
195+
rpc,
196+
keplr.getOfflineSigner(sourceChainId),
197+
{
198+
broadcastPollIntervalMs: 300,
199+
broadcastTimeoutMs: 8_000,
200+
}
201+
);
202+
203+
const fee = {
204+
amount: coins("0", token),
205+
gas: "222000",
206+
};
207+
208+
const response = await client.sendIbcTokens(
209+
source,
210+
target,
211+
coin(amount, token),
212+
"transfer",
213+
channelId,
214+
undefined, // timeout height
215+
Math.floor(Date.now() / 1000) + 60, // timeout timestamp
216+
fee,
217+
`${sourceChainId}->Namada`
218+
);
219+
220+
if (response.code !== 0) {
221+
throw new Error(response.code + " " + response.rawLog);
222+
}
223+
};

apps/namadillo/src/App/Ibc/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Ibc } from "./Ibc";

apps/namadillo/src/App/Ibc/routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const index = (): string => `/ibc`;
2+
3+
export default {
4+
index,
5+
};

packages/integrations/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@
4141
},
4242
"dependencies": {
4343
"@cosmjs/launchpad": "^0.27.1",
44-
"@cosmjs/proto-signing": "^0.27.1",
45-
"@cosmjs/stargate": "^0.29.5",
46-
"@cosmjs/tendermint-rpc": "~0.29.5",
44+
"@cosmjs/proto-signing": "^0.32.4",
45+
"@cosmjs/stargate": "^0.32.4",
46+
"@cosmjs/tendermint-rpc": "^0.32.4",
4747
"@keplr-wallet/types": "^0.10.19",
4848
"@metamask/providers": "^10.2.1",
4949
"ethers": "^6.7.1"

0 commit comments

Comments
 (0)