Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rough form for IBC withdrawals #1133

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 29 additions & 212 deletions apps/namadillo/src/App/Ibc/Ibc.tsx
Original file line number Diff line number Diff line change
@@ -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<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 [selectedTab, setSelectedTab] = useState<Tab>(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" ? <IbcToNamada />
: selectedTab === "namada->cosmos" ? <IbcFromNamada />
: assertNever(selectedTab);

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,
}
<>
<Panel>
{tabs.map((tab) => (
<div key={tab}>
<label>
<input
type="radio"
value={tab}
checked={selectedTab === tab}
onChange={(e) => setSelectedTab(e.target.value as Tab)}
></input>
{tab}
</label>
</div>
))}
</Panel>

{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);
}
};
Loading
Loading