diff --git a/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.test.ts b/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.test.ts index 2f3dc1798..bfd693312 100644 --- a/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.test.ts @@ -463,6 +463,50 @@ describe("Sushi Action Provider", () => { expect(result).toContain(`Swapped`); }); + it("should report an executed swap when the Route event is missing from the receipt", async () => { + const args: Parameters<(typeof actionProvider)["swap"]>[1] = { + amount: formatUnits(amountIn, tokenIn.decimals), + fromAssetAddress: tokenIn.address, + toAssetAddress: tokenOut.address, + maxSlippage: 0.005, + }; + + /* + * 1. Mock the readContract which checks the decimals of the fromAssetAddress token (18, default) + * 2. Mock the readContract which checks for the balance of the fromAssetAddress token (1000000, enough balance) + * 3. Mock the readContract which checks for the approval (1000000, approved) + */ + mockWallet.readContract + .mockResolvedValueOnce(tokenIn.decimals) + .mockResolvedValueOnce(amountIn) + .mockResolvedValueOnce(amountIn); + + mockWallet.sendTransaction.mockResolvedValue(txHash); + + // Swap tx succeeds on-chain but the receipt contains no Route event log + mockWallet.waitForTransactionReceipt.mockResolvedValueOnce({ + status: "success", + logs: [], + }); + + mockedGetSwap.mockReturnValue( + getSuccessfullSwapResponse({ + tokenIn, + amountIn, + tokenOut, + amountOut, + }), + ); + + const result = await actionProvider.swap(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledTimes(1); // Swap only + expect(result).toContain("Swap executed"); + expect(result).not.toContain("Error"); + expect(result).toContain(`Transaction hash: ${txHash}`); + expect(result).toContain("Do not retry this swap automatically"); + }); + it("should fail if there's no route", async () => { const args: Parameters<(typeof actionProvider)["swap"]>[1] = { amount: formatUnits(amountIn, tokenIn.decimals), diff --git a/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.ts b/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.ts index 0eab9575b..bdc844272 100644 --- a/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.ts +++ b/typescript/agentkit/src/action-providers/sushi/sushiRouterActionProvider.ts @@ -149,7 +149,7 @@ Important notes: } // Find the Route event log, which includes the actual amountOut - const [routeLog] = swapReceipt.logs + const [routeLog] = (swapReceipt.logs ?? []) .filter( log => encodeEventTopics({ @@ -165,6 +165,18 @@ Important notes: }), ); + if (!routeLog) { + // The swap succeeded on-chain, but the receipt has no Route event to + // decode amounts from (e.g. fills not routed through RouteProcessor9). + // Report this as an executed swap — returning a generic error here + // leads agents to retry and execute a second, unintended swap. + return `Swap executed on ${chain.shortName}, but the Route event was not found in the transaction receipt, so the exact output amount could not be decoded. + - Quoted AmountOut: ${formatUnits(BigInt(secondSwap.swap.assumedAmountOut), secondSwap.swap.tokenTo.decimals)} ${secondSwap.swap.tokenTo.symbol} (${args.toAssetAddress}) + - Transaction hash: ${swapHash} + - Transaction link: ${chain.getTransactionUrl(swapHash)} +Do not retry this swap automatically; check the transaction first.`; + } + return `Swapped ${formatUnits(routeLog.args.amountIn, secondSwap.swap.tokenFrom.decimals)} of ${secondSwap.swap.tokenFrom.symbol} (${args.fromAssetAddress}) for ${formatUnits(routeLog.args.amountOut, secondSwap.swap.tokenTo.decimals)} of ${secondSwap.swap.tokenTo.symbol} (${args.toAssetAddress}) on ${chain.shortName} - Transaction hash: ${swapHash} - Transaction link: ${chain.getTransactionUrl(swapHash)}`;