From baeee044771bc590b4564bded3256c3a188fa0cd Mon Sep 17 00:00:00 2001
From: jesse snyder
Date: Mon, 28 Oct 2024 20:03:23 -0600
Subject: [PATCH 01/10] Feature: add production config (#40)
* add production config. make it even easier to add configs in the future.
* doc comments
* memo not helping here
* format evm balance according to currency's decimals
* formatting
* mainnet configs
* ensure native token is formatted according to coinDecimals
* add ibc withdrawer fee
---
README.md | 21 +-
.../components/WithdrawCard/WithdrawCard.tsx | 2 +
.../config/chainConfigs/ChainConfigsDawn.ts | 4 +-
.../config/chainConfigs/ChainConfigsDusk.ts | 7 +-
.../config/chainConfigs/ChainConfigsLocal.ts | 8 +-
.../chainConfigs/ChainConfigsMainnet.ts | 200 ++++++++++++++++++
web/src/config/chainConfigs/index.ts | 189 ++++-------------
web/src/config/chainConfigs/types.ts | 89 ++++++++
web/src/config/config.test.ts | 188 ++++++++++++++--
web/src/config/contexts/ConfigContext.tsx | 23 +-
web/src/config/index.ts | 2 +-
.../EthWallet/contexts/EthWalletContext.tsx | 5 +-
.../EthWallet/hooks/useEvmChainSelection.tsx | 6 +-
.../AstriaWithdrawerService.test.ts | 65 ++----
.../AstriaWithdrawerService.ts | 51 ++---
web/src/testHelpers.tsx | 4 +-
16 files changed, 587 insertions(+), 277 deletions(-)
create mode 100644 web/src/config/chainConfigs/ChainConfigsMainnet.ts
create mode 100644 web/src/config/chainConfigs/types.ts
diff --git a/README.md b/README.md
index 7381033..eb14117 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,21 @@ just web build
```sh
touch web/src/config/chainConfigs/ChainConfigsMainnet.ts
```
- * update logic in `getIbcChains` and `getEvmChains`. add new condition to
- check for the new environment and use the correct config
+ * import new configs in
+ `astria-bridge-web-app/web/src/config/chainConfigs/index.ts`, while renaming
+ them
```typescript
- if (getEnvVariable("REACT_APP_ENV") === "mainnet") {
- return mainnetIbcChains;
- }
+ import {
+ evmChains as mainnetEvmChains,
+ ibcChains as mainnetIbcChains,
+ } from "./ChainConfigsMainnet";
+ ```
+ * add entry to `EVM_CHAIN_CONFIGS`
+ ```typescript
+ const ENV_CHAIN_CONFIGS = {
+ local: { evm: localEvmChains, ibc: localIbcChains },
+ dusk: { evm: duskEvmChains, ibc: duskIbcChains },
+ dawn: { evm: dawnEvmChains, ibc: dawnIbcChains },
+ mainnet: { evm: mainnetEvmChains, ibc: mainnetIbcChains },
+ } as const;
```
diff --git a/web/src/components/WithdrawCard/WithdrawCard.tsx b/web/src/components/WithdrawCard/WithdrawCard.tsx
index 4a75cf8..203c8d1 100644
--- a/web/src/components/WithdrawCard/WithdrawCard.tsx
+++ b/web/src/components/WithdrawCard/WithdrawCard.tsx
@@ -182,6 +182,8 @@ export default function WithdrawCard(): React.ReactElement {
fromAddress,
recipientAddress,
amount,
+ selectedEvmCurrency.coinDecimals,
+ selectedEvmCurrency.ibcWithdrawalFeeWei,
"",
);
addNotification({
diff --git a/web/src/config/chainConfigs/ChainConfigsDawn.ts b/web/src/config/chainConfigs/ChainConfigsDawn.ts
index 9250839..6317ab8 100644
--- a/web/src/config/chainConfigs/ChainConfigsDawn.ts
+++ b/web/src/config/chainConfigs/ChainConfigsDawn.ts
@@ -1,4 +1,4 @@
-import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from ".";
+import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from "./types";
const CelestiaChainInfo: IbcChainInfo = {
// Chain-id of the celestia chain.
@@ -194,6 +194,7 @@ const FlameChainInfo: EvmChainInfo = {
coinDecimals: 18,
nativeTokenWithdrawerContractAddress:
"0x77Af806d724699B3644F9CCBFD45CC999CCC3d49",
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-celestia",
},
{
@@ -201,6 +202,7 @@ const FlameChainInfo: EvmChainInfo = {
coinMinimalDenom: "uusdc",
coinDecimals: 18,
erc20ContractAddress: "0x6e18cE6Ec3Fc7b8E3EcFca4fA35e25F3f6FA879a",
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-noble",
},
],
diff --git a/web/src/config/chainConfigs/ChainConfigsDusk.ts b/web/src/config/chainConfigs/ChainConfigsDusk.ts
index f9a46a9..72df073 100644
--- a/web/src/config/chainConfigs/ChainConfigsDusk.ts
+++ b/web/src/config/chainConfigs/ChainConfigsDusk.ts
@@ -1,4 +1,4 @@
-import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from ".";
+import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from "./types";
const CelestiaChainInfo: IbcChainInfo = {
// Chain-id of the celestia chain.
@@ -192,17 +192,19 @@ const FlameChainInfo: EvmChainInfo = {
coinDenom: "RIA",
coinMinimalDenom: "uria",
coinDecimals: 18,
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-celestia",
},
{
coinDenom: "USDC",
coinMinimalDenom: "uusdc",
coinDecimals: 18,
- iconClass: "i-noble",
// address of erc20 contract on dusk-11
erc20ContractAddress: "0xa4f59B3E97EC22a2b949cB5b6E8Cd6135437E857",
// this value would only exist for native tokens
nativeTokenWithdrawerContractAddress: "",
+ ibcWithdrawalFeeWei: "10000000000000000",
+ iconClass: "i-noble",
},
{
coinDenom: "fakeTIA",
@@ -212,6 +214,7 @@ const FlameChainInfo: EvmChainInfo = {
// just using this for testing the UI.
erc20ContractAddress: "0xFc83F6A786728F448481B7D7d5C0659A92a62C4d",
nativeTokenWithdrawerContractAddress: "",
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-celestia",
},
],
diff --git a/web/src/config/chainConfigs/ChainConfigsLocal.ts b/web/src/config/chainConfigs/ChainConfigsLocal.ts
index 320a935..919d88e 100644
--- a/web/src/config/chainConfigs/ChainConfigsLocal.ts
+++ b/web/src/config/chainConfigs/ChainConfigsLocal.ts
@@ -1,10 +1,10 @@
-import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from ".";
+import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from "./types";
const CelestiaChainInfo: IbcChainInfo = {
// Chain-id of the celestia chain.
chainId: "celestia-local-0",
// The name of the chain to be displayed to the user.
- chainName: "celestia-local-0",
+ chainName: "Celestia Local",
// RPC endpoint of the chain
rpc: "http://rpc.app.celestia.localdev.me",
// REST endpoint of the chain.
@@ -185,6 +185,7 @@ const FlameChainInfo: EvmChainInfo = {
coinDenom: "RIA",
coinMinimalDenom: "uria",
coinDecimals: 18,
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-celestia",
},
{
@@ -193,6 +194,7 @@ const FlameChainInfo: EvmChainInfo = {
coinDecimals: 6,
nativeTokenWithdrawerContractAddress:
"0xA58639fB5458e65E4fA917FF951C390292C24A15",
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-celestia",
},
],
@@ -207,6 +209,7 @@ const FakeChainInfo: EvmChainInfo = {
coinDenom: "FAKE",
coinMinimalDenom: "ufake",
coinDecimals: 18,
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-celestia",
},
{
@@ -216,6 +219,7 @@ const FakeChainInfo: EvmChainInfo = {
// fake address here so it shows up in the currency dropdown
nativeTokenWithdrawerContractAddress:
"0x0000000000000000000000000000000000000000",
+ ibcWithdrawalFeeWei: "10000000000000000",
iconClass: "i-flame",
},
],
diff --git a/web/src/config/chainConfigs/ChainConfigsMainnet.ts b/web/src/config/chainConfigs/ChainConfigsMainnet.ts
new file mode 100644
index 0000000..6510e15
--- /dev/null
+++ b/web/src/config/chainConfigs/ChainConfigsMainnet.ts
@@ -0,0 +1,200 @@
+import type { EvmChainInfo, EvmChains, IbcChainInfo, IbcChains } from "./types";
+
+const CelestiaChainInfo: IbcChainInfo = {
+ // Chain-id of the celestia chain.
+ chainId: "celestia",
+ // The name of the chain to be displayed to the user.
+ chainName: "Celestia",
+ // RPC endpoint of the chain
+ rpc: "wss://rpc.celestia.pops.one",
+ // REST endpoint of the chain.
+ rest: "https://api.celestia.pops.one",
+ // Staking coin information
+ stakeCurrency: {
+ // Coin denomination to be displayed to the user.
+ coinDenom: "TIA",
+ // Actual denom (i.e. uatom, uscrt) used by the blockchain.
+ coinMinimalDenom: "utia",
+ // # of decimal points to convert minimal denomination to user-facing denomination.
+ coinDecimals: 6,
+ // (Optional) Keplr can show the fiat value of the coin if a coingecko id is provided.
+ // You can get id from https://api.coingecko.com/api/v3/coins/list if it is listed.
+ // coinGeckoId: ""
+ },
+ // (Optional) If you have a wallet webpage used to stake the coin then provide the url to the website in `walletUrlForStaking`.
+ // The 'stake' button in Keplr extension will link to the webpage.
+ // walletUrlForStaking: "",
+ // The BIP44 path.
+ bip44: {
+ // You can only set the coin type of BIP44.
+ // 'Purpose' is fixed to 44.
+ coinType: 118,
+ },
+ // The address prefix of the chain.
+ bech32Config: {
+ bech32PrefixAccAddr: "celestia",
+ bech32PrefixAccPub: "celestiapub",
+ bech32PrefixConsAddr: "celestiavalcons",
+ bech32PrefixConsPub: "celestiavalconspub",
+ bech32PrefixValAddr: "celestiavaloper",
+ bech32PrefixValPub: "celestiavaloperpub",
+ },
+ // List of all coin/tokens used in this chain.
+ currencies: [
+ {
+ // Coin denomination to be displayed to the user.
+ coinDenom: "TIA",
+ // Actual denom (i.e. uatom, uscrt) used by the blockchain.
+ coinMinimalDenom: "utia",
+ // # of decimal points to convert minimal denomination to user-facing denomination.
+ coinDecimals: 6,
+ // (Optional) Keplr can show the fiat value of the coin if a coingecko id is provided.
+ // You can get id from https://api.coingecko.com/api/v3/coins/list if it is listed.
+ // coinGeckoId: ""
+ ibcChannel: "channel-48",
+ sequencerBridgeAccount: "astria13vptdafyttpmlwppt0s844efey2cpc0mevy92p",
+ iconClass: "i-celestia",
+ },
+ ],
+ // List of coin/tokens used as a fee token in this chain.
+ feeCurrencies: [
+ {
+ // Coin denomination to be displayed to the user.
+ coinDenom: "TIA",
+ // Actual denom (i.e. nria, uscrt) used by the blockchain.
+ coinMinimalDenom: "utia",
+ // # of decimal points to convert minimal denomination to user-facing denomination.
+ coinDecimals: 6,
+ // (Optional) Keplr can show the fiat value of the coin if a coingecko id is provided.
+ // You can get id from https://api.coingecko.com/api/v3/coins/list if it is listed.
+ // coinGeckoId: ""
+ // (Optional) This is used to set the fee of the transaction.
+ // If this field is not provided and suggesting chain is not natively integrated, Keplr extension will set the Keplr default gas price (low: 0.01, average: 0.025, high: 0.04).
+ // Currently, Keplr doesn't support dynamic calculation of the gas prices based on on-chain data.
+ // Make sure that the gas prices are higher than the minimum gas prices accepted by chain validators and RPC/REST endpoint.
+ gasPriceStep: {
+ low: 0.01,
+ average: 0.02,
+ high: 0.1,
+ },
+ },
+ ],
+ iconClass: "i-celestia",
+};
+
+const NobleChainInfo: IbcChainInfo = {
+ chainId: "noble-1",
+ chainName: "Noble",
+ // RPC endpoint of the chain
+ rpc: "https://noble-rpc.polkachu.com:443",
+ // REST endpoint of the chain.
+ rest: "https://noble-api.polkachu.com",
+ // Staking coin information
+ stakeCurrency: {
+ // Coin denomination to be displayed to the user.
+ coinDenom: "USDC",
+ // Actual denom (i.e. uatom, uscrt) used by the blockchain.
+ coinMinimalDenom: "uusdc",
+ // # of decimal points to convert minimal denomination to user-facing denomination.
+ coinDecimals: 6,
+ // (Optional) Keplr can show the fiat value of the coin if a coingecko id is provided.
+ // You can get id from https://api.coingecko.com/api/v3/coins/list if it is listed.
+ // coinGeckoId: ""
+ },
+ // (Optional) If you have a wallet webpage used to stake the coin then provide the url to the website in `walletUrlForStaking`.
+ // The 'stake' button in Keplr extension will link to the webpage.
+ // walletUrlForStaking: "",
+ // The BIP44 path.
+ bip44: {
+ // You can only set the coin type of BIP44.
+ // 'Purpose' is fixed to 44.
+ coinType: 118,
+ },
+ // The address prefix of the chain.
+ bech32Config: {
+ bech32PrefixAccAddr: "noble",
+ bech32PrefixAccPub: "noblepub",
+ bech32PrefixConsAddr: "noblevalcons",
+ bech32PrefixConsPub: "noblevalconspub",
+ bech32PrefixValAddr: "noblevaloper",
+ bech32PrefixValPub: "noblevaloperpub",
+ },
+ // List of all coin/tokens used in this chain.
+ currencies: [
+ {
+ // Coin denomination to be displayed to the user.
+ coinDenom: "USDC",
+ // Actual denom (i.e. uatom, uscrt) used by the blockchain.
+ coinMinimalDenom: "uusdc",
+ // # of decimal points to convert minimal denomination to user-facing denomination.
+ coinDecimals: 6,
+ // (Optional) Keplr can show the fiat value of the coin if a coingecko id is provided.
+ // You can get id from https://api.coingecko.com/api/v3/coins/list if it is listed.
+ // coinGeckoId: ""
+ ibcChannel: "channel-104",
+ // NOTE - noble requires the astria compat address (https://slowli.github.io/bech32-buffer/)
+ sequencerBridgeAccount:
+ "astriacompat1eg8hhey0n4untdvqqdvlyl0e7zx8wfcaz3l6wu",
+ iconClass: "i-noble",
+ },
+ ],
+ // List of coin/tokens used as a fee token in this chain.
+ feeCurrencies: [
+ {
+ // Coin denomination to be displayed to the user.
+ coinDenom: "USDC",
+ // Actual denom (i.e. nria, uscrt) used by the blockchain.
+ coinMinimalDenom: "usdc",
+ // # of decimal points to convert minimal denomination to user-facing denomination.
+ coinDecimals: 6,
+ // (Optional) Keplr can show the fiat value of the coin if a coingecko id is provided.
+ // You can get id from https://api.coingecko.com/api/v3/coins/list if it is listed.
+ // coinGeckoId: ""
+ // (Optional) This is used to set the fee of the transaction.
+ // If this field is not provided and suggesting chain is not natively integrated, Keplr extension will set the Keplr default gas price (low: 0.01, average: 0.025, high: 0.04).
+ // Currently, Keplr doesn't support dynamic calculation of the gas prices based on on-chain data.
+ // Make sure that the gas prices are higher than the minimum gas prices accepted by chain validators and RPC/REST endpoint.
+ gasPriceStep: {
+ low: 0.01,
+ average: 0.02,
+ high: 0.1,
+ },
+ },
+ ],
+ iconClass: "i-noble",
+};
+
+export const ibcChains: IbcChains = {
+ Celestia: CelestiaChainInfo,
+ Noble: NobleChainInfo,
+};
+
+const FlameChainInfo: EvmChainInfo = {
+ chainId: 253368190,
+ chainName: "Flame",
+ rpcUrls: ["https://rpc.flame.astria.org"],
+ currencies: [
+ {
+ coinDenom: "TIA",
+ coinMinimalDenom: "utia",
+ coinDecimals: 18,
+ nativeTokenWithdrawerContractAddress:
+ "0xB086557f9B5F6fAe5081CC5850BE94e62B1dDE57",
+ ibcWithdrawalFeeWei: "10000000000000000",
+ iconClass: "i-celestia",
+ },
+ {
+ coinDenom: "USDC",
+ coinMinimalDenom: "uusdc",
+ coinDecimals: 6,
+ erc20ContractAddress: "0x3f65144F387f6545bF4B19a1B39C94231E1c849F",
+ ibcWithdrawalFeeWei: "10000000000000000",
+ iconClass: "i-noble",
+ },
+ ],
+ iconClass: "i-flame",
+};
+
+export const evmChains: EvmChains = {
+ Flame: FlameChainInfo,
+};
diff --git a/web/src/config/chainConfigs/index.ts b/web/src/config/chainConfigs/index.ts
index e4f93a5..0d62234 100644
--- a/web/src/config/chainConfigs/index.ts
+++ b/web/src/config/chainConfigs/index.ts
@@ -1,5 +1,4 @@
-import type { ChainInfo } from "@keplr-wallet/types";
-import { getEnvVariable } from "config";
+import { type EvmChains, getEnvVariable, type IbcChains } from "config";
import {
evmChains as localEvmChains,
@@ -13,172 +12,60 @@ import {
evmChains as dawnEvmChains,
ibcChains as dawnIbcChains,
} from "./ChainConfigsDawn";
+import {
+ evmChains as mainnetEvmChains,
+ ibcChains as mainnetIbcChains,
+} from "./ChainConfigsMainnet";
-/**
- * Represents information about an IBC (Inter-Blockchain Communication) chain.
- * This class extends the base class ChainInfo.
- *
- * @typedef {object} IbcChainInfo
- * @property {string} iconClass - The classname to use for the chain's icon.
- * @extends {ChainInfo}
- */
-export type IbcChainInfo = {
- iconClass?: string;
- currencies: IbcCurrency[];
-} & ChainInfo;
+// Map of environment labels to their chain configurations
+const ENV_CHAIN_CONFIGS = {
+ local: { evm: localEvmChains, ibc: localIbcChains },
+ dusk: { evm: duskEvmChains, ibc: duskIbcChains },
+ dawn: { evm: dawnEvmChains, ibc: dawnIbcChains },
+ mainnet: { evm: mainnetEvmChains, ibc: mainnetIbcChains },
+} as const;
-/**
- * Converts an IbcChainInfo object to a ChainInfo object.
- * @param chain
- */
-export function toChainInfo(chain: IbcChainInfo): ChainInfo {
- const { iconClass, ...chainInfo } = chain;
- return chainInfo as ChainInfo;
-}
+type Environment = keyof typeof ENV_CHAIN_CONFIGS;
-// IbcChains type maps labels to IbcChainInfo objects
-export type IbcChains = {
- [label: string]: IbcChainInfo;
+type ChainConfigs = {
+ evm: EvmChains;
+ ibc: IbcChains;
};
/**
- * Represents information about a currency used in an IBC chain.
- *
- * @typedef {object} IbcCurrency
- * @property {string} coinDenom - The coin denomination to display to the user.
- * @property {string} coinMinimalDenom - The actual denomination used by the blockchain.
- * @property {number} coinDecimals - The number of decimal points to convert the minimal denomination to the user-facing denomination.
- * @property {string} ibcChannel - The IBC channel to use for this currency.
- * @property {string} sequencerBridgeAccount - The account on the sequencer chain that bridges tokens to the EVM chain.
- * @property {string} iconClass - The classname to use for the currency's icon.
+ * Gets the chain configurations for the current environment.
+ * If the chain configurations are overridden by environment variables,
+ * those will be used instead.
*/
-export type IbcCurrency = {
- coinDenom: string;
- coinMinimalDenom: string;
- coinDecimals: number;
- ibcChannel?: string;
- sequencerBridgeAccount?: string;
- iconClass?: string;
-};
+export function getEnvChainConfigs(): ChainConfigs {
+ // get environment-specific configs as base
+ const env = getEnvVariable("REACT_APP_ENV").toLowerCase() as Environment;
+ const baseConfig = ENV_CHAIN_CONFIGS[env] || ENV_CHAIN_CONFIGS.local;
-/**
- * Returns true if the given currency belongs to the given chain.
- * @param {IbcCurrency} currency - The currency to check.
- * @param {IbcChainInfo} chain - The chain to check.
- */
-export function ibcCurrencyBelongsToChain(
- currency: IbcCurrency,
- chain: IbcChainInfo,
-): boolean {
- // FIXME - what if two chains have currencies with the same coinDenom?
- // e.g. USDC on Noble and USDC on Celestia
- return chain.currencies?.includes(currency);
-}
+ // copy baseConfig to result
+ const result = { ...baseConfig };
-/**
- * Retrieves the IBC chains from the environment variable override or the default chain configurations,
- * depending on the environment.
- *
- * @returns {IbcChains} - The IBC chains configuration.
- */
-export function getIbcChains(): IbcChains {
- // try to get the IBC chains from the environment variable override first
+ // try to get IBC chains override
try {
- const ibcChains = getEnvVariable("REACT_APP_IBC_CHAINS");
- if (ibcChains) {
- // TODO - validate the JSON against type
- return JSON.parse(ibcChains);
+ const ibcChainsOverride = getEnvVariable("REACT_APP_IBC_CHAINS");
+ if (ibcChainsOverride) {
+ result.ibc = JSON.parse(ibcChainsOverride);
+ console.debug("Using IBC chains override from environment");
}
} catch (e) {
- console.debug("REACT_APP_IBC_CHAINS not set. Continuing...");
+ console.debug("No valid IBC chains override found, using default");
}
- // get default chain configs based on REACT_APP_ENV
- if (getEnvVariable("REACT_APP_ENV") === "dusk") {
- return duskIbcChains;
- }
- if (getEnvVariable("REACT_APP_ENV") === "dawn") {
- return dawnIbcChains;
- }
- return localIbcChains;
-}
-
-/**
- * Represents information about an EVM chain.
- *
- * @typedef {object} EvmChainInfo
- * @property {number} chainId - The decimal representation of the EVM chain ID.
- * @property {string} chainName - The name of the EVM chain to be displayed to the user.
- * @property {EvmCurrency[]} currencies - The currencies used in the chain.
- * @property {string} rpcUrls - The RPC URLs of the EVM chain.
- * @property {string} iconClass - The classname to use for the chain's icon.
- */
-export type EvmChainInfo = {
- chainId: number;
- chainName: string;
- currencies: EvmCurrency[];
- rpcUrls?: string[];
- iconClass?: string;
-};
-
-/**
- * Represents information about a currency used in an EVM chain.
- *
- * @typedef {object} EvmCurrency
- * @property {string} coinDenom - The coin denomination to display to the user.
- * @property {string} coinMinimalDenom - The actual denomination used by the blockchain.
- * @property {number} coinDecimals - The number of decimal points to convert the minimal denomination to the user-facing denomination.
- * @property {string} erc20ContractAddress - The address of the contract of an ERC20 token.
- * @property {string} nativeTokenWithdrawerContractAddress - The address of the contract used to withdraw tokens from the EVM chain.
- * @property {string} iconClass - The classname to use for the currency's icon.
- */
-export type EvmCurrency = {
- coinDenom: string;
- coinMinimalDenom: string;
- coinDecimals: number;
- erc20ContractAddress?: string;
- nativeTokenWithdrawerContractAddress?: string;
- iconClass?: string;
-};
-
-export function evmCurrencyBelongsToChain(
- currency: EvmCurrency,
- chain: EvmChainInfo,
-): boolean {
- // FIXME - what if two chains have currencies with the same coinDenom?
- // e.g. USDC on Noble and USDC on Celestia
- return chain.currencies?.includes(currency);
-}
-
-// EvmChains type maps labels to EvmChainInfo objects
-export type EvmChains = {
- [label: string]: EvmChainInfo;
-};
-
-/**
- * Retrieves the EVM chains from the environment variable override or the default chain configurations,
- * depending on the environment.
- *
- * @returns {EvmChains} - The EVM chains configuration.
- */
-export function getEvmChains(): EvmChains {
- // try to get the EVM chains from the environment variable override first
+ // try to get EVM chains override
try {
- const evmChains = getEnvVariable("REACT_APP_EVM_CHAINS");
- if (evmChains) {
- // TODO - validate the JSON against type
- return JSON.parse(evmChains);
+ const evmChainsOverride = getEnvVariable("REACT_APP_EVM_CHAINS");
+ if (evmChainsOverride) {
+ result.evm = JSON.parse(evmChainsOverride);
+ console.debug("Using EVM chains override from environment");
}
} catch (e) {
- console.debug("REACT_APP_EVM_CHAINS not set. Continuing...");
+ console.debug("No valid EVM chains override found, using default");
}
- // get default chain configs based on REACT_APP_ENV
- if (getEnvVariable("REACT_APP_ENV") === "dusk") {
- return duskEvmChains;
- }
- if (getEnvVariable("REACT_APP_ENV") === "dawn") {
- return dawnEvmChains;
- }
- return localEvmChains;
+ return result;
}
diff --git a/web/src/config/chainConfigs/types.ts b/web/src/config/chainConfigs/types.ts
new file mode 100644
index 0000000..1da78f4
--- /dev/null
+++ b/web/src/config/chainConfigs/types.ts
@@ -0,0 +1,89 @@
+import type { ChainInfo } from "@keplr-wallet/types";
+import { ethers } from "ethers";
+
+/**
+ * Represents information about an IBC chain.
+ * This type extends the base ChainInfo type from Keplr.
+ */
+export type IbcChainInfo = {
+ iconClass?: string;
+ currencies: IbcCurrency[];
+} & ChainInfo;
+
+/**
+ * Converts an IbcChainInfo object to a ChainInfo object.
+ */
+export function toChainInfo(chain: IbcChainInfo): ChainInfo {
+ const { iconClass, ...chainInfo } = chain;
+ return chainInfo as ChainInfo;
+}
+
+// IbcChains type maps labels to IbcChainInfo objects
+export type IbcChains = {
+ [label: string]: IbcChainInfo;
+};
+
+/**
+ * Represents information about a currency used in an IBC chain.
+ */
+export type IbcCurrency = {
+ coinDenom: string;
+ coinMinimalDenom: string;
+ coinDecimals: number;
+ ibcChannel?: string;
+ sequencerBridgeAccount?: string;
+ iconClass?: string;
+};
+
+/**
+ * Returns true if the given currency belongs to the given chain.
+ */
+export function ibcCurrencyBelongsToChain(
+ currency: IbcCurrency,
+ chain: IbcChainInfo,
+): boolean {
+ return chain.currencies?.includes(currency);
+}
+
+/**
+ * Represents information about an EVM chain.
+ */
+export type EvmChainInfo = {
+ chainId: number;
+ chainName: string;
+ currencies: EvmCurrency[];
+ rpcUrls?: string[];
+ iconClass?: string;
+};
+
+/**
+ * Represents information about a currency used in an EVM chain.
+ */
+export type EvmCurrency = {
+ coinDenom: string;
+ coinMinimalDenom: string;
+ coinDecimals: number;
+ // contract address if this is a ERC20 token
+ erc20ContractAddress?: string;
+ // contract address if this a native token
+ nativeTokenWithdrawerContractAddress?: string;
+ // fee needed to pay for the ibc withdrawal, 18 decimals
+ ibcWithdrawalFeeWei: string;
+ iconClass?: string;
+};
+
+/**
+ * Returns true if the given currency belongs to the given chain.
+ */
+export function evmCurrencyBelongsToChain(
+ currency: EvmCurrency,
+ chain: EvmChainInfo,
+): boolean {
+ return chain.currencies?.includes(currency);
+}
+
+// Map of environment labels to their chain configurations
+// EvmChains type maps labels to EvmChainInfo objects
+export type EvmChains = {
+ [label: string]: EvmChainInfo;
+};
diff --git a/web/src/config/config.test.ts b/web/src/config/config.test.ts
index 1b39f15..d66b261 100644
--- a/web/src/config/config.test.ts
+++ b/web/src/config/config.test.ts
@@ -1,27 +1,183 @@
-import { getEnvVariable } from "./env";
+import { getEnvChainConfigs } from "./chainConfigs";
+import type { IbcChains, EvmChains } from "./chainConfigs/types";
-describe("config", () => {
- describe("getEnvVariable", () => {
- const OLD_ENV = process.env;
+// mock the config import to control getEnvVariable
+jest.mock("config", () => ({
+ getEnvVariable: jest.fn(),
+}));
- beforeEach(() => {
- jest.resetModules();
- process.env = { ...OLD_ENV };
+// import the mocked function for type safety
+import { getEnvVariable } from "config";
+
+describe("Chain Configs", () => {
+ // Store original env vars
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // clear all mocks before each test
+ jest.clearAllMocks();
+ // reset env vars
+ process.env = { ...originalEnv };
+ // reset the mock implementation
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (process.env[key]) return process.env[key];
+ throw new Error(`${key} not set`);
+ });
+ });
+
+ afterAll(() => {
+ // restore original env vars
+ process.env = originalEnv;
+ });
+
+ describe("getEnvChainConfigs", () => {
+ const mockIbcChains: IbcChains = {
+ "Test Chain": {
+ chainId: "test-1",
+ chainName: "Test Chain",
+ currencies: [
+ {
+ coinDenom: "TEST",
+ coinMinimalDenom: "utest",
+ coinDecimals: 6,
+ },
+ ],
+ bech32Config: {
+ bech32PrefixAccAddr: "test",
+ bech32PrefixAccPub: "testpub",
+ bech32PrefixConsAddr: "testvalcons",
+ bech32PrefixConsPub: "testvalconspub",
+ bech32PrefixValAddr: "testvaloper",
+ bech32PrefixValPub: "testvaloperpub",
+ },
+ bip44: { coinType: 118 },
+ stakeCurrency: {
+ coinDenom: "TEST",
+ coinMinimalDenom: "utest",
+ coinDecimals: 6,
+ },
+ feeCurrencies: [
+ {
+ coinDenom: "TEST",
+ coinMinimalDenom: "utest",
+ coinDecimals: 6,
+ },
+ ],
+ rest: "https://api.test.com",
+ rpc: "https://rpc.test.com",
+ },
+ };
+
+ const mockEvmChains: EvmChains = {
+ "Test EVM Chain": {
+ chainId: 1234,
+ chainName: "Test EVM Chain",
+ currencies: [
+ {
+ coinDenom: "TEST",
+ coinMinimalDenom: "utest",
+ coinDecimals: 18,
+ ibcWithdrawalFeeWei: "10000000000000000",
+ },
+ ],
+ },
+ };
+
+ it("should error when expected environment variables are not set", () => {
+ process.env.REACT_APP_ENV = "";
+ expect(() => getEnvChainConfigs()).toThrowError("REACT_APP_ENV not set");
+ });
+
+ it("should return environment-specific config for each valid environment", () => {
+ const environments = ["local", "dusk", "dawn", "mainnet"];
+
+ for (const env of environments) {
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (key === "REACT_APP_ENV") return env;
+ throw new Error(`${key} not set`);
+ });
+
+ const config = getEnvChainConfigs();
+ expect(config).toBeDefined();
+ expect(config.ibc).toBeDefined();
+ expect(config.evm).toBeDefined();
+ }
});
- afterAll(() => {
- process.env = OLD_ENV;
+ it("should override IBC chains when REACT_APP_IBC_CHAINS is set", () => {
+ // set up environment
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (key === "REACT_APP_ENV") return "local";
+ if (key === "REACT_APP_IBC_CHAINS")
+ return JSON.stringify(mockIbcChains);
+ throw new Error(`${key} not set`);
+ });
+
+ const config = getEnvChainConfigs();
+ expect(config.ibc).toEqual(mockIbcChains);
+ // EVM chains should still be from local config
+ expect(config.evm.Flame.chainName).toEqual("Flame (local)");
});
- it("should return the value of an existing environment variable", () => {
- process.env.REACT_APP_ENV = "test";
- expect(getEnvVariable("REACT_APP_ENV")).toBe("test");
+ it("should override EVM chains when REACT_APP_EVM_CHAINS is set", () => {
+ // set up environment
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (key === "REACT_APP_ENV") return "local";
+ if (key === "REACT_APP_EVM_CHAINS")
+ return JSON.stringify(mockEvmChains);
+ throw new Error(`${key} not set`);
+ });
+
+ const config = getEnvChainConfigs();
+ expect(config.evm).toEqual(mockEvmChains);
+ // IBC chains should still be from local config
+ expect(config.ibc["Celestia Local"].chainName).toEqual("Celestia Local");
});
- it("should throw an error if the environment variable is not set", () => {
- expect(() => getEnvVariable("REACT_APP_VERSION")).toThrow(
- "REACT_APP_VERSION not set",
- );
+ it("should override both chains when both environment variables are set", () => {
+ // set up environment
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (key === "REACT_APP_ENV") return "local";
+ if (key === "REACT_APP_IBC_CHAINS")
+ return JSON.stringify(mockIbcChains);
+ if (key === "REACT_APP_EVM_CHAINS")
+ return JSON.stringify(mockEvmChains);
+ throw new Error(`${key} not set`);
+ });
+
+ const config = getEnvChainConfigs();
+ expect(config.ibc).toEqual(mockIbcChains);
+ expect(config.evm).toEqual(mockEvmChains);
+ });
+
+ it("should handle invalid JSON in IBC and EVM chains override", () => {
+ // set up environment
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (key === "REACT_APP_ENV") return "local";
+ if (key === "REACT_APP_IBC_CHAINS") return "invalid json";
+ if (key === "REACT_APP_EVM_CHAINS") return "invalid json";
+ throw new Error(`${key} not set`);
+ });
+
+ const config = getEnvChainConfigs();
+ // should fall back to local config
+ expect(config.ibc["Celestia Local"].chainName).toEqual("Celestia Local");
+ expect(config.evm.Flame.chainName).toEqual("Flame (local)");
+ });
+
+ it("should handle mixed valid and invalid JSON overrides", () => {
+ // set up environment
+ (getEnvVariable as jest.Mock).mockImplementation((key: string) => {
+ if (key === "REACT_APP_ENV") return "local";
+ if (key === "REACT_APP_IBC_CHAINS")
+ return JSON.stringify(mockIbcChains);
+ if (key === "REACT_APP_EVM_CHAINS") return "invalid json";
+ throw new Error(`${key} not set`);
+ });
+
+ const config = getEnvChainConfigs();
+ expect(config.ibc).toEqual(mockIbcChains); // should use override
+ expect(config.evm.Flame.chainName).toEqual("Flame (local)"); // should fall back to local config
});
});
});
diff --git a/web/src/config/contexts/ConfigContext.tsx b/web/src/config/contexts/ConfigContext.tsx
index 655ba9b..5be5ff1 100644
--- a/web/src/config/contexts/ConfigContext.tsx
+++ b/web/src/config/contexts/ConfigContext.tsx
@@ -1,13 +1,12 @@
-import React, { useMemo } from "react";
+import React from "react";
import type { AppConfig } from "config";
-import {
- type EvmChainInfo,
- type EvmChains,
- getEvmChains,
- getIbcChains,
- type IbcChains,
-} from "config/chainConfigs";
+import type {
+ EvmChainInfo,
+ EvmChains,
+ IbcChains,
+} from "config/chainConfigs/types";
+import { getEnvChainConfigs } from "config/chainConfigs";
import { getEnvVariable } from "config/env";
export const ConfigContext = React.createContext(
@@ -25,13 +24,7 @@ type ConfigContextProps = {
export const ConfigContextProvider: React.FC = ({
children,
}) => {
- const evmChains: EvmChains = useMemo(() => {
- return getEvmChains();
- }, []);
- const ibcChains: IbcChains = useMemo(() => {
- return getIbcChains();
- }, []);
-
+ const { evm: evmChains, ibc: ibcChains } = getEnvChainConfigs();
const brandURL = getEnvVariable("REACT_APP_BRAND_URL");
const bridgeURL = getEnvVariable("REACT_APP_BRIDGE_URL");
const swapURL = getEnvVariable("REACT_APP_SWAP_URL");
diff --git a/web/src/config/index.ts b/web/src/config/index.ts
index 5bd3f9b..8b0f1b4 100644
--- a/web/src/config/index.ts
+++ b/web/src/config/index.ts
@@ -8,7 +8,7 @@ import {
type IbcCurrency,
ibcCurrencyBelongsToChain,
toChainInfo,
-} from "./chainConfigs";
+} from "./chainConfigs/types";
import { ConfigContextProvider } from "./contexts/ConfigContext";
import { getEnvVariable } from "./env";
import { useConfig } from "./hooks/useConfig";
diff --git a/web/src/features/EthWallet/contexts/EthWalletContext.tsx b/web/src/features/EthWallet/contexts/EthWalletContext.tsx
index 927a012..b2ae295 100644
--- a/web/src/features/EthWallet/contexts/EthWalletContext.tsx
+++ b/web/src/features/EthWallet/contexts/EthWalletContext.tsx
@@ -148,7 +148,10 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({
// get balance using ethers
const balance = await ethersProvider.getBalance(address);
- const formattedBalance = formatBalance(balance.toString());
+ const formattedBalance = formatBalance(
+ balance.toString(),
+ defaultChain.currencies[0].coinDecimals,
+ );
const userAccount: UserAccount = {
address: address,
balance: formattedBalance,
diff --git a/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx b/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
index 0b63384..3093d7d 100644
--- a/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
+++ b/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
@@ -66,10 +66,14 @@ export function useEvmChainSelection(evmChains: EvmChains) {
);
if (withdrawerSvc instanceof AstriaErc20WithdrawerService) {
const balanceRes = await withdrawerSvc.getBalance(evmAccountAddress);
- const balanceStr = formatBalance(balanceRes.toString());
+ const balanceStr = formatBalance(
+ balanceRes.toString(),
+ selectedEvmCurrency.coinDecimals,
+ );
const balance = `${balanceStr} ${selectedEvmCurrency.coinDenom}`;
setEvmBalance(balance);
} else {
+ // for native token balance
const balance = `${userAccount.balance} ${selectedEvmCurrency.coinDenom}`;
setEvmBalance(balance);
}
diff --git a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts
index e117516..025fe0e 100644
--- a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts
+++ b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.test.ts
@@ -13,6 +13,8 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
const mockDestinationAddress =
"celestia1m0ksdjl2p5nzhqy3p47fksv52at3ln885xvl96";
const mockAmount = "1.0";
+ const mockAmountDenom = 18;
+ const mockFee: string = "10000000000000000";
const mockMemo = "Test memo";
let mockProvider: jest.Mocked;
@@ -29,16 +31,20 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
mockSigner = {} as jest.Mocked;
mockContract = {
- withdrawToSequencer: jest.fn(),
withdrawToIbcChain: jest.fn(),
} as unknown as jest.Mocked;
(ethers.BrowserProvider as jest.Mock).mockReturnValue(mockProvider);
mockProvider.getSigner.mockResolvedValue(mockSigner);
(ethers.Contract as jest.Mock).mockReturnValue(mockContract);
- (ethers.parseEther as jest.Mock).mockReturnValue(
- ethers.parseUnits(mockAmount, 18),
- );
+ (ethers.parseUnits as jest.Mock).mockImplementation((amount, decimals) => {
+ // Create a number that would represent the amount in wei
+ const [whole, decimal = ""] = amount.split(".");
+ const paddedDecimal = decimal.padEnd(decimals, "0");
+ const fullNumber = whole + paddedDecimal;
+ // Return an object that mimics ethers BigNumber with toString
+ return BigInt(fullNumber.padEnd(decimals + whole.length, "0"));
+ });
});
describe("AstriaWithdrawerService", () => {
@@ -78,24 +84,6 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(2, newProvider);
});
- it("should call withdrawToSequencer with correct parameters", async () => {
- const service = getAstriaWithdrawerService(
- {} as ethers.Eip1193Provider,
- mockContractAddress,
- ) as AstriaWithdrawerService;
-
- await service.withdrawToSequencer(
- mockFromAddress,
- mockDestinationAddress,
- mockAmount,
- );
-
- expect(mockContract.withdrawToSequencer).toHaveBeenCalledWith(
- mockDestinationAddress,
- { value: ethers.parseUnits(mockAmount, 18) },
- );
- });
-
it("should call withdrawToIbcChain with correct parameters", async () => {
const service = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
@@ -106,13 +94,18 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
mockFromAddress,
mockDestinationAddress,
mockAmount,
+ mockAmountDenom,
+ mockFee,
mockMemo,
);
+ const total =
+ ethers.parseUnits(mockAmount, mockAmountDenom) + BigInt(mockFee);
+
expect(mockContract.withdrawToIbcChain).toHaveBeenCalledWith(
mockDestinationAddress,
mockMemo,
- { value: ethers.parseUnits(mockAmount, 18) },
+ { value: total },
);
});
});
@@ -158,26 +151,6 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(2, newProvider);
});
- it("should call withdrawToSequencer with correct parameters", async () => {
- const service = getAstriaWithdrawerService(
- {} as ethers.Eip1193Provider,
- mockContractAddress,
- true,
- ) as AstriaErc20WithdrawerService;
-
- await service.withdrawToSequencer(
- mockFromAddress,
- mockDestinationAddress,
- mockAmount,
- );
-
- expect(mockContract.withdrawToSequencer).toHaveBeenCalledWith(
- ethers.parseUnits(mockAmount, 18),
- mockDestinationAddress,
- { value: ethers.parseUnits(mockAmount, 18) },
- );
- });
-
it("should call withdrawToIbcChain with correct parameters", async () => {
const service = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
@@ -189,14 +162,16 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
mockFromAddress,
mockDestinationAddress,
mockAmount,
+ mockAmountDenom,
+ mockFee,
mockMemo,
);
expect(mockContract.withdrawToIbcChain).toHaveBeenCalledWith(
- ethers.parseUnits(mockAmount, 18),
+ ethers.parseUnits(mockAmount, mockAmountDenom),
mockDestinationAddress,
mockMemo,
- { value: ethers.parseUnits(mockAmount, 18) },
+ { value: BigInt(mockFee) },
);
});
});
diff --git a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts
index a4364dd..9cc5b80 100644
--- a/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts
+++ b/web/src/features/EthWallet/services/AstriaWithdrawerService/AstriaWithdrawerService.ts
@@ -3,7 +3,6 @@ import GenericContractService from "features/EthWallet/services/GenericContractS
export class AstriaWithdrawerService extends GenericContractService {
protected static override ABI: ethers.InterfaceAbi = [
- "function withdrawToSequencer(string destinationChainAddress) payable",
"function withdrawToIbcChain(string destinationChainAddress, string memo) payable",
];
@@ -18,32 +17,22 @@ export class AstriaWithdrawerService extends GenericContractService {
) as AstriaWithdrawerService;
}
- async withdrawToSequencer(
- fromAddress: string,
- destinationChainAddress: string,
- amount: string,
- ): Promise {
- const amountWei = ethers.parseEther(amount);
- return this.callContractMethod(
- "withdrawToSequencer",
- fromAddress,
- [destinationChainAddress],
- amountWei,
- );
- }
-
async withdrawToIbcChain(
fromAddress: string,
destinationChainAddress: string,
amount: string,
+ amountDenom: number,
+ fee: string,
memo: string,
): Promise {
- const amountWei = ethers.parseEther(amount);
+ const amountWei = ethers.parseUnits(amount, amountDenom);
+ const feeWei = BigInt(fee);
+ const totalAmount = amountWei + feeWei;
return this.callContractMethod(
"withdrawToIbcChain",
fromAddress,
[destinationChainAddress, memo],
- amountWei,
+ totalAmount,
);
}
}
@@ -54,7 +43,6 @@ export class AstriaWithdrawerService extends GenericContractService {
*/
export class AstriaErc20WithdrawerService extends GenericContractService {
protected static override ABI: ethers.InterfaceAbi = [
- "function withdrawToSequencer(uint256 amount, string destinationChainAddress) payable",
"function withdrawToIbcChain(uint256 amount, string destinationChainAddress, string memo) payable",
"function balanceOf(address owner) view returns (uint256)",
];
@@ -69,30 +57,23 @@ export class AstriaErc20WithdrawerService extends GenericContractService {
contractAddress,
) as AstriaErc20WithdrawerService;
}
- async withdrawToSequencer(
- fromAddress: string,
- destinationChainAddress: string,
- amount: string,
- ): Promise {
- const amountWei = ethers.parseEther(amount);
- return this.callContractMethod("withdrawToSequencer", fromAddress, [
- amountWei,
- destinationChainAddress,
- ]);
- }
async withdrawToIbcChain(
fromAddress: string,
destinationChainAddress: string,
amount: string,
+ amountDenom: number,
+ fee: string,
memo: string,
): Promise {
- const amountWei = ethers.parseEther(amount);
- return this.callContractMethod("withdrawToIbcChain", fromAddress, [
- amountWei,
- destinationChainAddress,
- memo,
- ]);
+ const amountBaseUnits = ethers.parseUnits(amount, amountDenom);
+ const feeWei = BigInt(fee);
+ return this.callContractMethod(
+ "withdrawToIbcChain",
+ fromAddress,
+ [amountBaseUnits, destinationChainAddress, memo],
+ feeWei,
+ );
}
async getBalance(
diff --git a/web/src/testHelpers.tsx b/web/src/testHelpers.tsx
index 9b5da65..aa0b31d 100644
--- a/web/src/testHelpers.tsx
+++ b/web/src/testHelpers.tsx
@@ -1,8 +1,8 @@
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { render } from "@testing-library/react";
import type React from "react";
-import { EthWalletContextProvider } from "./features/EthWallet/contexts/EthWalletContext";
-import { ConfigContextProvider } from "./config/contexts/ConfigContext";
+import { EthWalletContextProvider } from "features/EthWallet";
+import { ConfigContextProvider } from "config";
export const renderWithRouter = (element: React.JSX.Element) => {
render(
From d356549e4c0f6888a9072efdf8ed6c0807390482 Mon Sep 17 00:00:00 2001
From: Jesse Snyder
Date: Tue, 29 Oct 2024 10:43:31 -0600
Subject: [PATCH 02/10] comment cleanup, import cleanup
---
web/src/App.test.tsx | 1 -
web/src/config/chainConfigs/types.ts | 1 -
web/src/config/contexts/ConfigContext.tsx | 6 +-----
web/src/styles/icons.scss | 1 -
4 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx
index 2e16cf5..9a92a64 100644
--- a/web/src/App.test.tsx
+++ b/web/src/App.test.tsx
@@ -1,5 +1,4 @@
import type React from "react";
-import { screen } from "@testing-library/react";
import App from "./App";
import { renderWithRouter } from "testHelpers";
diff --git a/web/src/config/chainConfigs/types.ts b/web/src/config/chainConfigs/types.ts
index 1da78f4..842663a 100644
--- a/web/src/config/chainConfigs/types.ts
+++ b/web/src/config/chainConfigs/types.ts
@@ -1,5 +1,4 @@
import type { ChainInfo } from "@keplr-wallet/types";
-import { ethers } from "ethers";
/**
* Represents information about an IBC chain.
diff --git a/web/src/config/contexts/ConfigContext.tsx b/web/src/config/contexts/ConfigContext.tsx
index 5be5ff1..f03ed66 100644
--- a/web/src/config/contexts/ConfigContext.tsx
+++ b/web/src/config/contexts/ConfigContext.tsx
@@ -1,11 +1,7 @@
import React from "react";
import type { AppConfig } from "config";
-import type {
- EvmChainInfo,
- EvmChains,
- IbcChains,
-} from "config/chainConfigs/types";
+import type { EvmChainInfo } from "config/chainConfigs/types";
import { getEnvChainConfigs } from "config/chainConfigs";
import { getEnvVariable } from "config/env";
diff --git a/web/src/styles/icons.scss b/web/src/styles/icons.scss
index 6210d22..3510531 100644
--- a/web/src/styles/icons.scss
+++ b/web/src/styles/icons.scss
@@ -48,7 +48,6 @@ i.i-flame {
}
i.i-noble {
- // this was downloaded from figma
background-image: url('https://avatars.githubusercontent.com/u/133800472?s=200&v=4');
background-repeat: no-repeat;
background-size: contain;
From 3037d3eab38913a1c0c8c6033a46853eb15c9577 Mon Sep 17 00:00:00 2001
From: Jesse Snyder
Date: Tue, 29 Oct 2024 11:15:34 -0600
Subject: [PATCH 03/10] update readme, app version
---
README.md | 30 +++++++++++++++++++-----------
web/package.json | 2 +-
2 files changed, 20 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index eb14117..b87b753 100644
--- a/README.md
+++ b/README.md
@@ -15,26 +15,29 @@ the Astria bridge.
* main application component
* define routes
* use context providers
-* `src/chainInfos` - Celestia and Astria chain information
-* `src/components` - React components
-* `src/contexts` - React context definitions
+* `src/components` - More general React components for the app, e.g. Navbar,
+ Dropdown, CopyToClipboardButton, etc
+* `src/config` - Configuration for the web app
+ * `src/config/chainConfigs` - Celestia and Astria chain information
+ * `src/config/contexts` - Config context and context provider
+ * `src/config/hooks` - Custom hook to make config easy to use
+ * `src/config/env.ts` - Environment variable definitions plus utilities for
+ consuming them
+ * `src/config/index.ts` - AppConfig and exports
+* `src/features` - Organizes components, contexts, hooks, services, types, and
+ utils for different features
+ * `src/features/EthWallet` - Used for interacting with EVM wallets
+ * `src/features/KeplrWallet` - User for interacting with Keplr wallet
+ * `src/features/Notifications` - Used for displaying notifications and toasts
* `src/pages`
* React components for each page
* `src/pages/Layout.tsx`
* page layout component using ``
* contains ``, ``
-* `src/providers` - React context provider definitions
-* `src/services`
- * api services
- * Keplr services
- * IBC services
- * 3rd party wrappers
* `src/styles`
* all style definitions
* using scss
* using [bulma](https://bulma.io/documentation/) css framework
-* `src/types` - type definitions
-* `src/utils` - utility functions
## Commands
@@ -53,19 +56,24 @@ just web build
* How to add new chain configs for a new environment (e.g. you want to add new
chain configs for "mainnet")
* create file that will contain the config values
+
```sh
touch web/src/config/chainConfigs/ChainConfigsMainnet.ts
```
+
* import new configs in
`astria-bridge-web-app/web/src/config/chainConfigs/index.ts`, while renaming
them
+
```typescript
import {
evmChains as mainnetEvmChains,
ibcChains as mainnetIbcChains,
} from "./ChainConfigsMainnet";
```
+
* add entry to `EVM_CHAIN_CONFIGS`
+
```typescript
const ENV_CHAIN_CONFIGS = {
local: { evm: localEvmChains, ibc: localIbcChains },
diff --git a/web/package.json b/web/package.json
index ec15acd..7b59f8e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "astria-bridge-web-app",
- "version": "0.0.1",
+ "version": "0.10.0",
"private": true,
"dependencies": {
"@cosmjs/launchpad": "^0.27.1",
From 158c03565dfc24b4dcc9d98ea294319bd9feaa4e Mon Sep 17 00:00:00 2001
From: jesse snyder
Date: Tue, 29 Oct 2024 13:03:44 -0600
Subject: [PATCH 04/10] Feature/show withdraw fee (#43)
* move shared functionality to hooks
* didnt move all shared logic
* display fee under balance
* move withdrawal fee to under amount
* move fee back to under balance
---
.../components/DepositCard/DepositCard.tsx | 40 +++--------------
.../components/WithdrawCard/WithdrawCard.tsx | 43 ++++++-------------
.../EthWallet/hooks/useEvmChainSelection.tsx | 40 ++++++++++++++++-
.../hooks/useIbcChainSelection.tsx | 20 +++++++++
4 files changed, 77 insertions(+), 66 deletions(-)
diff --git a/web/src/components/DepositCard/DepositCard.tsx b/web/src/components/DepositCard/DepositCard.tsx
index 85398b9..d789dbf 100644
--- a/web/src/components/DepositCard/DepositCard.tsx
+++ b/web/src/components/DepositCard/DepositCard.tsx
@@ -3,8 +3,8 @@ import { useEffect, useMemo, useState } from "react";
import { Dec, DecUtils } from "@keplr-wallet/unit";
import AnimatedArrowSpacer from "components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer";
-import Dropdown, { type DropdownOption } from "components/Dropdown/Dropdown";
-import { useConfig, type EvmChainInfo, type IbcChainInfo } from "config";
+import Dropdown from "components/Dropdown/Dropdown";
+import { useConfig } from "config";
import { useEvmChainSelection } from "features/EthWallet";
import { sendIbcTransfer, useIbcChainSelection } from "features/KeplrWallet";
import { useNotifications, NotificationType } from "features/Notifications";
@@ -18,55 +18,29 @@ export default function DepositCard(): React.ReactElement {
selectEvmChain,
evmChainsOptions,
selectedEvmChain,
+ selectedEvmChainOption,
+ defaultEvmCurrencyOption,
selectEvmCurrency,
evmCurrencyOptions,
evmBalance,
isLoadingEvmBalance,
connectEVMWallet,
} = useEvmChainSelection(evmChains);
- const defaultEvmCurrencyOption = useMemo(() => {
- return evmCurrencyOptions[0] || null;
- }, [evmCurrencyOptions]);
const {
ibcAccountAddress: fromAddress,
selectIbcChain,
ibcChainsOptions,
selectedIbcChain,
+ selectedIbcChainOption,
+ defaultIbcCurrencyOption,
selectIbcCurrency,
- ibcCurrencyOptions,
selectedIbcCurrency,
+ ibcCurrencyOptions,
ibcBalance,
isLoadingIbcBalance,
connectKeplrWallet,
} = useIbcChainSelection(ibcChains);
- const defaultIbcCurrencyOption = useMemo(() => {
- return ibcCurrencyOptions[0] || null;
- }, [ibcCurrencyOptions]);
-
- // selectedIbcChainOption allows us to ensure the label is set properly
- // in the dropdown when connecting via an "additional option"s action,
- // e.g. the "Connect Keplr Wallet" option in the dropdown
- const selectedIbcChainOption = useMemo(() => {
- if (!selectedIbcChain) {
- return null;
- }
- return {
- label: selectedIbcChain?.chainName || "",
- value: selectedIbcChain,
- leftIconClass: selectedIbcChain?.iconClass || "",
- } as DropdownOption;
- }, [selectedIbcChain]);
- const selectedEvmChainOption = useMemo(() => {
- if (!selectedEvmChain) {
- return null;
- }
- return {
- label: selectedEvmChain?.chainName || "",
- value: selectedEvmChain,
- leftIconClass: selectedEvmChain?.iconClass || "",
- } as DropdownOption;
- }, [selectedEvmChain]);
// 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
diff --git a/web/src/components/WithdrawCard/WithdrawCard.tsx b/web/src/components/WithdrawCard/WithdrawCard.tsx
index 203c8d1..945794c 100644
--- a/web/src/components/WithdrawCard/WithdrawCard.tsx
+++ b/web/src/components/WithdrawCard/WithdrawCard.tsx
@@ -1,9 +1,9 @@
import type React from "react";
import { useEffect, useMemo, useState } from "react";
-import { useConfig, type EvmChainInfo, type IbcChainInfo } from "config";
+import { useConfig } from "config";
import AnimatedArrowSpacer from "components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer";
-import Dropdown, { type DropdownOption } from "components/Dropdown/Dropdown";
+import Dropdown from "components/Dropdown/Dropdown";
import {
getAstriaWithdrawerService,
useEthWallet,
@@ -22,6 +22,9 @@ export default function WithdrawCard(): React.ReactElement {
selectEvmChain,
evmChainsOptions,
selectedEvmChain,
+ selectedEvmChainOption,
+ withdrawFeeDisplay,
+ defaultEvmCurrencyOption,
selectEvmCurrency,
evmCurrencyOptions,
selectedEvmCurrency,
@@ -29,47 +32,20 @@ export default function WithdrawCard(): React.ReactElement {
isLoadingEvmBalance,
connectEVMWallet,
} = useEvmChainSelection(evmChains);
- const defaultEvmCurrencyOption = useMemo(() => {
- return evmCurrencyOptions[0] || null;
- }, [evmCurrencyOptions]);
const {
ibcAccountAddress: recipientAddress,
selectIbcChain,
ibcChainsOptions,
selectedIbcChain,
+ selectedIbcChainOption,
+ defaultIbcCurrencyOption,
selectIbcCurrency,
ibcCurrencyOptions,
ibcBalance,
isLoadingIbcBalance,
connectKeplrWallet,
} = useIbcChainSelection(ibcChains);
- const defaultIbcCurrencyOption = useMemo(() => {
- return ibcCurrencyOptions[0] || null;
- }, [ibcCurrencyOptions]);
-
- // selectedIbcChainOption allows us to ensure the label is set properly
- // in the dropdown when connecting via additional action
- const selectedIbcChainOption = useMemo(() => {
- if (!selectedIbcChain) {
- return null;
- }
- return {
- label: selectedIbcChain?.chainName || "",
- value: selectedIbcChain,
- leftIconClass: selectedIbcChain?.iconClass || "",
- } as DropdownOption;
- }, [selectedIbcChain]);
- const selectedEvmChainOption = useMemo(() => {
- if (!selectedEvmChain) {
- return null;
- }
- return {
- label: selectedEvmChain?.chainName || "",
- value: selectedEvmChain,
- leftIconClass: selectedEvmChain?.iconClass || "",
- } as DropdownOption;
- }, [selectedEvmChain]);
// the ibc currency selection is controlled by the sender's chosen evm currency,
// and should be updated when an ibc currency or ibc chain is selected
@@ -338,6 +314,11 @@ export default function WithdrawCard(): React.ReactElement {
Balance:
)}
+ {withdrawFeeDisplay && (
+
+ Withdrawal fee: {withdrawFeeDisplay}
+
+ )}
)}
diff --git a/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx b/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
index 3093d7d..c649f09 100644
--- a/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
+++ b/web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
@@ -5,6 +5,8 @@ import React, {
useRef,
useState,
} from "react";
+import { ethers } from "ethers";
+
import type { DropdownOption } from "components/Dropdown/Dropdown";
import {
type EvmChainInfo,
@@ -12,7 +14,7 @@ import {
type EvmCurrency,
evmCurrencyBelongsToChain,
} from "config";
-import { useNotifications, NotificationType } from "features/Notifications";
+import { NotificationType, useNotifications } from "features/Notifications";
import { useEthWallet } from "features/EthWallet/hooks/useEthWallet";
import EthWalletConnector from "features/EthWallet/components/EthWalletConnector/EthWalletConnector";
@@ -93,6 +95,18 @@ export function useEvmChainSelection(evmChains: EvmChains) {
evmAccountAddress,
]);
+ const selectedEvmChainNativeToken = useMemo(() => {
+ return selectedEvmChain?.currencies[0];
+ }, [selectedEvmChain]);
+
+ const withdrawFeeDisplay = useMemo(() => {
+ if (!selectedEvmChainNativeToken || !selectedEvmCurrency) {
+ return "";
+ }
+ const fee = ethers.formatUnits(selectedEvmCurrency.ibcWithdrawalFeeWei, 18);
+ return `${fee} ${selectedEvmChainNativeToken.coinDenom}`;
+ }, [selectedEvmChainNativeToken, selectedEvmCurrency]);
+
const evmChainsOptions = useMemo(() => {
return Object.entries(evmChains).map(
([chainLabel, chain]): DropdownOption => ({
@@ -103,6 +117,20 @@ export function useEvmChainSelection(evmChains: EvmChains) {
);
}, [evmChains]);
+ // selectedEvmChainOption allows us to ensure the label is set properly
+ // in the dropdown when connecting via an "additional option"s action,
+ // e.g. the "Connect Keplr Wallet" option in the dropdown
+ const selectedEvmChainOption = useMemo(() => {
+ if (!selectedEvmChain) {
+ return null;
+ }
+ return {
+ label: selectedEvmChain?.chainName || "",
+ value: selectedEvmChain,
+ leftIconClass: selectedEvmChain?.iconClass || "",
+ } as DropdownOption;
+ }, [selectedEvmChain]);
+
const selectEvmChain = useCallback((chain: EvmChainInfo | null) => {
setSelectedEvmChain(chain);
}, []);
@@ -112,7 +140,7 @@ export function useEvmChainSelection(evmChains: EvmChains) {
return [];
}
- // can only withdraw the currency if it has a withdraw contract address defined
+ // can only withdraw the currency if it has a withdrawer contract address defined
const withdrawableTokens = selectedEvmChain.currencies?.filter(
(currency) =>
currency.erc20ContractAddress ||
@@ -128,6 +156,10 @@ export function useEvmChainSelection(evmChains: EvmChains) {
);
}, [selectedEvmChain]);
+ const defaultEvmCurrencyOption = useMemo(() => {
+ return evmCurrencyOptions[0] || null;
+ }, [evmCurrencyOptions]);
+
const selectEvmCurrency = useCallback((currency: EvmCurrency) => {
setSelectedEvmCurrency(currency);
}, []);
@@ -191,7 +223,11 @@ export function useEvmChainSelection(evmChains: EvmChains) {
selectEvmCurrency,
selectedEvmChain,
+ selectedEvmChainNativeToken,
+ withdrawFeeDisplay,
selectedEvmCurrency,
+ defaultEvmCurrencyOption,
+ selectedEvmChainOption,
evmAccountAddress,
evmBalance,
diff --git a/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx b/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx
index d98bc49..aa490a9 100644
--- a/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx
+++ b/web/src/features/KeplrWallet/hooks/useIbcChainSelection.tsx
@@ -115,6 +115,24 @@ export function useIbcChainSelection(ibcChains: IbcChains) {
);
}, [selectedIbcChain]);
+ const defaultIbcCurrencyOption = useMemo(() => {
+ return ibcCurrencyOptions[0] || null;
+ }, [ibcCurrencyOptions]);
+
+ // selectedIbcChainOption allows us to ensure the label is set properly
+ // in the dropdown when connecting via an "additional option"s action,
+ // e.g. the "Connect Keplr Wallet" option in the dropdown
+ const selectedIbcChainOption = useMemo(() => {
+ if (!selectedIbcChain) {
+ return null;
+ }
+ return {
+ label: selectedIbcChain?.chainName || "",
+ value: selectedIbcChain,
+ leftIconClass: selectedIbcChain?.iconClass || "",
+ } as DropdownOption;
+ }, [selectedIbcChain]);
+
const selectIbcCurrency = useCallback((currency: IbcCurrency) => {
setSelectedIbcCurrency(currency);
}, []);
@@ -192,6 +210,8 @@ export function useIbcChainSelection(ibcChains: IbcChains) {
selectedIbcChain,
selectedIbcCurrency,
+ defaultIbcCurrencyOption,
+ selectedIbcChainOption,
ibcAccountAddress,
ibcBalance,
From c743627d1dc2238d897d380570a51a27417ca3a7 Mon Sep 17 00:00:00 2001
From: Jesse Snyder
Date: Tue, 29 Oct 2024 17:41:43 -0600
Subject: [PATCH 05/10] don't open navbar links in new tab
---
web/src/components/Navbar/Navbar.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/web/src/components/Navbar/Navbar.tsx b/web/src/components/Navbar/Navbar.tsx
index 0301e7c..3c311eb 100644
--- a/web/src/components/Navbar/Navbar.tsx
+++ b/web/src/components/Navbar/Navbar.tsx
@@ -54,7 +54,6 @@ function Navbar() {
BRIDGE
Date: Tue, 29 Oct 2024 17:41:56 -0600
Subject: [PATCH 06/10] icons
---
.../icons/logos/milk-tia-logo-color.png | Bin 0 -> 32244 bytes
.../assets/icons/logos/noble-logo-color.png | Bin 0 -> 2364 bytes
.../assets/icons/logos/osmosis-logo-color.svg | 118 ++++++++++++++++++
.../assets/icons/logos/stride-logo-color.svg | 4 +
.../icons/logos/stride-tia-logo-color.png | Bin 0 -> 18883 bytes
.../assets/icons/logos/usdc-logo-color.svg | 20 +++
web/src/styles/icons.scss | 44 ++++++-
7 files changed, 185 insertions(+), 1 deletion(-)
create mode 100644 web/public/assets/icons/logos/milk-tia-logo-color.png
create mode 100644 web/public/assets/icons/logos/noble-logo-color.png
create mode 100644 web/public/assets/icons/logos/osmosis-logo-color.svg
create mode 100644 web/public/assets/icons/logos/stride-logo-color.svg
create mode 100644 web/public/assets/icons/logos/stride-tia-logo-color.png
create mode 100644 web/public/assets/icons/logos/usdc-logo-color.svg
diff --git a/web/public/assets/icons/logos/milk-tia-logo-color.png b/web/public/assets/icons/logos/milk-tia-logo-color.png
new file mode 100644
index 0000000000000000000000000000000000000000..aeea2be79e5e0e7fc740aeda2c99b710559e0e5d
GIT binary patch
literal 32244
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%0D+Sv}5>Q3?N7ZFAly%%@DQUzvJu#(c{*Ybs
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/public/assets/icons/logos/stride-logo-color.svg b/web/public/assets/icons/logos/stride-logo-color.svg
new file mode 100644
index 0000000..68d1e18
--- /dev/null
+++ b/web/public/assets/icons/logos/stride-logo-color.svg
@@ -0,0 +1,4 @@
+
diff --git a/web/public/assets/icons/logos/stride-tia-logo-color.png b/web/public/assets/icons/logos/stride-tia-logo-color.png
new file mode 100644
index 0000000000000000000000000000000000000000..3f298de4edfa7ada95570389178ee409b70bf3e7
GIT binary patch
literal 18883
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