diff --git a/packages/bridge/package.json b/packages/bridge/package.json index 2d7cb64e8c..4173bc14e3 100644 --- a/packages/bridge/package.json +++ b/packages/bridge/package.json @@ -36,10 +36,9 @@ "@osmosis-labs/tx": "^1.0.0", "@osmosis-labs/utils": "^1.0.0", "cachified": "^3.5.4", - "ethers": "^6.8.0", "long": "^5.2.3", "lru-cache": "^10.0.1", - "web3-utils": "^1.7.4", + "viem": "2.13.3", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts index 00b278078c..4f9bfa221f 100644 --- a/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts +++ b/packages/bridge/src/axelar/__tests__/axelar-bridge-provider.spec.ts @@ -14,17 +14,15 @@ import { NativeEVMTokenConstantAddress } from "../../ethereum"; import { BridgeProviderContext } from "../../interface"; import { AxelarBridgeProvider } from "../index"; -jest.mock("ethers", () => { - const originalModule = jest.requireActual("ethers"); +jest.mock("viem", () => { + const originalModule = jest.requireActual("viem"); return { ...originalModule, - ethers: { - ...originalModule.ethers, - JsonRpcProvider: jest.fn().mockImplementation(() => ({ - estimateGas: jest.fn().mockResolvedValue("21000"), - _perform: jest.fn().mockResolvedValue("0x4a817c800"), - })), - }, + createPublicClient: jest.fn().mockImplementation(() => ({ + estimateGas: jest.fn().mockResolvedValue(BigInt("21000")), + getGasPrice: jest.fn().mockResolvedValue(BigInt("0x4a817c800")), + })), + http: jest.fn(), }; }); @@ -42,6 +40,10 @@ beforeEach(() => { ); }); +afterEach(() => { + jest.clearAllMocks(); +}); + describe("AxelarBridgeProvider", () => { let provider: AxelarBridgeProvider; let ctx: BridgeProviderContext; @@ -186,6 +188,45 @@ describe("AxelarBridgeProvider", () => { }); }); + it("should create an EVM transaction with native token", async () => { + const mockDepositClient: Partial = { + getDepositAddress: jest + .fn() + .mockResolvedValue("0x1234567890abcdef1234567890abcdef12345678"), + }; + + jest + .spyOn(provider, "getAssetTransferClient") + .mockResolvedValue(mockDepositClient as unknown as AxelarAssetTransfer); + + const transaction = await provider.createEvmTransaction({ + fromChain: { chainId: "1", chainName: "Ethereum", chainType: "evm" }, + toChain: { chainId: "43114", chainName: "Avalanche", chainType: "evm" }, + fromAsset: { + denom: "ETH", + address: NativeEVMTokenConstantAddress, + decimals: 18, + sourceDenom: "eth", + }, + toAsset: { + denom: "AVAX", + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + sourceDenom: "avax", + }, + fromAmount: "1", + fromAddress: "0x1234567890abcdef1234567890abcdef12345678", + toAddress: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef", + simulated: false, + }); + + expect(transaction).toEqual({ + value: "0x1", // same as from amount + type: "evm", + to: "0x1234567890abcdef1234567890abcdef12345678", + }); + }); + it("should throw an error when creating an EVM transaction with a non-native token", async () => { const mockDepositClient: Partial = { getDepositAddress: jest diff --git a/packages/bridge/src/axelar/index.ts b/packages/bridge/src/axelar/index.ts index 2020723465..8ea7e35371 100644 --- a/packages/bridge/src/axelar/index.ts +++ b/packages/bridge/src/axelar/index.ts @@ -6,15 +6,17 @@ import { CoinPretty, Dec } from "@keplr-wallet/unit"; import type { IbcTransferMethod } from "@osmosis-labs/types"; import { getAssetFromAssetList, getKeyByValue } from "@osmosis-labs/utils"; import { cachified } from "cachified"; -import { ethers } from "ethers"; -import { hexToNumberString, toHex } from "web3-utils"; +import { + Address, + createPublicClient, + encodeFunctionData, + erc20Abi, + http, + numberToHex, +} from "viem"; import { BridgeError, BridgeQuoteError } from "../errors"; -import { - Erc20Abi, - EthereumChainInfo, - NativeEVMTokenConstantAddress, -} from "../ethereum"; +import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeCoin, @@ -260,26 +262,27 @@ export class AxelarBridgeProvider implements BridgeProvider { if (transactionData.type === "evm") { const evmChain = Object.values(EthereumChainInfo).find( - ({ chainId }) => String(chainId) === String(params.fromChain.chainId) + ({ id: chainId }) => + String(chainId) === String(params.fromChain.chainId) ); if (!evmChain) throw new Error("Could not find EVM chain"); - const fromProvider = new ethers.JsonRpcProvider(evmChain.rpcUrls[0]); + const fromProvider = createPublicClient({ + chain: evmChain, + transport: http(evmChain.rpcUrls.default.http[0]), + }); const gasAmountUsed = String( await fromProvider.estimateGas({ - from: params.fromAddress, + account: params.fromAddress as Address, to: transactionData.to, - value: transactionData.value, + value: BigInt(transactionData.value ?? ""), data: transactionData.data, }) ); - const gasPrice = hexToNumberString( - await fromProvider._perform({ - method: "getGasPrice", - }) - ); + + const gasPrice = (await fromProvider.getGasPrice()).toString(); const gasCost = new Dec(gasAmountUsed).mul(new Dec(gasPrice)); return { @@ -356,17 +359,18 @@ export class AxelarBridgeProvider implements BridgeProvider { if (isNativeToken) { return { type: "evm", - to: depositAddress, - value: toHex(fromAmount), + to: depositAddress as Address, + value: numberToHex(BigInt(fromAmount)), }; } else { return { type: "evm", - to: fromAsset.address, // ERC20 token address - data: Erc20Abi.encodeFunctionData("transfer", [ - depositAddress, - toHex(fromAmount), - ]), + to: fromAsset.address as Address, // ERC20 token address + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [depositAddress as `0x${string}`, BigInt(fromAmount)], + }), }; } } @@ -546,7 +550,7 @@ export class AxelarBridgeProvider implements BridgeProvider { } const ethereumChainName = Object.values(EthereumChainInfo).find( - ({ chainId }) => String(chainId) === String(chain.chainId) + ({ id: chainId }) => String(chainId) === String(chain.chainId) )?.chainName; if (!ethereumChainName) return undefined; diff --git a/packages/bridge/src/axelar/tokens.ts b/packages/bridge/src/axelar/tokens.ts index 72abbec19c..099ebbf76c 100644 --- a/packages/bridge/src/axelar/tokens.ts +++ b/packages/bridge/src/axelar/tokens.ts @@ -53,8 +53,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Goerli Testnet"].chainName : EthereumChainInfo["Ethereum"].chainName, chainId: isTestnet - ? EthereumChainInfo["Goerli Testnet"].chainId - : EthereumChainInfo["Ethereum"].chainId, + ? EthereumChainInfo["Goerli Testnet"].id + : EthereumChainInfo["Ethereum"].id, erc20ContractAddress: isTestnet ? "0x254d06f33bDc5b8ee05b2ea472107E300226659A" : "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // test: 'aUSDC' on metamask/etherscan @@ -65,8 +65,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Avalanche Fuji Testnet"].chainName : EthereumChainInfo["Avalanche"].chainName, chainId: isTestnet - ? EthereumChainInfo["Avalanche Fuji Testnet"].chainId - : EthereumChainInfo["Avalanche"].chainId, + ? EthereumChainInfo["Avalanche Fuji Testnet"].id + : EthereumChainInfo["Avalanche"].id, erc20ContractAddress: isTestnet ? "0x57F1c63497AEe0bE305B8852b354CEc793da43bB" : "0xfaB550568C688d5D8A52C7d794cb93Edc26eC0eC", @@ -77,8 +77,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["BSC Testnet"].chainName : EthereumChainInfo["Binance Smart Chain"].chainName, chainId: isTestnet - ? EthereumChainInfo["BSC Testnet"].chainId - : EthereumChainInfo["Binance Smart Chain"].chainId, + ? EthereumChainInfo["BSC Testnet"].id + : EthereumChainInfo["Binance Smart Chain"].id, erc20ContractAddress: isTestnet ? "0xc2fA98faB811B785b81c64Ac875b31CC9E40F9D2" : "0x4268B8F0B87b6Eae5d897996E6b845ddbD99Adf3", @@ -89,8 +89,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Fantom Testnet"].chainName : EthereumChainInfo["Fantom"].chainName, chainId: isTestnet - ? EthereumChainInfo["Fantom Testnet"].chainId - : EthereumChainInfo["Fantom"].chainId, + ? EthereumChainInfo["Fantom Testnet"].id + : EthereumChainInfo["Fantom"].id, erc20ContractAddress: isTestnet ? "0x75Cc4fDf1ee3E781C1A3Ee9151D5c6Ce34Cf5C61" : "0x1B6382DBDEa11d97f24495C9A90b7c88469134a4", @@ -101,8 +101,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Moonbase Alpha"].chainName : EthereumChainInfo["Moonbeam"].chainName, chainId: isTestnet - ? EthereumChainInfo["Moonbase Alpha"].chainId - : EthereumChainInfo["Moonbeam"].chainId, + ? EthereumChainInfo["Moonbase Alpha"].id + : EthereumChainInfo["Moonbeam"].id, erc20ContractAddress: isTestnet ? "0xD1633F7Fb3d716643125d6415d4177bC36b7186b" : "0xCa01a1D0993565291051daFF390892518ACfAD3A", @@ -113,8 +113,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Mumbai"].chainName : EthereumChainInfo["Polygon"].chainName, chainId: isTestnet - ? EthereumChainInfo["Mumbai"].chainId - : EthereumChainInfo["Polygon"].chainId, + ? EthereumChainInfo["Mumbai"].id + : EthereumChainInfo["Polygon"].id, erc20ContractAddress: isTestnet ? "0x2c852e740B62308c46DD29B982FBb650D063Bd07" : "0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed", @@ -143,8 +143,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Moonbase Alpha"].chainName : EthereumChainInfo["Moonbeam"].chainName, chainId: isTestnet - ? EthereumChainInfo["Moonbase Alpha"].chainId - : EthereumChainInfo["Moonbeam"].chainId, + ? EthereumChainInfo["Moonbase Alpha"].id + : EthereumChainInfo["Moonbeam"].id, erc20ContractAddress: isTestnet ? "0x1436aE0dF0A8663F18c0Ec51d7e2E46591730715" : "0xAcc15dC74880C9944775448304B263D191c6077F", @@ -158,7 +158,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { wbtc: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", logoUrl: "/networks/ethereum.svg", }, @@ -166,7 +166,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { dai: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", logoUrl: "/networks/ethereum.svg", }, @@ -174,7 +174,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { dot: { moonbeam: { id: EthereumChainInfo["Moonbeam"].chainName, - chainId: EthereumChainInfo["Moonbeam"].chainId, + chainId: EthereumChainInfo["Moonbeam"].id, erc20ContractAddress: "0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080", logoUrl: "/networks/moonbeam.svg", }, @@ -182,7 +182,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { usdt: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xdAC17F958D2ee523a2206206994597C13D831ec7", logoUrl: "/networks/ethereum.svg", }, @@ -190,7 +190,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { frax: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", logoUrl: "/networks/ethereum.svg", }, @@ -198,7 +198,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { link: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x514910771AF9Ca656af840dff83E8264EcF986CA", logoUrl: "/networks/ethereum.svg", }, @@ -206,7 +206,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { aave: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", logoUrl: "/networks/ethereum.svg", }, @@ -214,7 +214,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ape: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x4d224452801ACEd8B2F0aebE155379bb5D594381", logoUrl: "/networks/ethereum.svg", }, @@ -222,7 +222,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { axs: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xBB0E17EF65F82Ab018d8EDd776e8DD940327B28b", logoUrl: "/networks/ethereum.svg", }, @@ -230,7 +230,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { mkr: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", logoUrl: "/networks/ethereum.svg", }, @@ -238,7 +238,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { rai: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x03ab458634910AaD20eF5f1C8ee96F1D6ac54919", logoUrl: "/networks/ethereum.svg", }, @@ -246,7 +246,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { shib: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", logoUrl: "/networks/ethereum.svg", }, @@ -254,7 +254,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { uni: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", logoUrl: "/networks/ethereum.svg", }, @@ -262,7 +262,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { xcn: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xA2cd3D43c775978A96BdBf12d733D5A1ED94fb18", logoUrl: "/networks/ethereum.svg", }, @@ -270,7 +270,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { pepe: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x6982508145454Ce325dDbE47a25d4ec3d2311933", logoUrl: "/networks/ethereum.svg", }, @@ -278,7 +278,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { cbeth: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xbe9895146f7af43049ca1c1ae358b0541ea49704", logoUrl: "/networks/ethereum.svg", }, @@ -286,7 +286,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { reth: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xae78736cd615f374d3085123a210448e74fc6393", logoUrl: "/networks/ethereum.svg", }, @@ -294,7 +294,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { sfrxeth: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xac3e018457b222d93114458476f3e3416abbe38f", logoUrl: "/networks/ethereum.svg", }, @@ -302,7 +302,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { wsteth: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", logoUrl: "/networks/ethereum.svg", }, @@ -310,7 +310,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { yieldeth: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0xb5b29320d2Dde5BA5BAFA1EbcD270052070483ec", logoUrl: "/networks/ethereum.svg", }, @@ -321,8 +321,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["BSC Testnet"].chainName : EthereumChainInfo["Binance Smart Chain"].chainName, chainId: isTestnet - ? EthereumChainInfo["BSC Testnet"].chainId - : EthereumChainInfo["Binance Smart Chain"].chainId, + ? EthereumChainInfo["BSC Testnet"].id + : EthereumChainInfo["Binance Smart Chain"].id, erc20ContractAddress: isTestnet ? "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd" : "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", @@ -339,8 +339,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Mumbai"].chainName : EthereumChainInfo["Polygon"].chainName, chainId: isTestnet - ? EthereumChainInfo["Mumbai"].chainId - : EthereumChainInfo["Polygon"].chainId, + ? EthereumChainInfo["Mumbai"].id + : EthereumChainInfo["Polygon"].id, erc20ContractAddress: isTestnet ? "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889" : "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", @@ -354,7 +354,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { busd: { ethereum: { id: EthereumChainInfo["Ethereum"].chainName, - chainId: EthereumChainInfo["Ethereum"].chainId, + chainId: EthereumChainInfo["Ethereum"].id, erc20ContractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", logoUrl: "/networks/ethereum.svg", }, @@ -365,8 +365,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Avalanche Fuji Testnet"].chainName : EthereumChainInfo["Avalanche"].chainName, chainId: isTestnet - ? EthereumChainInfo["Avalanche Fuji Testnet"].chainId - : EthereumChainInfo["Avalanche"].chainId, + ? EthereumChainInfo["Avalanche Fuji Testnet"].id + : EthereumChainInfo["Avalanche"].id, erc20ContractAddress: isTestnet ? "0xd00ae08403B9bbb9124bB305C09058E32C39A48c" : "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", @@ -396,7 +396,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { polygonusdc: { polygon: { id: EthereumChainInfo["Polygon"].chainName, - chainId: EthereumChainInfo["Polygon"].chainId, + chainId: EthereumChainInfo["Polygon"].id, erc20ContractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", logoUrl: "/networks/polygon.svg", }, @@ -404,7 +404,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { avalancheusdc: { avalanche: { id: EthereumChainInfo["Avalanche"].chainName, - chainId: EthereumChainInfo["Avalanche"].chainId, + chainId: EthereumChainInfo["Avalanche"].id, erc20ContractAddress: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", logoUrl: "/networks/avalanche.svg", }, @@ -415,8 +415,8 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { ? EthereumChainInfo["Filecoin Hyperspace"].chainName : EthereumChainInfo["Filecoin"].chainName, chainId: isTestnet - ? EthereumChainInfo["Filecoin Hyperspace"].chainId - : EthereumChainInfo["Filecoin"].chainId, + ? EthereumChainInfo["Filecoin Hyperspace"].id + : EthereumChainInfo["Filecoin"].id, erc20ContractAddress: isTestnet ? "0x6C297AeD654816dc5d211c956DE816Ba923475D2" : "0x60E1773636CF5E4A227d9AC24F20fEca034ee25A", @@ -430,7 +430,7 @@ export const AxelarSourceChainTokenConfigs: (env: BridgeEnvironment) => { arb: { arbitrum: { id: EthereumChainInfo["Arbitrum"].chainName, - chainId: EthereumChainInfo["Arbitrum"].chainId, + chainId: EthereumChainInfo["Arbitrum"].id, erc20ContractAddress: "0x912CE59144191C1204E64559FE8253a0e49E6548", logoUrl: "/networks/arbitrum.svg", }, diff --git a/packages/bridge/src/ethereum.ts b/packages/bridge/src/ethereum.ts index a3d60711a0..f37522ace7 100644 --- a/packages/bridge/src/ethereum.ts +++ b/packages/bridge/src/ethereum.ts @@ -1,4 +1,21 @@ -import { Interface } from "ethers"; +import { + arbitrum, + avalanche, + avalancheFuji, + bsc, + bscTestnet, + Chain, + fantom, + fantomTestnet, + filecoin, + filecoinHyperspace, + goerli, + mainnet, + moonbaseAlpha, + moonbeam, + polygon, + polygonMumbai, +} from "viem/chains"; import { SourceChain } from "./chain"; @@ -7,16 +24,9 @@ const createEthereumChainInfo = < Dict extends Partial< Record< SourceChain, - { - chainId: number; + Chain & { chainName: SourceChain; clientChainId: string; - rpcUrls: string[]; - nativeCurrency: { - name: string; - symbol: string; - decimals: number; - }; } > > @@ -24,172 +34,96 @@ const createEthereumChainInfo = < dict: Dict ) => dict; +const mapChainInfo = ({ + chain, + axelarChainName: chainName, + clientChainId, +}: { + chain: Chain; + axelarChainName: SourceChain; + clientChainId: string; +}) => ({ + ...chain, + chainName: chainName, + clientChainId: clientChainId, +}); + export const EthereumChainInfo = createEthereumChainInfo({ - Ethereum: { - chainId: 1, - chainName: "Ethereum", + Ethereum: mapChainInfo({ + chain: mainnet, + axelarChainName: "Ethereum", clientChainId: "Ethereum Main Network", - rpcUrls: ["https://ethereum.publicnode.com"], - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - }, - "Goerli Testnet": { - chainId: 5, - chainName: "Goerli Testnet", + }), + "Goerli Testnet": mapChainInfo({ + chain: goerli, + axelarChainName: "Goerli Testnet", clientChainId: "Goerli Test Network", - rpcUrls: ["https://optimism-goerli.publicnode.com"], - nativeCurrency: { - name: "Goerli Ether", - symbol: "ETH", - decimals: 18, - }, - }, - "Binance Smart Chain": { - chainId: 56, - chainName: "Binance Smart Chain", + }), + "Binance Smart Chain": mapChainInfo({ + chain: bsc, + axelarChainName: "Binance Smart Chain", clientChainId: "Binance Smart Chain Mainnet", - rpcUrls: ["https://bsc-dataseed.binance.org/"], - nativeCurrency: { - name: "Binance Chain Native Token", - symbol: "BNB", - decimals: 18, - }, - }, - "BSC Testnet": { - chainId: 97, - chainName: "BSC Testnet", + }), + "BSC Testnet": mapChainInfo({ + chain: bscTestnet, + axelarChainName: "BSC Testnet", clientChainId: "Binance Smart Chain Testnet", - rpcUrls: ["https://binance.llamarpc.com"], - nativeCurrency: { - name: "Binance Chain Native Token", - symbol: "BNB", - decimals: 18, - }, - }, - Polygon: { - chainId: 137, - chainName: "Polygon", + }), + Polygon: mapChainInfo({ + chain: polygon, + axelarChainName: "Polygon", clientChainId: "Polygon Mainnet", - rpcUrls: ["https://polygon-rpc.com/"], - nativeCurrency: { - name: "Matic", - symbol: "MATIC", - decimals: 18, - }, - }, - Mumbai: { - chainId: 80001, - chainName: "Mumbai", + }), + Mumbai: mapChainInfo({ + chain: polygonMumbai, + axelarChainName: "Mumbai", clientChainId: "Mumbai", - rpcUrls: ["https://polygon-mumbai-bor.publicnode.com"], - nativeCurrency: { - name: "Matic", - symbol: "MATIC", - decimals: 18, - }, - }, - Moonbeam: { - chainId: 1284, - chainName: "Moonbeam", + }), + Moonbeam: mapChainInfo({ + chain: moonbeam, + axelarChainName: "Moonbeam", clientChainId: "Moonbeam Mainnet", - rpcUrls: ["https://moonbeam.publicnode.com"], - nativeCurrency: { - name: "Moonbeam", - symbol: "GLMR", - decimals: 18, - }, - }, - "Moonbase Alpha": { - chainId: 1287, - chainName: "Moonbase Alpha", + }), + "Moonbase Alpha": mapChainInfo({ + chain: moonbaseAlpha, + axelarChainName: "Moonbase Alpha", clientChainId: "Moonbase Alpha", - rpcUrls: ["https://moonbase-alpha.public.blastapi.io"], - nativeCurrency: { - name: "Moonbase Alpha", - symbol: "DEV", - decimals: 18, - }, - }, - Fantom: { - chainId: 250, - chainName: "Fantom", + }), + Fantom: mapChainInfo({ + chain: fantom, + axelarChainName: "Fantom", clientChainId: "Fantom Opera", - rpcUrls: ["https://fantom.publicnode.com"], - nativeCurrency: { - name: "Fantom", - symbol: "FTM", - decimals: 18, - }, - }, - "Fantom Testnet": { - chainId: 4002, - chainName: "Fantom Testnet", + }), + "Fantom Testnet": mapChainInfo({ + chain: fantomTestnet, + axelarChainName: "Fantom Testnet", clientChainId: "Fantom Testnet", - rpcUrls: ["https://fantom-testnet.publicnode.com"], - nativeCurrency: { - name: "Fantom", - symbol: "FTM", - decimals: 18, - }, - }, - "Avalanche Fuji Testnet": { - chainId: 43113, - chainName: "Avalanche Fuji Testnet", + }), + "Avalanche Fuji Testnet": mapChainInfo({ + chain: avalancheFuji, + axelarChainName: "Avalanche Fuji Testnet", clientChainId: "Avalanche Fuji Testnet", - rpcUrls: ["https://api.avax-test.network/ext/bc/C/rpc"], - nativeCurrency: { - name: "Avalanche", - symbol: "AVAX", - decimals: 18, - }, - }, - Avalanche: { - chainId: 43114, - chainName: "Avalanche", + }), + Avalanche: mapChainInfo({ + chain: avalanche, + axelarChainName: "Avalanche", clientChainId: "Avalanche C-Chain", - rpcUrls: ["https://api.avax.network/ext/bc/C/rpc"], - nativeCurrency: { - name: "Avalanche", - symbol: "AVAX", - decimals: 18, - }, - }, - Arbitrum: { - chainId: 42161, - chainName: "Arbitrum", + }), + Arbitrum: mapChainInfo({ + chain: arbitrum, + axelarChainName: "Arbitrum", clientChainId: "Arbitrum One", - rpcUrls: ["https://arbitrum-one.publicnode.com"], - nativeCurrency: { - name: "Arbitrum", - symbol: "ETH", - decimals: 18, - }, - }, - Filecoin: { - chainId: 461, - chainName: "Filecoin", + }), + Filecoin: mapChainInfo({ + chain: filecoin, + axelarChainName: "Filecoin", clientChainId: "Filecoin - Mainnet", - rpcUrls: ["https://rpc.ankr.com/filecoin"], - nativeCurrency: { - name: "Filecoin", - symbol: "FIL", - decimals: 18, - }, - }, - "Filecoin Hyperspace": { - chainId: 3141, - chainName: "Filecoin Hyperspace", + }), + "Filecoin Hyperspace": mapChainInfo({ + chain: filecoinHyperspace, + axelarChainName: "Filecoin Hyperspace", clientChainId: "Filecoin Hyperspace", - rpcUrls: [""], - nativeCurrency: { - name: "Filecoin", - symbol: "FIL", - decimals: 18, - }, - }, + }), }); /** @@ -198,227 +132,3 @@ export const EthereumChainInfo = createEthereumChainInfo({ */ export const NativeEVMTokenConstantAddress = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; - -/** ABI spec for interfacing with ERC20 token contracts on EVM chains. */ -export const Erc20Abi = new Interface([ - { - constant: true, - inputs: [], - name: "name", - outputs: [ - { - name: "", - type: "string", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, - { - constant: false, - inputs: [ - { - name: "_spender", - type: "address", - }, - { - name: "_value", - type: "uint256", - }, - ], - name: "approve", - outputs: [ - { - name: "", - type: "bool", - }, - ], - payable: false, - stateMutability: "nonpayable", - type: "function", - }, - { - constant: true, - inputs: [], - name: "totalSupply", - outputs: [ - { - name: "", - type: "uint256", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, - { - constant: false, - inputs: [ - { - name: "_from", - type: "address", - }, - { - name: "_to", - type: "address", - }, - { - name: "_value", - type: "uint256", - }, - ], - name: "transferFrom", - outputs: [ - { - name: "", - type: "bool", - }, - ], - payable: false, - stateMutability: "nonpayable", - type: "function", - }, - { - constant: true, - inputs: [], - name: "decimals", - outputs: [ - { - name: "", - type: "uint8", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, - { - constant: true, - inputs: [ - { - name: "_owner", - type: "address", - }, - ], - name: "balanceOf", // balanceOf - outputs: [ - { - name: "balance", - type: "uint256", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, - { - constant: true, - inputs: [], - name: "symbol", - outputs: [ - { - name: "", - type: "string", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, - { - constant: false, - inputs: [ - { - name: "_to", - type: "address", - }, - { - name: "_value", - type: "uint256", - }, - ], - name: "transfer", - outputs: [ - { - name: "", - type: "bool", - }, - ], - payable: false, - stateMutability: "nonpayable", - type: "function", - }, - { - constant: true, - inputs: [ - { - name: "_owner", - type: "address", - }, - { - name: "_spender", - type: "address", - }, - ], - name: "allowance", - outputs: [ - { - name: "", - type: "uint256", - }, - ], - payable: false, - stateMutability: "view", - type: "function", - }, - { - payable: true, - stateMutability: "payable", - type: "fallback", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - name: "owner", - type: "address", - }, - { - indexed: true, - name: "spender", - type: "address", - }, - { - indexed: false, - name: "value", - type: "uint256", - }, - ], - name: "Approval", - type: "event", - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - name: "from", - type: "address", - }, - { - indexed: true, - name: "to", - type: "address", - }, - { - indexed: false, - name: "value", - type: "uint256", - }, - ], - name: "Transfer", - type: "event", - }, -]); diff --git a/packages/bridge/src/interface.ts b/packages/bridge/src/interface.ts index bdd02eb0d6..cbe01b3e6c 100644 --- a/packages/bridge/src/interface.ts +++ b/packages/bridge/src/interface.ts @@ -1,6 +1,7 @@ import type { AssetList, Chain } from "@osmosis-labs/types"; import type { CacheEntry } from "cachified"; import type { LRUCache } from "lru-cache"; +import { Address, Hex } from "viem"; import { z } from "zod"; export type BridgeEnvironment = "mainnet" | "testnet"; @@ -178,8 +179,8 @@ export type GetBridgeQuoteParams = z.infer; export interface EvmBridgeTransactionRequest { type: "evm"; - to: string; - data?: string; + to: Address; + data?: Hex; value?: string; gasPrice?: string; maxPriorityFeePerGas?: string; diff --git a/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts b/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts index 9a230e6435..aa89681566 100644 --- a/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts +++ b/packages/bridge/src/skip/__tests__/skip-bridge-provider.spec.ts @@ -16,31 +16,19 @@ import { import { SkipBridgeProvider } from ".."; import { SkipMsg } from "../types"; -jest.mock("ethers", () => { - const originalModule = jest.requireActual("ethers"); +jest.mock("viem", () => { + const originalModule = jest.requireActual("viem"); return { ...originalModule, - ethers: { - ...originalModule.ethers, - JsonRpcProvider: jest.fn().mockImplementation(() => ({ - estimateGas: jest.fn().mockResolvedValue("21000"), - send: jest.fn().mockResolvedValue("0x4a817c800"), - getFeeData: jest.fn().mockResolvedValue({ - gasPrice: BigInt("20000000000"), - maxFeePerGas: BigInt("30000000000"), - maxPriorityFeePerGas: BigInt("1000000000"), - }), - })), - Contract: jest.fn().mockImplementation(() => ({ - allowance: jest.fn().mockResolvedValue(BigInt("100")), - approve: { - populateTransaction: jest.fn().mockResolvedValue({ - to: "0x123", - data: "0xabcdef", - }), - }, - })), - }, + createPublicClient: jest.fn().mockImplementation(() => ({ + estimateGas: jest.fn().mockResolvedValue(BigInt("21000")), + request: jest.fn().mockResolvedValue("0x4a817c800"), + getGasPrice: jest.fn().mockResolvedValue(BigInt("20000000000")), + readContract: jest.fn().mockResolvedValue(BigInt("100")), + })), + encodeFunctionData: jest.fn().mockReturnValue("0xabcdef"), + encodePacked: jest.fn().mockReturnValue("0xabcdef"), + keccak256: jest.fn().mockReturnValue("0xabcdef"), }; }); @@ -144,6 +132,10 @@ beforeEach(() => { ); }); +afterEach(() => { + jest.clearAllMocks(); +}); + describe("SkipBridgeProvider", () => { let provider: SkipBridgeProvider; let ctx: BridgeProviderContext; diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index 81f55fc059..c0a5d5e20c 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -1,15 +1,21 @@ import { fromBech32, toBech32 } from "@cosmjs/encoding"; import { CoinPretty } from "@keplr-wallet/unit"; +import { isNil } from "@osmosis-labs/utils"; import cachified from "cachified"; -import { ethers, JsonRpcProvider } from "ethers"; -import { toHex } from "web3-utils"; +import { + Address, + createPublicClient, + encodeFunctionData, + encodePacked, + erc20Abi, + http, + keccak256, + maxUint256, + numberToHex, +} from "viem"; import { BridgeError, BridgeQuoteError } from "../errors"; -import { - Erc20Abi, - EthereumChainInfo, - NativeEVMTokenConstantAddress, -} from "../ethereum"; +import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, @@ -151,7 +157,7 @@ export class SkipBridgeProvider implements BridgeProvider { const transactionRequest = await this.createTransaction( fromChain.chainId.toString(), - fromAddress, + fromAddress as Address, msgs ); @@ -211,7 +217,7 @@ export class SkipBridgeProvider implements BridgeProvider { async createTransaction( chainID: string, - address: string, + address: Address, messages: SkipMsg[] ) { for (const message of messages) { @@ -262,7 +268,7 @@ export class SkipBridgeProvider implements BridgeProvider { async createEvmTransaction( chainID: string, - sender: string, + sender: Address, message: SkipEvmTx ): Promise { let approvalTransactionRequest; @@ -278,32 +284,35 @@ export class SkipBridgeProvider implements BridgeProvider { return { type: "evm", - to: message.to, + to: message.to as Address, data: `0x${message.data}`, - value: toHex(message.value), + value: numberToHex(BigInt(message.value)), approvalTransactionRequest, }; } - private getEthersProvider(chainID: string) { + private getViemProvider(chainID: string) { const evmChain = Object.values(EthereumChainInfo).find( - (chain) => chain.chainId.toString() === chainID + (chain) => chain.id.toString() === chainID ); if (!evmChain) { throw new Error("Could not find EVM chain"); } - const provider = new ethers.JsonRpcProvider(evmChain.rpcUrls[0]); + const provider = createPublicClient({ + chain: evmChain, + transport: http(evmChain.rpcUrls.default.http[0]), + }); return provider; } async getApprovalTransactionRequest( chainID: string, - tokenAddress: string, - owner: string, - spender: string, + tokenAddress: Address, + owner: Address, + spender: Address, amount: string ): Promise< | { @@ -312,28 +321,28 @@ export class SkipBridgeProvider implements BridgeProvider { } | undefined > { - const provider = this.getEthersProvider(chainID); - - const fromTokenContract = new ethers.Contract( - tokenAddress, - Erc20Abi, - provider - ); + const provider = this.getViemProvider(chainID); - const allowance = await fromTokenContract.allowance(owner, spender); + const allowance = await provider.readContract({ + abi: erc20Abi, + address: tokenAddress, + functionName: "allowance", + args: [owner, spender], + }); if (BigInt(allowance.toString()) >= BigInt(amount)) { return; } - const approveTx = await fromTokenContract.approve.populateTransaction( - spender, - amount - ); + const approveTxData = encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, BigInt(amount)], + }); return { - to: approveTx.to!, - data: approveTx.data!, + to: tokenAddress, + data: approveTxData, }; } @@ -493,12 +502,15 @@ export class SkipBridgeProvider implements BridgeProvider { } const evmChain = Object.values(EthereumChainInfo).find( - ({ chainId }) => chainId === params.fromChain.chainId + ({ id: chainId }) => chainId === params.fromChain.chainId ); if (!evmChain) throw new Error("Could not find EVM chain"); - const provider = new ethers.JsonRpcProvider(evmChain.rpcUrls[0]); + const provider = createPublicClient({ + chain: evmChain, + transport: http(evmChain.rpcUrls.default.http[0]), + }); const estimatedGas = await this.estimateEvmGasWithStateOverrides( provider, @@ -509,13 +521,13 @@ export class SkipBridgeProvider implements BridgeProvider { return; } - const feeData = await provider.getFeeData(); + const gasPrice = await provider.getGasPrice(); - if (!feeData.gasPrice) { + if (!gasPrice) { throw new Error("Failed to get gas price"); } - const gasCost = estimatedGas * feeData.gasPrice; + const gasCost = estimatedGas * gasPrice; return { amount: gasCost.toString(), @@ -526,17 +538,17 @@ export class SkipBridgeProvider implements BridgeProvider { } async estimateEvmGasWithStateOverrides( - provider: JsonRpcProvider, + provider: ReturnType, params: GetBridgeQuoteParams, txData: EvmBridgeTransactionRequest ) { try { if (!txData.approvalTransactionRequest) { const estimatedGas = await provider.estimateGas({ - from: params.fromAddress, + account: params.fromAddress as Address, to: txData.to, data: txData.data, - value: txData.value, + value: !isNil(txData.value) ? BigInt(txData.value) : undefined, }); return BigInt(estimatedGas); @@ -544,13 +556,14 @@ export class SkipBridgeProvider implements BridgeProvider { const slot = 10; // Allowance slot (differs from contract to contract but is usually 10) - const temp = ethers.solidityPackedKeccak256( - ["uint256", "uint256"], - [params.fromAddress, slot] + const temp = keccak256( + encodePacked( + ["uint256", "uint256"], + [BigInt(params.fromAddress), BigInt(slot)] + ) ); - const index = ethers.solidityPackedKeccak256( - ["uint256", "uint256"], - [txData.to, temp] + const index = keccak256( + encodePacked(["uint256", "uint256"], [BigInt(txData.to), BigInt(temp)]) ); const callParams = [ @@ -566,16 +579,16 @@ export class SkipBridgeProvider implements BridgeProvider { const stateDiff = { [txData.approvalTransactionRequest.to]: { stateDiff: { - [index]: `0x${ethers.MaxUint256.toString(16)}`, + [index]: `0x${maxUint256.toString(16)}`, }, }, }; // Call with no state overrides - const callResult = await provider.send("eth_estimateGas", [ - ...callParams, - stateDiff, - ]); + const callResult = await provider.request({ + method: "eth_estimateGas", + params: [...callParams, stateDiff], + }); return BigInt(callResult); } catch (err) { diff --git a/packages/bridge/src/skip/types.ts b/packages/bridge/src/skip/types.ts index 1927deef47..aa3577c113 100644 --- a/packages/bridge/src/skip/types.ts +++ b/packages/bridge/src/skip/types.ts @@ -1,3 +1,5 @@ +import { Address } from "viem"; + export type SkipAsset = { denom: string; chain_id: string; @@ -171,15 +173,15 @@ export type SkipMultiChainMsg = { export type SkipEvmTx = { chain_id: string; - to: string; + to: Address; value: string; data: string; required_erc20_approvals: SkipERC20Approval[]; }; export type SkipERC20Approval = { - token_contract: string; - spender: string; + token_contract: Address; + spender: Address; amount: string; }; diff --git a/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts b/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts index 416f94d320..59bd335aba 100644 --- a/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts +++ b/packages/bridge/src/squid/__tests__/squid-bridge-provider.spec.ts @@ -2,6 +2,7 @@ import { CacheEntry } from "cachified"; import { LRUCache } from "lru-cache"; // eslint-disable-next-line import/no-extraneous-dependencies import { rest } from "msw"; +import { createPublicClient, http } from "viem"; import { MockAssetLists } from "../../__tests__/mock-asset-lists"; import { server } from "../../__tests__/msw"; @@ -9,28 +10,23 @@ import { BridgeQuoteError } from "../../errors"; import { BridgeProviderContext } from "../../interface"; import { SquidBridgeProvider } from "../index"; -jest.mock("ethers", () => { - const originalModule = jest.requireActual("ethers"); +jest.mock("viem", () => { + const originalModule = jest.requireActual("viem"); return { ...originalModule, - ethers: { - ...originalModule.ethers, - JsonRpcProvider: jest.fn().mockImplementation(() => ({ - estimateGas: jest.fn().mockResolvedValue("21000"), - send: jest.fn().mockResolvedValue("0x4a817c800"), - })), - Contract: jest.fn().mockImplementation(() => ({ - allowance: jest.fn().mockResolvedValue(BigInt("100")), - approve: { - populateTransaction: jest.fn().mockResolvedValue({ - to: "0x123", - data: "0xabcdef", - }), - }, - })), - }, + createPublicClient: jest.fn().mockImplementation(() => ({ + readContract: jest.fn().mockImplementation(({ functionName }) => { + if (functionName === "allowance") { + return Promise.resolve(BigInt("100")); + } + return Promise.reject(new Error("Unknown function")); + }), + })), + encodeFunctionData: jest.fn().mockImplementation(() => "0xabcdef"), + http: jest.fn().mockImplementation(() => ({})), }; }); + beforeEach(() => { server.use( rest.get("https://api.0xsquid.com/v1/route", (_req, res, ctx) => { @@ -84,6 +80,10 @@ beforeEach(() => { ); }); +afterEach(() => { + jest.clearAllMocks(); +}); + describe("SquidBridgeProvider", () => { let provider: SquidBridgeProvider; let ctx: BridgeProviderContext; @@ -269,4 +269,66 @@ describe("SquidBridgeProvider", () => { }) ).rejects.toThrow("toAsset mismatch"); }); + + it("should return approval transaction data if allowance is less than amount", async () => { + const fromTokenContract = createPublicClient({ + transport: http(), + }); + (fromTokenContract.readContract as jest.Mock).mockResolvedValueOnce( + BigInt("50") + ); + + const approvalTx = await provider.getApprovalTx({ + fromTokenContract, + tokenAddress: "0xTokenAddress", + isFromAssetNative: false, + fromAmount: "100", + fromAddress: "0xFromAddress", + fromChain: { chainId: "1", chainName: "Ethereum", chainType: "evm" }, + targetAddress: "0xTargetAddress", + }); + + expect(approvalTx).toEqual({ + to: "0xTokenAddress", + data: "0xabcdef", // Mocked data from encodeFunctionData + }); + }); + + it("should return undefined if allowance is greater than or equal to amount", async () => { + const fromTokenContract = createPublicClient({ + transport: http(), + }); + (fromTokenContract.readContract as jest.Mock).mockResolvedValueOnce( + BigInt("150") + ); + + const approvalTx = await provider.getApprovalTx({ + fromTokenContract, + tokenAddress: "0xTokenAddress", + isFromAssetNative: false, + fromAmount: "100", + fromAddress: "0xFromAddress", + fromChain: { chainId: "1", chainName: "Ethereum", chainType: "evm" }, + targetAddress: "0xTargetAddress", + }); + + expect(approvalTx).toBeUndefined(); + }); + + it("should return undefined if the asset is native", async () => { + const fromTokenContract = createPublicClient({ + transport: http(), + }); + const approvalTx = await provider.getApprovalTx({ + fromTokenContract, + tokenAddress: "0xTokenAddress", + isFromAssetNative: true, + fromAmount: "100", + fromAddress: "0xFromAddress", + fromChain: { chainId: "1", chainName: "Ethereum", chainType: "evm" }, + targetAddress: "0xTargetAddress", + }); + + expect(approvalTx).toBeUndefined(); + }); }); diff --git a/packages/bridge/src/squid/__tests__/squid-transfer-status.spec.ts b/packages/bridge/src/squid/__tests__/squid-transfer-status.spec.ts index 0052465e3e..1d2166a72d 100644 --- a/packages/bridge/src/squid/__tests__/squid-transfer-status.spec.ts +++ b/packages/bridge/src/squid/__tests__/squid-transfer-status.spec.ts @@ -21,6 +21,10 @@ jest.mock("@osmosis-labs/utils", () => { }; }); +afterEach(() => { + jest.clearAllMocks(); +}); + describe("SquidTransferStatusProvider", () => { let provider: SquidTransferStatusProvider; const mockReceiver: TransferStatusReceiver = { @@ -32,10 +36,6 @@ describe("SquidTransferStatusProvider", () => { provider.statusReceiverDelegate = mockReceiver; }); - afterEach(() => { - jest.clearAllMocks(); - }); - it("should initialize with correct URLs", () => { expect(provider.squidScanBaseUrl).toBe("https://axelarscan.io"); }); diff --git a/packages/bridge/src/squid/index.ts b/packages/bridge/src/squid/index.ts index 4963280776..0c036efe84 100644 --- a/packages/bridge/src/squid/index.ts +++ b/packages/bridge/src/squid/index.ts @@ -7,12 +7,18 @@ import type { import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { apiClient, ApiClientError, isNil } from "@osmosis-labs/utils"; import { cachified } from "cachified"; -import { ethers } from "ethers"; import Long from "long"; -import { toHex } from "web3-utils"; +import { + Address, + createPublicClient, + encodeFunctionData, + erc20Abi, + http, + numberToHex, +} from "viem"; import { BridgeError, BridgeQuoteError } from "../errors"; -import { Erc20Abi, NativeEVMTokenConstantAddress } from "../ethereum"; +import { EthereumChainInfo, NativeEVMTokenConstantAddress } from "../ethereum"; import { BridgeAsset, BridgeChain, @@ -276,21 +282,29 @@ export class SquidBridgeProvider implements BridgeProvider { ]); } - let approvalTx: ethers.ContractTransaction | undefined; + let approvalTx: { to: Address; data: string } | undefined; try { - const fromProvider = new ethers.JsonRpcProvider(squidFromChain.rpc); - const fromTokenContract = new ethers.Contract( - fromAsset.address, - Erc20Abi, - fromProvider + const evmChain = Object.values(EthereumChainInfo).find( + ({ id: chainId }) => String(chainId) === String(squidFromChain.chainId) ); + + if (!evmChain) { + throw new Error("Could not find EVM chain"); + } + + const fromTokenContract = createPublicClient({ + chain: evmChain, + transport: http(evmChain.rpcUrls.default.http[0]), + }); + approvalTx = await this.getApprovalTx({ - fromAddress, + fromAddress: fromAddress as Address, fromAmount: estimateFromAmount, fromChain, isFromAssetNative, fromTokenContract, - targetAddress: transactionRequest.targetAddress, + targetAddress: transactionRequest.targetAddress as Address, + tokenAddress: fromAsset.address as Address, }); } catch (e) { throw new BridgeQuoteError([ @@ -303,23 +317,23 @@ export class SquidBridgeProvider implements BridgeProvider { return { type: "evm", - to: transactionRequest.targetAddress, - data: transactionRequest.data, + to: transactionRequest.targetAddress as Address, + data: transactionRequest.data as Address, value: transactionRequest.routeType !== "SEND" - ? toHex(transactionRequest.value) + ? numberToHex(BigInt(transactionRequest.value)) : undefined, ...(transactionRequest.maxPriorityFeePerGas ? { - gas: toHex(transactionRequest.gasLimit), - maxFeePerGas: toHex(transactionRequest.maxFeePerGas), - maxPriorityFeePerGas: toHex( - transactionRequest.maxPriorityFeePerGas + gas: numberToHex(BigInt(transactionRequest.gasLimit)), + maxFeePerGas: numberToHex(BigInt(transactionRequest.maxFeePerGas)), + maxPriorityFeePerGas: numberToHex( + BigInt(transactionRequest.maxPriorityFeePerGas) ), } : { - gas: toHex(transactionRequest.gasLimit), - gasPrice: toHex(transactionRequest.gasPrice), + gas: numberToHex(BigInt(transactionRequest.gasLimit)), + gasPrice: numberToHex(BigInt(transactionRequest.gasPrice)), }), approvalTransactionRequest: approvalTx, }; @@ -437,34 +451,42 @@ export class SquidBridgeProvider implements BridgeProvider { isFromAssetNative, fromAddress, targetAddress, + tokenAddress, }: { - fromTokenContract: ethers.Contract; + fromTokenContract: ReturnType; + tokenAddress: Address; isFromAssetNative: boolean; fromAmount: string; - fromAddress: string; + fromAddress: Address; fromChain: BridgeChain; /** * The address of the contract that will be called with the approval, in this case, Squid's router contract. */ - targetAddress: string; + targetAddress: Address; }) { const _sourceAmount = BigInt(fromAmount); if (!isFromAssetNative) { - const allowance = await fromTokenContract.allowance( - fromAddress, - targetAddress - ); + const allowance = await fromTokenContract.readContract({ + abi: erc20Abi, + address: tokenAddress, + functionName: "allowance", + args: [fromAddress, targetAddress], + }); if (_sourceAmount > allowance) { const amountToApprove = _sourceAmount; - const approveTx = await fromTokenContract.approve.populateTransaction( - targetAddress, - amountToApprove - ); + const approveTxData = encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [targetAddress, BigInt(amountToApprove)], + }); - return approveTx; + return { + to: tokenAddress, + data: approveTxData, + }; } } } diff --git a/packages/web/hooks/use-swap.tsx b/packages/web/hooks/use-swap.tsx index b634cac848..fb90b5bd31 100644 --- a/packages/web/hooks/use-swap.tsx +++ b/packages/web/hooks/use-swap.tsx @@ -117,6 +117,7 @@ export function useSwap( // load flags const isToFromAssets = Boolean(swapAssets.fromAsset) && Boolean(swapAssets.toAsset); + const quoteQueryEnabled = isToFromAssets && Boolean(inAmountInput.debouncedInAmount?.toDec().isPositive()) && @@ -124,9 +125,10 @@ export function useSwap( // with the input amount when switching assets inAmountInput.debouncedInAmount?.currency.coinMinimalDenom === swapAssets.fromAsset?.coinMinimalDenom && + inAmountInput.amount?.currency.coinMinimalDenom === + swapAssets.fromAsset?.coinMinimalDenom && !account?.txTypeInProgress && !isWalletLoading; - const { data: quote, isLoading: isQuoteLoading_, @@ -195,13 +197,17 @@ export function useSwap( const networkFeeQueryEnabled = featureFlags.swapToolSimulateFee && !Boolean(precedentError) && - // includes check for quoteQueryEnabled !isQuoteLoading && - Boolean(quote) && + quoteQueryEnabled && + Boolean(quote?.messages) && Boolean(account?.address) && inAmountInput.debouncedInAmount !== null && inAmountInput.balance && - inAmountInput.debouncedInAmount.toDec().lte(inAmountInput.balance.toDec()); + inAmountInput.amount && + inAmountInput.debouncedInAmount + .toDec() + .lte(inAmountInput.balance.toDec()) && + inAmountInput.amount.toDec().lte(inAmountInput.balance.toDec()); const { data: networkFee, error: networkFeeError, @@ -790,12 +796,16 @@ function useSwapAmountInput({ }); const balanceQuoteQueryEnabled = + featureFlags.swapToolSimulateFee && !isLoadingWallet && + !account?.txTypeInProgress && Boolean(swapAssets.fromAsset) && Boolean(swapAssets.toAsset) && // since the in amount is debounced, the asset could be wrong when switching assets inAmountInput.debouncedInAmount?.currency.coinMinimalDenom === swapAssets.fromAsset!.coinMinimalDenom && + inAmountInput.amount?.currency.coinMinimalDenom === + swapAssets.fromAsset!.coinMinimalDenom && !!inAmountInput.balance && !inAmountInput.balance.toDec().isZero() && inAmountInput.balance.currency.coinMinimalDenom === @@ -818,11 +828,9 @@ function useSwapAmountInput({ isQuoteForCurrentBalanceLoading_ && balanceQuoteQueryEnabled; const networkFeeQueryEnabled = - featureFlags.swapToolSimulateFee && - // includes check for balanceQuoteQueryEnabled !isQuoteForCurrentBalanceLoading && - Boolean(quoteForCurrentBalance) && - !account?.txTypeInProgress; + balanceQuoteQueryEnabled && + Boolean(quoteForCurrentBalance); const { data: currentBalanceNetworkFee, isLoading: isLoadingCurrentBalanceNetworkFee_, diff --git a/packages/web/modals/bridge-transfer-v2.tsx b/packages/web/modals/bridge-transfer-v2.tsx index a972c8e138..74141132e5 100644 --- a/packages/web/modals/bridge-transfer-v2.tsx +++ b/packages/web/modals/bridge-transfer-v2.tsx @@ -735,8 +735,11 @@ export const TransferContent: FunctionComponent< const isInsufficientFee = inputAmountRaw !== "" && selectedQuote?.transferFee !== undefined && + selectedQuote?.transferFee.denom === assetToBridge.balance.denom && // make sure the fee is in the same denom as the asset new CoinPretty(assetToBridge.balance.currency, inputAmount) .toDec() + .sub(availableBalance?.toDec() ?? new Dec(0)) // subtract by available balance to get the maximum transfer amount + .abs() .lt(selectedQuote?.transferFee.toDec()); const isInsufficientBal = diff --git a/packages/web/package.json b/packages/web/package.json index d5a90b0e70..6abc564dfc 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -136,7 +136,7 @@ "sharp": "^0.30.4", "tailwindcss-animate": "^1.0.7", "utility-types": "^3.10.0", - "viem": "2.12.0", + "viem": "2.13.3", "wagmi": "^2.9.6", "web3-utils": "^1.7.4", "zod": "^3.22.4" diff --git a/yarn.lock b/yarn.lock index 834a199a87..53f0d5c84d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22275,7 +22275,7 @@ protobufjs@6.11.3: "@types/node" ">=13.7.0" long "^4.0.0" -protobufjs@7.2.6, protobufjs@^7.0.0, protobufjs@^7.2.6: +protobufjs@7.2.6: version "7.2.6" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.6.tgz#4a0ccd79eb292717aacf07530a07e0ed20278215" integrity sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw== @@ -22312,6 +22312,24 @@ protobufjs@^6.11.2, protobufjs@^6.11.3, protobufjs@^6.8.8, protobufjs@~6.11.2, p "@types/node" ">=13.7.0" long "^4.0.0" +protobufjs@^7.0.0, protobufjs@^7.2.6: + version "7.3.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.0.tgz#a32ec0422c039798c41a0700306a6e305b9cb32c" + integrity sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protobufjs@~6.10.2: version "6.10.3" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.3.tgz#11ed1dd02acbfcb330becf1611461d4b407f9eef" @@ -25953,10 +25971,10 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" -viem@2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.12.0.tgz#699ba326a1ce0df81042dc8b6f22fa751f9cefce" - integrity sha512-XBvORspE4x2/gfy7idH6IVFwkJiXirygFCU3lxUH6fttsj8zufLtgiokfvZF/LAZUEDvdxSgL08whSYgffM2fw== +viem@2.13.3: + version "2.13.3" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.3.tgz#950426e4cacf5e12fab2c202a339371901712481" + integrity sha512-3tlwDRKHSelupFjbFMdUxF41f79ktyH2F9PAQ9Dltbs1DpdDlR1x+Ksa0th6qkyjjAbpDZP3F5nMTJv/1GVPdQ== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.2.0"