From c3731945efb7d94878fe050f3fe68e046236d21a Mon Sep 17 00:00:00 2001
From: jeff <113397187+cyberhorsey@users.noreply.github.com>
Date: Wed, 21 Dec 2022 08:54:47 -0800
Subject: [PATCH] feat(bridge-ui): recommend fee (#457)
---
.../src/components/AddressDropdown.svelte | 6 +-
.../src/components/form/BridgeForm.svelte | 7 +-
.../src/components/form/ProcessingFee.svelte | 17 +-
packages/bridge-ui/src/domain/proof.ts | 2 +
packages/bridge-ui/src/erc20/bridge.spec.ts | 10 +
packages/bridge-ui/src/erc20/bridge.ts | 4 +
packages/bridge-ui/src/eth/bridge.spec.ts | 9 +-
packages/bridge-ui/src/eth/bridge.ts | 14 +-
packages/bridge-ui/src/proof/service.spec.ts | 51 ++---
packages/bridge-ui/src/proof/service.ts | 15 +-
.../src/utils/recommendProcessingFee.spec.ts | 192 ++++++++++++++++++
.../src/utils/recommendProcessingFee.ts | 59 ++++++
12 files changed, 349 insertions(+), 37 deletions(-)
create mode 100644 packages/bridge-ui/src/utils/recommendProcessingFee.spec.ts
create mode 100644 packages/bridge-ui/src/utils/recommendProcessingFee.ts
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);
+}