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(