diff --git a/api/_bridges/types.ts b/api/_bridges/types.ts index 0ff69c680..52b5e2694 100644 --- a/api/_bridges/types.ts +++ b/api/_bridges/types.ts @@ -1,6 +1,6 @@ import { BigNumber } from "ethers"; -import { CrossSwap, CrossSwapQuotes, Token } from "../_dexes/types"; +import { CrossSwap, CrossSwapQuotes, SwapQuote, Token } from "../_dexes/types"; import { AppFee, CrossSwapType } from "../_dexes/utils"; import { Logger } from "@across-protocol/sdk/dist/types/relayFeeCalculator"; @@ -78,7 +78,8 @@ export type BridgeStrategy = { getBridgeQuoteMessage: ( crossSwap: CrossSwap, - appFee?: AppFee + appFee?: AppFee, + originSwapQuote?: SwapQuote ) => string | undefined; getQuoteForExactInput: (params: GetExactInputBridgeQuoteParams) => Promise<{ diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index bb42dc439..4e7908615 100644 --- a/api/_dexes/cross-swap-service.ts +++ b/api/_dexes/cross-swap-service.ts @@ -843,7 +843,11 @@ export async function getCrossSwapQuotesForExactInputA2B( appFeeRecipient: crossSwap.appFeeRecipient, isNative: crossSwap.isOutputNative, }); - bridgeQuote.message = bridge.getBridgeQuoteMessage(crossSwap, appFee); + bridgeQuote.message = bridge.getBridgeQuoteMessage( + crossSwap, + appFee, + prioritizedStrategy.originSwapQuote + ); return { crossSwap, @@ -948,12 +952,11 @@ export async function getCrossSwapQuotesForOutputA2B( isNative: crossSwapWithAppFee.isOutputNative, }); - if (appFee.feeAmount.gt(0)) { - bridgeQuote.message = bridge.getBridgeQuoteMessage( - crossSwapWithAppFee, - appFee - ); - } + bridgeQuote.message = bridge.getBridgeQuoteMessage( + crossSwapWithAppFee, + appFee, + prioritizedStrategy.originSwapQuote + ); return { crossSwap: crossSwapWithAppFee, @@ -1306,6 +1309,7 @@ export async function getCrossSwapQuotesForExactInputByRouteA2A( bridgeableOutputToken, router: destinationRouter, appFee, + originSwapQuote: prioritizedOriginStrategy.originSwapQuote, }); return { @@ -1526,6 +1530,7 @@ export async function getCrossSwapQuotesForOutputByRouteA2A( bridgeableOutputToken, router: destinationRouter, appFee, + originSwapQuote, }); assertMinOutputAmount( finalDestinationSwapQuote.minAmountOut, diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index f3cfea2f1..6f0448750 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -16,6 +16,13 @@ import { encodeWithdrawAllWethCalldata, getMultiCallHandlerAddress, } from "../_multicall-handler"; +import { + getEventEmitterAddress, + encodeSwapMetadata, + encodeEmitDataCalldata, + SwapType, + SwapSide, +} from "../_event-emitter"; import { CrossSwap, CrossSwapQuotes, @@ -98,6 +105,12 @@ export type QuoteFetchStrategies = Partial<{ }; }>; +type Action = { + target: string; + callData: string; + value: string; +}; + export const AMOUNT_TYPE = { EXACT_INPUT: "exactInput", EXACT_OUTPUT: "exactOutput", @@ -205,28 +218,77 @@ export function getBridgeQuoteRecipient(crossSwap: CrossSwap) { return getMultiCallHandlerAddress(crossSwap.outputToken.chainId); } -export function getBridgeQuoteMessage(crossSwap: CrossSwap, appFee?: AppFee) { +export function getBridgeQuoteMessage( + crossSwap: CrossSwap, + appFee?: AppFee, + originSwapQuote?: SwapQuote +) { if (crossSwap.isDestinationSvm) { // Until we support messages for SVM destinations, we don't need to build a message return undefined; } + + const eventEmitterActions: Action[] = []; + if (originSwapQuote) { + const crossSwapType = + crossSwap.type === AMOUNT_TYPE.EXACT_INPUT + ? SwapType.EXACT_INPUT + : crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT + ? SwapType.MIN_OUTPUT + : SwapType.EXACT_OUTPUT; + const eventEmitterAddress = getEventEmitterAddress( + crossSwap.outputToken.chainId + ); + const originSwapMetadataParams = { + version: 1, + type: crossSwapType, + side: SwapSide.ORIGIN_SWAP, + address: crossSwap.inputToken.address, + maximumAmountIn: originSwapQuote.maximumAmountIn, + minAmountOut: originSwapQuote.minAmountOut, + expectedAmountOut: originSwapQuote.expectedAmountOut, + expectedAmountIn: originSwapQuote.expectedAmountIn, + swapProvider: originSwapQuote.swapProvider.name, + slippage: originSwapQuote.slippageTolerance, + autoSlippage: crossSwap.slippageTolerance === "auto", + recipient: crossSwap.recipient, + appFeeRecipient: crossSwap.appFeeRecipient || constants.AddressZero, + }; + const originSwapMetadata = encodeSwapMetadata(originSwapMetadataParams); + eventEmitterActions.push({ + target: eventEmitterAddress, + callData: encodeEmitDataCalldata(originSwapMetadata), + value: "0", + }); + } + switch (crossSwap.type) { case AMOUNT_TYPE.EXACT_INPUT: - return buildExactInputBridgeTokenMessage(crossSwap, appFee); + return buildExactInputBridgeTokenMessage( + crossSwap, + appFee, + eventEmitterActions + ); case AMOUNT_TYPE.EXACT_OUTPUT: return buildExactOutputBridgeTokenMessage( crossSwap, crossSwap.amount, - appFee + appFee, + eventEmitterActions ); case AMOUNT_TYPE.MIN_OUTPUT: - return buildMinOutputBridgeTokenMessage(crossSwap, appFee); + return buildMinOutputBridgeTokenMessage( + crossSwap, + appFee, + eventEmitterActions + ); } } export function buildExactInputBridgeTokenMessage( crossSwap: CrossSwap, - appFee?: AppFee + appFee?: AppFee, + eventEmitterActions: Action[] = [] ) { const multicallHandlerAddress = getMultiCallHandlerAddress( crossSwap.outputToken.chainId @@ -283,6 +345,8 @@ export function buildExactInputBridgeTokenMessage( ), value: "0", }, + // emit swap metadata events + ...eventEmitterActions, ], }); } @@ -295,7 +359,8 @@ export function buildExactInputBridgeTokenMessage( export function buildExactOutputBridgeTokenMessage( crossSwap: CrossSwap, exactOutputAmount: BigNumber, - appFee?: AppFee + appFee?: AppFee, + eventEmitterActions: Action[] = [] ) { const { feeActions: appFeeActions } = appFee || { feeAmount: BigNumber.from(0), @@ -365,6 +430,8 @@ export function buildExactOutputBridgeTokenMessage( ), value: "0", }, + // emit swap metadata events + ...eventEmitterActions, ], }); } @@ -412,7 +479,8 @@ function _getExactOutputDustRecipient(crossSwap: CrossSwap) { */ export function buildMinOutputBridgeTokenMessage( crossSwap: CrossSwap, - appFee?: AppFee + appFee?: AppFee, + eventEmitterActions: Action[] = [] ) { const multicallHandlerAddress = getMultiCallHandlerAddress( crossSwap.outputToken.chainId @@ -467,6 +535,8 @@ export function buildMinOutputBridgeTokenMessage( ), value: "0", }, + // emit swap metadata events + ...eventEmitterActions, ], }); } @@ -595,6 +665,7 @@ export function buildDestinationSwapCrossChainMessage({ bridgeableOutputToken, router, appFee, + originSwapQuote, }: { crossSwap: CrossSwap; bridgeableOutputToken: Token; @@ -604,6 +675,7 @@ export function buildDestinationSwapCrossChainMessage({ transferType?: TransferType; }; appFee?: AppFee; + originSwapQuote?: SwapQuote; }) { // Ensure all swapTxns are EVM ecosystem since this function handles only EVM messages if (!destinationSwapQuote.swapTxns.every(isEvmSwapTxn)) { @@ -621,12 +693,6 @@ export function buildDestinationSwapCrossChainMessage({ swapTxn.to === "0x0" && swapTxn.data === "0x0" && swapTxn.value === "0x0" ); - type Action = { - target: string; - callData: string; - value: string; - }; - let transferActions: Action[] = []; let unwrapActions: Action[] = []; let appFeeActions: Action[] = []; @@ -771,6 +837,65 @@ export function buildDestinationSwapCrossChainMessage({ value: "0", }; + // Build event emitter actions for destination and origin swaps + const crossSwapType = + crossSwap.type === AMOUNT_TYPE.EXACT_INPUT + ? SwapType.EXACT_INPUT + : crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT + ? SwapType.MIN_OUTPUT + : SwapType.EXACT_OUTPUT; + const eventEmitterActions: Action[] = []; + const eventEmitterAddress = getEventEmitterAddress(destinationSwapChainId); + if (!isIndicativeQuote) { + const destinationSwapMetadataParams = { + version: 1, + type: crossSwapType, + side: SwapSide.DESTINATION_SWAP, + address: crossSwap.outputToken.address, + maximumAmountIn: destinationSwapQuote.maximumAmountIn, + minAmountOut: destinationSwapQuote.minAmountOut, + expectedAmountOut: destinationSwapQuote.expectedAmountOut, + expectedAmountIn: destinationSwapQuote.expectedAmountIn, + swapProvider: destinationSwapQuote.swapProvider.name, + slippage: destinationSwapQuote.slippageTolerance, + autoSlippage: crossSwap.slippageTolerance === "auto", + recipient: crossSwap.recipient, + appFeeRecipient: crossSwap.appFeeRecipient || constants.AddressZero, + }; + const destinationSwapMetadata = encodeSwapMetadata( + destinationSwapMetadataParams + ); + eventEmitterActions.push({ + target: eventEmitterAddress, + callData: encodeEmitDataCalldata(destinationSwapMetadata), + value: "0", + }); + + if (originSwapQuote) { + const originSwapMetadataParams = { + version: 1, + type: crossSwapType, + side: SwapSide.ORIGIN_SWAP, + address: crossSwap.inputToken.address, + maximumAmountIn: originSwapQuote.maximumAmountIn, + minAmountOut: originSwapQuote.minAmountOut, + expectedAmountOut: originSwapQuote.expectedAmountOut, + expectedAmountIn: originSwapQuote.expectedAmountIn, + swapProvider: originSwapQuote.swapProvider.name, + slippage: originSwapQuote.slippageTolerance, + autoSlippage: crossSwap.slippageTolerance === "auto", + recipient: crossSwap.recipient, + appFeeRecipient: crossSwap.appFeeRecipient || constants.AddressZero, + }; + const originSwapMetadata = encodeSwapMetadata(originSwapMetadataParams); + eventEmitterActions.push({ + target: eventEmitterAddress, + callData: encodeEmitDataCalldata(originSwapMetadata), + value: "0", + }); + } + } + return buildMulticallHandlerMessage({ fallbackRecipient: getFallbackRecipient(crossSwap, destinationRecipient), actions: [ @@ -806,6 +931,8 @@ export function buildDestinationSwapCrossChainMessage({ }, ] : []), + // emit swap metadata events + ...eventEmitterActions, ], }); } diff --git a/api/_event-emitter.ts b/api/_event-emitter.ts new file mode 100644 index 000000000..9f134d124 --- /dev/null +++ b/api/_event-emitter.ts @@ -0,0 +1,157 @@ +import { CHAIN_IDs } from "@across-protocol/constants"; +import { ethers } from "ethers"; + +/** + * Swap type for metadata encoding + */ +export enum SwapType { + EXACT_INPUT = 0, + MIN_OUTPUT = 1, + EXACT_OUTPUT = 2, +} + +/** + * Swap side for metadata encoding + */ +export enum SwapSide { + ORIGIN_SWAP = 0, + DESTINATION_SWAP = 1, +} + +// AcrossEventEmitter contract addresses per chain +export const ACROSS_EVENT_EMITTER_ADDRESS: Record = { + [CHAIN_IDs.MAINNET]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.OPTIMISM]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.ARBITRUM]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.BASE]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.POLYGON]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.LINEA]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.MODE]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.BLAST]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.LISK]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.REDSTONE]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.SCROLL]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.ZORA]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.WORLD_CHAIN]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.INK]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.SONEIUM]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.UNICHAIN]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.BSC]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.PLASMA]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", + [CHAIN_IDs.ZK_SYNC]: "0x74a2966e23B61A360b1345eA359127aF325beC4C", + [CHAIN_IDs.LENS]: "0x6AA1d7390C0B9F36f08503207985fEAE6C6a13Dc", + [CHAIN_IDs.HYPEREVM]: "0xBF75133b48b0a42AB9374027902E83C5E2949034", +}; + +/** + * Get the AcrossEventEmitter contract address for a given chain + * @param chainId The chain ID + * @returns The contract address or undefined if not deployed on that chain + */ +export function getEventEmitterAddress(chainId: number): string { + return ACROSS_EVENT_EMITTER_ADDRESS[chainId]; +} + +/** + * Encodes metadata for swap events + * @param version Metadata version (1 byte) + * @param type Swap type (EXACT_INPUT, MIN_OUTPUT, or EXACT_OUTPUT) (1 byte) + * @param side Swap side (ORIGIN_SWAP or DESTINATION_SWAP) (1 byte) + * @param address The token address (32 bytes) + * @param maximumAmountIn The maximum amount in (32 bytes) + * @param minAmountOut The minimum amount out (32 bytes) + * @param expectedAmountOut The expected amount out (32 bytes) + * @param expectedAmountIn The expected amount in (32 bytes) + * @param swapProvider The name of the swap provider (UTF-8 encoded, variable length) + * @param slippage The slippage tolerance in percentage (0-100, converted to basis points, 32 bytes) + * @param autoSlippage Whether auto slippage was used (1 byte) + * @param recipient The final recipient address (32 bytes) + * @param appFeeRecipient The app fee recipient address (32 bytes, zero address if no app fee) + * @returns Encoded bytes + */ +export function encodeSwapMetadata(params: { + version: number; + type: SwapType; + side: SwapSide; + address: string; + maximumAmountIn: ethers.BigNumberish; + minAmountOut: ethers.BigNumberish; + expectedAmountOut: ethers.BigNumberish; + expectedAmountIn: ethers.BigNumberish; + swapProvider: string; + slippage: ethers.BigNumberish; + autoSlippage: boolean; + recipient: string; + appFeeRecipient: string; +}): string { + const { + version, + type, + side, + address, + maximumAmountIn, + minAmountOut, + expectedAmountOut, + expectedAmountIn, + swapProvider, + slippage, + autoSlippage, + recipient, + appFeeRecipient, + } = params; + + const abiCoder = ethers.utils.defaultAbiCoder; + + // Encode the metadata as a packed bytes structure + // Convert slippage percentage (0-100) to basis points (e.g., 0.5% -> 50 bps) + const slippageBps = + typeof slippage === "number" && slippage <= 100 + ? Math.round(slippage * 100) + : slippage; + + const encoded = abiCoder.encode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + [ + version, + type, + side, + address, + ethers.BigNumber.from(maximumAmountIn), + ethers.BigNumber.from(minAmountOut), + ethers.BigNumber.from(expectedAmountOut), + ethers.BigNumber.from(expectedAmountIn), + swapProvider, + ethers.BigNumber.from(slippageBps), + autoSlippage, + recipient, + appFeeRecipient, + ] + ); + + return encoded; +} + +/** + * Encodes calldata for calling AcrossEventEmitter.emitData + * @param data The bytes data to emit + * @returns Encoded calldata + */ +export function encodeEmitDataCalldata(data: string): string { + const emitDataFunction = "function emitData(bytes calldata data)"; + const eventEmitterInterface = new ethers.utils.Interface([emitDataFunction]); + return eventEmitterInterface.encodeFunctionData("emitData", [data]); +} diff --git a/test/api/_event-emitter.test.ts b/test/api/_event-emitter.test.ts new file mode 100644 index 000000000..6dd76d2f6 --- /dev/null +++ b/test/api/_event-emitter.test.ts @@ -0,0 +1,397 @@ +import { ethers } from "ethers"; +import { CHAIN_IDs } from "@across-protocol/constants"; + +import { + getEventEmitterAddress, + encodeSwapMetadata, + encodeEmitDataCalldata, + SwapType, + SwapSide, +} from "../../api/_event-emitter"; + +describe("Event Emitter Module", () => { + describe("getEventEmitterAddress", () => { + it("should return address for valid chain ID", () => { + const address = getEventEmitterAddress(CHAIN_IDs.ARBITRUM); + expect(address).toBeDefined(); + expect(typeof address).toBe("string"); + }); + + it("should return undefined for unknown chain ID", () => { + const address = getEventEmitterAddress(999999); + expect(address).toBeUndefined(); + }); + }); + + describe("encodeSwapMetadata", () => { + it("should encode metadata with all required fields", () => { + const metadata = encodeSwapMetadata({ + version: 1, + type: SwapType.EXACT_INPUT, + side: SwapSide.DESTINATION_SWAP, + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // WETH on Arbitrum + maximumAmountIn: ethers.utils.parseEther("2.1"), + minAmountOut: ethers.utils.parseEther("1.5"), + expectedAmountOut: ethers.utils.parseEther("2.0"), + expectedAmountIn: ethers.utils.parseEther("1.8"), + swapProvider: "Uniswap V3", + slippage: 0.5, // 0.5% + autoSlippage: true, + recipient: "0x1111111111111111111111111111111111111111", + appFeeRecipient: "0x2222222222222222222222222222222222222222", + }); + + expect(metadata).toBeDefined(); + expect(metadata.startsWith("0x")).toBe(true); + expect(metadata.length).toBeGreaterThan(2); + + // Decode and verify + const abiCoder = ethers.utils.defaultAbiCoder; + const decoded = abiCoder.decode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + metadata + ); + + expect(decoded[0]).toBe(1); // version + expect(decoded[1]).toBe(SwapType.EXACT_INPUT); // type + expect(decoded[2]).toBe(SwapSide.DESTINATION_SWAP); // side + expect(decoded[3].toLowerCase()).toBe( + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".toLowerCase() + ); // address + expect(decoded[4]).toEqual(ethers.utils.parseEther("2.1")); // maximumAmountIn + expect(decoded[5]).toEqual(ethers.utils.parseEther("1.5")); // minAmountOut + expect(decoded[6]).toEqual(ethers.utils.parseEther("2.0")); // expectedAmountOut + expect(decoded[7]).toEqual(ethers.utils.parseEther("1.8")); // expectedAmountIn + expect(decoded[8]).toBe("Uniswap V3"); // swapProvider + expect(decoded[9]).toEqual(ethers.BigNumber.from(50)); // slippage (0.5% = 50 bps) + expect(decoded[10]).toBe(true); // autoSlippage + expect(decoded[11].toLowerCase()).toBe( + "0x1111111111111111111111111111111111111111".toLowerCase() + ); // recipient + expect(decoded[12].toLowerCase()).toBe( + "0x2222222222222222222222222222222222222222".toLowerCase() + ); // appFeeRecipient + }); + + it("should handle zero address for appFeeRecipient", () => { + const metadata = encodeSwapMetadata({ + version: 1, + type: SwapType.EXACT_OUTPUT, + side: SwapSide.DESTINATION_SWAP, + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + maximumAmountIn: ethers.utils.parseEther("1.2"), + minAmountOut: ethers.utils.parseEther("1"), + expectedAmountOut: ethers.utils.parseEther("1.1"), + expectedAmountIn: ethers.utils.parseEther("1.05"), + swapProvider: "0x", + slippage: 1, // 1% + autoSlippage: false, + recipient: "0x1111111111111111111111111111111111111111", + appFeeRecipient: ethers.constants.AddressZero, + }); + + const abiCoder = ethers.utils.defaultAbiCoder; + const decoded = abiCoder.decode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + metadata + ); + + expect(decoded[12]).toBe(ethers.constants.AddressZero); + }); + + it("should handle different swap providers", () => { + const providers = ["Uniswap V3", "0x", "1inch", "Jupiter"]; + + providers.forEach((provider) => { + const metadata = encodeSwapMetadata({ + version: 1, + type: SwapType.EXACT_INPUT, + side: SwapSide.ORIGIN_SWAP, + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + maximumAmountIn: ethers.utils.parseEther("1.1"), + minAmountOut: ethers.utils.parseEther("1"), + expectedAmountOut: ethers.utils.parseEther("1.05"), + expectedAmountIn: ethers.utils.parseEther("1.02"), + swapProvider: provider, + slippage: 0.5, // 0.5% + autoSlippage: false, + recipient: "0x1111111111111111111111111111111111111111", + appFeeRecipient: ethers.constants.AddressZero, + }); + + const abiCoder = ethers.utils.defaultAbiCoder; + const decoded = abiCoder.decode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + metadata + ); + + expect(decoded[8]).toBe(provider); + }); + }); + + it("should handle different token amounts", () => { + const amounts = [ + ethers.utils.parseUnits("1", 6), // 1 USDC + ethers.utils.parseEther("0.001"), // 0.001 ETH + ethers.utils.parseEther("1000"), // 1000 tokens + ethers.BigNumber.from("1"), // 1 wei + ]; + + amounts.forEach((minAmountOut) => { + const expectedAmountOut = minAmountOut.mul(110).div(100); // 10% higher + const maximumAmountIn = minAmountOut.mul(120).div(100); // 20% higher + const expectedAmountIn = minAmountOut.mul(105).div(100); // 5% higher + const metadata = encodeSwapMetadata({ + version: 1, + type: SwapType.EXACT_INPUT, + side: SwapSide.DESTINATION_SWAP, + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + maximumAmountIn, + minAmountOut, + expectedAmountOut, + expectedAmountIn, + swapProvider: "Test", + slippage: 1, // 1% + autoSlippage: false, + recipient: "0x1111111111111111111111111111111111111111", + appFeeRecipient: ethers.constants.AddressZero, + }); + + const abiCoder = ethers.utils.defaultAbiCoder; + const decoded = abiCoder.decode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + metadata + ); + + expect(decoded[4]).toEqual(maximumAmountIn); // maximumAmountIn + expect(decoded[5]).toEqual(minAmountOut); // minAmountOut + expect(decoded[6]).toEqual(expectedAmountOut); // expectedAmountOut + expect(decoded[7]).toEqual(expectedAmountIn); // expectedAmountIn + }); + }); + }); + + describe("encodeEmitDataCalldata", () => { + it("should encode calldata with correct function selector", () => { + const testData = "0x1234567890abcdef"; + const calldata = encodeEmitDataCalldata(testData); + + expect(calldata).toBeDefined(); + expect(calldata.startsWith("0x")).toBe(true); + + // Check function selector (first 4 bytes) + const selector = calldata.slice(0, 10); + const expectedSelector = ethers.utils.id("emitData(bytes)").slice(0, 10); + expect(selector).toBe(expectedSelector); + }); + + it("should correctly encode and decode data parameter", () => { + const testData = "0xabcdef1234567890"; + const calldata = encodeEmitDataCalldata(testData); + + // Decode it back + const emitDataInterface = new ethers.utils.Interface([ + "function emitData(bytes calldata data)", + ]); + const decoded = emitDataInterface.decodeFunctionData( + "emitData", + calldata + ); + + expect(decoded.data).toBe(testData); + }); + + it("should handle full metadata encoding in calldata", () => { + // First encode the metadata + const metadata = encodeSwapMetadata({ + version: 1, + type: SwapType.EXACT_OUTPUT, + side: SwapSide.DESTINATION_SWAP, + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + maximumAmountIn: ethers.utils.parseEther("1.3"), + minAmountOut: ethers.utils.parseEther("1"), + expectedAmountOut: ethers.utils.parseEther("1.1"), + expectedAmountIn: ethers.utils.parseEther("1.15"), + swapProvider: "Uniswap V3", + slippage: 1, // 1% + autoSlippage: true, + recipient: "0x1111111111111111111111111111111111111111", + appFeeRecipient: "0x2222222222222222222222222222222222222222", + }); + + // Then encode it as calldata + const calldata = encodeEmitDataCalldata(metadata); + + // Decode the calldata + const emitDataInterface = new ethers.utils.Interface([ + "function emitData(bytes calldata data)", + ]); + const decodedCalldata = emitDataInterface.decodeFunctionData( + "emitData", + calldata + ); + + // Decode the inner metadata + const abiCoder = ethers.utils.defaultAbiCoder; + const decodedMetadata = abiCoder.decode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + decodedCalldata.data + ); + + // Verify all fields + expect(decodedMetadata[0]).toBe(1); // version + expect(decodedMetadata[1]).toBe(SwapType.EXACT_OUTPUT); // type + expect(decodedMetadata[2]).toBe(SwapSide.DESTINATION_SWAP); // side + expect(decodedMetadata[3].toLowerCase()).toBe( + "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1".toLowerCase() + ); + expect(decodedMetadata[4]).toEqual(ethers.utils.parseEther("1.3")); // maximumAmountIn + expect(decodedMetadata[5]).toEqual(ethers.utils.parseEther("1")); // minAmountOut + expect(decodedMetadata[6]).toEqual(ethers.utils.parseEther("1.1")); // expectedAmountOut + expect(decodedMetadata[7]).toEqual(ethers.utils.parseEther("1.15")); // expectedAmountIn + expect(decodedMetadata[8]).toBe("Uniswap V3"); // swapProvider + expect(decodedMetadata[9]).toEqual(ethers.BigNumber.from(100)); // slippage (1% = 100 bps) + expect(decodedMetadata[10]).toBe(true); // autoSlippage + expect(decodedMetadata[11].toLowerCase()).toBe( + "0x1111111111111111111111111111111111111111".toLowerCase() + ); + expect(decodedMetadata[12].toLowerCase()).toBe( + "0x2222222222222222222222222222222222222222".toLowerCase() + ); + }); + }); + + describe("Integration: Full encoding flow", () => { + it("should encode metadata that can be used in MulticallHandler", () => { + // This simulates the full flow of encoding metadata for the event emitter + const swapMetadata = encodeSwapMetadata({ + version: 1, + type: SwapType.EXACT_INPUT, + side: SwapSide.ORIGIN_SWAP, + address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + maximumAmountIn: ethers.utils.parseEther("3.0"), + minAmountOut: ethers.utils.parseEther("2.5"), + expectedAmountOut: ethers.utils.parseEther("2.8"), + expectedAmountIn: ethers.utils.parseEther("2.6"), + swapProvider: "0x", + slippage: 1.2, // 1.2% + autoSlippage: false, + recipient: "0x1111111111111111111111111111111111111111", + appFeeRecipient: "0x2222222222222222222222222222222222222222", + }); + + const emitDataCalldata = encodeEmitDataCalldata(swapMetadata); + + // Verify the calldata can be decoded + const emitDataInterface = new ethers.utils.Interface([ + "function emitData(bytes calldata data)", + ]); + const decodedCalldata = emitDataInterface.decodeFunctionData( + "emitData", + emitDataCalldata + ); + + // Verify the inner metadata + const abiCoder = ethers.utils.defaultAbiCoder; + const decodedMetadata = abiCoder.decode( + [ + "uint8", + "uint8", + "uint8", + "address", + "uint256", + "uint256", + "uint256", + "uint256", + "string", + "uint256", + "bool", + "address", + "address", + ], + decodedCalldata.data + ); + + expect(decodedMetadata[0]).toBe(1); // version + expect(decodedMetadata[1]).toBe(SwapType.EXACT_INPUT); // type + expect(decodedMetadata[2]).toBe(SwapSide.ORIGIN_SWAP); // side + expect(decodedMetadata[8]).toBe("0x"); // swapProvider + expect(decodedMetadata[4]).toEqual(ethers.utils.parseEther("3.0")); // maximumAmountIn + expect(decodedMetadata[5]).toEqual(ethers.utils.parseEther("2.5")); // minAmountOut + expect(decodedMetadata[6]).toEqual(ethers.utils.parseEther("2.8")); // expectedAmountOut + expect(decodedMetadata[7]).toEqual(ethers.utils.parseEther("2.6")); // expectedAmountIn + expect(decodedMetadata[9]).toEqual(ethers.BigNumber.from(120)); // slippage (1.2% = 120 bps) + expect(decodedMetadata[10]).toBe(false); // autoSlippage + }); + }); +}); diff --git a/test/api/_utils.test.ts b/test/api/_utils.test.ts index df37ef2f7..845565b2e 100644 --- a/test/api/_utils.test.ts +++ b/test/api/_utils.test.ts @@ -8,7 +8,6 @@ import { validSvmAddress, validAddress, getTokenByAddress, - getChainInfo, } from "../../api/_utils"; import { is } from "superstruct";