From faee65714d274f5adde3543ac42b59e1e685da78 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Fri, 18 Apr 2025 15:09:12 +0200 Subject: [PATCH 1/5] add swaps via 0x --- typescript/.changeset/whole-times-fetch.md | 5 + typescript/agentkit/README.md | 9 + .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/zeroX/README.md | 62 +++ .../src/action-providers/zeroX/index.ts | 1 + .../src/action-providers/zeroX/schemas.ts | 57 +++ .../zeroX/zeroXActionProvider.test.ts | 350 +++++++++++++ .../zeroX/zeroXActionProvider.ts | 468 ++++++++++++++++++ 8 files changed, 953 insertions(+) create mode 100644 typescript/.changeset/whole-times-fetch.md create mode 100644 typescript/agentkit/src/action-providers/zeroX/README.md create mode 100644 typescript/agentkit/src/action-providers/zeroX/index.ts create mode 100644 typescript/agentkit/src/action-providers/zeroX/schemas.ts create mode 100644 typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts diff --git a/typescript/.changeset/whole-times-fetch.md b/typescript/.changeset/whole-times-fetch.md new file mode 100644 index 000000000..053423a02 --- /dev/null +++ b/typescript/.changeset/whole-times-fetch.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": minor +--- + +Added ZeroX Action Provider to enable token swaps using the 0x Protocol API diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 457109e79..1dd2f92a1 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -524,6 +524,15 @@ it will return payment details that can be used on retry. make_http_request_with_x402 Combines make_http_request and retry_http_request_with_x402 into a single step. +ZeroX + + + + + + + +
get_swap_price_quote_from_0xFetches a price quote for swapping between two tokens using the 0x API.
execute_swap_on_0xExecutes a token swap between two tokens using the 0x API.
diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 35ff1f726..7ac55703e 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -33,4 +33,5 @@ export * from "./vaultsfyi"; export * from "./x402"; export * from "./zerion"; export * from "./zerodev"; +export * from "./zeroX"; export * from "./zora"; diff --git a/typescript/agentkit/src/action-providers/zeroX/README.md b/typescript/agentkit/src/action-providers/zeroX/README.md new file mode 100644 index 000000000..a60de91aa --- /dev/null +++ b/typescript/agentkit/src/action-providers/zeroX/README.md @@ -0,0 +1,62 @@ +# 0x Action Provider + +This directory contains the **ZeroXActionProvider** implementation, which provides actions to interact with the **0x Protocol** for token swaps on EVM-compatible networks. + +## Directory Structure + +``` +zeroX/ +├── zeroXActionProvider.ts # Main provider with 0x Protocol functionality +├── zeroXActionProvider.test.ts # Tests +├── schemas.ts # Swap schemas +├── index.ts # Main exports +└── README.md # This file +``` + +## Actions + +- `get_swap_price_quote_from_0x`: Get a price quote for swapping tokens + - Returns detailed price information including exchange rate, fees, and minimum buy amount + - Does not execute any transactions + +- `execute_swap_on_0x`: Execute a token swap between two tokens + - Handles token approvals automatically if needed + - Executes the swap transaction + - Returns the transaction hash and swap details upon success + +## Adding New Actions + +To add new 0x Protocol actions: + +1. Define your action schema in `schemas.ts` +2. Implement the action in `zeroXActionProvider.ts` +3. Add tests in `zeroXActionProvider.test.ts` + +## Network Support + +The 0x provider supports all EVM-compatible networks where the 0x API is available. + +## Configuration + +The provider requires the following configuration: + +```typescript +const provider = zeroXActionProvider({ + apiKey: "your-0x-api-key" // Required for 0x API access +}); +``` + +You can also set the API key via environment variable: + +``` +ZEROX_API_KEY=your-0x-api-key +``` + +## Notes + +- The contract address for native ETH is `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` +- All token amounts should be specified in whole units (e.g., "1.5" ETH, not wei) +- Slippage setting is optional and defaults to 1% (100 basis points) if not specified +- Always check price quotes before executing swaps to ensure favorable rates + +For more information on the **0x Protocol**, visit [0x Protocol Documentation](https://docs.0x.org/). \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/zeroX/index.ts b/typescript/agentkit/src/action-providers/zeroX/index.ts new file mode 100644 index 000000000..dae43042d --- /dev/null +++ b/typescript/agentkit/src/action-providers/zeroX/index.ts @@ -0,0 +1 @@ +export * from "./zeroXActionProvider"; diff --git a/typescript/agentkit/src/action-providers/zeroX/schemas.ts b/typescript/agentkit/src/action-providers/zeroX/schemas.ts new file mode 100644 index 000000000..fe2640449 --- /dev/null +++ b/typescript/agentkit/src/action-providers/zeroX/schemas.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; + +/** + * Input schema for getting a swap price. + */ +export const GetSwapPriceSchema = z + .object({ + sellToken: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .describe("The token contract address to sell"), + buyToken: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .describe("The token contract address to buy"), + sellAmount: z + .string() + .describe("The amount of sellToken to sell in whole units (e.g., 1.5 WETH, 10 USDC)"), + slippageBps: z + .number() + .int() + .min(0) + .max(10000) + .optional() + .default(100) + .describe("The maximum acceptable slippage in basis points (0-10000, default: 100)"), + }) + .strip() + .describe("Get a price quote for swapping one token for another"); + +/** + * Input schema for executing a swap. + */ +export const ExecuteSwapSchema = z + .object({ + sellToken: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .describe("The token contract address to sell"), + buyToken: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .describe("The token contract address to buy"), + sellAmount: z + .string() + .describe("The amount of sellToken to sell in whole units (e.g., 1.5 WETH, 10 USDC)"), + slippageBps: z + .number() + .int() + .min(0) + .max(10000) + .optional() + .default(100) + .describe("The maximum acceptable slippage in basis points (0-10000, default: 100)"), + }) + .strip() + .describe("Execute a swap between two tokens"); diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts new file mode 100644 index 000000000..936a030ed --- /dev/null +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts @@ -0,0 +1,350 @@ +import { zeroXActionProvider } from "./zeroXActionProvider"; +import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { Hex } from "viem"; + +// Mock the fetch function +global.fetch = jest.fn(); + +describe("ZeroX Schema Validation", () => { + it("should validate GetSwapPrice schema with valid input", () => { + const validInput = { + sellToken: "0x1234567890123456789012345678901234567890", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + slippageBps: 50, + }; + + const result = GetSwapPriceSchema.safeParse(validInput); + expect(result.success).toBe(true); + }); + + it("should fail validation with invalid address format", () => { + const invalidInput = { + sellToken: "invalid-address", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + slippageBps: 50, + }; + + const result = GetSwapPriceSchema.safeParse(invalidInput); + expect(result.success).toBe(false); + }); + + it("should use default slippageBps when not provided", () => { + const inputWithoutSlippage = { + sellToken: "0x1234567890123456789012345678901234567890", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + }; + + const result = GetSwapPriceSchema.safeParse(inputWithoutSlippage); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.slippageBps).toBe(100); // Default value from schema + } + }); + + it("should validate ExecuteSwap schema with valid input", () => { + const validInput = { + sellToken: "0x1234567890123456789012345678901234567890", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + slippageBps: 50, + }; + + const result = ExecuteSwapSchema.safeParse(validInput); + expect(result.success).toBe(true); + }); +}); + +describe("ZeroX Action Provider", () => { + let provider: ReturnType; + let mockWalletProvider: jest.Mocked; + + const MOCK_SELL_TOKEN = "0x1234567890123456789012345678901234567890"; + const MOCK_BUY_TOKEN = "0x0987654321098765432109876543210987654321"; + const MOCK_SELL_AMOUNT = "1.5"; + const MOCK_CHAIN_ID = 1; + const MOCK_ADDRESS = "0xabcdef1234567890abcdef1234567890abcdef12"; + + beforeEach(() => { + provider = zeroXActionProvider({ apiKey: "test-api-key" }); + + mockWalletProvider = { + getAddress: jest.fn().mockReturnValue(MOCK_ADDRESS), + getNetwork: jest.fn().mockReturnValue({ + chainId: MOCK_CHAIN_ID, + protocolFamily: "evm", + }), + readContract: jest.fn(), + sendTransaction: jest.fn(), + waitForTransactionReceipt: jest.fn(), + signTypedData: jest.fn(), + } as unknown as jest.Mocked; + + // Reset mocks + (global.fetch as jest.Mock).mockReset(); + }); + + describe("getSwapPrice", () => { + beforeEach(() => { + // Mock readContract for decimals + mockWalletProvider.readContract + .mockResolvedValueOnce(18) // sellToken decimals + .mockResolvedValueOnce(6); // buyToken decimals + + // Mock fetch for price API + const mockPriceResponse = { + buyAmount: "1000000", // 1 USDC with 6 decimals + minBuyAmount: "990000", // 0.99 USDC with 6 decimals + totalNetworkFee: "100000000000000", // 0.0001 ETH + issues: null, + liquidityAvailable: true, + }; + + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockPriceResponse), + }); + }); + + it("should get swap price successfully", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + const response = await provider.getSwapPrice(mockWalletProvider, args); + const parsedResponse = JSON.parse(response); + + // Verify fetch was called with correct URL params + expect(global.fetch).toHaveBeenCalledTimes(1); + expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain( + "api.0x.org/swap/permit2/price", + ); + expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(`chainId=${MOCK_CHAIN_ID}`); + expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain( + `sellToken=${MOCK_SELL_TOKEN}`, + ); + expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(`buyToken=${MOCK_BUY_TOKEN}`); + + // Verify response formatting + expect(parsedResponse.sellToken).toBe(MOCK_SELL_TOKEN); + expect(parsedResponse.buyToken).toBe(MOCK_BUY_TOKEN); + expect(parsedResponse.liquidityAvailable).toBe(true); + expect(parsedResponse.buyAmount).toBeDefined(); + expect(parsedResponse.minBuyAmount).toBeDefined(); + }); + + it("should handle API errors", async () => { + (global.fetch as jest.Mock).mockReset(); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: "Bad Request", + text: jest.fn().mockResolvedValueOnce("Invalid request parameters"), + }); + + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + const response = await provider.getSwapPrice(mockWalletProvider, args); + const parsedResponse = JSON.parse(response); + + expect(parsedResponse.success).toBe(false); + expect(parsedResponse.error).toContain("Error fetching swap price"); + }); + + it("should handle fetch errors", async () => { + (global.fetch as jest.Mock).mockReset(); + (global.fetch as jest.Mock).mockRejectedValueOnce(new Error("Network error")); + + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + const response = await provider.getSwapPrice(mockWalletProvider, args); + const parsedResponse = JSON.parse(response); + + expect(parsedResponse.success).toBe(false); + expect(parsedResponse.error).toContain("Error fetching swap price"); + }); + }); + + describe("executeSwap", () => { + const MOCK_TX_HASH = "0xtxhash123456"; + + beforeEach(() => { + // Mock readContract for decimals + mockWalletProvider.readContract + .mockResolvedValueOnce(18) // sellToken decimals + .mockResolvedValueOnce(6); // buyToken decimals + + // Mock API responses + const mockPriceResponse = { + buyAmount: "1000000", // 1 USDC with 6 decimals + minBuyAmount: "990000", // 0.99 USDC with 6 decimals + totalNetworkFee: "100000000000000", // 0.0001 ETH + issues: null, + liquidityAvailable: true, + }; + + const mockQuoteResponse = { + buyAmount: "1000000", + minBuyAmount: "990000", + totalNetworkFee: "100000000000000", + transaction: { + to: "0x0000000000000000000000000000000000000001", + data: "0x12345678", + value: "1500000000000000000", // 1.5 ETH + gas: "300000", + gasPrice: "20000000000", + }, + }; + + // First fetch for price + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockPriceResponse), + }); + + // Second fetch for quote + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockQuoteResponse), + }); + + // Mock transaction functions + mockWalletProvider.sendTransaction.mockResolvedValueOnce(MOCK_TX_HASH as Hex); + mockWalletProvider.waitForTransactionReceipt.mockResolvedValueOnce({ + transactionHash: MOCK_TX_HASH, + }); + }); + + it("should execute swap successfully", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + const response = await provider.executeSwap(mockWalletProvider, args); + const parsedResponse = JSON.parse(response); + + // Verify API calls + expect(global.fetch).toHaveBeenCalledTimes(2); + expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain( + "api.0x.org/swap/permit2/price", + ); + expect((global.fetch as jest.Mock).mock.calls[1][0]).toContain( + "api.0x.org/swap/permit2/quote", + ); + + // Verify transaction was sent + expect(mockWalletProvider.sendTransaction).toHaveBeenCalledTimes(1); + expect(mockWalletProvider.waitForTransactionReceipt).toHaveBeenCalledWith(MOCK_TX_HASH); + + // Verify response formatting + expect(parsedResponse.success).toBe(true); + expect(parsedResponse.sellToken).toBe(MOCK_SELL_TOKEN); + expect(parsedResponse.buyToken).toBe(MOCK_BUY_TOKEN); + expect(parsedResponse.swapTxHash).toBe(MOCK_TX_HASH); + }); + + it("should handle price API errors", async () => { + (global.fetch as jest.Mock).mockReset(); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: "Bad Request", + text: jest.fn().mockResolvedValueOnce("Invalid request parameters"), + }); + + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + const response = await provider.executeSwap(mockWalletProvider, args); + const parsedResponse = JSON.parse(response); + + expect(parsedResponse.success).toBe(false); + expect(parsedResponse.error).toContain("Error fetching swap price"); + }); + + it("should handle no liquidity available", async () => { + (global.fetch as jest.Mock).mockReset(); + + const mockPriceResponse = { + liquidityAvailable: false, + }; + + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce(mockPriceResponse), + }); + + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + const response = await provider.executeSwap(mockWalletProvider, args); + const parsedResponse = JSON.parse(response); + + expect(parsedResponse.success).toBe(false); + expect(parsedResponse.error).toContain("No liquidity available"); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for evm networks", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm" })).toBe(true); + }); + + it("should return false for non-evm networks", () => { + expect(provider.supportsNetwork({ protocolFamily: "solana" })).toBe(false); + }); + }); + + describe("isNativeEth", () => { + it("should identify native ETH address", () => { + // Test private method through the action methods + const args = { + sellToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native ETH address + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + }; + + // We just need to mock enough to reach the method + (global.fetch as jest.Mock).mockRejectedValueOnce(new Error("Test error")); + mockWalletProvider.readContract.mockRejectedValueOnce(new Error("Test error")); + + provider.getSwapPrice(mockWalletProvider, args); + + // Native ETH should skip the decimals readContract call + expect(mockWalletProvider.readContract).not.toHaveBeenCalledWith( + expect.objectContaining({ + address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + functionName: "decimals", + }), + ); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts new file mode 100644 index 000000000..3bb88de58 --- /dev/null +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts @@ -0,0 +1,468 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { CreateAction } from "../actionDecorator"; +import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { + erc20Abi, + formatUnits, + parseUnits, + maxUint256, + encodeFunctionData, + size, + concat, + Hex, + numberToHex, +} from "viem"; + +/** + * Configuration for the ZeroXActionProvider. + */ +export interface ZeroXActionProviderConfig { + /** + * The API key to use for 0x API requests. + */ + apiKey?: string; +} + +/** + * 0x API Action Provider for token swaps. + * Requires a 0x API key. + */ +export class ZeroXActionProvider extends ActionProvider { + #apiKey: string; + + /** + * Constructor for the ZeroXActionProvider. + * + * @param config - Configuration for the provider. + */ + constructor(config: ZeroXActionProviderConfig) { + super("zerox", []); + const apiKey = config.apiKey || process.env.ZEROX_API_KEY; + if (!apiKey) { + throw new Error("0x API key not provided."); + } + this.#apiKey = apiKey; + } + + /** + * Gets a price quote for swapping one token for another. + * + * @param walletProvider - The wallet provider to get information from. + * @param args - The input arguments for the action. + * @returns A message containing the price quote. + */ + @CreateAction({ + name: "get_swap_price_quote_from_0x", + description: ` +This tool fetches a price quote for swapping between two tokens using the 0x API. + +It takes the following inputs: +- sellToken: The contract address of the token to sell +- buyToken: The contract address of the token to buy +- sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC) +- slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%) + +Important notes: +- The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +- This only fetches a price quote and does not execute a swap +- Supported on all EVM networks compatible with 0x API +- Returns detailed price information including exchange rate and fees +`, + schema: GetSwapPriceSchema, + }) + async getSwapPrice( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + const network = walletProvider.getNetwork(); + const chainId = network.chainId; + if (!chainId) throw new Error("Chain ID not available from wallet provider"); + + try { + // Determine sell token decimals + let sellTokenDecimals = 18; + if (!this.isNativeEth(args.sellToken)) { + sellTokenDecimals = (await walletProvider.readContract({ + address: args.sellToken as Hex, + abi: erc20Abi, + functionName: "decimals", + args: [], + })) as number; + } + + // Convert sell amount to base units + const sellAmount = parseUnits(args.sellAmount, sellTokenDecimals).toString(); + + // Determine buy token decimals + let buyTokenDecimals = 18; + if (!this.isNativeEth(args.buyToken)) { + buyTokenDecimals = (await walletProvider.readContract({ + address: args.buyToken as Hex, + abi: erc20Abi, + functionName: "decimals", + args: [], + })) as number; + } + + // Create URL for the price API request + const url = new URL("https://api.0x.org/swap/permit2/price"); + url.searchParams.append("chainId", chainId.toString()); + url.searchParams.append("sellToken", args.sellToken); + url.searchParams.append("buyToken", args.buyToken); + url.searchParams.append("sellAmount", sellAmount); + url.searchParams.append("taker", walletProvider.getAddress()); + url.searchParams.append("slippageBps", args.slippageBps.toString()); + + // Make the request + const response = await fetch(url.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + "0x-api-key": this.#apiKey, + "0x-version": "v2", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + return JSON.stringify({ + success: false, + error: `Error fetching swap price: ${response.status} ${response.statusText} - ${errorText}`, + }); + } + + const data = await response.json(); + + // Format the response + const formattedResponse = { + sellAmount: formatUnits(BigInt(sellAmount), sellTokenDecimals), + sellToken: args.sellToken, + buyAmount: formatUnits(data.buyAmount, buyTokenDecimals), + minBuyAmount: data.minBuyAmount ? formatUnits(data.minBuyAmount, buyTokenDecimals) : null, + buyToken: args.buyToken, + totalNetworkFeeInETH: data.totalNetworkFee ? formatUnits(data.totalNetworkFee, 18) : null, + issues: data.issues || null, + liquidityAvailable: data.liquidityAvailable, + priceOfBuyTokenInSellToken: ( + Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) / + Number(formatUnits(data.buyAmount, buyTokenDecimals)) + ).toString(), + priceOfSellTokenInBuyToken: ( + Number(formatUnits(data.buyAmount, buyTokenDecimals)) / + Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) + ).toString(), + }; + + return JSON.stringify(formattedResponse); + } catch (error) { + return JSON.stringify({ + success: false, + error: `Error fetching swap price: ${error}`, + }); + } + } + + /** + * Executes a token swap using the 0x API. + * + * @param walletProvider - The wallet provider to use for the swap. + * @param args - The input arguments for the action. + * @returns A message containing the result of the swap. + */ + @CreateAction({ + name: "execute_swap_on_0x", + description: ` +This tool executes a token swap between two tokens using the 0x API. + +It takes the following inputs: +- sellToken: The contract address of the token to sell +- buyToken: The contract address of the token to buy +- sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC) +- slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%) + +Important notes: +- The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +- This will execute an actual swap transaction that sends tokens from your wallet +- If needed, it will automatically approve the permit2 contract to spend the sell token +- The approval transaction is only needed once per token +- Ensure you have sufficient balance of the sell token before executing +- The trade size might influence the excecution price depending on available liquidity +- First fetch a price quote and only execute swap if you are happy with the indicated price +- Supported on all EVM networks compatible with 0x API +`, + schema: ExecuteSwapSchema, + }) + async executeSwap( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + const network = walletProvider.getNetwork(); + const chainId = network.chainId; + if (!chainId) throw new Error("Chain ID not available from wallet provider"); + + // Check price impact + + try { + // Determine sell token decimals + let sellTokenDecimals = 18; + if (!this.isNativeEth(args.sellToken)) { + sellTokenDecimals = (await walletProvider.readContract({ + address: args.sellToken as Hex, + abi: erc20Abi, + functionName: "decimals", + args: [], + })) as number; + } + + // Convert sell amount to base units + const sellAmount = parseUnits(args.sellAmount, sellTokenDecimals).toString(); + + // Determine buy token decimals + let buyTokenDecimals = 18; + if (!this.isNativeEth(args.buyToken)) { + buyTokenDecimals = (await walletProvider.readContract({ + address: args.buyToken as Hex, + abi: erc20Abi, + functionName: "decimals", + args: [], + })) as number; + } + + // Get the wallet address + const walletAddress = walletProvider.getAddress(); + + // Fetch price quote first + const priceUrl = new URL("https://api.0x.org/swap/permit2/price"); + priceUrl.searchParams.append("chainId", chainId.toString()); + priceUrl.searchParams.append("sellToken", args.sellToken); + priceUrl.searchParams.append("buyToken", args.buyToken); + priceUrl.searchParams.append("sellAmount", sellAmount); + priceUrl.searchParams.append("taker", walletAddress); + priceUrl.searchParams.append("slippageBps", args.slippageBps.toString()); + + const priceResponse = await fetch(priceUrl.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + "0x-api-key": this.#apiKey, + "0x-version": "v2", + }, + }); + + if (!priceResponse.ok) { + const errorText = await priceResponse.text(); + return JSON.stringify({ + success: false, + error: `Error fetching swap price: ${priceResponse.status} ${priceResponse.statusText} - ${errorText}`, + }); + } + + const priceData = await priceResponse.json(); + + // Check if liquidity is available + if (priceData.liquidityAvailable === false) { + return JSON.stringify({ + success: false, + error: "No liquidity available for this swap.", + }); + } + + // Check if balance of sell token is enough + if (priceData.balance != null) { + return JSON.stringify({ + success: false, + error: `Insufficient balance of sell token ${priceData.balance.token}. Requested to swap ${priceData.balance.expected}, but balance is only ${priceData.balance.actual}.`, + }); + } + + // Check if permit2 approval is needed for ERC20 tokens + // Only needed once per token per address + let approvalTxHash: Hex | null = null; + if (!this.isNativeEth(args.sellToken) && priceData.issues?.allowance) { + try { + // Get token approval data + const spender = priceData.issues.allowance.spender as Hex; // permit2 contract address + + approvalTxHash = await walletProvider.sendTransaction({ + to: args.sellToken as Hex, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, maxUint256], + }), + }); + + await walletProvider.waitForTransactionReceipt(approvalTxHash); + } catch (error) { + return JSON.stringify({ + success: false, + error: `Error approving token: ${error}`, + }); + } + } + + // Fetch the swap quote + const quoteUrl = new URL("https://api.0x.org/swap/permit2/quote"); + quoteUrl.searchParams.append("chainId", chainId.toString()); + quoteUrl.searchParams.append("sellToken", args.sellToken); + quoteUrl.searchParams.append("buyToken", args.buyToken); + quoteUrl.searchParams.append("sellAmount", sellAmount); + quoteUrl.searchParams.append("taker", walletAddress); + quoteUrl.searchParams.append("slippageBps", args.slippageBps.toString()); + + const quoteResponse = await fetch(quoteUrl.toString(), { + method: "GET", + headers: { + "Content-Type": "application/json", + "0x-api-key": this.#apiKey, + "0x-version": "v2", + }, + }); + + if (!quoteResponse.ok) { + const errorText = await quoteResponse.text(); + return JSON.stringify({ + success: false, + error: `Error fetching swap quote: ${quoteResponse.status} ${quoteResponse.statusText} - ${errorText}`, + }); + } + + const quoteData = await quoteResponse.json(); + + // Sign Permit2.eip712 returned from quote + let signature: Hex | undefined; + if (quoteData.permit2?.eip712) { + try { + // Create a new types object without EIP712Domain + const types = { ...quoteData.permit2.eip712.types }; + delete types.EIP712Domain; + + // Create correctly structured typedData object + const typedData = { + domain: quoteData.permit2.eip712.domain, + types: types, + primaryType: quoteData.permit2.eip712.primaryType, + message: quoteData.permit2.eip712.message, + } as const; + + signature = await walletProvider.signTypedData(typedData); + + // Append sig length and sig data to transaction.data + if (signature && quoteData.transaction?.data) { + const signatureLengthInHex = numberToHex(size(signature), { + signed: false, + size: 32, + }); + + const transactionData = quoteData.transaction.data as Hex; + const sigLengthHex = signatureLengthInHex as Hex; + const sig = signature as Hex; + + quoteData.transaction.data = concat([transactionData, sigLengthHex, sig]); + } + } catch (error) { + return JSON.stringify({ + success: false, + error: `Error signing permit2 message: ${error}`, + }); + } + } + + // Execute swap + try { + // Prepare transaction parameters + const txParams: { + to: Hex; + data: Hex; + gas?: bigint; + gasPrice?: bigint; + value?: bigint; + } = { + to: quoteData.transaction.to as Hex, + data: quoteData.transaction.data as Hex, + gas: quoteData?.transaction.gas ? BigInt(quoteData.transaction.gas) : undefined, + gasPrice: quoteData?.transaction.gasPrice + ? BigInt(quoteData.transaction.gasPrice) + : undefined, + }; + + // Add value parameter only for selling native tokens + if (this.isNativeEth(args.sellToken)) { + txParams.value = BigInt(quoteData.transaction.value || 0); + } + + // Send transaction + const txHash = await walletProvider.sendTransaction(txParams); + const receipt = await walletProvider.waitForTransactionReceipt(txHash); + + // Format the response + const formattedResponse = { + success: true, + sellAmount: formatUnits(BigInt(sellAmount), sellTokenDecimals), + sellToken: args.sellToken, + buyAmount: formatUnits(quoteData.buyAmount, buyTokenDecimals), + minBuyAmount: quoteData.minBuyAmount + ? formatUnits(quoteData.minBuyAmount, buyTokenDecimals) + : null, + buyToken: args.buyToken, + totalNetworkFeeInETH: quoteData.totalNetworkFee + ? formatUnits(quoteData.totalNetworkFee, 18) + : null, + priceOfBuyTokenInSellToken: ( + Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) / + Number(formatUnits(quoteData.buyAmount, buyTokenDecimals)) + ).toString(), + priceOfSellTokenInBuyToken: ( + Number(formatUnits(quoteData.buyAmount, buyTokenDecimals)) / + Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) + ).toString(), + permit2ApprovalTxHash: approvalTxHash, + swapTxHash: receipt.transactionHash, + }; + + return JSON.stringify(formattedResponse); + } catch (error) { + return JSON.stringify({ + success: false, + error: `Error sending swap transaction: ${error}`, + approvalTxHash: approvalTxHash, + }); + } + } catch (error) { + return JSON.stringify({ + success: false, + error: `Error executing swap: ${error}`, + }); + } + } + + /** + * Checks if the ZeroX action provider supports the given network. + * + * @param network - The network to check. + * @returns True if the ZeroX action provider supports the network, false otherwise. + */ + supportsNetwork = (network: Network) => network.protocolFamily === "evm"; + + /** + * Checks if a token is native ETH. + * + * @param token - The token address to check. + * @returns True if the token is native ETH, false otherwise. + */ + private isNativeEth(token: string): boolean { + return token.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + } +} + +/** + * Creates a new ZeroXActionProvider with the provided configuration. + * + * @param config - Optional configuration for the provider. + * @returns A new ZeroXActionProvider. + */ +export const zeroXActionProvider = (config: ZeroXActionProviderConfig) => + new ZeroXActionProvider(config); From f63c22a2bae75e056021c6442ab584aaac001967 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Wed, 20 Aug 2025 21:12:21 +0200 Subject: [PATCH 2/5] fix signature for cdpwallet --- typescript/.changeset/whole-times-fetch.md | 2 +- .../src/action-providers/zeroX/utils.ts | 130 ++++++++++++++++ .../zeroX/zeroXActionProvider.test.ts | 74 ++++----- .../zeroX/zeroXActionProvider.ts | 142 ++++++------------ 4 files changed, 218 insertions(+), 130 deletions(-) create mode 100644 typescript/agentkit/src/action-providers/zeroX/utils.ts diff --git a/typescript/.changeset/whole-times-fetch.md b/typescript/.changeset/whole-times-fetch.md index 053423a02..63bb04da8 100644 --- a/typescript/.changeset/whole-times-fetch.md +++ b/typescript/.changeset/whole-times-fetch.md @@ -1,5 +1,5 @@ --- -"@coinbase/agentkit": minor +"@coinbase/agentkit": patch --- Added ZeroX Action Provider to enable token swaps using the 0x Protocol API diff --git a/typescript/agentkit/src/action-providers/zeroX/utils.ts b/typescript/agentkit/src/action-providers/zeroX/utils.ts new file mode 100644 index 000000000..4d9eee7d5 --- /dev/null +++ b/typescript/agentkit/src/action-providers/zeroX/utils.ts @@ -0,0 +1,130 @@ +import { Hex, erc20Abi } from "viem"; +import { EvmWalletProvider } from "../../wallet-providers"; + +// Permit2 contract address is the same across all networks +export const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; + +/** + * Checks if a token is native ETH. + * + * @param token - The token address to check. + * @returns True if the token is native ETH, false otherwise. + */ +export function isNativeEth(token: string): boolean { + return token.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; +} + +/** + * Gets the details (decimals and name) for both fromToken and toToken + * + * @param walletProvider - The EVM wallet provider to read contracts + * @param fromToken - The contract address of the from token + * @param toToken - The contract address of the to token + * @returns Promise<{fromTokenDecimals: number, toTokenDecimals: number, fromTokenName: string, toTokenName: string}> + */ +export async function getTokenDetails( + walletProvider: EvmWalletProvider, + fromToken: string, + toToken: string, +): Promise<{ + fromTokenDecimals: number; + toTokenDecimals: number; + fromTokenName: string; + toTokenName: string; +}> { + // Initialize default values for native ETH + let fromTokenDecimals = 18; + let fromTokenName = "ETH"; + let toTokenDecimals = 18; + let toTokenName = "ETH"; + + // Prepare multicall contracts array + const contracts: { + address: Hex; + abi: typeof erc20Abi; + functionName: "decimals" | "name"; + }[] = []; + const contractIndexMap = { + fromDecimals: -1, + fromName: -1, + toDecimals: -1, + toName: -1, + }; + + // Add from token contracts if not native ETH + if (!isNativeEth(fromToken)) { + contractIndexMap.fromDecimals = contracts.length; + contracts.push({ + address: fromToken as Hex, + abi: erc20Abi, + functionName: "decimals", + }); + + contractIndexMap.fromName = contracts.length; + contracts.push({ + address: fromToken as Hex, + abi: erc20Abi, + functionName: "name", + }); + } + + // Add to token contracts if not native ETH + if (!isNativeEth(toToken)) { + contractIndexMap.toDecimals = contracts.length; + contracts.push({ + address: toToken as Hex, + abi: erc20Abi, + functionName: "decimals", + }); + + contractIndexMap.toName = contracts.length; + contracts.push({ + address: toToken as Hex, + abi: erc20Abi, + functionName: "name", + }); + } + + // Execute multicall if there are contracts to call + if (contracts.length > 0) { + try { + const results = await walletProvider.getPublicClient().multicall({ + contracts, + }); + + // Extract from token details + if (contractIndexMap.fromDecimals !== -1) { + const decimalsResult = results[contractIndexMap.fromDecimals]; + const nameResult = results[contractIndexMap.fromName]; + + if (decimalsResult.status === "success" && nameResult.status === "success") { + fromTokenDecimals = decimalsResult.result as number; + fromTokenName = nameResult.result as string; + } else { + throw new Error( + `Failed to read details for fromToken ${fromToken}. This address may not be a valid ERC20 contract.`, + ); + } + } + + // Extract to token details + if (contractIndexMap.toDecimals !== -1) { + const decimalsResult = results[contractIndexMap.toDecimals]; + const nameResult = results[contractIndexMap.toName]; + + if (decimalsResult.status === "success" && nameResult.status === "success") { + toTokenDecimals = decimalsResult.result as number; + toTokenName = nameResult.result as string; + } else { + throw new Error( + `Failed to read details for toToken ${toToken}. This address may not be a valid ERC20 contract.`, + ); + } + } + } catch (error) { + throw new Error(`Failed to read token details via multicall. Error: ${error}`); + } + } + + return { fromTokenDecimals, toTokenDecimals, fromTokenName, toTokenName }; +} diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts index 936a030ed..576208793 100644 --- a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts @@ -76,8 +76,12 @@ describe("ZeroX Action Provider", () => { getNetwork: jest.fn().mockReturnValue({ chainId: MOCK_CHAIN_ID, protocolFamily: "evm", + networkId: "ethereum-mainnet", }), readContract: jest.fn(), + getPublicClient: jest.fn().mockReturnValue({ + multicall: jest.fn(), + }), sendTransaction: jest.fn(), waitForTransactionReceipt: jest.fn(), signTypedData: jest.fn(), @@ -89,17 +93,24 @@ describe("ZeroX Action Provider", () => { describe("getSwapPrice", () => { beforeEach(() => { - // Mock readContract for decimals - mockWalletProvider.readContract - .mockResolvedValueOnce(18) // sellToken decimals - .mockResolvedValueOnce(6); // buyToken decimals + // Mock multicall for token details (decimals and names) + const mockMulticallResults = [ + { status: "success", result: 18 }, // sellToken decimals + { status: "success", result: "TEST" }, // sellToken name + { status: "success", result: 6 }, // buyToken decimals + { status: "success", result: "USDC" }, // buyToken name + ]; + + (mockWalletProvider.getPublicClient().multicall as jest.Mock).mockResolvedValue( + mockMulticallResults, + ); // Mock fetch for price API const mockPriceResponse = { buyAmount: "1000000", // 1 USDC with 6 decimals minBuyAmount: "990000", // 0.99 USDC with 6 decimals totalNetworkFee: "100000000000000", // 0.0001 ETH - issues: null, + issues: { balance: null }, liquidityAvailable: true, }; @@ -132,9 +143,14 @@ describe("ZeroX Action Provider", () => { expect((global.fetch as jest.Mock).mock.calls[0][0]).toContain(`buyToken=${MOCK_BUY_TOKEN}`); // Verify response formatting + expect(parsedResponse.success).toBe(true); expect(parsedResponse.sellToken).toBe(MOCK_SELL_TOKEN); + expect(parsedResponse.sellTokenName).toBe("TEST"); expect(parsedResponse.buyToken).toBe(MOCK_BUY_TOKEN); + expect(parsedResponse.buyTokenName).toBe("USDC"); expect(parsedResponse.liquidityAvailable).toBe(true); + expect(parsedResponse.balanceEnough).toBe(true); + expect(parsedResponse.slippageBps).toBe(50); expect(parsedResponse.buyAmount).toBeDefined(); expect(parsedResponse.minBuyAmount).toBeDefined(); }); @@ -185,10 +201,17 @@ describe("ZeroX Action Provider", () => { const MOCK_TX_HASH = "0xtxhash123456"; beforeEach(() => { - // Mock readContract for decimals - mockWalletProvider.readContract - .mockResolvedValueOnce(18) // sellToken decimals - .mockResolvedValueOnce(6); // buyToken decimals + // Mock multicall for token details (decimals and names) + const mockMulticallResults = [ + { status: "success", result: 18 }, // sellToken decimals + { status: "success", result: "TEST" }, // sellToken name + { status: "success", result: 6 }, // buyToken decimals + { status: "success", result: "USDC" }, // buyToken name + ]; + + (mockWalletProvider.getPublicClient().multicall as jest.Mock).mockResolvedValue( + mockMulticallResults, + ); // Mock API responses const mockPriceResponse = { @@ -228,6 +251,7 @@ describe("ZeroX Action Provider", () => { mockWalletProvider.sendTransaction.mockResolvedValueOnce(MOCK_TX_HASH as Hex); mockWalletProvider.waitForTransactionReceipt.mockResolvedValueOnce({ transactionHash: MOCK_TX_HASH, + status: "success", }); }); @@ -258,8 +282,12 @@ describe("ZeroX Action Provider", () => { // Verify response formatting expect(parsedResponse.success).toBe(true); expect(parsedResponse.sellToken).toBe(MOCK_SELL_TOKEN); + expect(parsedResponse.sellTokenName).toBe("TEST"); expect(parsedResponse.buyToken).toBe(MOCK_BUY_TOKEN); - expect(parsedResponse.swapTxHash).toBe(MOCK_TX_HASH); + expect(parsedResponse.buyTokenName).toBe("USDC"); + expect(parsedResponse.transactionHash).toBe(MOCK_TX_HASH); + expect(parsedResponse.slippageBps).toBe(50); + expect(parsedResponse.network).toBe("ethereum-mainnet"); }); it("should handle price API errors", async () => { @@ -321,30 +349,4 @@ describe("ZeroX Action Provider", () => { expect(provider.supportsNetwork({ protocolFamily: "solana" })).toBe(false); }); }); - - describe("isNativeEth", () => { - it("should identify native ETH address", () => { - // Test private method through the action methods - const args = { - sellToken: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native ETH address - buyToken: MOCK_BUY_TOKEN, - sellAmount: MOCK_SELL_AMOUNT, - slippageBps: 50, - }; - - // We just need to mock enough to reach the method - (global.fetch as jest.Mock).mockRejectedValueOnce(new Error("Test error")); - mockWalletProvider.readContract.mockRejectedValueOnce(new Error("Test error")); - - provider.getSwapPrice(mockWalletProvider, args); - - // Native ETH should skip the decimals readContract call - expect(mockWalletProvider.readContract).not.toHaveBeenCalledWith( - expect.objectContaining({ - address: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - functionName: "decimals", - }), - ); - }); - }); }); diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts index 3bb88de58..1ddbf6a46 100644 --- a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts @@ -3,7 +3,7 @@ import { ActionProvider } from "../actionProvider"; import { Network } from "../../network"; import { CreateAction } from "../actionDecorator"; import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas"; -import { EvmWalletProvider } from "../../wallet-providers"; +import { CdpSmartWalletProvider, EvmWalletProvider } from "../../wallet-providers"; import { erc20Abi, formatUnits, @@ -15,7 +15,7 @@ import { Hex, numberToHex, } from "viem"; - +import { getTokenDetails, PERMIT2_ADDRESS } from "./utils"; /** * Configuration for the ZeroXActionProvider. */ @@ -69,7 +69,7 @@ Important notes: - The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - This only fetches a price quote and does not execute a swap - Supported on all EVM networks compatible with 0x API -- Returns detailed price information including exchange rate and fees +- Use sellToken units exactly as provided, do not convert to wei or any other units `, schema: GetSwapPriceSchema, }) @@ -82,31 +82,17 @@ Important notes: if (!chainId) throw new Error("Chain ID not available from wallet provider"); try { - // Determine sell token decimals - let sellTokenDecimals = 18; - if (!this.isNativeEth(args.sellToken)) { - sellTokenDecimals = (await walletProvider.readContract({ - address: args.sellToken as Hex, - abi: erc20Abi, - functionName: "decimals", - args: [], - })) as number; - } + // Get token details + const { + fromTokenDecimals: sellTokenDecimals, + toTokenDecimals: buyTokenDecimals, + fromTokenName: sellTokenName, + toTokenName: buyTokenName, + } = await getTokenDetails(walletProvider, args.sellToken, args.buyToken); // Convert sell amount to base units const sellAmount = parseUnits(args.sellAmount, sellTokenDecimals).toString(); - // Determine buy token decimals - let buyTokenDecimals = 18; - if (!this.isNativeEth(args.buyToken)) { - buyTokenDecimals = (await walletProvider.readContract({ - address: args.buyToken as Hex, - abi: erc20Abi, - functionName: "decimals", - args: [], - })) as number; - } - // Create URL for the price API request const url = new URL("https://api.0x.org/swap/permit2/price"); url.searchParams.append("chainId", chainId.toString()); @@ -138,14 +124,17 @@ Important notes: // Format the response const formattedResponse = { + success: true, sellAmount: formatUnits(BigInt(sellAmount), sellTokenDecimals), + sellTokenName: sellTokenName, sellToken: args.sellToken, buyAmount: formatUnits(data.buyAmount, buyTokenDecimals), minBuyAmount: data.minBuyAmount ? formatUnits(data.minBuyAmount, buyTokenDecimals) : null, + buyTokenName: buyTokenName, buyToken: args.buyToken, - totalNetworkFeeInETH: data.totalNetworkFee ? formatUnits(data.totalNetworkFee, 18) : null, - issues: data.issues || null, + slippageBps: args.slippageBps, liquidityAvailable: data.liquidityAvailable, + balanceEnough: data.issues?.balance === null, priceOfBuyTokenInSellToken: ( Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) / Number(formatUnits(data.buyAmount, buyTokenDecimals)) @@ -192,6 +181,7 @@ Important notes: - The trade size might influence the excecution price depending on available liquidity - First fetch a price quote and only execute swap if you are happy with the indicated price - Supported on all EVM networks compatible with 0x API +- Use sellToken units exactly as provided, do not convert to wei or any other units `, schema: ExecuteSwapSchema, }) @@ -199,38 +189,29 @@ Important notes: walletProvider: EvmWalletProvider, args: z.infer, ): Promise { + // Sanity checks const network = walletProvider.getNetwork(); const chainId = network.chainId; if (!chainId) throw new Error("Chain ID not available from wallet provider"); - // Check price impact + if (walletProvider instanceof CdpSmartWalletProvider) { + throw new Error( + "CdpSmartWalletProvider is currently not supported for 0x swaps, use swap action from CdpApiActionProvider instead", + ); + } try { - // Determine sell token decimals - let sellTokenDecimals = 18; - if (!this.isNativeEth(args.sellToken)) { - sellTokenDecimals = (await walletProvider.readContract({ - address: args.sellToken as Hex, - abi: erc20Abi, - functionName: "decimals", - args: [], - })) as number; - } + // Get token details + const { + fromTokenDecimals: sellTokenDecimals, + toTokenDecimals: buyTokenDecimals, + fromTokenName: sellTokenName, + toTokenName: buyTokenName, + } = await getTokenDetails(walletProvider, args.sellToken, args.buyToken); // Convert sell amount to base units const sellAmount = parseUnits(args.sellAmount, sellTokenDecimals).toString(); - // Determine buy token decimals - let buyTokenDecimals = 18; - if (!this.isNativeEth(args.buyToken)) { - buyTokenDecimals = (await walletProvider.readContract({ - address: args.buyToken as Hex, - abi: erc20Abi, - functionName: "decimals", - args: [], - })) as number; - } - // Get the wallet address const walletAddress = walletProvider.getAddress(); @@ -281,17 +262,14 @@ Important notes: // Check if permit2 approval is needed for ERC20 tokens // Only needed once per token per address let approvalTxHash: Hex | null = null; - if (!this.isNativeEth(args.sellToken) && priceData.issues?.allowance) { + if (priceData.issues?.allowance) { try { - // Get token approval data - const spender = priceData.issues.allowance.spender as Hex; // permit2 contract address - approvalTxHash = await walletProvider.sendTransaction({ to: args.sellToken as Hex, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", - args: [spender, maxUint256], + args: [PERMIT2_ADDRESS, maxUint256], }), }); @@ -336,14 +314,9 @@ Important notes: let signature: Hex | undefined; if (quoteData.permit2?.eip712) { try { - // Create a new types object without EIP712Domain - const types = { ...quoteData.permit2.eip712.types }; - delete types.EIP712Domain; - - // Create correctly structured typedData object const typedData = { domain: quoteData.permit2.eip712.domain, - types: types, + types: quoteData.permit2.eip712.types, primaryType: quoteData.permit2.eip712.primaryType, message: quoteData.permit2.eip712.message, } as const; @@ -378,49 +351,42 @@ Important notes: to: Hex; data: Hex; gas?: bigint; - gasPrice?: bigint; value?: bigint; } = { to: quoteData.transaction.to as Hex, data: quoteData.transaction.data as Hex, - gas: quoteData?.transaction.gas ? BigInt(quoteData.transaction.gas) : undefined, - gasPrice: quoteData?.transaction.gasPrice - ? BigInt(quoteData.transaction.gasPrice) - : undefined, + ...(quoteData?.transaction.gas ? { gas: BigInt(quoteData.transaction.gas) } : {}), + ...(quoteData.transaction.value ? { value: BigInt(quoteData.transaction.value) } : {}), }; - // Add value parameter only for selling native tokens - if (this.isNativeEth(args.sellToken)) { - txParams.value = BigInt(quoteData.transaction.value || 0); - } - // Send transaction const txHash = await walletProvider.sendTransaction(txParams); const receipt = await walletProvider.waitForTransactionReceipt(txHash); + if (receipt.status !== "complete" && receipt.status !== "success") { + return JSON.stringify({ + success: false, + ...(approvalTxHash ? { approvalTxHash } : {}), + transactionHash: receipt.transactionHash, + error: `Swap transaction failed`, + }); + } // Format the response const formattedResponse = { success: true, + ...(approvalTxHash ? { approvalTxHash } : {}), + transactionHash: receipt.transactionHash, sellAmount: formatUnits(BigInt(sellAmount), sellTokenDecimals), + sellTokenName: sellTokenName, sellToken: args.sellToken, buyAmount: formatUnits(quoteData.buyAmount, buyTokenDecimals), minBuyAmount: quoteData.minBuyAmount ? formatUnits(quoteData.minBuyAmount, buyTokenDecimals) : null, + buyTokenName: buyTokenName, buyToken: args.buyToken, - totalNetworkFeeInETH: quoteData.totalNetworkFee - ? formatUnits(quoteData.totalNetworkFee, 18) - : null, - priceOfBuyTokenInSellToken: ( - Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) / - Number(formatUnits(quoteData.buyAmount, buyTokenDecimals)) - ).toString(), - priceOfSellTokenInBuyToken: ( - Number(formatUnits(quoteData.buyAmount, buyTokenDecimals)) / - Number(formatUnits(BigInt(sellAmount), sellTokenDecimals)) - ).toString(), - permit2ApprovalTxHash: approvalTxHash, - swapTxHash: receipt.transactionHash, + slippageBps: args.slippageBps, + network: network.networkId, }; return JSON.stringify(formattedResponse); @@ -428,7 +394,7 @@ Important notes: return JSON.stringify({ success: false, error: `Error sending swap transaction: ${error}`, - approvalTxHash: approvalTxHash, + ...(approvalTxHash ? { approvalTxHash } : {}), }); } } catch (error) { @@ -446,16 +412,6 @@ Important notes: * @returns True if the ZeroX action provider supports the network, false otherwise. */ supportsNetwork = (network: Network) => network.protocolFamily === "evm"; - - /** - * Checks if a token is native ETH. - * - * @param token - The token address to check. - * @returns True if the token is native ETH, false otherwise. - */ - private isNativeEth(token: string): boolean { - return token.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; - } } /** From be492c3c5844838e32976e43fb95d9d7d2d50a70 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Wed, 20 Aug 2025 21:27:37 +0200 Subject: [PATCH 3/5] fix signature for LegacyCdpWalletProvider --- .../zeroX/zeroXActionProvider.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts index 1ddbf6a46..03cde288a 100644 --- a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts @@ -3,7 +3,11 @@ import { ActionProvider } from "../actionProvider"; import { Network } from "../../network"; import { CreateAction } from "../actionDecorator"; import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas"; -import { CdpSmartWalletProvider, EvmWalletProvider } from "../../wallet-providers"; +import { + CdpSmartWalletProvider, + EvmWalletProvider, + LegacyCdpWalletProvider, +} from "../../wallet-providers"; import { erc20Abi, formatUnits, @@ -314,9 +318,19 @@ Important notes: let signature: Hex | undefined; if (quoteData.permit2?.eip712) { try { + // For LegacyCdpWalletProvider, remove EIP712Domain to avoid ambiguous primary types + const types = + walletProvider instanceof LegacyCdpWalletProvider + ? (() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { EIP712Domain, ...rest } = quoteData.permit2.eip712.types; + return rest; + })() + : quoteData.permit2.eip712.types; + const typedData = { domain: quoteData.permit2.eip712.domain, - types: quoteData.permit2.eip712.types, + types, primaryType: quoteData.permit2.eip712.primaryType, message: quoteData.permit2.eip712.message, } as const; From 3e29e0ab9c637666b8cc21682ffd3e4a27526207 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Fri, 22 Aug 2025 01:29:56 +0200 Subject: [PATCH 4/5] add optional swap fee --- .../src/action-providers/zeroX/README.md | 4 + .../src/action-providers/zeroX/schemas.ts | 28 +++ .../zeroX/zeroXActionProvider.test.ts | 190 ++++++++++++++++++ .../zeroX/zeroXActionProvider.ts | 19 ++ 4 files changed, 241 insertions(+) diff --git a/typescript/agentkit/src/action-providers/zeroX/README.md b/typescript/agentkit/src/action-providers/zeroX/README.md index a60de91aa..7abee511c 100644 --- a/typescript/agentkit/src/action-providers/zeroX/README.md +++ b/typescript/agentkit/src/action-providers/zeroX/README.md @@ -18,11 +18,13 @@ zeroX/ - `get_swap_price_quote_from_0x`: Get a price quote for swapping tokens - Returns detailed price information including exchange rate, fees, and minimum buy amount - Does not execute any transactions + - Supports optional affiliate fee collection - `execute_swap_on_0x`: Execute a token swap between two tokens - Handles token approvals automatically if needed - Executes the swap transaction - Returns the transaction hash and swap details upon success + - Supports optional affiliate fee collection ## Adding New Actions @@ -58,5 +60,7 @@ ZEROX_API_KEY=your-0x-api-key - All token amounts should be specified in whole units (e.g., "1.5" ETH, not wei) - Slippage setting is optional and defaults to 1% (100 basis points) if not specified - Always check price quotes before executing swaps to ensure favorable rates +- For affiliate fees: provide swapFeeRecipient to enable fees (swapFeeBps defaults to 1%) +- Affiliate fees are automatically collected from the sellToken and sent to the specified recipient address For more information on the **0x Protocol**, visit [0x Protocol Documentation](https://docs.0x.org/). \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/zeroX/schemas.ts b/typescript/agentkit/src/action-providers/zeroX/schemas.ts index fe2640449..a55013e4e 100644 --- a/typescript/agentkit/src/action-providers/zeroX/schemas.ts +++ b/typescript/agentkit/src/action-providers/zeroX/schemas.ts @@ -24,6 +24,20 @@ export const GetSwapPriceSchema = z .optional() .default(100) .describe("The maximum acceptable slippage in basis points (0-10000, default: 100)"), + swapFeeRecipient: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .optional() + .describe("The wallet address to receive an affiliate fee on the trade"), + swapFeeBps: z + .number() + .int() + .min(0) + .max(1000) + .default(100) + .describe( + "The amount in basis points (0-1000) of the sellToken to charge as trading fee (defaults to 100 = 1%), only used if swapFeeRecipient is provided", + ), }) .strip() .describe("Get a price quote for swapping one token for another"); @@ -52,6 +66,20 @@ export const ExecuteSwapSchema = z .optional() .default(100) .describe("The maximum acceptable slippage in basis points (0-10000, default: 100)"), + swapFeeRecipient: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address format") + .optional() + .describe("The wallet address to receive an affiliate fee on the trade"), + swapFeeBps: z + .number() + .int() + .min(0) + .max(1000) + .default(100) + .describe( + "The amount in basis points (0-1000) of the sellToken to charge as trading fee (defaults to 100 = 1%), only used if swapFeeRecipient is provided", + ), }) .strip() .describe("Execute a swap between two tokens"); diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts index 576208793..4b4e231cf 100644 --- a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.test.ts @@ -45,6 +45,48 @@ describe("ZeroX Schema Validation", () => { } }); + it("should validate swap fee parameters when both provided", () => { + const inputWithSwapFees = { + sellToken: "0x1234567890123456789012345678901234567890", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + swapFeeBps: 50, + }; + + const result = GetSwapPriceSchema.safeParse(inputWithSwapFees); + expect(result.success).toBe(true); + }); + + it("should validate when only swapFeeRecipient provided (swapFeeBps defaults to 100)", () => { + const inputWithOnlyRecipient = { + sellToken: "0x1234567890123456789012345678901234567890", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + swapFeeBps: 100, + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + }; + + const result = GetSwapPriceSchema.safeParse(inputWithOnlyRecipient); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.swapFeeBps).toBe(100); // Default value + } + }); + + it("should fail validation when swapFeeBps exceeds maximum", () => { + const inputWithInvalidSwapFeeBps = { + sellToken: "0x1234567890123456789012345678901234567890", + buyToken: "0x0987654321098765432109876543210987654321", + sellAmount: "1.5", + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + swapFeeBps: 1500, // Exceeds maximum of 1000 + }; + + const result = GetSwapPriceSchema.safeParse(inputWithInvalidSwapFeeBps); + expect(result.success).toBe(false); + }); + it("should validate ExecuteSwap schema with valid input", () => { const validInput = { sellToken: "0x1234567890123456789012345678901234567890", @@ -126,6 +168,7 @@ describe("ZeroX Action Provider", () => { buyToken: MOCK_BUY_TOKEN, sellAmount: MOCK_SELL_AMOUNT, slippageBps: 50, + swapFeeBps: 100, }; const response = await provider.getSwapPrice(mockWalletProvider, args); @@ -169,6 +212,7 @@ describe("ZeroX Action Provider", () => { buyToken: MOCK_BUY_TOKEN, sellAmount: MOCK_SELL_AMOUNT, slippageBps: 50, + swapFeeBps: 100, }; const response = await provider.getSwapPrice(mockWalletProvider, args); @@ -187,6 +231,7 @@ describe("ZeroX Action Provider", () => { buyToken: MOCK_BUY_TOKEN, sellAmount: MOCK_SELL_AMOUNT, slippageBps: 50, + swapFeeBps: 100, }; const response = await provider.getSwapPrice(mockWalletProvider, args); @@ -195,6 +240,65 @@ describe("ZeroX Action Provider", () => { expect(parsedResponse.success).toBe(false); expect(parsedResponse.error).toContain("Error fetching swap price"); }); + + it("should include swap fee parameters in API call when provided", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + swapFeeBps: 100, + }; + + await provider.getSwapPrice(mockWalletProvider, args); + + // Verify fetch was called with swap fee parameters + expect(global.fetch).toHaveBeenCalledTimes(1); + const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(fetchUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12"); + expect(fetchUrl).toContain("swapFeeBps=100"); + expect(fetchUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`); + }); + + it("should not include swap fee parameters when not provided", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + swapFeeBps: 100, + }; + + await provider.getSwapPrice(mockWalletProvider, args); + + // Verify fetch was called without swap fee parameters + expect(global.fetch).toHaveBeenCalledTimes(1); + const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(fetchUrl).not.toContain("swapFeeRecipient"); + expect(fetchUrl).not.toContain("swapFeeBps"); + expect(fetchUrl).not.toContain("swapFeeToken"); + }); + + it("should include swap fee parameters with default swapFeeBps when only recipient provided", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + swapFeeBps: 100, + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + }; + + await provider.getSwapPrice(mockWalletProvider, args); + + // Verify fetch was called with swap fee parameters including default swapFeeBps + expect(global.fetch).toHaveBeenCalledTimes(1); + const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(fetchUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12"); + expect(fetchUrl).toContain("swapFeeBps=100"); // Default value + expect(fetchUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`); + }); }); describe("executeSwap", () => { @@ -261,6 +365,7 @@ describe("ZeroX Action Provider", () => { buyToken: MOCK_BUY_TOKEN, sellAmount: MOCK_SELL_AMOUNT, slippageBps: 50, + swapFeeBps: 100, }; const response = await provider.executeSwap(mockWalletProvider, args); @@ -304,6 +409,7 @@ describe("ZeroX Action Provider", () => { buyToken: MOCK_BUY_TOKEN, sellAmount: MOCK_SELL_AMOUNT, slippageBps: 50, + swapFeeBps: 100, }; const response = await provider.executeSwap(mockWalletProvider, args); @@ -330,6 +436,7 @@ describe("ZeroX Action Provider", () => { buyToken: MOCK_BUY_TOKEN, sellAmount: MOCK_SELL_AMOUNT, slippageBps: 50, + swapFeeBps: 100, }; const response = await provider.executeSwap(mockWalletProvider, args); @@ -338,6 +445,89 @@ describe("ZeroX Action Provider", () => { expect(parsedResponse.success).toBe(false); expect(parsedResponse.error).toContain("No liquidity available"); }); + + it("should include swap fee parameters in both API calls when provided", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + swapFeeBps: 100, + }; + + await provider.executeSwap(mockWalletProvider, args); + + // Verify both API calls include swap fee parameters + expect(global.fetch).toHaveBeenCalledTimes(2); + + // Check price API call + const priceUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(priceUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12"); + expect(priceUrl).toContain("swapFeeBps=100"); + expect(priceUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`); + + // Check quote API call + const quoteUrl = (global.fetch as jest.Mock).mock.calls[1][0]; + expect(quoteUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12"); + expect(quoteUrl).toContain("swapFeeBps=100"); + expect(quoteUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`); + }); + + it("should not include swap fee parameters when not provided", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + swapFeeBps: 100, + }; + + await provider.executeSwap(mockWalletProvider, args); + + // Verify both API calls exclude swap fee parameters + expect(global.fetch).toHaveBeenCalledTimes(2); + + // Check price API call + const priceUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(priceUrl).not.toContain("swapFeeRecipient"); + expect(priceUrl).not.toContain("swapFeeBps"); + expect(priceUrl).not.toContain("swapFeeToken"); + + // Check quote API call + const quoteUrl = (global.fetch as jest.Mock).mock.calls[1][0]; + expect(quoteUrl).not.toContain("swapFeeRecipient"); + expect(quoteUrl).not.toContain("swapFeeBps"); + expect(quoteUrl).not.toContain("swapFeeToken"); + }); + + it("should include swap fee parameters with default swapFeeBps when only recipient provided", async () => { + const args = { + sellToken: MOCK_SELL_TOKEN, + buyToken: MOCK_BUY_TOKEN, + sellAmount: MOCK_SELL_AMOUNT, + slippageBps: 50, + swapFeeBps: 100, + swapFeeRecipient: "0xabcdef1234567890abcdef1234567890abcdef12", + }; + + await provider.executeSwap(mockWalletProvider, args); + + // Verify both API calls include swap fee parameters with default swapFeeBps + expect(global.fetch).toHaveBeenCalledTimes(2); + + // Check price API call + const priceUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(priceUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12"); + expect(priceUrl).toContain("swapFeeBps=100"); // Default value + expect(priceUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`); + + // Check quote API call + const quoteUrl = (global.fetch as jest.Mock).mock.calls[1][0]; + expect(quoteUrl).toContain("swapFeeRecipient=0xabcdef1234567890abcdef1234567890abcdef12"); + expect(quoteUrl).toContain("swapFeeBps=100"); // Default value + expect(quoteUrl).toContain(`swapFeeToken=${MOCK_SELL_TOKEN}`); + }); }); describe("supportsNetwork", () => { diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts index 03cde288a..eda9c9f62 100644 --- a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts @@ -68,6 +68,8 @@ It takes the following inputs: - buyToken: The contract address of the token to buy - sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC) - slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%) +- swapFeeRecipient: (Optional) The wallet address to receive affiliate trading fees +- swapFeeBps: The amount in basis points (0-1000) to charge as affiliate fees (defaults to 100 = 1%), only used if swapFeeRecipient is provided Important notes: - The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" @@ -105,6 +107,11 @@ Important notes: url.searchParams.append("sellAmount", sellAmount); url.searchParams.append("taker", walletProvider.getAddress()); url.searchParams.append("slippageBps", args.slippageBps.toString()); + if (args.swapFeeRecipient) { + url.searchParams.append("swapFeeRecipient", args.swapFeeRecipient); + url.searchParams.append("swapFeeBps", args.swapFeeBps.toString()); + url.searchParams.append("swapFeeToken", args.sellToken); + } // Make the request const response = await fetch(url.toString(), { @@ -175,6 +182,8 @@ It takes the following inputs: - buyToken: The contract address of the token to buy - sellAmount: The amount of sellToken to swap in whole units (e.g. 1 ETH or 10 USDC) - slippageBps: (Optional) Maximum allowed slippage in basis points (100 = 1%) +- swapFeeRecipient: (Optional) The wallet address to receive affiliate trading fees +- swapFeeBps: The amount in basis points (0-1000) to charge as affiliate fees (defaults to 100 = 1%), only used if swapFeeRecipient is provided Important notes: - The contract address for native ETH is "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" @@ -227,6 +236,11 @@ Important notes: priceUrl.searchParams.append("sellAmount", sellAmount); priceUrl.searchParams.append("taker", walletAddress); priceUrl.searchParams.append("slippageBps", args.slippageBps.toString()); + if (args.swapFeeRecipient) { + priceUrl.searchParams.append("swapFeeRecipient", args.swapFeeRecipient); + priceUrl.searchParams.append("swapFeeBps", args.swapFeeBps.toString()); + priceUrl.searchParams.append("swapFeeToken", args.sellToken); + } const priceResponse = await fetch(priceUrl.toString(), { method: "GET", @@ -294,6 +308,11 @@ Important notes: quoteUrl.searchParams.append("sellAmount", sellAmount); quoteUrl.searchParams.append("taker", walletAddress); quoteUrl.searchParams.append("slippageBps", args.slippageBps.toString()); + if (args.swapFeeRecipient) { + quoteUrl.searchParams.append("swapFeeRecipient", args.swapFeeRecipient); + quoteUrl.searchParams.append("swapFeeBps", args.swapFeeBps.toString()); + quoteUrl.searchParams.append("swapFeeToken", args.sellToken); + } const quoteResponse = await fetch(quoteUrl.toString(), { method: "GET", From fe54b09f6356053cfb6bae75ba3824512c4bd03f Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Mon, 1 Sep 2025 19:30:46 +0900 Subject: [PATCH 5/5] remove smart wallet restriction --- .../action-providers/zeroX/zeroXActionProvider.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts index eda9c9f62..7079d6f76 100644 --- a/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts +++ b/typescript/agentkit/src/action-providers/zeroX/zeroXActionProvider.ts @@ -3,11 +3,7 @@ import { ActionProvider } from "../actionProvider"; import { Network } from "../../network"; import { CreateAction } from "../actionDecorator"; import { GetSwapPriceSchema, ExecuteSwapSchema } from "./schemas"; -import { - CdpSmartWalletProvider, - EvmWalletProvider, - LegacyCdpWalletProvider, -} from "../../wallet-providers"; +import { EvmWalletProvider, LegacyCdpWalletProvider } from "../../wallet-providers"; import { erc20Abi, formatUnits, @@ -207,12 +203,6 @@ Important notes: const chainId = network.chainId; if (!chainId) throw new Error("Chain ID not available from wallet provider"); - if (walletProvider instanceof CdpSmartWalletProvider) { - throw new Error( - "CdpSmartWalletProvider is currently not supported for 0x swaps, use swap action from CdpApiActionProvider instead", - ); - } - try { // Get token details const {