diff --git a/packages/bridge-ui/src/components/AddressDropdown.svelte b/packages/bridge-ui/src/components/AddressDropdown.svelte index 81d3e735be1..b977e242741 100644 --- a/packages/bridge-ui/src/components/AddressDropdown.svelte +++ b/packages/bridge-ui/src/components/AddressDropdown.svelte @@ -16,8 +16,8 @@ import { fromChain } from "../store/chain"; import { truncateString } from "../utils/truncateString"; - let address: string; - let addressAvatarImgData: string; + let address: string = ""; + let addressAvatarImgData: string = ""; let tokenBalance: string = ""; onMount(async () => { @@ -26,6 +26,8 @@ $: getUserBalance($signer); + $: setAddress($signer); + async function getUserBalance(signer) { if (signer) { const userBalance = await signer.getBalance("latest"); diff --git a/packages/bridge-ui/src/components/form/BridgeForm.svelte b/packages/bridge-ui/src/components/form/BridgeForm.svelte index f80cafd2e0c..1f6d649eacd 100644 --- a/packages/bridge-ui/src/components/form/BridgeForm.svelte +++ b/packages/bridge-ui/src/components/form/BridgeForm.svelte @@ -39,7 +39,8 @@ let requiresAllowance: boolean = true; let btnDisabled: boolean = true; let tokenBalance: string; - let customFee: string = "0.01"; + let customFee: string = "0"; + let recommendedFee: string = "0"; let memo: string = ""; let loading: boolean = false; @@ -249,7 +250,7 @@ } if ($processingFee === ProcessingFeeMethod.RECOMMENDED) { - return ethers.utils.parseEther("0.01"); + return BigNumber.from(ethers.utils.parseEther(recommendedFee)); } } @@ -281,7 +282,7 @@ - + diff --git a/packages/bridge-ui/src/components/form/ProcessingFee.svelte b/packages/bridge-ui/src/components/form/ProcessingFee.svelte index e0d1436289d..9a80ed5cd28 100644 --- a/packages/bridge-ui/src/components/form/ProcessingFee.svelte +++ b/packages/bridge-ui/src/components/form/ProcessingFee.svelte @@ -2,8 +2,23 @@ import { _ } from "svelte-i18n"; import { processingFee } from "../../store/fee"; import { ProcessingFeeMethod, PROCESSING_FEE_META } from "../../domain/fee"; + import { toChain, fromChain } from "../../store/chain"; + import { token } from "../../store/token"; + import { signer } from "../../store/signer"; + import { recommendProcessingFee } from "../../utils/recommendProcessingFee"; export let customFee: string; + export let recommendedFee: string = "0"; + + $: recommendProcessingFee( + $toChain, + $fromChain, + $processingFee, + $token, + $signer + ) + .then((fee) => (recommendedFee = fee)) + .catch((e) => console.error(e)); function selectProcessingFee(fee) { $processingFee = fee; @@ -45,7 +60,7 @@ {:else if $processingFee === ProcessingFeeMethod.RECOMMENDED}
- 0.01 ETH + {recommendedFee} ETH
{/if} diff --git a/packages/bridge-ui/src/domain/proof.ts b/packages/bridge-ui/src/domain/proof.ts index 6a42b45b507..3ea1742061c 100644 --- a/packages/bridge-ui/src/domain/proof.ts +++ b/packages/bridge-ui/src/domain/proof.ts @@ -17,6 +17,8 @@ type GenerateProofOpts = { signal: string; sender: string; srcBridgeAddress: string; + destChain: number; + destHeaderSyncAddress: string; srcChain: number; }; diff --git a/packages/bridge-ui/src/erc20/bridge.spec.ts b/packages/bridge-ui/src/erc20/bridge.spec.ts index c2a04d55c82..0dce4d84edf 100644 --- a/packages/bridge-ui/src/erc20/bridge.spec.ts +++ b/packages/bridge-ui/src/erc20/bridge.spec.ts @@ -250,6 +250,8 @@ describe("bridge tests", () => { bridge.Claim({ message: { srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -272,6 +274,8 @@ describe("bridge tests", () => { bridge.Claim({ message: { srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -299,6 +303,8 @@ describe("bridge tests", () => { message: { owner: "0x", srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -331,7 +337,9 @@ describe("bridge tests", () => { message: { owner: "0x", srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), sender: "0x01", + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -365,7 +373,9 @@ describe("bridge tests", () => { message: { owner: "0x", srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), sender: "0x01", + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", diff --git a/packages/bridge-ui/src/erc20/bridge.ts b/packages/bridge-ui/src/erc20/bridge.ts index e9cf5932903..e16aafeb2f5 100644 --- a/packages/bridge-ui/src/erc20/bridge.ts +++ b/packages/bridge-ui/src/erc20/bridge.ts @@ -11,6 +11,7 @@ import ERC20 from "../constants/abi/ERC20"; import type { Prover } from "../domain/proof"; import { MessageStatus } from "../domain/message"; import BridgeABI from "../constants/abi/Bridge"; +import { chains } from "../domain/chain"; class ERC20Bridge implements Bridge { private readonly prover: Prover; @@ -145,6 +146,9 @@ class ERC20Bridge implements Bridge { signal: opts.signal, sender: opts.srcBridgeAddress, srcBridgeAddress: opts.srcBridgeAddress, + destChain: opts.message.destChainId.toNumber(), + destHeaderSyncAddress: + chains[opts.message.destChainId.toNumber()].headerSyncAddress, }); return await contract.processMessage(opts.message, proof, { diff --git a/packages/bridge-ui/src/eth/bridge.spec.ts b/packages/bridge-ui/src/eth/bridge.spec.ts index 000600051bd..71f2132de13 100644 --- a/packages/bridge-ui/src/eth/bridge.spec.ts +++ b/packages/bridge-ui/src/eth/bridge.spec.ts @@ -3,7 +3,6 @@ import { mainnet, taiko } from "../domain/chain"; import type { Bridge, BridgeOpts } from "../domain/bridge"; import ETHBridge from "./bridge"; import { Message, MessageStatus } from "../domain/message"; -import { src_url_equal } from "svelte/internal"; const mockSigner = { getAddress: jest.fn(), @@ -133,6 +132,8 @@ describe("bridge tests", () => { bridge.Claim({ message: { srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -160,6 +161,8 @@ describe("bridge tests", () => { message: { owner: "0x", srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -192,7 +195,9 @@ describe("bridge tests", () => { message: { owner: "0x", srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), sender: "0x01", + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", @@ -226,7 +231,9 @@ describe("bridge tests", () => { message: { owner: "0x", srcChainId: BigNumber.from(167001), + destChainId: BigNumber.from(31336), sender: "0x01", + gasLimit: BigNumber.from(1), } as unknown as Message, signal: "0x", srcBridgeAddress: "0x", diff --git a/packages/bridge-ui/src/eth/bridge.ts b/packages/bridge-ui/src/eth/bridge.ts index 1f139cf4dc9..e72dd6fb1c5 100644 --- a/packages/bridge-ui/src/eth/bridge.ts +++ b/packages/bridge-ui/src/eth/bridge.ts @@ -10,6 +10,7 @@ import TokenVault from "../constants/abi/TokenVault"; import type { Prover } from "../domain/proof"; import { MessageStatus } from "../domain/message"; import Bridge from "../constants/abi/Bridge"; +import { chains } from "../domain/chain"; class ETHBridge implements BridgeInterface { private readonly prover: Prover; @@ -90,19 +91,24 @@ class ETHBridge implements BridgeInterface { } if (messageStatus === MessageStatus.New) { - const proof = await this.prover.GenerateProof({ + const proofOpts = { srcChain: opts.message.srcChainId.toNumber(), signal: opts.signal, sender: opts.srcBridgeAddress, srcBridgeAddress: opts.srcBridgeAddress, - }); + destChain: opts.message.destChainId.toNumber(), + destHeaderSyncAddress: + chains[opts.message.destChainId.toNumber()].headerSyncAddress, + }; + + const proof = await this.prover.GenerateProof(proofOpts); return await contract.processMessage(opts.message, proof, { - gasLimit: 1500000, + gasLimit: opts.message.gasLimit.add(1000000), }); } else { return await contract.retryMessage(opts.message, { - gasLimit: 1500000, + gasLimit: opts.message.gasLimit.add(1000000), }); } } diff --git a/packages/bridge-ui/src/proof/service.spec.ts b/packages/bridge-ui/src/proof/service.spec.ts index 439521437af..a14bf3bdd5b 100644 --- a/packages/bridge-ui/src/proof/service.spec.ts +++ b/packages/bridge-ui/src/proof/service.spec.ts @@ -6,6 +6,18 @@ const mockProvider = { send: jest.fn(), }; +const mockContract = { + getLatestSyncedHeader: jest.fn(), +}; + +jest.mock("ethers", () => ({ + /* eslint-disable-next-line */ + ...(jest.requireActual("ethers") as object), + Contract: function () { + return mockContract; + }, +})); + const block = { parentHash: "0xa7881266ca0a344c43cb24175d9dbd243b58d45d6ae6ad71310a273a3d1d3afb", @@ -67,6 +79,13 @@ const expectedProof = const expectedProofWithBaseFee = "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000360a7881266ca0a344c43cb24175d9dbd243b58d45d6ae6ad71310a273a3d1d3afb1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347000000000000000000000000ea674fdde714fd979de3edf0f56aa9716b898ec8c0dcf937b3f6136dd70a1ad11cc57b040fd410f3c49a5146f20c732895a3cc217273ade6b6ed865a9975ac281da23b90b141a8b607d874d2cd95e65e81336f8e74bb61e381e9238a08b169580f3cbf9b8b79d7d5ee708d3e286103eb291dfd0800000000000400000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000020000000000000000000000000000000000000000000000000100000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000002e0f5ba25df1e92e89a09e0b32063b81795f631100801158f5fa733f2ba26843bd0000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001265746865726d696e652d75732d7765737431000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e1a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; +const srcChain = 167001; +const destChain = 31336; + +const map = new Map(); +map.set(srcChain, mockProvider as unknown as ethers.providers.JsonRpcProvider); +map.set(destChain, mockProvider as unknown as ethers.providers.JsonRpcProvider); + describe("prover tests", () => { beforeEach(() => { jest.resetAllMocks(); @@ -76,7 +95,7 @@ describe("prover tests", () => { it("throws on invalid proof", async () => { mockProvider.send.mockImplementation( (method: string, params: unknown[]) => { - if (method === "eth_getBlockByNumber") { + if (method === "eth_getBlockByHash") { return block; } @@ -86,13 +105,6 @@ describe("prover tests", () => { } ); - const srcChain = 167001; - const map = new Map(); - map.set( - srcChain, - mockProvider as unknown as ethers.providers.JsonRpcProvider - ); - const prover: ProofService = new ProofService(map); await expect( @@ -101,6 +113,8 @@ describe("prover tests", () => { sender: ethers.constants.AddressZero, srcBridgeAddress: ethers.constants.AddressZero, srcChain: srcChain, + destChain: destChain, + destHeaderSyncAddress: ethers.constants.AddressZero, }) ).rejects.toThrowError("invalid proof"); }); @@ -108,7 +122,7 @@ describe("prover tests", () => { it("generates proof", async () => { mockProvider.send.mockImplementation( (method: string, params: unknown[]) => { - if (method === "eth_getBlockByNumber") { + if (method === "eth_getBlockByHash") { return block; } @@ -118,13 +132,6 @@ describe("prover tests", () => { } ); - const srcChain = 167001; - const map = new Map(); - map.set( - srcChain, - mockProvider as unknown as ethers.providers.JsonRpcProvider - ); - const prover: ProofService = new ProofService(map); const proof = await prover.GenerateProof({ @@ -132,6 +139,8 @@ describe("prover tests", () => { sender: ethers.constants.AddressZero, srcBridgeAddress: ethers.constants.AddressZero, srcChain: srcChain, + destChain: destChain, + destHeaderSyncAddress: ethers.constants.AddressZero, }); expect(proof).toBe(expectedProof); }); @@ -139,7 +148,7 @@ describe("prover tests", () => { it("generates proof with baseFeePerGas set", async () => { mockProvider.send.mockImplementation( (method: string, params: unknown[]) => { - if (method === "eth_getBlockByNumber") { + if (method === "eth_getBlockByHash") { return block; } @@ -150,12 +159,6 @@ describe("prover tests", () => { ); block.baseFeePerGas = "1"; - const srcChain = 167001; - const map = new Map(); - map.set( - srcChain, - mockProvider as unknown as ethers.providers.JsonRpcProvider - ); const prover: ProofService = new ProofService(map); @@ -164,6 +167,8 @@ describe("prover tests", () => { sender: ethers.constants.AddressZero, srcBridgeAddress: ethers.constants.AddressZero, srcChain: srcChain, + destChain: destChain, + destHeaderSyncAddress: ethers.constants.AddressZero, }); expect(proof).toBe(expectedProofWithBaseFee); }); diff --git a/packages/bridge-ui/src/proof/service.ts b/packages/bridge-ui/src/proof/service.ts index f8cb7a7e529..b1ea6b0c5b1 100644 --- a/packages/bridge-ui/src/proof/service.ts +++ b/packages/bridge-ui/src/proof/service.ts @@ -1,5 +1,6 @@ -import { ethers } from "ethers"; +import { Contract, ethers } from "ethers"; import { RLP } from "ethers/lib/utils.js"; +import HeaderSync from "../constants/abi/HeaderSync"; import type { Block, BlockHeader } from "../domain/block"; import type { Prover, @@ -24,8 +25,16 @@ class ProofService implements Prover { const provider = this.providerMap.get(opts.srcChain); - const block: Block = await provider.send("eth_getBlockByNumber", [ - "latest", + const contract = new Contract( + opts.destHeaderSyncAddress, + HeaderSync, + this.providerMap.get(opts.destChain) + ); + + const latestSyncedHeader = await contract.getLatestSyncedHeader(); + + const block: Block = await provider.send("eth_getBlockByHash", [ + latestSyncedHeader, false, ]); diff --git a/packages/bridge-ui/src/utils/recommendProcessingFee.spec.ts b/packages/bridge-ui/src/utils/recommendProcessingFee.spec.ts new file mode 100644 index 00000000000..063c98fba15 --- /dev/null +++ b/packages/bridge-ui/src/utils/recommendProcessingFee.spec.ts @@ -0,0 +1,192 @@ +const mockChainIdToTokenVaultAddress = jest.fn(); +jest.mock("../store/bridge", () => ({ + chainIdToTokenVaultAddress: mockChainIdToTokenVaultAddress, +})); + +const mockGet = jest.fn(); + +import { BigNumber, ethers, Signer } from "ethers"; +import { chainIdToTokenVaultAddress } from "../store/bridge"; +import { get } from "svelte/store"; +import { CHAIN_MAINNET, CHAIN_TKO } from "../domain/chain"; +import { ProcessingFeeMethod } from "../domain/fee"; +import { ETH, HORSE } from "../domain/token"; +import { signer } from "../store/signer"; +import { + erc20DeployedGasLimit, + erc20NotDeployedGasLimit, + ethGasLimit, + recommendProcessingFee, +} from "./recommendProcessingFee"; + +jest.mock("svelte/store", () => ({ + ...(jest.requireActual("svelte/store") as object), + get: function () { + return mockGet(); + }, +})); + +const mockContract = { + canonicalToBridged: jest.fn(), +}; + +const mockProver = { + GenerateProof: jest.fn(), +}; + +jest.mock("ethers", () => ({ + /* eslint-disable-next-line */ + ...(jest.requireActual("ethers") as object), + Contract: function () { + return mockContract; + }, +})); + +const gasPrice = 2; +const mockProvider = { + getGasPrice: () => { + return 2; + }, +}; + +const mockSigner = {}; + +describe("recommendProcessingFee()", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("returns zero if values not set", async () => { + expect( + await recommendProcessingFee( + null, + CHAIN_MAINNET, + ProcessingFeeMethod.RECOMMENDED, + ETH, + get(signer) + ) + ).toStrictEqual("0"); + + expect( + await recommendProcessingFee( + CHAIN_MAINNET, + null, + ProcessingFeeMethod.RECOMMENDED, + ETH, + get(signer) + ) + ).toStrictEqual("0"); + + expect( + await recommendProcessingFee( + CHAIN_MAINNET, + CHAIN_TKO, + null, + ETH, + get(signer) + ) + ).toStrictEqual("0"); + + expect( + await recommendProcessingFee( + CHAIN_TKO, + CHAIN_MAINNET, + ProcessingFeeMethod.RECOMMENDED, + null, + get(signer) + ) + ).toStrictEqual("0"); + + expect( + await recommendProcessingFee( + CHAIN_TKO, + CHAIN_MAINNET, + ProcessingFeeMethod.RECOMMENDED, + ETH, + null + ) + ).toStrictEqual("0"); + }); + + it("uses ethGasLimit if the token is ETH", async () => { + mockGet.mockImplementationOnce(() => + new Map().set( + CHAIN_TKO.id, + mockProvider as unknown as ethers.providers.JsonRpcProvider + ) + ); + + const fee = await recommendProcessingFee( + CHAIN_TKO, + CHAIN_MAINNET, + ProcessingFeeMethod.RECOMMENDED, + ETH, + mockSigner as unknown as Signer + ); + + const expected = ethers.utils.formatEther( + BigNumber.from(gasPrice).mul(ethGasLimit) + ); + + expect(fee).toStrictEqual(expected); + }); + + it("uses erc20NotDeployedGasLimit if the token is not ETH and token is not deployed on dest layer", async () => { + mockGet.mockImplementation((store: any) => { + if (typeof store === typeof chainIdToTokenVaultAddress) { + return new Map().set(CHAIN_MAINNET.id, "0x12345"); + } else { + return new Map().set( + CHAIN_TKO.id, + mockProvider as unknown as ethers.providers.JsonRpcProvider + ); + } + }); + mockContract.canonicalToBridged.mockImplementationOnce( + () => ethers.constants.AddressZero + ); + + const fee = await recommendProcessingFee( + CHAIN_TKO, + CHAIN_MAINNET, + ProcessingFeeMethod.RECOMMENDED, + HORSE, + mockSigner as unknown as Signer + ); + + const expected = ethers.utils.formatEther( + BigNumber.from(gasPrice).mul(erc20NotDeployedGasLimit) + ); + + expect(fee).toStrictEqual(expected); + }); + + it("uses erc20NotDeployedGasLimit if the token is not ETH and token is not deployed on dest layer", async () => { + mockGet.mockImplementation((store: any) => { + if (typeof store === typeof chainIdToTokenVaultAddress) { + return new Map().set(CHAIN_MAINNET.id, "0x12345"); + } else { + return new Map().set( + CHAIN_TKO.id, + mockProvider as unknown as ethers.providers.JsonRpcProvider + ); + } + }); + + mockContract.canonicalToBridged.mockImplementationOnce(() => "0x123"); + + const fee = await recommendProcessingFee( + CHAIN_TKO, + CHAIN_MAINNET, + ProcessingFeeMethod.RECOMMENDED, + HORSE, + mockSigner as unknown as Signer + ); + + const expected = ethers.utils.formatEther( + BigNumber.from(gasPrice).mul(erc20DeployedGasLimit) + ); + + expect(fee).toStrictEqual(expected); + }); +}); diff --git a/packages/bridge-ui/src/utils/recommendProcessingFee.ts b/packages/bridge-ui/src/utils/recommendProcessingFee.ts new file mode 100644 index 00000000000..6f8012777de --- /dev/null +++ b/packages/bridge-ui/src/utils/recommendProcessingFee.ts @@ -0,0 +1,59 @@ +import { BigNumber, Contract, ethers, Signer } from "ethers"; +import TokenVault from "../constants/abi/TokenVault"; +import type { Chain } from "../domain/chain"; +import type { ProcessingFeeMethod } from "../domain/fee"; +import type { Token } from "../domain/token"; +import { ETH } from "../domain/token"; +import { chainIdToTokenVaultAddress } from "../store/bridge"; +import { providers } from "../store/providers"; +import { get } from "svelte/store"; + +export const ethGasLimit = 900000; +export const erc20NotDeployedGasLimit = 3100000; +export const erc20DeployedGasLimit = 1100000; + +export async function recommendProcessingFee( + toChain: Chain, + fromChain: Chain, + feeType: ProcessingFeeMethod, + token: Token, + signer: Signer +): Promise { + if (!toChain || !fromChain || !token || !signer || !feeType) return "0"; + const p = get(providers); + const provider = p.get(toChain.id); + const gasPrice = await provider.getGasPrice(); + // gasLimit for processMessage call for ETH is about ~800k. + // to make it enticing, we say 900k. + let gasLimit = ethGasLimit; + if (token.symbol.toLowerCase() !== ETH.symbol.toLowerCase()) { + const srcChainAddr = token.addresses.find( + (t) => t.chainId === fromChain.id + ).address; + + const chainIdsToTokenVault = get(chainIdToTokenVaultAddress); + const tokenVault = new Contract( + chainIdsToTokenVault.get(fromChain.id), + TokenVault, + signer + ); + + const bridged = await tokenVault.canonicalToBridged( + toChain.id, + srcChainAddr + ); + + // gas limit for erc20 if not deployed on the dest chain already + // is about ~2.9m so we add some to make it enticing + if (bridged == ethers.constants.AddressZero) { + gasLimit = erc20NotDeployedGasLimit; + } else { + // gas limit for erc20 if already deployed on the dest chain is about ~1m + // so again, add some to ensure processing + gasLimit = erc20DeployedGasLimit; + } + } + + const recommendedFee = BigNumber.from(gasPrice).mul(gasLimit); + return ethers.utils.formatEther(recommendedFee); +}