diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml new file mode 100644 index 0000000..d8a4b23 --- /dev/null +++ b/.github/workflows/app.yml @@ -0,0 +1,10 @@ +name: app +on: pull_request +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # pinning the commit to v2 + - run: bun install --frozen-lockfile + - run: bun run check diff --git a/bun.lock b/bun.lock index f76b067..7b6c775 100644 --- a/bun.lock +++ b/bun.lock @@ -7,11 +7,15 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "binary-layout": "^1.3.0", + "cors": "^2.8.5", "express": "^5.1.0", "viem": "^2.31.7", }, "devDependencies": { "@types/bun": "latest", + "@types/cors": "^2.8.17", + "prettier": "^3.5.3", }, "peerDependencies": { "typescript": "^5", @@ -39,6 +43,8 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], @@ -47,7 +53,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@24.0.11", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-CJV8eqrYnwQJGMrvcRhQmZfpyniDavB+7nAZYJc6w99hFYJyFN3INV1/2W3QfQrqM36WTLrijJ1fxxvGBmCSxA=="], + "@types/node": ["@types/node@24.0.12", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -87,6 +93,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -157,6 +165,8 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -169,6 +179,8 @@ "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], diff --git a/package.json b/package.json index 85c158a..58336d4 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,27 @@ "type": "module", "private": true, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/cors": "^2.8.17", + "prettier": "^3.5.3" }, "peerDependencies": { "typescript": "^5" }, "scripts": { - "start": "bun src/index.ts" + "start": "bun src/index.ts", + "check": "bun run type:check && bun run prettier:check", + "type:check": "tsc --noEmit", + "prettier:check": "prettier . --check" }, "dependencies": { "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "binary-layout": "^1.3.0", + "cors": "^2.8.5", "express": "^5.1.0", "viem": "^2.31.7" - } + }, + "prettier": {} } diff --git a/src/api/capabilities.ts b/src/api/capabilities.ts new file mode 100644 index 0000000..ea1da7f --- /dev/null +++ b/src/api/capabilities.ts @@ -0,0 +1,17 @@ +import { type Request, type Response } from "express"; +import { enabledChains } from "../chains"; + +export const capabilitiesHandler = async (req: Request, res: Response) => { + const capabilities: Record = {}; + + for (const [_, chainConfig] of Object.entries(enabledChains)) { + capabilities[chainConfig.wormholeChainId.toString()] = { + requestPrefixes: chainConfig.capabilities.requestPrefixes, + gasDropOffLimit: chainConfig.capabilities.gasDropOffLimit.toString(), + maxGasLimit: chainConfig.capabilities.maxGasLimit.toString(), + maxMsgValue: chainConfig.capabilities.maxMsgValue.toString(), + }; + } + + res.json(capabilities); +}; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..fd71773 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,3 @@ +export { quoteHandler } from "./quote"; +export { statusHandler } from "./status"; +export { capabilitiesHandler } from "./capabilities"; diff --git a/src/api/quote.ts b/src/api/quote.ts new file mode 100644 index 0000000..393c46d --- /dev/null +++ b/src/api/quote.ts @@ -0,0 +1,143 @@ +import { type Request, type Response } from "express"; +import { enabledChains, type ChainConfig } from "../chains"; +import { createPublicClient, http, isHex, padHex, toBytes } from "viem"; +import type { Quote } from "@wormhole-foundation/sdk-definitions"; +import { + PAYEE_PUBLIC_KEY, + QUOTER_PRIVATE_KEY, + QUOTER_PUBLIC_KEY, +} from "../consts"; +import { + estimateQuote, + getTotalGasLimitAndMsgValue, + signQuote, +} from "../utils"; +import { anvil } from "viem/chains"; + +function getChainConfig(chainId: string): ChainConfig | undefined { + const numericId = parseInt(chainId); + return enabledChains[numericId]; +} + +async function getGasPrice(chainConfig: ChainConfig): Promise { + try { + const transport = http(chainConfig.rpc); + const client = createPublicClient({ + chain: anvil, + transport, + }); + return await client.getGasPrice(); + } catch (e) { + throw new Error(`unable to determine gas price`); + } +} + +export const quoteHandler = async (req: Request, res: Response) => { + const enabledChainIds = Object.keys(enabledChains); + + const srcChainId = req.body.srcChain; + const dstChainId = req.body.dstChain; + const relayInstructions = req.body.relayInstructions; + + if (relayInstructions && !isHex(relayInstructions)) { + res.status(400).send(`Invalid hex string for "relayInstructions"`); + return; + } + + if (!enabledChainIds.includes(srcChainId.toString())) { + res + .status(400) + .send( + `Unsupported source chain: ${srcChainId}, supported chains: ${enabledChainIds.join( + ",", + )}`, + ); + return; + } + + if (!enabledChainIds.includes(dstChainId.toString())) { + res + .status(400) + .send( + `Unsupported destination chain: ${dstChainId}, supported chains: ${enabledChainIds.join( + ",", + )}`, + ); + return; + } + + const srcChain = getChainConfig(srcChainId); + const dstChain = getChainConfig(dstChainId); + + if (!srcChain || !dstChain) { + res.status(500).send("Internal error: Invalid chain configuration"); + return; + } + + const expiryTime = new Date(); + expiryTime.setHours(expiryTime.getHours() + 1); + + const dstGasPrice = await getGasPrice(dstChain); + + const quote: Quote = { + quote: { + prefix: "EQ01", + quoterAddress: toBytes(QUOTER_PUBLIC_KEY), + payeeAddress: toBytes( + padHex(PAYEE_PUBLIC_KEY, { + dir: "left", + size: 32, + }), + ), + srcChain: parseInt(srcChainId), + dstChain: parseInt(dstChainId), + expiryTime, + baseFee: 1n, + dstGasPrice: dstGasPrice, + srcPrice: 10000000000n, + dstPrice: 10000000000n, + }, + }; + + const signedQuote = await signQuote(quote, QUOTER_PRIVATE_KEY); + + let response: { + signedQuote: `0x${string}`; + estimatedCost?: bigint; + } = { + signedQuote, + }; + + if (relayInstructions) { + const { gasLimit, msgValue } = + getTotalGasLimitAndMsgValue(relayInstructions); + + if (gasLimit > dstChain.capabilities.maxGasLimit) { + res + .status(400) + .send( + `Request exceeds maxGasLimit: ${gasLimit.toString()} requested, ${dstChain.capabilities.maxGasLimit.toString()} maximum.`, + ); + return; + } + + if (msgValue > dstChain.capabilities.maxMsgValue) { + res + .status(400) + .send( + `Request exceeds maxMsgValue: ${msgValue.toString()} requested, ${dstChain.capabilities.maxMsgValue.toString()} maximum.`, + ); + return; + } + + response.estimatedCost = estimateQuote( + quote, + gasLimit, + msgValue, + dstChain.gasPriceDecimals, + srcChain.nativeDecimals, + dstChain.nativeDecimals, + ); + } + res.status(200).json(response); +}; diff --git a/src/api/status.ts b/src/api/status.ts new file mode 100644 index 0000000..cecba52 --- /dev/null +++ b/src/api/status.ts @@ -0,0 +1,5 @@ +import { type Request, type Response } from "express"; + +export const statusHandler = async (req: Request, res: Response) => { + res.status(500).send(); +}; diff --git a/src/chains.ts b/src/chains.ts new file mode 100644 index 0000000..824fa12 --- /dev/null +++ b/src/chains.ts @@ -0,0 +1,42 @@ +import { RequestPrefix, type Capabilities } from "./types"; + +export interface ChainConfig { + wormholeChainId: number; + evmChainId: number; + rpc: string; + name: string; + gasPriceDecimals: number; + nativeDecimals: number; + capabilities: Capabilities; +} + +export const enabledChains: Record = { + 10002: { + wormholeChainId: 10002, + evmChainId: 11155111, + rpc: "http://anvil-eth-sepolia:8545", + name: "Ethereum Sepolia", + gasPriceDecimals: 18, + nativeDecimals: 18, + capabilities: { + requestPrefixes: [RequestPrefix.ERV1], + gasDropOffLimit: 100_000_000_000n, + maxGasLimit: 1_000_000n, + maxMsgValue: 100_000_000_000n * 2n, + }, + }, + 10004: { + wormholeChainId: 10004, + evmChainId: 84532, + rpc: "http://anvil-base-sepolia:8545", + name: "Base Sepolia", + gasPriceDecimals: 18, + nativeDecimals: 18, + capabilities: { + requestPrefixes: [RequestPrefix.ERV1], + gasDropOffLimit: 100_000_000_000n, + maxGasLimit: 1_000_000n, + maxMsgValue: 100_000_000_000n * 2n, + }, + }, +}; diff --git a/src/consts.ts b/src/consts.ts index 1027025..69f8079 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -3,8 +3,13 @@ import { mnemonicToAccount } from "viem/accounts"; const account = mnemonicToAccount( "test test test test test test test test test test test junk", - { addressIndex: 9 } + { addressIndex: 9 }, ); +export const PAYEE_PUBLIC_KEY = account.address; + export const EVM_PUBLIC_KEY = account.address; export const EVM_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x"); + +export const QUOTER_PUBLIC_KEY = account.address; +export const QUOTER_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x"); diff --git a/src/index.ts b/src/index.ts index 1acdac2..634e3fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,31 @@ -import express, { type Request, type Response } from "express"; +import express from "express"; +import cors from "cors"; import { overrideGuardianSet } from "./overrideGuardianSet"; +import { quoteHandler, statusHandler, capabilitiesHandler } from "./api"; + +// @ts-ignore +BigInt.prototype.toJSON = function () { + // Can also be JSON.rawJSON(this.toString()); + return this.toString(); +}; await overrideGuardianSet( "http://anvil-eth-sepolia:8545", - "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78" + "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", ); await overrideGuardianSet( "http://anvil-base-sepolia:8545", - "0x79A1027a6A159502049F10906D333EC57E95F083" + "0x79A1027a6A159502049F10906D333EC57E95F083", ); const app = express(); + +app.use(cors()); app.use(express.json()); -app.post("/v0/quote", async (req: Request, res: Response) => { - res.status(500).send(); -}); -app.post("/v0/status/tx", async (req: Request, res: Response) => { - res.status(500).send(); -}); -app.get("/v0/capabilities", async (req: Request, res: Response) => { - res.status(500).send(); -}); +app.post("/v0/quote", quoteHandler); +app.post("/v0/status/tx", statusHandler); +app.get("/v0/capabilities", capabilitiesHandler); + const server = app.listen(3000, () => { console.log(`Server is running at http://localhost:3000`); }); diff --git a/src/lib/ScaledMath.ts b/src/lib/ScaledMath.ts new file mode 100644 index 0000000..94cd48a --- /dev/null +++ b/src/lib/ScaledMath.ts @@ -0,0 +1,36 @@ +export const ScaledMath = { + min(value: bigint, ...values: Array) { + for (const v of values) { + if (v < value) { + value = v; + } + } + + return value; + }, + max(value: bigint, ...values: Array) { + for (const v of values) { + if (v > value) { + value = v; + } + } + + return value; + }, + + normalize(amount: bigint, from: number, to: number) { + if (from > to) { + return amount / 10n ** BigInt(from - to); + } else if (from < to) { + return amount * 10n ** BigInt(to - from); + } + return amount; + }, + + mul(a: bigint, b: bigint, decimals: number) { + return (a * b) / 10n ** BigInt(decimals); + }, + div(a: bigint, b: bigint, decimals: number) { + return (a * 10n ** BigInt(decimals)) / b; + }, +} as const; diff --git a/src/mockGuardian.ts b/src/mockGuardian.ts index 20ded86..eb071bc 100644 --- a/src/mockGuardian.ts +++ b/src/mockGuardian.ts @@ -23,7 +23,7 @@ import { EVM_PRIVATE_KEY } from "./consts"; async function getWormholeMessage( rpc: string, txHash: Hex, - coreContractAddress: Hex + coreContractAddress: Hex, ): Promise | undefined> { console.log(`Mocking guardian signatures for ${rpc} ${txHash}`); const transport = http(rpc); @@ -60,12 +60,12 @@ async function getWormholeMessage( blockHash: transaction.blockHash, includeTransactions: false, }) - ).timestamp + ).timestamp, ), // NOTE: the Wormhole SDK requires this be a known chain, though that is not strictly necessary for our use case. emitterChain: toChain(chainId), emitterAddress: new UniversalAddress( - toBytes(padHex(emitter, { dir: "left", size: 32 })) + toBytes(padHex(emitter, { dir: "left", size: 32 })), ), consistencyLevel: topic.args.consistencyLevel, sequence: topic.args.sequence, @@ -83,7 +83,7 @@ async function getWormholeMessage( export async function mockWormhole( rpc: string, txHash: Hex, - coreContractAddress: Hex + coreContractAddress: Hex, ): Promise { const vaa = await getWormholeMessage(rpc, txHash, coreContractAddress); if (vaa) { diff --git a/src/overrideGuardianSet.ts b/src/overrideGuardianSet.ts index ddc7a55..58e1063 100644 --- a/src/overrideGuardianSet.ts +++ b/src/overrideGuardianSet.ts @@ -16,7 +16,7 @@ import { EVM_PUBLIC_KEY } from "./consts"; export async function overrideGuardianSet( anvilRpcUrl: string, - coreContractAddress: Hex + coreContractAddress: Hex, ) { const transport = http(anvilRpcUrl); const publicClient = createPublicClient({ @@ -39,7 +39,7 @@ export async function overrideGuardianSet( encodeAbiParameters(parseAbiParameters("uint32, bytes32"), [ guardianSetIndex, GUARDIAN_SETS_SLOT, - ]) + ]), ); const firstIndexStorageSlot = BigInt(keccak256(addressesStorageSlot)); await anvilClient.setStorageAt({ @@ -61,7 +61,7 @@ export async function overrideGuardianSet( ]); console.log( `Overrode guardian set ${guardianSetIndex} of ${anvilRpcUrl} contract ${coreContractAddress} to ${guardianSet.keys.join( - ", " - )}` + ", ", + )}`, ); } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a8dce1e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,14 @@ +export enum RequestPrefix { + ERM1 = "ERM1", // MM + ERV1 = "ERV1", // VAA_V1 + ERN1 = "ERN1", // NTT_V1 + ERC1 = "ERC1", // CCTP_V1 + ERC2 = "ERC2", // CCTP_V2 +} + +export type Capabilities = { + requestPrefixes: Array; + gasDropOffLimit: bigint; + maxGasLimit: bigint; + maxMsgValue: bigint; +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..a02488d --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,107 @@ +import { + quoteLayout, + relayInstructionsLayout, + type Quote, + type RelayInstructions, +} from "@wormhole-foundation/sdk-definitions"; +import { deserialize, serialize } from "binary-layout"; +import { concat, fromBytes, fromHex, keccak256 } from "viem"; +import { sign } from "viem/accounts"; +import { ScaledMath } from "./lib/ScaledMath"; + +const SIGNED_QUOTE_DECIMALS = 10; + +export async function signQuote(quote: Quote, privateKey: `0x${string}`) { + const serialized = serialize(quoteLayout, quote); + const signature = await sign({ + hash: keccak256(serialized), + privateKey, + to: "hex", + }); + return concat([fromBytes(serialized, "hex"), signature]); +} + +export function getTotalGasLimitAndMsgValue( + relayInstructionsHex: `0x${string}`, +) { + const relayInstructions = deserialize( + relayInstructionsLayout, + fromHex(relayInstructionsHex, "bytes"), + ); + + return totalGasLimitAndMsgValue(relayInstructions); +} + +function totalGasLimitAndMsgValue(relayInstructions: RelayInstructions): { + gasLimit: bigint; + msgValue: bigint; +} { + let gasLimit = 0n; + let msgValue = 0n; + + for (const relayInstruction of relayInstructions.requests) { + const type = relayInstruction.request.type; + if (type === "GasInstruction") { + gasLimit += relayInstruction.request.gasLimit; + msgValue += relayInstruction.request.msgValue; + } else if (type === "GasDropOffInstruction") { + msgValue += relayInstruction.request.dropOff; + } else { + const relayInstructionType: never = type; + throw new Error(`Unsupported type: ${relayInstructionType}`); + } + } + return { gasLimit, msgValue }; +} + +export function estimateQuote( + { quote }: Quote, + gasLimit: bigint, + msgValue: bigint, + dstGasPriceDecimals: number, + srcTokenDecimals: number, + dstNativeDecimals: number, +): bigint { + const r = 18; + const srcChainValueForBaseFee = ScaledMath.normalize( + quote.baseFee, + SIGNED_QUOTE_DECIMALS, + srcTokenDecimals, + ); + + const nSrcPrice = ScaledMath.normalize( + quote.srcPrice, + SIGNED_QUOTE_DECIMALS, + r, + ); + const nDstPrice = ScaledMath.normalize( + quote.dstPrice, + SIGNED_QUOTE_DECIMALS, + r, + ); + const scaledConversion = ScaledMath.div(nDstPrice, nSrcPrice, r); + + const nGasLimitCost = ScaledMath.normalize( + gasLimit * quote.dstGasPrice, + dstGasPriceDecimals, + r, + ); + + const srcChainValueForGasLimit = ScaledMath.normalize( + ScaledMath.mul(nGasLimitCost, scaledConversion, r), + r, + srcTokenDecimals, + ); + + const nMsgValue = ScaledMath.normalize(msgValue, dstNativeDecimals, r); + const srcChainValueForMsgValue = ScaledMath.normalize( + ScaledMath.mul(nMsgValue, scaledConversion, r), + r, + srcTokenDecimals, + ); + return ( + srcChainValueForBaseFee + + srcChainValueForGasLimit + + srcChainValueForMsgValue + ); +}