From 32e9c61959db5344290764c60c7eee62ddcefc4f Mon Sep 17 00:00:00 2001 From: Seun Lanlege Date: Sat, 1 Aug 2026 12:56:38 +0000 Subject: [PATCH 1/2] fix: rpc fallback transports and native balance display for weth wrappers --- src/core/config/registry/hft/README.md | 3 ++- src/core/config/wagmi.ts | 12 +++++++++--- src/core/lib/balance-helper.ts | 4 +++- src/shared/config/registry/evm-networks.ts | 3 ++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/core/config/registry/hft/README.md b/src/core/config/registry/hft/README.md index e12c03b..565eb5e 100644 --- a/src/core/config/registry/hft/README.md +++ b/src/core/config/registry/hft/README.md @@ -34,7 +34,8 @@ use HFT contracts on EVM and `pallet-hft` on Substrate. type: "wrapped-hft", underlying: "0x...", // canonical ERC20 weth: true, // only if the contract's isWeth() is true: - // sends spend native via msg.value, balances read native + // sends spend native via msg.value; the app shows the + // native balance and symbol (e.g. tBNB) in the form }, { chainId: Base.chainId, diff --git a/src/core/config/wagmi.ts b/src/core/config/wagmi.ts index 455efad..5ef75e2 100644 --- a/src/core/config/wagmi.ts +++ b/src/core/config/wagmi.ts @@ -1,7 +1,7 @@ import type { NetworkConfig } from "@hyperbridge-fe/shared" import { EVM_NETWORKS, VIEM_CHAIN_MAP } from "@hyperbridge-fe/shared/config" import { getDefaultConfig } from "connectkit" -import type { HttpTransport } from "viem" +import { fallback, type Transport } from "viem" import { createConfig, http } from "wagmi" import { coinbaseWallet, injected, walletConnect } from "wagmi/connectors" import { APP_NAME, APP_URL, NETWORK_ENV, WALLET_CONNECT_ID } from "@/config/constants" @@ -52,10 +52,16 @@ function createWagmiConfig(): any { return createConfig(getDefaultConfig(config as any)) } +// Fall back through every configured RPC so one flaky endpoint doesn't +// zero out balances or block sends for the whole chain. const networkToTransport = ( network: NetworkConfig, -): [number, HttpTransport] => { - return [Number(network.chainId), http(network.rpcUrls[0])] +): [number, Transport] => { + const transports = network.rpcUrls.map((url) => http(url)) + return [ + Number(network.chainId), + transports.length > 1 ? fallback(transports) : transports[0], + ] } export type WagmiChainId = number diff --git a/src/core/lib/balance-helper.ts b/src/core/lib/balance-helper.ts index df45de3..dae42d2 100644 --- a/src/core/lib/balance-helper.ts +++ b/src/core/lib/balance-helper.ts @@ -227,7 +227,9 @@ export const BalanceHelper = { chainId, }) - return BalanceImpl.create(balance.value, decimals, symbol) + // Users spend the native token, so show its symbol (e.g. tBNB), not the + // wrapper's registry symbol. + return BalanceImpl.create(balance.value, balance.decimals, balance.symbol) } const underlying = diff --git a/src/shared/config/registry/evm-networks.ts b/src/shared/config/registry/evm-networks.ts index b825654..584b5d0 100644 --- a/src/shared/config/registry/evm-networks.ts +++ b/src/shared/config/registry/evm-networks.ts @@ -225,8 +225,9 @@ export const BscTestnet: EVMChainConfig = { chainId: 97, stateMachineId: "EVM-97", rpcUrls: [ - "https://bsc-testnet-rpc.publicnode.com", + "https://data-seed-prebsc-1-s1.bnbchain.org:8545", `https://bnb-testnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`, + "https://bsc-testnet-rpc.publicnode.com", "https://bsc-geth-testnet-rpc.blockops.network", ], networkType: "testnet", From 908544dc9dd402ca012e7881ea13353b3230eeb7 Mon Sep 17 00:00:00 2001 From: Seun Lanlege Date: Sat, 1 Aug 2026 13:18:05 +0000 Subject: [PATCH 2/2] fix: enforce polygon minimum priority fee on hft transactions --- src/core/lib/fee-token-preparation.ts | 4 ++ src/core/lib/transactions/bridge-hft.ts | 5 +++ src/core/lib/utils/__tests__/gas.test.ts | 40 ++++++++++++++++++ src/core/lib/utils/gas.ts | 52 ++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 src/core/lib/utils/__tests__/gas.test.ts create mode 100644 src/core/lib/utils/gas.ts diff --git a/src/core/lib/fee-token-preparation.ts b/src/core/lib/fee-token-preparation.ts index e6c61e1..0bc5fee 100644 --- a/src/core/lib/fee-token-preparation.ts +++ b/src/core/lib/fee-token-preparation.ts @@ -15,6 +15,7 @@ import { TokenFaucetABI } from "@/abis/TokenFaucet" import { gatewayConfig } from "@/config/services/gateway-config" import { WagmiConfig } from "@/config/wagmi" import { rootLogger } from "@/lib/logger" +import { evmFeeOverrides } from "@/lib/utils/gas" const logger = rootLogger.withTag("FeeTokenPrep") @@ -134,6 +135,8 @@ export async function requestFeeTokenDrip(params: { await switchChain(WagmiConfig, { chainId: params.source.chainId }) + const feeOverrides = await evmFeeOverrides(params.source.chainId) + const hash = await writeContract(WagmiConfig, { abi: TokenFaucetABI, address: faucet, @@ -141,6 +144,7 @@ export async function requestFeeTokenDrip(params: { args: [params.feeTokenAddress], chainId: params.source.chainId, account: params.owner, + ...feeOverrides, }) await waitForTransactionReceipt(WagmiConfig, { diff --git a/src/core/lib/transactions/bridge-hft.ts b/src/core/lib/transactions/bridge-hft.ts index bb9e61c..6cf3612 100644 --- a/src/core/lib/transactions/bridge-hft.ts +++ b/src/core/lib/transactions/bridge-hft.ts @@ -20,6 +20,7 @@ import { isHftToken, } from "@/lib/hft/hyper-fungible-token" import { rootLogger } from "@/lib/logger" +import { evmFeeOverrides } from "@/lib/utils/gas" import type { EVMToken } from "@/types" import type { TxCreationEvents } from "@/types/tx" import { WagmiConfig, type WagmiChainId } from "@/config/wagmi" @@ -90,6 +91,8 @@ export class HftBridgeTx implements BridgeTxExecutor { throw new Error("Wallet not connected") } + const feeOverrides = await evmFeeOverrides(source.chainId) + const gen = hft.bridge({ token: token.address, from: account.address, @@ -113,6 +116,7 @@ export class HftBridgeTx implements BridgeTxExecutor { to: step.tx.to, data: step.tx.data, chainId: source.chainId as WagmiChainId, + ...feeOverrides, }) await waitForTransactionReceipt(WagmiConfig, { hash, @@ -129,6 +133,7 @@ export class HftBridgeTx implements BridgeTxExecutor { data: step.tx.data, value: step.tx.value, chainId: source.chainId as WagmiChainId, + ...feeOverrides, }) yield { diff --git a/src/core/lib/utils/__tests__/gas.test.ts b/src/core/lib/utils/__tests__/gas.test.ts new file mode 100644 index 0000000..0bac685 --- /dev/null +++ b/src/core/lib/utils/__tests__/gas.test.ts @@ -0,0 +1,40 @@ +import { parseGwei } from "viem" +import { describe, expect, it } from "vitest" +import { clampToMinPriorityFee } from "../gas" + +describe("clampToMinPriorityFee", () => { + const floor = parseGwei("25") + + it("raises an undershooting tip to the floor and keeps base-fee headroom", () => { + const result = clampToMinPriorityFee( + { + maxPriorityFeePerGas: parseGwei("1.5"), + maxFeePerGas: parseGwei("2"), // 0.5 gwei of base-fee headroom + }, + floor, + ) + + expect(result.maxPriorityFeePerGas).toBe(floor) + expect(result.maxFeePerGas).toBe(floor + parseGwei("0.5")) + }) + + it("leaves estimates above the floor untouched", () => { + const fees = { + maxPriorityFeePerGas: parseGwei("30"), + maxFeePerGas: parseGwei("80"), + } + + expect(clampToMinPriorityFee(fees, floor)).toEqual(fees) + }) + + it("never returns maxFeePerGas below the tip", () => { + const result = clampToMinPriorityFee( + { maxPriorityFeePerGas: parseGwei("1"), maxFeePerGas: parseGwei("1") }, + floor, + ) + + expect(result.maxFeePerGas).toBeGreaterThanOrEqual( + result.maxPriorityFeePerGas, + ) + }) +}) diff --git a/src/core/lib/utils/gas.ts b/src/core/lib/utils/gas.ts new file mode 100644 index 0000000..ebe756e --- /dev/null +++ b/src/core/lib/utils/gas.ts @@ -0,0 +1,52 @@ +import { estimateFeesPerGas } from "@wagmi/core" +import { parseGwei } from "viem" +import { WagmiConfig } from "@/config/wagmi" +import { rootLogger } from "@/lib/logger" + +const logger = rootLogger.withTag("Gas") + +/** + * Bor (Polygon) chains reject transactions with a priority fee below 25 gwei, + * and wallet estimators regularly undershoot it (MetaMask suggests 1.5 gwei), + * failing broadcasts with "transaction gas price below minimum". + */ +const MIN_PRIORITY_FEE: Record = { + 137: parseGwei("25"), + 80002: parseGwei("25"), +} + +export type FeeOverrides = { + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint +} + +/** Raises the priority fee to the floor, keeping the base-fee headroom intact. */ +export function clampToMinPriorityFee( + fees: { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint }, + minPriorityFee: bigint, +): Required { + const tip = + fees.maxPriorityFeePerGas >= minPriorityFee + ? fees.maxPriorityFeePerGas + : minPriorityFee + const basePortion = fees.maxFeePerGas - fees.maxPriorityFeePerGas + + return { maxPriorityFeePerGas: tip, maxFeePerGas: basePortion + tip } +} + +/** + * Explicit fee fields for chains whose minimum priority fee wallets undershoot. + * Returns {} elsewhere so the wallet keeps estimating on its own. + */ +export async function evmFeeOverrides(chainId: number): Promise { + const floor = MIN_PRIORITY_FEE[chainId] + if (!floor) return {} + + try { + const fees = await estimateFeesPerGas(WagmiConfig, { chainId }) + return clampToMinPriorityFee(fees, floor) + } catch (err) { + logger.warn(`Fee estimation failed for Chain(${chainId})`, err) + return {} + } +}