Skip to content

Commit

Permalink
implement cosmos kit
Browse files Browse the repository at this point in the history
  • Loading branch information
steezeburger committed Dec 6, 2024
1 parent bd35e21 commit 615a678
Show file tree
Hide file tree
Showing 15 changed files with 3,414 additions and 548 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ the Astria bridge.
* `src/index.tsx`
* React application setup
* import styles
* ConfigContextProvider is here to provide config to App
* `src/App.tsx`
* main application component
* define routes
* use context providers
* rest of context providers, e.g. notifications, rainbowkit, etc
* `src/components` - More general React components for the app, e.g. Navbar,
Dropdown, CopyToClipboardButton, etc
* `src/config` - Configuration for the web app
Expand Down
3,473 changes: 3,077 additions & 396 deletions web/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"dependencies": {
"@cosmjs/launchpad": "^0.27.1",
"@cosmjs/stargate": "^0.32.3",
"@cosmos-kit/keplr": "^2.14.1",
"@cosmos-kit/react": "^2.20.1",
"@creativebulma/bulma-tooltip": "^1.2.0",
"@keplr-wallet/common": "^0.12.22",
"@keplr-wallet/cosmos": "^0.12.22",
Expand All @@ -18,6 +20,7 @@
"@types/react-dom": "^18.3.1",
"@types/uuid": "^9.0.8",
"bulma": "^0.9.4",
"chain-registry": "^1.69.45",
"ethers": "^6.13.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
47 changes: 35 additions & 12 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import type React from "react";
import { Route, Routes } from "react-router-dom";
import BridgePage from "pages/BridgePage/BridgePage";
import Layout from "pages/Layout";
import { useConfig } from "config";
import { NotificationsContextProvider } from "features/Notifications";

import { ChainProvider } from "@cosmos-kit/react";
import { assets } from "chain-registry";
import { wallets } from "@cosmos-kit/keplr";
import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { evmChainsToRainbowKitChains } from "./config/chainConfigs/types.ts";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";

import { evmChainsToRainbowKitChains, useConfig } from "config";
import { NotificationsContextProvider } from "features/Notifications";
import BridgePage from "pages/BridgePage/BridgePage";
import Layout from "pages/Layout";

// global styles
import "styles/index.scss";
// contrib styles
import "@rainbow-me/rainbowkit/styles.css";
import "@interchain-ui/react/styles";
import { toCosmosChainNames } from "./config/chainConfigs/types.ts";

/**
* App component with routes.
* Sets up the RainbowKitProvider and QueryClientProvider for tanstack/react-query.
*/
export default function App(): React.ReactElement {
const { evmChains } = useConfig();
const { evmChains, ibcChains } = useConfig();

const rainbowKitConfig = getDefaultConfig({
appName: "Flame Bridge",
Expand All @@ -24,16 +34,29 @@ export default function App(): React.ReactElement {

const queryClient = new QueryClient();

const cosmosWalletConnectOptions = {
signClient: {
projectId: "YOUR_PROJECT_ID", // TODO
},
};

return (
<NotificationsContextProvider>
<WagmiProvider config={rainbowKitConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<Routes>
<Route element={<Layout />}>
<Route index element={<BridgePage />} />
</Route>
</Routes>
<ChainProvider
chains={toCosmosChainNames(ibcChains)} // supported chains
assetLists={assets} // supported asset lists
wallets={wallets} // supported wallets
walletConnectOptions={cosmosWalletConnectOptions} // required if `wallets` contains mobile wallets
>
<Routes>
<Route element={<Layout />}>
<Route index element={<BridgePage />} />
</Route>
</Routes>
</ChainProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
Expand Down
45 changes: 39 additions & 6 deletions web/src/components/DepositCard/DepositCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import {
useIbcChainSelection,
} from "features/KeplrWallet";
import { NotificationType, useNotifications } from "features/Notifications";
import { useChain, useChains } from "@cosmos-kit/react";
import {
cosmosChainNameFromId,
toCosmosChainNames,
} from "../../config/chainConfigs/types.ts";

export default function DepositCard(): React.ReactElement {
const { evmChains, ibcChains } = useConfig();
Expand Down Expand Up @@ -50,6 +55,27 @@ export default function DepositCard(): React.ReactElement {
connectKeplrWallet,
} = useIbcChainSelection(ibcChains);

// FIXME - there has to be a better way to do this?
// maybe it's okay to use "celestia" as a default,
// but the default should be programmatically determined and not hardcoded here
// - problem: the selectedIbcChain is not set initially.
// - also can't call a hook in useEffect so can't create `openView` in the useEffect
// that is triggered when selectedIbcChain changes
const chainName = cosmosChainNameFromId(
selectedIbcChain?.chainId || "celestia",
);
const { openView } = useChain(chainName);

// FIXME - why does useChains throw an error?
// - `Uncaught TypeError: Cannot read properties of undefined (reading 'chain_id')`
// - if it wasn't broken, i think i could do the following:
// `const chains = useChains(['cosmoshub', 'osmosis', 'stargaze', 'juno', 'akash']);`
// and in the useEffect, `const { openView } = chains[selectedIbcChain.chainId]`.
// const chainNames = toCosmosChainNames(ibcChains);
// console.log({ chainNames });
// const chains = useChains(['cosmoshub']);
// const { openView } = chains[0];

// the evm currency selection is controlled by the sender's chosen ibc currency,
// and should be updated when an ibc currency or evm chain is selected
const selectedEvmCurrencyOption = useMemo(() => {
Expand Down Expand Up @@ -137,6 +163,7 @@ export default function DepositCard(): React.ReactElement {
};

const handleConnectEVMWallet = async () => {
// clear recipient address override values when user attempts to connect evm wallet
setIsRecipientAddressEditable(false);
setRecipientAddressOverride("");
await connectEVMWallet();
Expand All @@ -148,16 +175,22 @@ export default function DepositCard(): React.ReactElement {
if (!selectedEvmChain) {
return;
}
// FIXME - there is a bad implicit loop of logic here.
// - see comment in `features/EthWallet/hooks/useEvmChainSelection.tsx`
// 1. user can click "Connect EVM Wallet", which calls `connectEVMWallet`, before selecting a chain
// 2. `connectEVMWallet` will set the selected evm chain if it's not set
// 3. this `useEffect` is then triggered, which ultimately calls `connectEVMWallet`,
// but now a chain is set so it will open the connect modal
handleConnectEVMWallet().then((_) => {});
}, [selectedEvmChain]);

// ensure keplr wallet connection when selected ibc chain changes
/* biome-ignore lint/correctness/useExhaustiveDependencies: */
useEffect(() => {
if (!selectedIbcChain) {
return;
}
connectKeplrWallet().then((_) => {});
// TODO - do we need to call openView here?
// connectKeplrWallet().then((_) => {});
}, [selectedIbcChain]);

const handleDeposit = async () => {
Expand Down Expand Up @@ -273,14 +306,14 @@ export default function DepositCard(): React.ReactElement {
const additionalIbcOptions = useMemo(
() => [
{
label: "Connect Keplr Wallet",
action: connectKeplrWallet,
label: "Connect Cosmos Wallet",
action: openView,
className: "has-text-primary",
leftIconClass: "i-keplr",
leftIconClass: "i-keplr", // TODO - replace icon?
rightIconClass: "fas fa-plus",
},
],
[connectKeplrWallet],
[openView],
);

const additionalEvmOptions = useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/WithdrawCard/WithdrawCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export default function WithdrawCard(): React.ReactElement {
if (amount || ibcAccountAddress || recipientAddressOverride) {
setHasTouchedForm(true);
}
const recipientAddress = recipientAddressOverride || ibcAccountAddress;
const recipientAddress =
recipientAddressOverride || ibcAccountAddress || null;
checkIsFormValid(recipientAddress, amount);
}, [amount, ibcAccountAddress, recipientAddressOverride]);

Expand Down
30 changes: 30 additions & 0 deletions web/src/config/chainConfigs/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Chain as CosmosChain } from "@chain-registry/types";
import type { ChainInfo } from "@keplr-wallet/types";
import type { Chain } from "@rainbow-me/rainbowkit";

Expand All @@ -18,6 +19,35 @@ export function toChainInfo(chain: IbcChainInfo): ChainInfo {
return chainInfo as ChainInfo;
}

export function cosmosChainNameFromId(chainName: string) {
return chainName.split("-")[0];
}

export function toCosmosChainNames(ibcChains: IbcChains): string[] {
return Object.values(ibcChains).map((chain) =>
cosmosChainNameFromId(chain.chainId),
);
}

export function ibcChainInfoToCosmosChain(chain: IbcChainInfo): CosmosChain {
return {
...chain,
chain_name: chain.chainName,
chain_id: chain.chainId,
chain_type: "cosmos",
};
}
export function ibcChainInfosToCosmosChains(
ibcChains: IbcChains,
): [CosmosChain, ...CosmosChain[]] {
if (!ibcChains || Object.keys(ibcChains).length === 0) {
throw new Error("At least one chain must be provided");
}
return Object.values(ibcChains).map((ibcChain) =>
ibcChainInfoToCosmosChain(ibcChain),
) as [CosmosChain, ...CosmosChain[]];
}

// IbcChains type maps labels to IbcChainInfo objects
export type IbcChains = {
[label: string]: IbcChainInfo;
Expand Down
17 changes: 2 additions & 15 deletions web/src/config/contexts/ConfigContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from "react";

import type { AppConfig } from "config";
import type { EvmChainInfo } from "config/chainConfigs/types";
import { getEnvChainConfigs } from "config/chainConfigs";
import { getEnvVariable } from "config/env";

Expand All @@ -26,24 +25,13 @@ export const ConfigContextProvider: React.FC<ConfigContextProps> = ({
const swapURL = getEnvVariable("REACT_APP_SWAP_URL");
const poolURL = getEnvVariable("REACT_APP_POOL_URL");

let feedbackFormURL: string;
let feedbackFormURL: string | null;
try {
feedbackFormURL = getEnvVariable("REACT_APP_FEEDBACK_FORM_URL");
} catch (e) {
feedbackFormURL = brandURL;
feedbackFormURL = null;
}

// retrieves the EVM chain with the given chain ID.
const getEvmChainById = (chainIdHex: string): EvmChainInfo => {
const chainIdNum = Number.parseInt(chainIdHex, 16);
for (const chain of Object.values(evmChains)) {
if (chain.chainId === chainIdNum) {
return chain;
}
}
throw new Error(`Chain with ID ${chainIdHex} (${chainIdNum}) not found`);
};

return (
<ConfigContext.Provider
value={{
Expand All @@ -54,7 +42,6 @@ export const ConfigContextProvider: React.FC<ConfigContextProps> = ({
swapURL,
poolURL,
feedbackFormURL,
getEvmChainById,
}}
>
{children}
Expand Down
27 changes: 14 additions & 13 deletions web/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
type EvmChainInfo,
type EvmChains,
evmChainsToRainbowKitChains,
type EvmCurrency,
evmCurrencyBelongsToChain,
type IbcChainInfo,
ibcChainInfosToCosmosChains,
type IbcChains,
type IbcCurrency,
ibcCurrencyBelongsToChain,
Expand All @@ -15,26 +17,23 @@ import { useConfig } from "./hooks/useConfig";

/**
* Represents the configuration object for the application.
*
* @typedef {Object} AppConfig
* @property {IbcChains} ibcChains - The configurations for IBC chains.
* @property {EvmChains} evmChains - The configurations for EVM chains.
* @property {string} brandURL - The URL for the brand link in the navbar.
* @property {string} bridgeURL - The URL for the bridge link in the navbar.
* @property {string} swapURL - The URL for the swap link in the navbar.
* @property {string} poolURL - The URL for the pool link in the navbar.
* @property {function} getEvmChainById - Retrieves the EVM chain with the given chain ID from the config context.
*/
export type AppConfig = {
export interface AppConfig {
// The configurations for IBC chains.
ibcChains: IbcChains;
// The configurations for EVM chains.
evmChains: EvmChains;
// The URL for the brand link in the navbar.
brandURL: string;
// The URL for the bridge link in the navbar.
bridgeURL: string;
// The URL for the swap link in the navbar.
swapURL: string;
// The URL for the pool link in the navbar.
poolURL: string;
feedbackFormURL: string;
getEvmChainById(chainIdHex: string): EvmChainInfo;
};
// The URL for the feedback form side tag. Hides side tag when null.
feedbackFormURL: string | null;
}

export {
ConfigContextProvider,
Expand All @@ -47,6 +46,8 @@ export {
type IbcChains,
type IbcCurrency,
ibcCurrencyBelongsToChain,
ibcChainInfosToCosmosChains,
toChainInfo,
evmChainsToRainbowKitChains,
useConfig,
};
7 changes: 6 additions & 1 deletion web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,14 @@ export function useEvmChainSelection(evmChains: EvmChains) {
setSelectedEvmCurrency(currency);
}, []);

// opens RainbowKit modal for user to connect their EVM wallet
const connectEVMWallet = async () => {
if (!selectedEvmChain) {
// select default chain if none selected, then return. effect handles retriggering.
// FIXME - the fact this function needs to be called again after setting an evm chain
// in the parent component is implicit and should be somehow made explicit. this is
// hard to debug, especially since the parent uses `useEffect` to call this function.
// select default chain if none selected, then return.
// useEffect in parent component handles recalling this function.
setSelectedEvmChain(evmChainsOptions[0]?.value);
return;
}
Expand Down
Loading

0 comments on commit 615a678

Please sign in to comment.