diff --git a/apps/faucet/package.json b/apps/faucet/package.json index a7bf8b423..5fa2c17fd 100644 --- a/apps/faucet/package.json +++ b/apps/faucet/package.json @@ -19,7 +19,6 @@ "@cosmjs/encoding": "^0.29.0", "buffer": "^6.0.3", "dompurify": "^3.0.5", - "ethers": "6.7.1", "framer-motion": "^11.5.4", "node-forge": "^1.3.1", "react": "^18.3.0", diff --git a/apps/namadillo/package.json b/apps/namadillo/package.json index 9432e8996..af441cf42 100644 --- a/apps/namadillo/package.json +++ b/apps/namadillo/package.json @@ -16,7 +16,6 @@ "bignumber.js": "^9.1.1", "clsx": "^2.1.1", "crypto-browserify": "^3.12.0", - "ethers": "^6.7.1", "fp-ts": "^2.16.1", "framer-motion": "^11.3.28", "idb-keyval": "^6.2.1", diff --git a/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx b/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx index a81fc0d57..1214e7f84 100644 --- a/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx +++ b/apps/namadillo/src/App/AccountOverview/AccountOverview.tsx @@ -8,6 +8,7 @@ import { applicationFeaturesAtom, namadaExtensionConnectedAtom, } from "atoms/settings"; +import { useUserHasAccount } from "hooks/useUserHasAccount"; import { useAtomValue } from "jotai"; import { twMerge } from "tailwind-merge"; import { AccountBalanceContainer } from "./AccountBalanceContainer"; @@ -16,16 +17,17 @@ import { NavigationFooter } from "./NavigationFooter"; export const AccountOverview = (): JSX.Element => { const isConnected = useAtomValue(namadaExtensionConnectedAtom); + const hasAccount = useUserHasAccount(); const { claimRewardsEnabled, maspEnabled } = useAtomValue( applicationFeaturesAtom ); - const showSidebar = isConnected; + const showSidebar = isConnected && hasAccount !== undefined; return (
- {!isConnected && ( + {(!isConnected || hasAccount === false) && (
@@ -33,7 +35,7 @@ export const AccountOverview = (): JSX.Element => {
)} - {isConnected && !claimRewardsEnabled && ( + {isConnected && hasAccount && !claimRewardsEnabled && (
@@ -42,7 +44,7 @@ export const AccountOverview = (): JSX.Element => {
)} - {isConnected && claimRewardsEnabled && ( + {isConnected && hasAccount && claimRewardsEnabled && (
diff --git a/apps/namadillo/src/App/AppRoutes.tsx b/apps/namadillo/src/App/AppRoutes.tsx index 86837cc87..722a19819 100644 --- a/apps/namadillo/src/App/AppRoutes.tsx +++ b/apps/namadillo/src/App/AppRoutes.tsx @@ -1,4 +1,5 @@ import { Router } from "@remix-run/router"; +import { useAtomValue } from "jotai"; import { Route, Routes, @@ -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"; @@ -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 = @@ -46,6 +51,9 @@ export const MainRoutes = (): JSX.Element => { element={} /> } /> + {features.ibcTransfersEnabled && ( + } /> + )} diff --git a/apps/namadillo/src/App/Common/ConnectExtensionButton.tsx b/apps/namadillo/src/App/Common/ConnectExtensionButton.tsx index 3098cd12d..9934178cd 100644 --- a/apps/namadillo/src/App/Common/ConnectExtensionButton.tsx +++ b/apps/namadillo/src/App/Common/ConnectExtensionButton.tsx @@ -13,7 +13,7 @@ export const ConnectExtensionButton = (): JSX.Element => { <> {extensionAttachStatus === "attached" && !isExtensionConnected && ( - Connect Extension + Connect Keychain )} {extensionAttachStatus === "detached" && ( @@ -24,7 +24,7 @@ export const ConnectExtensionButton = (): JSX.Element => { backgroundColor="yellow" size="sm" > - Download Extension + Download Keychain )} diff --git a/apps/namadillo/src/App/Common/Intro.tsx b/apps/namadillo/src/App/Common/Intro.tsx index 471bb9a7f..122c81c67 100644 --- a/apps/namadillo/src/App/Common/Intro.tsx +++ b/apps/namadillo/src/App/Common/Intro.tsx @@ -13,7 +13,7 @@ export const Intro = (): JSX.Element => { "uppercase text-center font-medium text-yellow leading-10 text-4xl" )} > - Your Gateway to Shielded Multichain + Your Gateway to the Shielded Multichain
@@ -25,7 +25,7 @@ export const Intro = (): JSX.Element => { size="sm" outlineColor="yellow" > - Help + Community Help
diff --git a/apps/namadillo/src/App/Governance/GovernanceOverview.tsx b/apps/namadillo/src/App/Governance/GovernanceOverview.tsx index bec8443d2..8830604b2 100644 --- a/apps/namadillo/src/App/Governance/GovernanceOverview.tsx +++ b/apps/namadillo/src/App/Governance/GovernanceOverview.tsx @@ -8,6 +8,7 @@ import { atomsAreLoaded, useNotifyOnAtomError, } from "atoms/utils"; +import { useUserHasAccount } from "hooks/useUserHasAccount"; import { useAtomValue } from "jotai"; import { AllProposalsTable } from "./AllProposalsTable"; import { LiveGovernanceProposals } from "./LiveGovernanceProposals"; @@ -19,10 +20,11 @@ export const GovernanceOverview: React.FC = () => { const isConnected = useAtomValue(namadaExtensionConnectedAtom); const allProposals = useAtomValue(allProposalsAtom); const votedProposals = useAtomValue(votedProposalsAtom); + const hasAccount = useUserHasAccount(); // TODO: is there a better way than this to show that votedProposalIdsAtom // is dependent on isConnected? - const extensionAtoms = isConnected ? [votedProposals] : []; + const extensionAtoms = isConnected && hasAccount ? [votedProposals] : []; const activeAtoms = [allProposals, ...extensionAtoms]; const liveProposals = @@ -44,6 +46,9 @@ export const GovernanceOverview: React.FC = () => { {!isConnected && ( )} + {isConnected && hasAccount === false && ( + + )} ; - vote: AtomWithQueryResult; + vote: AtomWithQueryResult; proposalId: bigint; }> = ({ proposal, vote, proposalId }) => { const navigate = useNavigate(); @@ -317,7 +317,7 @@ const VoteButton: React.FC<{ const disabled = !isExtensionConnected || !canVote.data || status !== "ongoing"; - const voted = typeof vote.data !== "undefined"; + const voted = vote.data !== null; const text = voted ? "Edit Vote" : "Vote"; return { @@ -344,9 +344,9 @@ const VoteButton: React.FC<{ }; const VotedLabel: React.FC<{ - vote: AtomWithQueryResult; + vote: AtomWithQueryResult; }> = ({ vote }) => { - if (vote.isSuccess && typeof vote.data !== "undefined") { + if (vote.isSuccess && vote.data !== null) { return ( ); diff --git a/apps/namadillo/src/App/Ibc/Ibc.tsx b/apps/namadillo/src/App/Ibc/Ibc.tsx new file mode 100644 index 000000000..e3be5b628 --- /dev/null +++ b/apps/namadillo/src/App/Ibc/Ibc.tsx @@ -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(); + 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); + } +}; diff --git a/apps/namadillo/src/App/Ibc/index.ts b/apps/namadillo/src/App/Ibc/index.ts new file mode 100644 index 000000000..863deea33 --- /dev/null +++ b/apps/namadillo/src/App/Ibc/index.ts @@ -0,0 +1 @@ +export { Ibc } from "./Ibc"; diff --git a/apps/namadillo/src/App/Ibc/routes.ts b/apps/namadillo/src/App/Ibc/routes.ts new file mode 100644 index 000000000..369def705 --- /dev/null +++ b/apps/namadillo/src/App/Ibc/routes.ts @@ -0,0 +1,5 @@ +export const index = (): string => `/ibc`; + +export default { + index, +}; diff --git a/apps/namadillo/src/App/Layout/Navigation.tsx b/apps/namadillo/src/App/Layout/Navigation.tsx index e3ab1ad8d..6431d38fb 100644 --- a/apps/namadillo/src/App/Layout/Navigation.tsx +++ b/apps/namadillo/src/App/Layout/Navigation.tsx @@ -1,60 +1,68 @@ import { SidebarMenuItem } from "App/Common/SidebarMenuItem"; import GovernanceRoutes from "App/Governance/routes"; import { MASPIcon } from "App/Icons/MASPIcon"; -import { SwapIcon } from "App/Icons/SwapIcon"; +import { useAtomValue } from "jotai"; import { AiFillHome } from "react-icons/ai"; import { BsDiscord, BsTwitterX } from "react-icons/bs"; import { FaVoteYea } from "react-icons/fa"; import { GoStack } from "react-icons/go"; 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", + icon: , + url: "/", + }, + { + label: "Staking", + icon: , + url: StakingRoutes.index(), + }, + { + label: "Governance", + icon: , + url: GovernanceRoutes.index(), + }, + { + label: "MASP", + icon: ( + + + + ), + }, + { + label: "IBC Transfer", + icon: , + url: features.ibcTransfersEnabled ? IbcRoutes.index() : undefined, + }, + { + label: "Transfer", + icon: , + }, + ]; + return (
    -
  • - - - Overview - -
  • -
  • - - - Staking - -
  • -
  • - - - Governance - -
  • -
  • - - - Transfer - -
  • -
  • - - - - - MASP - -
  • -
  • - - - - - Swap - -
  • + {menuItems.map((item) => ( +
  • + + {item.icon} + {item.label} + +
  • + ))}