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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 + + + + + + + + diff --git a/web/src/styles/icons.scss b/web/src/styles/icons.scss index 3510531..d5c039e 100644 --- a/web/src/styles/icons.scss +++ b/web/src/styles/icons.scss @@ -48,9 +48,51 @@ i.i-flame { } i.i-noble { - background-image: url('https://avatars.githubusercontent.com/u/133800472?s=200&v=4'); + background-image: url('../../public/assets/icons/logos/noble-logo-color.png'); background-repeat: no-repeat; background-size: contain; height: 100%; width: 100%; + // crop to circle + border-radius: 50%; } + +i.i-usdc { + background-image: url('../../public/assets/icons/logos/usdc-logo-color.svg'); + background-repeat: no-repeat; + background-size: contain; + height: 100%; + width: 100%; +} + +i.i-milk-tia { + background-image: url('../../public/assets/icons/logos/milk-tia-logo-color.png'); + background-repeat: no-repeat; + background-size: contain; + height: 100%; + width: 100%; +} + +i.i-stride { + background-image: url('../../public/assets/icons/logos/stride-logo-color.svg'); + background-repeat: no-repeat; + background-size: contain; + height: 100%; + width: 100%; +} + +i.i-stride-tia { + background-image: url('../../public/assets/icons/logos/stride-tia-logo-color.png'); + background-repeat: no-repeat; + background-size: contain; + height: 100%; + width: 100%; +} + +i.i-osmosis { + background-image: url('../../public/assets/icons/logos/osmosis-logo-color.svg'); + background-repeat: no-repeat; + background-size: contain; + height: 100%; + width: 100%; +} \ No newline at end of file From 4cf7c444d317c629bdf944a5371c1896b3b67151 Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Tue, 29 Oct 2024 17:42:19 -0600 Subject: [PATCH 07/10] milktia and sttia configs --- .../chainConfigs/ChainConfigsMainnet.ts | 149 +++++++++++++----- 1 file changed, 112 insertions(+), 37 deletions(-) diff --git a/web/src/config/chainConfigs/ChainConfigsMainnet.ts b/web/src/config/chainConfigs/ChainConfigsMainnet.ts index 6510e15..54c1a28 100644 --- a/web/src/config/chainConfigs/ChainConfigsMainnet.ts +++ b/web/src/config/chainConfigs/ChainConfigsMainnet.ts @@ -85,32 +85,16 @@ const CelestiaChainInfo: IbcChainInfo = { 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", @@ -119,41 +103,22 @@ const NobleChainInfo: IbcChainInfo = { 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", + iconClass: "i-usdc", }, ], - // 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, @@ -164,9 +129,103 @@ const NobleChainInfo: IbcChainInfo = { iconClass: "i-noble", }; +const OsmosisChainInfo: IbcChainInfo = { + chainId: "osmosis-1", + chainName: "Osmosis", + rpc: "https://osmosis-rpc.polkachu.com/", + rest: "https://osmosis-api.polkachu.com/", + stakeCurrency: { + coinDenom: "milkTIA", + coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinDecimals: 6, + }, + bip44: { + coinType: 118, + }, + bech32Config: { + bech32PrefixAccAddr: "osmosis", + bech32PrefixAccPub: "osmosispub", + bech32PrefixConsAddr: "osmosisvalcons", + bech32PrefixConsPub: "osmosisvalconspub", + bech32PrefixValAddr: "osmosisvaloper", + bech32PrefixValPub: "osmosisvaloperpub", + }, + currencies: [ + { + coinDenom: "milkTIA", + coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinDecimals: 6, + ibcChannel: "channel-85486", + sequencerBridgeAccount: "astria1kgxhyhvynhcwwrylkzzx6q3a8rn3tuvasxvuy8", + iconClass: "i-milk-tia", + }, + ], + feeCurrencies: [ + { + coinDenom: "milkTIA", + coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.02, + high: 0.1, + }, + }, + ], + iconClass: "i-osmosis", +}; + +const StrideChainInfo: IbcChainInfo = { + chainId: "stride-1", + chainName: "Stride", + rpc: "https://stride-rpc.polkachu.com", + rest: "https://stride-api.polkachu.com/", + stakeCurrency: { + coinDenom: "stTIA", + coinMinimalDenom: "stutia", + coinDecimals: 6, + }, + bip44: { + coinType: 118, + }, + bech32Config: { + bech32PrefixAccAddr: "stride", + bech32PrefixAccPub: "stridepub", + bech32PrefixConsAddr: "stridevalcons", + bech32PrefixConsPub: "stridevalconspub", + bech32PrefixValAddr: "stridevaloper", + bech32PrefixValPub: "stridevaloperpub", + }, + currencies: [ + { + coinDenom: "stTIA", + coinMinimalDenom: "stutia", + coinDecimals: 6, + ibcChannel: "channel-285", + sequencerBridgeAccount: "astria1dllx9d9karss9ca8le25a4vqhf67a67d5d4l6r", + iconClass: "i-stride-tia", + }, + ], + feeCurrencies: [ + { + coinDenom: "stTIA", + coinMinimalDenom: "stutia", + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.02, + high: 0.1, + }, + }, + ], + iconClass: "i-stride", +}; + export const ibcChains: IbcChains = { Celestia: CelestiaChainInfo, Noble: NobleChainInfo, + Osmosis: OsmosisChainInfo, + Stride: StrideChainInfo, }; const FlameChainInfo: EvmChainInfo = { @@ -189,7 +248,23 @@ const FlameChainInfo: EvmChainInfo = { coinDecimals: 6, erc20ContractAddress: "0x3f65144F387f6545bF4B19a1B39C94231E1c849F", ibcWithdrawalFeeWei: "10000000000000000", - iconClass: "i-noble", + iconClass: "i-usdc", + }, + { + coinDenom: "milkTIA", + coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinDecimals: 18, + erc20ContractAddress: "0xcbb93e854AA4EF5Db51c3b094F28952eF0dC67bE", + ibcWithdrawalFeeWei: "10000000000000000", + iconClass: "i-milk-tia", + }, + { + coinDenom: "stTIA", + coinMinimalDenom: "stutia", + coinDecimals: 18, + erc20ContractAddress: "0xdf941D092b10FF07eAb44bD174dEe915c13FECcd", + ibcWithdrawalFeeWei: "10000000000000000", + iconClass: "i-stride-tia", }, ], iconClass: "i-flame", From 1b05a7f7c5f5f71f4e13cc99500d0d9a087eecfc Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Tue, 29 Oct 2024 17:42:49 -0600 Subject: [PATCH 08/10] fix icon styling --- web/src/styles/dropdown-customizations.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/styles/dropdown-customizations.scss b/web/src/styles/dropdown-customizations.scss index 2191e8c..2764562 100644 --- a/web/src/styles/dropdown-customizations.scss +++ b/web/src/styles/dropdown-customizations.scss @@ -28,10 +28,6 @@ color: $dropdown-item-color; } - &.icon-left { - height: 20px; - } - &.icon-right { margin-left: auto; } From fedcdf7b7787adb5abf5d3738bd69ae827b9da2c Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Tue, 29 Oct 2024 17:43:10 -0600 Subject: [PATCH 09/10] use selected currency's denom. increase gaslimit --- web/src/features/KeplrWallet/services/ibc.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/src/features/KeplrWallet/services/ibc.ts b/web/src/features/KeplrWallet/services/ibc.ts index 8b9ba83..eaf3263 100644 --- a/web/src/features/KeplrWallet/services/ibc.ts +++ b/web/src/features/KeplrWallet/services/ibc.ts @@ -64,9 +64,10 @@ export const sendIbcTransfer = async ( throw new Error("Failed to get account from Keplr wallet."); } - // TODO - does the fee need to be configurable in the ui? - const feeDenom = selectedIbcChain.feeCurrencies[0].coinMinimalDenom; + const feeDenom = currency.coinMinimalDenom; const memo = JSON.stringify({ rollupDepositAddress: recipient }); + + // TODO - does the fee need to be configurable in the ui? const fee = { amount: [ { @@ -74,7 +75,7 @@ export const sendIbcTransfer = async ( amount: "0", }, ], - gas: "180000", + gas: "350000", }; const msgIBCTransfer = { From 55b7644e066d06d7b32c1f2cdc0b27890d3a6012 Mon Sep 17 00:00:00 2001 From: Jesse Snyder Date: Tue, 29 Oct 2024 17:49:50 -0600 Subject: [PATCH 10/10] linting, formatting --- web/src/components/Navbar/Navbar.tsx | 12 ++---------- web/src/config/chainConfigs/ChainConfigsMainnet.ts | 12 ++++++++---- web/src/styles/icons.scss | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/web/src/components/Navbar/Navbar.tsx b/web/src/components/Navbar/Navbar.tsx index 3c311eb..5aab975 100644 --- a/web/src/components/Navbar/Navbar.tsx +++ b/web/src/components/Navbar/Navbar.tsx @@ -53,18 +53,10 @@ function Navbar() { BRIDGE - + SWAP - + POOL diff --git a/web/src/config/chainConfigs/ChainConfigsMainnet.ts b/web/src/config/chainConfigs/ChainConfigsMainnet.ts index 54c1a28..4cc604b 100644 --- a/web/src/config/chainConfigs/ChainConfigsMainnet.ts +++ b/web/src/config/chainConfigs/ChainConfigsMainnet.ts @@ -136,7 +136,8 @@ const OsmosisChainInfo: IbcChainInfo = { rest: "https://osmosis-api.polkachu.com/", stakeCurrency: { coinDenom: "milkTIA", - coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinMinimalDenom: + "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", coinDecimals: 6, }, bip44: { @@ -153,7 +154,8 @@ const OsmosisChainInfo: IbcChainInfo = { currencies: [ { coinDenom: "milkTIA", - coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinMinimalDenom: + "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", coinDecimals: 6, ibcChannel: "channel-85486", sequencerBridgeAccount: "astria1kgxhyhvynhcwwrylkzzx6q3a8rn3tuvasxvuy8", @@ -163,7 +165,8 @@ const OsmosisChainInfo: IbcChainInfo = { feeCurrencies: [ { coinDenom: "milkTIA", - coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinMinimalDenom: + "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", coinDecimals: 6, gasPriceStep: { low: 0.01, @@ -252,7 +255,8 @@ const FlameChainInfo: EvmChainInfo = { }, { coinDenom: "milkTIA", - coinMinimalDenom: "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + coinMinimalDenom: + "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", coinDecimals: 18, erc20ContractAddress: "0xcbb93e854AA4EF5Db51c3b094F28952eF0dC67bE", ibcWithdrawalFeeWei: "10000000000000000", diff --git a/web/src/styles/icons.scss b/web/src/styles/icons.scss index d5c039e..d44b8b4 100644 --- a/web/src/styles/icons.scss +++ b/web/src/styles/icons.scss @@ -95,4 +95,4 @@ i.i-osmosis { background-size: contain; height: 100%; width: 100%; -} \ No newline at end of file +}