From 2598e3d342d49ea209ff67cdb4d1333e48a30168 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Thu, 13 Mar 2025 23:27:16 +0100 Subject: [PATCH 1/6] add accross action provider --- typescript/agentkit/package.json | 1 + .../across/acrossActionProvider.ts | 125 ++++++++++++++++++ .../src/action-providers/across/constants.ts | 19 +++ .../src/action-providers/across/index.ts | 1 + .../src/action-providers/across/schemas.ts | 15 +++ .../agentkit/src/action-providers/index.ts | 1 + .../examples/langchain-cdp-chatbot/chatbot.ts | 2 + typescript/package-lock.json | 19 +++ 8 files changed, 183 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/across/acrossActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/across/constants.ts create mode 100644 typescript/agentkit/src/action-providers/across/index.ts create mode 100644 typescript/agentkit/src/action-providers/across/schemas.ts diff --git a/typescript/agentkit/package.json b/typescript/agentkit/package.json index 0189fc102..e9ec219f8 100644 --- a/typescript/agentkit/package.json +++ b/typescript/agentkit/package.json @@ -40,6 +40,7 @@ "typescript" ], "dependencies": { + "@across-protocol/app-sdk": "^0.2.0", "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.20.0", "@jup-ag/api": "^6.0.39", diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts new file mode 100644 index 000000000..73855ec7f --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts @@ -0,0 +1,125 @@ +import { z } from "zod"; +import { parseUnits, Hex, parseEther } from "viem"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { CreateAction } from "../actionDecorator"; +import { BridgeTokenSchema } from "./schemas"; +import { SUPPORTED_CHAINS, INTEGRATOR_ID } from "./constants"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { arbitrum, baseSepolia, optimism } from "viem/chains"; +import { sepolia } from "viem/chains"; + +/** + * AcrossActionProvider provides actions for cross-chain bridging via Across Protocol. + */ +export class AcrossActionProvider extends ActionProvider { + /** + * Constructor for the AcrossActionProvider. + */ + constructor() { + super("across", []); + } + + /** + * Bridges a token from one chain to another using Across Protocol. + * + * @param walletProvider - The wallet provider to use for the transaction. + * @param args - The input arguments for the action. + * @returns A message containing the bridge details. + */ + @CreateAction({ + name: "bridge_token", + description: ` + This tool will bridge tokens from one chain to another using the Across Protocol. + + It takes the following inputs: + - originChainId: The chain ID of the origin chain + - destinationChainId: The chain ID of the destination chain + - inputToken: The address of the token to bridge from the origin chain + - outputToken: The address of the token to receive on the destination chain + - amount: The amount of tokens to bridge + - recipient: (Optional) The recipient address on the destination chain (defaults to sender) + + Important notes: + - Ensure sufficient balance of the input token before bridging + - The wallet must be connected to the origin chain + - For native tokens like ETH, use the WETH address + `, + schema: BridgeTokenSchema, + }) + async bridgeToken( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + // Check if wallet is on the correct network + const currentNetwork = walletProvider.getNetwork(); + + try { + // Use dynamic import to get the Across SDK + const acrossModule = await import("@across-protocol/app-sdk"); + const createAcrossClient = acrossModule.createAcrossClient; + + // Convert string addresses to Hex type + const inputToken = args.inputToken as Hex; + const outputToken = args.outputToken as Hex; + const recipient = (args.recipient || walletProvider.getAddress()) as Hex; + + // Create Across client + const acrossClient = createAcrossClient({ + //integratorId: INTEGRATOR_ID, + chains: [arbitrum, optimism, baseSepolia, sepolia], + //useTestnet: true, + }); + console.log("acrossClient", acrossClient); + + // Define route with proper types + const route = { + originChainId: arbitrum.id, + destinationChainId: optimism.id, + inputToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" as Hex, // WETH arb + outputToken: "0x4200000000000000000000000000000000000006" as Hex, // WETH opt + }; + console.log("route", route); + + // Get token decimals for amount parsing + const decimals = 18; + const inputAmount = parseUnits(args.amount, decimals); + + // Get quote + const quote = await acrossClient.getQuote({ + route, + inputAmount: parseEther("1"), + recipient, + }); + console.log("quote", quote); + return ` +Successfully retrieved bridge quote: +- From: Chain ${currentNetwork} +- To: Chain ${args.destinationChainId} +- Token: ${args.inputToken} → ${args.outputToken} +- Amount: ${args.amount} +- Recipient: ${recipient} + `; + } catch (innerError) { + // Handle SDK-specific errors + return `Error with Across SDK: ${innerError}`; + } + } catch (error) { + return `Error bridging token: ${error}`; + } + } + + /** + * Checks if the Across action provider supports the given network. + * + * @param network - The network to check. + * @returns True if the Across action provider supports the network, false otherwise. + */ + supportsNetwork = (network: Network) => { + // Across only supports EVM-compatible chains + return network.protocolFamily === "evm"; + }; +} + +export const acrossActionProvider = () => new AcrossActionProvider(); diff --git a/typescript/agentkit/src/action-providers/across/constants.ts b/typescript/agentkit/src/action-providers/across/constants.ts new file mode 100644 index 000000000..a7071afc6 --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/constants.ts @@ -0,0 +1,19 @@ +import { mainnet, optimism, arbitrum, base, polygon, sepolia, baseSepolia } from "viem/chains"; + +/** + * Supported chains for Across Protocol + */ +export const SUPPORTED_CHAINS = [ + mainnet, + optimism, + arbitrum, + base, + polygon, + sepolia, + baseSepolia, +]; + +/** + * Integrator ID for AgentKit + */ +export const INTEGRATOR_ID = "0x4147"; // "AG" in hex for AgentKit \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/across/index.ts b/typescript/agentkit/src/action-providers/across/index.ts new file mode 100644 index 000000000..f8f69aa99 --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/index.ts @@ -0,0 +1 @@ +export * from "./acrossActionProvider"; \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/across/schemas.ts b/typescript/agentkit/src/action-providers/across/schemas.ts new file mode 100644 index 000000000..9c656808f --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/schemas.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +/** + * Input schema for bridge token action. + */ +export const BridgeTokenSchema = z + .object({ + destinationChainId: z.number().describe("The chain ID of the destination chain"), + inputToken: z.string().describe("The address of the token to bridge from the origin chain").default("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), + outputToken: z.string().describe("The address of the token to receive on the destination chain").default("0x4200000000000000000000000000000000000006"), + amount: z.string().describe("The amount of tokens to bridge (in token's smallest unit)"), + recipient: z.string().optional().describe("The recipient address on the destination chain (defaults to sender)"), + }) + .strip() + .describe("Instructions for bridging tokens across chains using Across Protocol"); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index a4e9453dd..37627ec46 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -3,6 +3,7 @@ export * from "./actionProvider"; export * from "./customActionProvider"; +export * from "./across"; export * from "./alchemy"; export * from "./basename"; export * from "./cdp"; diff --git a/typescript/examples/langchain-cdp-chatbot/chatbot.ts b/typescript/examples/langchain-cdp-chatbot/chatbot.ts index 182c82517..9addc689c 100644 --- a/typescript/examples/langchain-cdp-chatbot/chatbot.ts +++ b/typescript/examples/langchain-cdp-chatbot/chatbot.ts @@ -10,6 +10,7 @@ import { pythActionProvider, openseaActionProvider, alloraActionProvider, + acrossActionProvider, } from "@coinbase/agentkit"; import { getLangChainTools } from "@coinbase/agentkit-langchain"; import { HumanMessage } from "@langchain/core/messages"; @@ -122,6 +123,7 @@ async function initializeAgent() { ] : []), alloraActionProvider(), + acrossActionProvider(), ], }); diff --git a/typescript/package-lock.json b/typescript/package-lock.json index ffefc61c7..7f0c11320 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -47,6 +47,7 @@ "version": "0.3.0", "license": "Apache-2.0", "dependencies": { + "@across-protocol/app-sdk": "^0.2.0", "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.20.0", "@jup-ag/api": "^6.0.39", @@ -622,6 +623,17 @@ "viem": "^2.21.26" } }, + "node_modules/@across-protocol/app-sdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@across-protocol/app-sdk/-/app-sdk-0.2.0.tgz", + "integrity": "sha512-2evSZQHC+xBvH4gBMDwxwwAgCimjBRId0n/bOAW/EHZv179xdP1wQJu6/bfCbEeCrBwhQaGrZEsUtxrrJ+SJNg==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "viem": "^2.20.1" + } + }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -19369,6 +19381,12 @@ } }, "dependencies": { + "@across-protocol/app-sdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@across-protocol/app-sdk/-/app-sdk-0.2.0.tgz", + "integrity": "sha512-2evSZQHC+xBvH4gBMDwxwwAgCimjBRId0n/bOAW/EHZv179xdP1wQJu6/bfCbEeCrBwhQaGrZEsUtxrrJ+SJNg==", + "requires": {} + }, "@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -20140,6 +20158,7 @@ "@coinbase/agentkit": { "version": "file:agentkit", "requires": { + "@across-protocol/app-sdk": "^0.2.0", "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.20.0", "@jup-ag/api": "^6.0.39", From adf3a490e252aa0b5910067706740bb19ced8e00 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Fri, 14 Mar 2025 15:38:19 +0100 Subject: [PATCH 2/6] add tests --- typescript/.changeset/neat-emus-exist.md | 5 + .../src/action-providers/across/README.md | 43 +++ .../across/acrossActionProvider.test.ts | 317 ++++++++++++++++++ .../across/acrossActionProvider.ts | 298 ++++++++++++---- .../src/action-providers/across/constants.ts | 19 -- .../src/action-providers/across/index.ts | 2 +- .../src/action-providers/across/schemas.ts | 26 +- .../src/action-providers/across/utils.ts | 23 ++ .../examples/langchain-cdp-chatbot/chatbot.ts | 5 +- 9 files changed, 642 insertions(+), 96 deletions(-) create mode 100644 typescript/.changeset/neat-emus-exist.md create mode 100644 typescript/agentkit/src/action-providers/across/README.md create mode 100644 typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/across/utils.ts diff --git a/typescript/.changeset/neat-emus-exist.md b/typescript/.changeset/neat-emus-exist.md new file mode 100644 index 000000000..61fcc8957 --- /dev/null +++ b/typescript/.changeset/neat-emus-exist.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": minor +--- + +Added a new action provider to interact with Across protocol diff --git a/typescript/agentkit/src/action-providers/across/README.md b/typescript/agentkit/src/action-providers/across/README.md new file mode 100644 index 000000000..64c8a684b --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/README.md @@ -0,0 +1,43 @@ +# Across Action Provider + +This directory contains the **AcrossActionProvider** implementation, which provides actions to interact with the **Across Protocol** for bridging tokens across multiple EVM chains. + +## Directory Structure + +``` +across/ +├── acrossActionProvider.ts # Main provider with Across Protocol functionality +├── acrossActionProvider.test.ts # Tests +├── schemas.ts # Bridge token schema +├── utils.ts # Utility functions for Across integration +├── index.ts # Main exports +└── README.md # This file +``` + +## Actions + +- `bridge_token`: Bridge tokens from one chain to another using the Across Protocol + +## Adding New Actions + +To add new Across actions: + +1. Define your action schema in `schemas.ts` +2. Implement the action in `acrossActionProvider.ts` +3. Add tests in `acrossActionProvider.test.ts` + +## Network Support + +The Across provider supports EVM-compatible chains, for example: +- Ethereum Mainnet and Sepolia +- Base Mainnet and Sepolia + +## Configuration + +The provider requires the following configuration: +- `privateKey`: Private key of the signer +- `networkId`: (Optional) Network ID to use, e.g., "base-sepolia" or "ethereum-mainnet" + +## Notes + +For more information on the **Across Protocol**, visit [Across Protocol Documentation](https://docs.across.to/). \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts new file mode 100644 index 000000000..e6a571120 --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts @@ -0,0 +1,317 @@ +import { acrossActionProvider } from "./acrossActionProvider"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { Network } from "../../network"; +import { createPublicClient, createWalletClient, PublicClient } from "viem"; + +// Mock the necessary imports and modules +jest.mock("viem", () => { + return { + ...jest.requireActual("viem"), + createPublicClient: jest.fn(), + createWalletClient: jest.fn(() => ({ + writeContract: jest.fn().mockResolvedValue("0xmocktxhash"), + })), + http: jest.fn(), + formatUnits: jest.fn().mockImplementation((value, decimals) => { + // Simple mock implementation just for testing + if (typeof value === "bigint") { + if (decimals === 18) { + return (Number(value) / 10 ** 18).toString(); + } + return value.toString(); + } + return value.toString(); + }), + parseUnits: jest.fn().mockImplementation((value, decimals) => { + if (decimals === 18) { + return BigInt(Number(value) * 10 ** 18); + } + return BigInt(value); + }), + }; +}); + +jest.mock("viem/accounts", () => ({ + privateKeyToAccount: jest.fn().mockReturnValue({ + address: "0x9876543210987654321098765432109876543210", + }), +})); + +// Mock the network module +jest.mock("../../network", () => { + return { + ...jest.requireActual("../../network"), + NETWORK_ID_TO_VIEM_CHAIN: { + "ethereum-mainnet": { + id: 1, + name: "Ethereum", + }, + optimism: { + id: 10, + name: "Optimism", + }, + "base-sepolia": { + id: 84532, + name: "Base Sepolia", + }, + }, + CHAIN_ID_TO_NETWORK_ID: { + "1": "ethereum-mainnet", + "10": "optimism", + "84532": "base-sepolia", + }, + }; +}); + +// Mock the Across SDK +const mockCreateAcrossClient = jest.fn(); +jest.mock("@across-protocol/app-sdk", () => ({ + createAcrossClient: mockCreateAcrossClient, +})); + +// Default implementation for the createAcrossClient mock +const defaultClientImplementation = () => ({ + getSupportedChains: jest.fn().mockResolvedValue([ + { + chainId: 1, // Ethereum + inputTokens: [ + { + symbol: "ETH", + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + decimals: 18, + }, + { + symbol: "USDC", + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + decimals: 6, + }, + ], + }, + { + chainId: 10, // Optimism + inputTokens: [ + { + symbol: "ETH", + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + decimals: 18, + }, + ], + }, + ]), + getAvailableRoutes: jest.fn().mockResolvedValue([ + { + isNative: true, + originToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + { + isNative: false, + originToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }, + ]), + getQuote: jest.fn().mockResolvedValue({ + deposit: { + inputAmount: BigInt("1000000000000000000"), // 1 ETH + outputAmount: BigInt("990000000000000000"), // 0.99 ETH (1% difference) + spokePoolAddress: "0x1234567890123456789012345678901234567890", + }, + limits: { + minDeposit: BigInt("100000000000000000"), // 0.1 ETH + maxDeposit: BigInt("10000000000000000000"), // 10 ETH + }, + }), + simulateDepositTx: jest.fn().mockResolvedValue({ + request: { + address: "0x1234567890123456789012345678901234567890", + abi: [], + functionName: "deposit", + args: [], + }, + }), +}); + +// Set the default implementation +mockCreateAcrossClient.mockImplementation(defaultClientImplementation); + +// Mock the isTestnet function +jest.mock("./utils", () => ({ + isTestnet: jest.fn().mockReturnValue(false), +})); + +describe("Across Action Provider", () => { + const MOCK_PRIVATE_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + const MOCK_NETWORK_ID = "ethereum-mainnet"; + const MOCK_INPUT_TOKEN_SYMBOL = "ETH"; + const MOCK_AMOUNT = "1.0"; + const MOCK_DESTINATION_CHAIN_ID = "10"; // Optimism + const MOCK_RECIPIENT = "0x9876543210987654321098765432109876543210"; + const MOCK_MAX_SLIPPAGE = 2.0; + + let mockWallet: jest.Mocked; + let actionProvider: ReturnType; + let mockPublicClient: PublicClient; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset to default implementation + mockCreateAcrossClient.mockImplementation(defaultClientImplementation); + + mockPublicClient = { + getBalance: jest.fn().mockResolvedValue(BigInt("2000000000000000000")), // 2 ETH + readContract: jest.fn().mockResolvedValue(BigInt("2000000000000000000")), // 2 ETH or 2 USDC + waitForTransactionReceipt: jest.fn().mockResolvedValue({}), + } as unknown as PublicClient; + + (createPublicClient as jest.Mock).mockReturnValue(mockPublicClient); + + mockWallet = { + getAddress: jest.fn().mockReturnValue(MOCK_RECIPIENT), + sendTransaction: jest.fn(), + waitForTransactionReceipt: jest.fn(), + } as unknown as jest.Mocked; + + actionProvider = acrossActionProvider({ + privateKey: MOCK_PRIVATE_KEY, + networkId: MOCK_NETWORK_ID, + }); + }); + + describe("bridgeToken", () => { + it("should successfully bridge native ETH", async () => { + const args = { + inputTokenSymbol: MOCK_INPUT_TOKEN_SYMBOL, + amount: MOCK_AMOUNT, + destinationChainId: MOCK_DESTINATION_CHAIN_ID, + recipient: MOCK_RECIPIENT, + maxSplippage: MOCK_MAX_SLIPPAGE, + }; + + const response = await actionProvider.bridgeToken(mockWallet, args); + + // Verify the SDK interactions and response + expect(response).toContain("Successfully deposited tokens"); + expect(response).toContain(`Token: ${MOCK_INPUT_TOKEN_SYMBOL}`); + expect(response).toContain("Transaction Hash for deposit: 0xmocktxhash"); + }); + + it("should successfully bridge ERC20 tokens", async () => { + const args = { + inputTokenSymbol: "USDC", + amount: "100", + destinationChainId: MOCK_DESTINATION_CHAIN_ID, + recipient: MOCK_RECIPIENT, + maxSplippage: MOCK_MAX_SLIPPAGE, + }; + + // Mock the wallet client for the approval transaction + (createWalletClient as jest.Mock).mockReturnValue({ + writeContract: jest + .fn() + .mockResolvedValueOnce("0xapprovalTxHash") // First call for approval + .mockResolvedValueOnce("0xdepositTxHash"), // Second call for deposit + }); + + const response = await actionProvider.bridgeToken(mockWallet, args); + + // Verify the SDK interactions and response + expect(response).toContain("Successfully deposited tokens"); + expect(response).toContain(`Token: ${args.inputTokenSymbol}`); + expect(response).toContain("Transaction Hash for approval: 0xapprovalTxHash"); + expect(response).toContain("Transaction Hash for deposit: 0xdepositTxHash"); + }); + + it("should fail when slippage is too high", async () => { + // Override the default mock with high slippage for this test only + mockCreateAcrossClient.mockImplementationOnce(() => ({ + getSupportedChains: jest.fn().mockResolvedValue([ + { + chainId: 1, + inputTokens: [ + { + symbol: "ETH", + address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + decimals: 18, + }, + ], + }, + ]), + getAvailableRoutes: jest.fn().mockResolvedValue([ + { + isNative: true, + originToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + }, + ]), + getQuote: jest.fn().mockResolvedValue({ + deposit: { + inputAmount: BigInt("1000000000000000000"), // 1 ETH + outputAmount: BigInt("800000000000000000"), // 0.8 ETH (20% difference) + spokePoolAddress: "0x1234567890123456789012345678901234567890", + }, + limits: { + minDeposit: BigInt("100000000000000000"), + maxDeposit: BigInt("10000000000000000000"), + }, + }), + simulateDepositTx: jest.fn().mockResolvedValue({ + request: { + address: "0x1234567890123456789012345678901234567890", + abi: [], + functionName: "deposit", + args: [], + }, + }), + })); + + // Set a low max slippage + const args = { + inputTokenSymbol: MOCK_INPUT_TOKEN_SYMBOL, + amount: MOCK_AMOUNT, + destinationChainId: MOCK_DESTINATION_CHAIN_ID, + recipient: MOCK_RECIPIENT, + maxSplippage: 0.5, // Only allow 0.5% slippage + }; + + const response = await actionProvider.bridgeToken(mockWallet, args); + + // Verify the error response + expect(response).toContain("Error with Across SDK"); + expect(response).toContain("exceeds the maximum allowed slippage of 0.5%"); + }); + + it("should handle errors in bridging", async () => { + const error = new Error("Insufficient balance"); + (mockPublicClient.getBalance as jest.Mock).mockRejectedValue(error); + + const args = { + inputTokenSymbol: MOCK_INPUT_TOKEN_SYMBOL, + amount: MOCK_AMOUNT, + destinationChainId: MOCK_DESTINATION_CHAIN_ID, + recipient: MOCK_RECIPIENT, + maxSplippage: MOCK_MAX_SLIPPAGE, + }; + + const response = await actionProvider.bridgeToken(mockWallet, args); + + expect(response).toContain("Error with Across SDK"); + expect(response).toContain(error.message); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for supported networks", () => { + const evmNetwork: Network = { + protocolFamily: "evm", + networkId: "ethereum", + chainId: "1", + }; + expect(actionProvider.supportsNetwork(evmNetwork)).toBe(true); + }); + + it("should return false for unsupported networks", () => { + const nonEvmNetwork: Network = { + protocolFamily: "solana", + networkId: "mainnet", + }; + expect(actionProvider.supportsNetwork(nonEvmNetwork)).toBe(false); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts index 73855ec7f..13a42736f 100644 --- a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts @@ -1,28 +1,66 @@ import { z } from "zod"; -import { parseUnits, Hex, parseEther } from "viem"; +import { + parseUnits, + Hex, + createWalletClient, + http, + Chain, + formatUnits, + PublicClient, + createPublicClient, +} from "viem"; import { ActionProvider } from "../actionProvider"; -import { Network } from "../../network"; +import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN } from "../../network"; import { CreateAction } from "../actionDecorator"; import { BridgeTokenSchema } from "./schemas"; -import { SUPPORTED_CHAINS, INTEGRATOR_ID } from "./constants"; import { EvmWalletProvider } from "../../wallet-providers"; -import { arbitrum, baseSepolia, optimism } from "viem/chains"; -import { sepolia } from "viem/chains"; +import { isTestnet } from "./utils"; +import { privateKeyToAccount } from "viem/accounts"; +import { abi as ERC20_ABI } from "../erc20/constants"; +/** + * Configuration options for the SafeWalletProvider. + */ +export interface AcrossActionProviderConfig { + /** + * Private key of the signer that (co-)owns the Safe. + */ + privateKey: string; + /** + * Network ID, for example "base-sepolia" or "ethereum-mainnet". + */ + networkId?: string; +} /** * AcrossActionProvider provides actions for cross-chain bridging via Across Protocol. */ export class AcrossActionProvider extends ActionProvider { + #privateKey: string; + #chain: Chain; + #publicClient: PublicClient; /** * Constructor for the AcrossActionProvider. + * + * @param config - The configuration options for the AcrossActionProvider. */ - constructor() { + constructor(config: AcrossActionProviderConfig) { super("across", []); + this.#privateKey = config.privateKey; + const account = privateKeyToAccount(this.#privateKey as Hex); + if (!account) throw new Error("Invalid private key"); + + this.#chain = NETWORK_ID_TO_VIEM_CHAIN[config.networkId || "base-sepolia"]; + if (!this.#chain) throw new Error(`Unsupported network: ${config.networkId}`); + + this.#publicClient = createPublicClient({ + chain: this.#chain, + transport: http(), + }); } - + /** * Bridges a token from one chain to another using Across Protocol. - * + * * @param walletProvider - The wallet provider to use for the transaction. * @param args - The input arguments for the action. * @returns A message containing the bridge details. @@ -30,20 +68,16 @@ export class AcrossActionProvider extends ActionProvider { @CreateAction({ name: "bridge_token", description: ` - This tool will bridge tokens from one chain to another using the Across Protocol. + This tool will bridge tokens from the current chain to another chain using the Across Protocol. It takes the following inputs: - - originChainId: The chain ID of the origin chain - destinationChainId: The chain ID of the destination chain - - inputToken: The address of the token to bridge from the origin chain - - outputToken: The address of the token to receive on the destination chain - - amount: The amount of tokens to bridge + - inputTokenSymbol: The symbol of the token to bridge (e.g., 'ETH', 'WETH', 'USDC') + - amount: The amount of tokens to bridge in whole units (e.g. 1.5 WETH, 10 USDC) - recipient: (Optional) The recipient address on the destination chain (defaults to sender) - + - maxSplippage: (Optional) The maximum slippage percentage (defaults to 1.5%) Important notes: - Ensure sufficient balance of the input token before bridging - - The wallet must be connected to the origin chain - - For native tokens like ETH, use the WETH address `, schema: BridgeTokenSchema, }) @@ -52,67 +86,192 @@ export class AcrossActionProvider extends ActionProvider { args: z.infer, ): Promise { try { - // Check if wallet is on the correct network - const currentNetwork = walletProvider.getNetwork(); - - try { - // Use dynamic import to get the Across SDK - const acrossModule = await import("@across-protocol/app-sdk"); - const createAcrossClient = acrossModule.createAcrossClient; - - // Convert string addresses to Hex type - const inputToken = args.inputToken as Hex; - const outputToken = args.outputToken as Hex; - const recipient = (args.recipient || walletProvider.getAddress()) as Hex; - - // Create Across client - const acrossClient = createAcrossClient({ - //integratorId: INTEGRATOR_ID, - chains: [arbitrum, optimism, baseSepolia, sepolia], - //useTestnet: true, + // Use dynamic import to get the Across SDK + const acrossModule = await import("@across-protocol/app-sdk"); + const createAcrossClient = acrossModule.createAcrossClient; + + // Create wallet client + const account = privateKeyToAccount(this.#privateKey as Hex); + const walletClient = createWalletClient({ + account, + chain: this.#chain, + transport: http(), + }); + + // Get recipient address if provided, otherwise use sender + const recipient = (args.recipient || walletProvider.getAddress()) as Hex; + + // Get destination chain + const destinationNetworkId = CHAIN_ID_TO_NETWORK_ID[Number(args.destinationChainId)]; + const destinationChain = NETWORK_ID_TO_VIEM_CHAIN[destinationNetworkId]; + if (!destinationChain) { + throw new Error(`Unsupported destination chain: ${args.destinationChainId}`); + } + + // Create Across client + const acrossClient = createAcrossClient({ + //integratorId: INTEGRATOR_ID, + chains: [this.#chain, destinationChain], + useTestnet: isTestnet(this.#chain.id), + }); + + // Get chain details to find token information + const chainDetails = await acrossClient.getSupportedChains({}); + const originChainDetails = chainDetails.find(chain => chain.chainId === this.#chain.id); + + if (!originChainDetails) { + throw new Error(`Origin chain ${this.#chain.id} not supported by Across Protocol`); + } + + // Find token by symbol on the origin chain + const inputTokens = originChainDetails.inputTokens; + if (!inputTokens || inputTokens.length === 0) { + throw new Error(`No input tokens available on chain ${this.#chain.id}`); + } + const tokenInfo = inputTokens.find( + token => token.symbol.toUpperCase() === args.inputTokenSymbol.toUpperCase(), + ); + if (!tokenInfo) { + throw new Error( + `Token ${args.inputTokenSymbol} not found on chain ${this.#chain.id}. Available tokens: ${inputTokens.map(t => t.symbol).join(", ")}`, + ); + } + + // Get token address and decimals to parse the amount + const inputToken = tokenInfo.address as Hex; + const decimals = tokenInfo.decimals; + const inputAmount = parseUnits(args.amount, decimals); + + // Check balance + const isNative = args.inputTokenSymbol.toUpperCase() === "ETH"; + if (isNative) { + // Check native ETH balance + const ethBalance = await this.#publicClient.getBalance({ + address: account.address, }); - console.log("acrossClient", acrossClient); - - // Define route with proper types - const route = { - originChainId: arbitrum.id, - destinationChainId: optimism.id, - inputToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" as Hex, // WETH arb - outputToken: "0x4200000000000000000000000000000000000006" as Hex, // WETH opt - }; - console.log("route", route); - - // Get token decimals for amount parsing - const decimals = 18; - const inputAmount = parseUnits(args.amount, decimals); - - // Get quote - const quote = await acrossClient.getQuote({ - route, - inputAmount: parseEther("1"), - recipient, + if (ethBalance < inputAmount) { + throw new Error( + `Insufficient balance. Requested to bridge ${formatUnits(inputAmount, decimals)} ${args.inputTokenSymbol} but balance is only ${formatUnits(ethBalance, decimals)} ${args.inputTokenSymbol}`, + ); + } + } else { + // Check ERC20 token balance + const tokenBalance = (await this.#publicClient.readContract({ + address: inputToken, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [account.address], + })) as bigint; + if (tokenBalance < inputAmount) { + throw new Error( + `Insufficient balance. Requested to bridge ${formatUnits(inputAmount, decimals)} ${args.inputTokenSymbol} but balance is only ${formatUnits(tokenBalance, decimals)} ${args.inputTokenSymbol}`, + ); + } + } + + // Get available routes + const routeInfo = await acrossClient.getAvailableRoutes({ + originChainId: this.#chain.id, + destinationChainId: destinationChain.id, + originToken: inputToken, + }); + + // Select the appropriate route for native ETH or ERC20 token + let route; + for (let i = 0; i < routeInfo.length; i++) { + if (routeInfo[i].isNative === isNative) { + route = routeInfo[i]; + break; + } + } + + if (!route) { + throw new Error( + `No routes available from chain ${this.#chain.name} to chain ${destinationChain.name} for token ${args.inputTokenSymbol}`, + ); + } + + // Get quote + const quote = await acrossClient.getQuote({ + route, + inputAmount: inputAmount, + recipient, + }); + + // Convert units to readable format + const formattedInfo = { + minDeposit: formatUnits(quote.limits.minDeposit, decimals), + maxDeposit: formatUnits(quote.limits.maxDeposit, decimals), + inputAmount: formatUnits(quote.deposit.inputAmount, decimals), + outputAmount: formatUnits(quote.deposit.outputAmount, decimals), + }; + + // Check if input amount is within valid deposit range + if (quote.deposit.inputAmount < quote.limits.minDeposit) { + throw new Error( + `Input amount ${formattedInfo.inputAmount} ${args.inputTokenSymbol} is below the minimum deposit of ${formattedInfo.minDeposit} ${args.inputTokenSymbol}`, + ); + } + if (quote.deposit.inputAmount > quote.limits.maxDeposit) { + throw new Error( + `Input amount ${formattedInfo.inputAmount} ${args.inputTokenSymbol} exceeds the maximum deposit of ${formattedInfo.maxDeposit} ${args.inputTokenSymbol}`, + ); + } + + // Check if output amount is within acceptable slippage limits + const actualSlippagePercentage = + ((Number(formattedInfo.inputAmount) - Number(formattedInfo.outputAmount)) / + Number(formattedInfo.inputAmount)) * + 100; + if (actualSlippagePercentage > args.maxSplippage) { + throw new Error( + `Output amount has high slippage of ${actualSlippagePercentage.toFixed(2)}%, which exceeds the maximum allowed slippage of ${args.maxSplippage}%. ` + + `Input: ${formattedInfo.inputAmount} ${args.inputTokenSymbol}, Output: ${formattedInfo.outputAmount} ${args.inputTokenSymbol}`, + ); + } + + // Approve ERC20 token if needed + let approvalTxHash; + if (!isNative) { + const approvalAmount = quote.deposit.inputAmount; + approvalTxHash = await walletClient.writeContract({ + address: inputToken, + abi: ERC20_ABI, + functionName: "approve", + args: [quote.deposit.spokePoolAddress, approvalAmount], }); - console.log("quote", quote); - return ` -Successfully retrieved bridge quote: -- From: Chain ${currentNetwork} -- To: Chain ${args.destinationChainId} -- Token: ${args.inputToken} → ${args.outputToken} -- Amount: ${args.amount} + await this.#publicClient.waitForTransactionReceipt({ hash: approvalTxHash }); + } + + // Simulate the deposit transaction + const { request } = await acrossClient.simulateDepositTx({ + walletClient: walletClient, + deposit: quote.deposit, + }); + + // Execute the deposit transaction + const transactionHash = await walletClient.writeContract(request); + + return ` +Successfully deposited tokens: +- From: Chain ${this.#chain.id} (${this.#chain.name}) +- To: Chain ${destinationChain.id} (${destinationChain.name}) +- Token: ${args.inputTokenSymbol} (${inputToken}) +- Input Amount: ${formattedInfo.inputAmount} ${args.inputTokenSymbol} +- Output Amount: ${formattedInfo.outputAmount} ${args.inputTokenSymbol} - Recipient: ${recipient} +${!isNative ? `- Transaction Hash for approval: ${approvalTxHash}\n` : ""} +- Transaction Hash for deposit: ${transactionHash} `; - } catch (innerError) { - // Handle SDK-specific errors - return `Error with Across SDK: ${innerError}`; - } } catch (error) { - return `Error bridging token: ${error}`; + // Handle SDK-specific errors + return `Error with Across SDK: ${error}`; } } - + /** * Checks if the Across action provider supports the given network. - * + * * @param network - The network to check. * @returns True if the Across action provider supports the network, false otherwise. */ @@ -122,4 +281,5 @@ Successfully retrieved bridge quote: }; } -export const acrossActionProvider = () => new AcrossActionProvider(); +export const acrossActionProvider = (config: AcrossActionProviderConfig) => + new AcrossActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/across/constants.ts b/typescript/agentkit/src/action-providers/across/constants.ts index a7071afc6..e69de29bb 100644 --- a/typescript/agentkit/src/action-providers/across/constants.ts +++ b/typescript/agentkit/src/action-providers/across/constants.ts @@ -1,19 +0,0 @@ -import { mainnet, optimism, arbitrum, base, polygon, sepolia, baseSepolia } from "viem/chains"; - -/** - * Supported chains for Across Protocol - */ -export const SUPPORTED_CHAINS = [ - mainnet, - optimism, - arbitrum, - base, - polygon, - sepolia, - baseSepolia, -]; - -/** - * Integrator ID for AgentKit - */ -export const INTEGRATOR_ID = "0x4147"; // "AG" in hex for AgentKit \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/across/index.ts b/typescript/agentkit/src/action-providers/across/index.ts index f8f69aa99..4801f6f28 100644 --- a/typescript/agentkit/src/action-providers/across/index.ts +++ b/typescript/agentkit/src/action-providers/across/index.ts @@ -1 +1 @@ -export * from "./acrossActionProvider"; \ No newline at end of file +export * from "./acrossActionProvider"; diff --git a/typescript/agentkit/src/action-providers/across/schemas.ts b/typescript/agentkit/src/action-providers/across/schemas.ts index 9c656808f..3b9f8b275 100644 --- a/typescript/agentkit/src/action-providers/across/schemas.ts +++ b/typescript/agentkit/src/action-providers/across/schemas.ts @@ -5,11 +5,25 @@ import { z } from "zod"; */ export const BridgeTokenSchema = z .object({ - destinationChainId: z.number().describe("The chain ID of the destination chain"), - inputToken: z.string().describe("The address of the token to bridge from the origin chain").default("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), - outputToken: z.string().describe("The address of the token to receive on the destination chain").default("0x4200000000000000000000000000000000000006"), - amount: z.string().describe("The amount of tokens to bridge (in token's smallest unit)"), - recipient: z.string().optional().describe("The recipient address on the destination chain (defaults to sender)"), + destinationChainId: z + .string() + .describe("The chain ID of the destination chain (e.g. 11155111 for base-sepolia)"), + inputTokenSymbol: z + .string() + .describe("The symbol of the token to bridge (e.g., 'ETH', 'WETH', 'USDC')") + .default("ETH"), + amount: z + .string() + .describe("The amount of tokens to bridge in whole units (e.g. 1.5 WETH, 10 USDC)"), + recipient: z + .string() + .optional() + .describe("The recipient address on the destination chain (defaults to sender)"), + maxSplippage: z + .number() + .optional() + .describe("The maximum slippage percentage (e.g. 10 for 10%)") + .default(1.5), }) .strip() - .describe("Instructions for bridging tokens across chains using Across Protocol"); \ No newline at end of file + .describe("Instructions for bridging tokens across chains using Across Protocol"); diff --git a/typescript/agentkit/src/action-providers/across/utils.ts b/typescript/agentkit/src/action-providers/across/utils.ts new file mode 100644 index 000000000..08d7fbd1c --- /dev/null +++ b/typescript/agentkit/src/action-providers/across/utils.ts @@ -0,0 +1,23 @@ +/** + * Checks if a chain ID corresponds to a testnet network + * + * @param chainId - The blockchain network chain ID + * @returns true if the chain ID corresponds to a testnet, false otherwise + */ +export function isTestnet(chainId: number): boolean { + // List of testnet chain IDs + const testnetChainIds = [ + 11155111, // Sepolia + 84532, // Base Sepolia + 421614, // Arbitrum Sepolia + 11155420, // Optimism Sepolia + 919, // Mode Sepolia + 80002, // Polygon Amoy + 168587773, // Blast Sepolia + 4202, // Lisk Sepolia + 37111, // Lens Sepolia + 1301, // Unichain Sepolia + ]; + + return testnetChainIds.includes(chainId); +} diff --git a/typescript/examples/langchain-cdp-chatbot/chatbot.ts b/typescript/examples/langchain-cdp-chatbot/chatbot.ts index 9addc689c..aa571951f 100644 --- a/typescript/examples/langchain-cdp-chatbot/chatbot.ts +++ b/typescript/examples/langchain-cdp-chatbot/chatbot.ts @@ -123,7 +123,10 @@ async function initializeAgent() { ] : []), alloraActionProvider(), - acrossActionProvider(), + acrossActionProvider({ + networkId: walletProvider.getNetwork().networkId, + privateKey: await (await walletProvider.getWallet().getDefaultAddress()).export(), + }), ], }); From da5f89ee6688a0c8ac48b7f8743238e1d3c9341c Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Sat, 22 Mar 2025 12:47:34 +0100 Subject: [PATCH 3/6] add check_deposit_status action --- .../across/acrossActionProvider.test.ts | 73 ++++++++++++ .../across/acrossActionProvider.ts | 111 ++++++++++++++++-- .../src/action-providers/across/schemas.ts | 18 ++- 3 files changed, 189 insertions(+), 13 deletions(-) diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts index e6a571120..5abe34df1 100644 --- a/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts @@ -127,6 +127,9 @@ const defaultClientImplementation = () => ({ args: [], }, }), + waitForDepositTx: jest.fn().mockResolvedValue({ + depositId: "123456", + }), }); // Set the default implementation @@ -259,6 +262,9 @@ describe("Across Action Provider", () => { args: [], }, }), + waitForDepositTx: jest.fn().mockResolvedValue({ + depositId: "123456", + }), })); // Set a low max slippage @@ -296,6 +302,73 @@ describe("Across Action Provider", () => { }); }); + describe("checkDepositStatus", () => { + beforeEach(() => { + global.fetch = jest.fn(); + }); + + it("should successfully check deposit status", async () => { + // Mock successful API response + const mockApiResponse = { + status: "filled", + originChainId: 1, + destinationChainId: 10, + depositTxHash: "0xdepositTxHash", + fillTx: "0xfillTxHash", + }; + + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockApiResponse, + }); + + const args = { + originChainId: "1", + depositId: "123456", + }; + + const response = await actionProvider.checkDepositStatus(mockWallet, args); + const parsedResponse = JSON.parse(response); + + expect(parsedResponse.status).toEqual("filled"); + expect(parsedResponse.depositTxInfo.txHash).toEqual("0xdepositTxHash"); + expect(parsedResponse.fillTxInfo.txHash).toEqual("0xfillTxHash"); + }); + + it("should handle API errors", async () => { + // Mock API error + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + const args = { + originChainId: "1", + depositId: "123456", + }; + + const response = await actionProvider.checkDepositStatus(mockWallet, args); + + expect(response).toContain("Error checking deposit status"); + expect(response).toContain("404"); + }); + + it("should handle network errors", async () => { + // Mock network error + (global.fetch as jest.Mock).mockRejectedValueOnce(new Error("Network error")); + + const args = { + originChainId: "1", + depositId: "123456", + }; + + const response = await actionProvider.checkDepositStatus(mockWallet, args); + + expect(response).toContain("Error checking deposit status"); + expect(response).toContain("Network error"); + }); + }); + describe("supportsNetwork", () => { it("should return true for supported networks", () => { const evmNetwork: Network = { diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts index 13a42736f..cc8617a4e 100644 --- a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts @@ -10,9 +10,9 @@ import { createPublicClient, } from "viem"; import { ActionProvider } from "../actionProvider"; -import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN } from "../../network"; +import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN, getChain } from "../../network"; import { CreateAction } from "../actionDecorator"; -import { BridgeTokenSchema } from "./schemas"; +import { BridgeTokenSchema, CheckDepositStatusSchema } from "./schemas"; import { EvmWalletProvider } from "../../wallet-providers"; import { isTestnet } from "./utils"; import { privateKeyToAccount } from "viem/accounts"; @@ -90,14 +90,6 @@ export class AcrossActionProvider extends ActionProvider { const acrossModule = await import("@across-protocol/app-sdk"); const createAcrossClient = acrossModule.createAcrossClient; - // Create wallet client - const account = privateKeyToAccount(this.#privateKey as Hex); - const walletClient = createWalletClient({ - account, - chain: this.#chain, - transport: http(), - }); - // Get recipient address if provided, otherwise use sender const recipient = (args.recipient || walletProvider.getAddress()) as Hex; @@ -108,6 +100,14 @@ export class AcrossActionProvider extends ActionProvider { throw new Error(`Unsupported destination chain: ${args.destinationChainId}`); } + // Create wallet client + const account = privateKeyToAccount(this.#privateKey as Hex); + const walletClient = createWalletClient({ + account, + chain: this.#chain, + transport: http(), + }); + // Create Across client const acrossClient = createAcrossClient({ //integratorId: INTEGRATOR_ID, @@ -230,7 +230,7 @@ export class AcrossActionProvider extends ActionProvider { ); } - // Approve ERC20 token if needed + //Approve ERC20 token if needed let approvalTxHash; if (!isNative) { const approvalAmount = quote.deposit.inputAmount; @@ -252,6 +252,12 @@ export class AcrossActionProvider extends ActionProvider { // Execute the deposit transaction const transactionHash = await walletClient.writeContract(request); + // Wait for tx to be mined + const { depositId } = await acrossClient.waitForDepositTx({ + transactionHash, + originChainId: this.#chain.id, + }); + return ` Successfully deposited tokens: - From: Chain ${this.#chain.id} (${this.#chain.name}) @@ -262,13 +268,94 @@ Successfully deposited tokens: - Recipient: ${recipient} ${!isNative ? `- Transaction Hash for approval: ${approvalTxHash}\n` : ""} - Transaction Hash for deposit: ${transactionHash} +- Deposit ID: ${depositId} `; } catch (error) { - // Handle SDK-specific errors return `Error with Across SDK: ${error}`; } } + /** + * Checks the status of a bridge deposit via Across Protocol. + * + * @param walletProvider - The wallet provider to use for the transaction. + * @param args - The input arguments for the action. + * @returns A message containing the deposit status details. + */ + @CreateAction({ + name: "check_deposit_status", + description: ` + This tool will check the status of a cross-chain bridge deposit on the Across Protocol. + + It takes the following inputs: + - originChainId: The chain ID of the origin chain (defaults to the current chain) + - depositId: The ID of the deposit to check (returned by the bridge deposit transaction) + `, + schema: CheckDepositStatusSchema, + }) + async checkDepositStatus( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + const originChainId = Number(args.originChainId) || this.#chain.id; + + if (isTestnet(originChainId)) { + throw new Error( + "Checking deposit status on testnets is currently not supported by the Across API", + ); + } + + try { + const response = await fetch( + `https://app.across.to/api/deposit/status?originChainId=${originChainId}&depositId=${args.depositId}`, + { + method: "GET", + }, + ); + + if (!response.ok) { + throw new Error(`Across API request failed with status ${response.status}`); + } + + const apiData = await response.json(); + + // Get chain names + const originChainName = getChain(String(apiData.originChainId))?.name || "Unknown Chain"; + const destinationChainName = + getChain(String(apiData.destinationChainId))?.name || "Unknown Chain"; + + // Create structured response + const structuredResponse = { + status: apiData.status || "unknown", + depositTxInfo: apiData.depositTxHash + ? { + txHash: apiData.depositTxHash, + chainId: apiData.originChainId, + chainName: originChainName, + } + : null, + fillTxInfo: apiData.fillTx + ? { + txHash: apiData.fillTx, + chainId: apiData.destinationChainId, + chainName: destinationChainName, + } + : null, + depositRefundTxInfo: apiData.depositRefundTxHash + ? { + txHash: apiData.depositRefundTxHash, + chainId: apiData.originChainId, + chainName: originChainName, + } + : null, + }; + + return JSON.stringify(structuredResponse, null, 2); + } catch (error) { + return `Error checking deposit status: ${error}`; + } + } + /** * Checks if the Across action provider supports the given network. * diff --git a/typescript/agentkit/src/action-providers/across/schemas.ts b/typescript/agentkit/src/action-providers/across/schemas.ts index 3b9f8b275..4a6dd228b 100644 --- a/typescript/agentkit/src/action-providers/across/schemas.ts +++ b/typescript/agentkit/src/action-providers/across/schemas.ts @@ -7,7 +7,7 @@ export const BridgeTokenSchema = z .object({ destinationChainId: z .string() - .describe("The chain ID of the destination chain (e.g. 11155111 for base-sepolia)"), + .describe("The chain ID of the destination chain (e.g. 11155111 for ethereum-sepolia)"), inputTokenSymbol: z .string() .describe("The symbol of the token to bridge (e.g., 'ETH', 'WETH', 'USDC')") @@ -27,3 +27,19 @@ export const BridgeTokenSchema = z }) .strip() .describe("Instructions for bridging tokens across chains using Across Protocol"); + +/** + * Input schema for check deposit status action. + */ +export const CheckDepositStatusSchema = z + .object({ + originChainId: z + .string() + .optional() + .describe("The chain ID of the origin chain (defaults to the current chain)"), + depositId: z + .string() + .describe("The ID of the deposit to check (returned by the bridge deposit transaction)"), + }) + .strip() + .describe("Instructions for checking the status of a deposit on Across Protocol"); From c2beb7ea2df19b4100a0c9e71655b003eabc37d9 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Mon, 24 Mar 2025 10:47:03 +0100 Subject: [PATCH 4/6] improve across tool description --- .../src/action-providers/across/acrossActionProvider.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts index cc8617a4e..7818514a1 100644 --- a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts @@ -69,14 +69,17 @@ export class AcrossActionProvider extends ActionProvider { name: "bridge_token", description: ` This tool will bridge tokens from the current chain to another chain using the Across Protocol. - + Supports testnet to testnet (e.g. ethereum-sepolia to base-sepolia) or mainnet to mainnet (e.g. ethereum-mainnet to base-mainnet) bridging. + It takes the following inputs: - - destinationChainId: The chain ID of the destination chain - - inputTokenSymbol: The symbol of the token to bridge (e.g., 'ETH', 'WETH', 'USDC') + - destinationChainId: The chain ID of the destination chain (e.g. 84532 for base-sepolia, 1 for ethereum-mainnet) + - inputTokenSymbol: The symbol of the token to bridge (e.g., 'ETH', 'USDC') - amount: The amount of tokens to bridge in whole units (e.g. 1.5 WETH, 10 USDC) - recipient: (Optional) The recipient address on the destination chain (defaults to sender) - maxSplippage: (Optional) The maximum slippage percentage (defaults to 1.5%) Important notes: + - Origin chain is the currently connected chain of the wallet provider (e.g. base-sepolia, ethereum-mainnet) + - Testnet deposits are not be refunded if not filled on destination chain - Ensure sufficient balance of the input token before bridging `, schema: BridgeTokenSchema, From 6bc63d87e17a5b93238250300708d67545677871 Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Tue, 25 Mar 2025 10:57:25 +0100 Subject: [PATCH 5/6] pr review --- typescript/agentkit/README.md | 13 ++ .../src/action-providers/across/README.md | 13 +- .../across/acrossActionProvider.test.ts | 80 ++++++++--- .../across/acrossActionProvider.ts | 124 ++++++++---------- .../src/action-providers/across/utils.ts | 6 +- .../examples/langchain-cdp-chatbot/chatbot.ts | 5 - 6 files changed, 143 insertions(+), 98 deletions(-) diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index 1584741f4..c66fb7033 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -137,6 +137,19 @@ const agent = createReactAgent({ ## Action Providers
+Across + + + + + + + + + +
bridge_tokenBridges tokens between supported chains using Across Protocol.
check_deposit_statusChecks the status of a cross-chain bridge deposit on the Across Protocol (mainnet networks only).
+
+
Basename diff --git a/typescript/agentkit/src/action-providers/across/README.md b/typescript/agentkit/src/action-providers/across/README.md index 64c8a684b..a7b2f1a55 100644 --- a/typescript/agentkit/src/action-providers/across/README.md +++ b/typescript/agentkit/src/action-providers/across/README.md @@ -7,7 +7,7 @@ This directory contains the **AcrossActionProvider** implementation, which provi ``` across/ ├── acrossActionProvider.ts # Main provider with Across Protocol functionality -├── acrossActionProvider.test.ts # Tests +├── acrossActionProvider.test.ts # Tests ├── schemas.ts # Bridge token schema ├── utils.ts # Utility functions for Across integration ├── index.ts # Main exports @@ -17,6 +17,7 @@ across/ ## Actions - `bridge_token`: Bridge tokens from one chain to another using the Across Protocol +- `check_deposit_status`: Check the status of a cross-chain bridge deposit on the Across Protocol ## Adding New Actions @@ -28,15 +29,15 @@ To add new Across actions: ## Network Support -The Across provider supports EVM-compatible chains, for example: -- Ethereum Mainnet and Sepolia -- Base Mainnet and Sepolia +The Across provider supports cross-chain transfers between EVM-compatible chains, for example: +- Ethereum Mainnet to Base Mainnet +- Base Sepolia to Ethereum Sepolia +The status of bridge deposit can only be checked on Mainnets. ## Configuration The provider requires the following configuration: -- `privateKey`: Private key of the signer -- `networkId`: (Optional) Network ID to use, e.g., "base-sepolia" or "ethereum-mainnet" +- `privateKey`: Private key of the wallet provider ## Notes diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts index 5abe34df1..61374d0a3 100644 --- a/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts @@ -1,7 +1,7 @@ import { acrossActionProvider } from "./acrossActionProvider"; import { EvmWalletProvider } from "../../wallet-providers"; import { Network } from "../../network"; -import { createPublicClient, createWalletClient, PublicClient } from "viem"; +import { createPublicClient, PublicClient } from "viem"; // Mock the necessary imports and modules jest.mock("viem", () => { @@ -9,7 +9,7 @@ jest.mock("viem", () => { ...jest.requireActual("viem"), createPublicClient: jest.fn(), createWalletClient: jest.fn(() => ({ - writeContract: jest.fn().mockResolvedValue("0xmocktxhash"), + writeContract: jest.fn().mockResolvedValue("0xdepositTxHash"), })), http: jest.fn(), formatUnits: jest.fn().mockImplementation((value, decimals) => { @@ -45,14 +45,17 @@ jest.mock("../../network", () => { "ethereum-mainnet": { id: 1, name: "Ethereum", + network: "mainnet", }, optimism: { id: 10, name: "Optimism", + network: "optimism", }, "base-sepolia": { id: 84532, name: "Base Sepolia", + network: "base-sepolia", }, }, CHAIN_ID_TO_NETWORK_ID: { @@ -74,6 +77,8 @@ const defaultClientImplementation = () => ({ getSupportedChains: jest.fn().mockResolvedValue([ { chainId: 1, // Ethereum + name: "Ethereum", + network: "mainnet", inputTokens: [ { symbol: "ETH", @@ -89,6 +94,8 @@ const defaultClientImplementation = () => ({ }, { chainId: 10, // Optimism + name: "Optimism", + network: "optimism", inputTokens: [ { symbol: "ETH", @@ -133,16 +140,33 @@ const defaultClientImplementation = () => ({ }); // Set the default implementation -mockCreateAcrossClient.mockImplementation(defaultClientImplementation); +mockCreateAcrossClient.mockImplementation(() => { + const client = defaultClientImplementation(); + // Add the chains property to match what the code expects + return { + ...client, + chains: [ + { + id: 1, + name: "Ethereum", + network: "mainnet", + }, + { + id: 10, + name: "Optimism", + network: "optimism", + }, + ], + }; +}); // Mock the isTestnet function jest.mock("./utils", () => ({ - isTestnet: jest.fn().mockReturnValue(false), + isAcrossSupportedTestnet: jest.fn().mockReturnValue(false), })); describe("Across Action Provider", () => { const MOCK_PRIVATE_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; - const MOCK_NETWORK_ID = "ethereum-mainnet"; const MOCK_INPUT_TOKEN_SYMBOL = "ETH"; const MOCK_AMOUNT = "1.0"; const MOCK_DESTINATION_CHAIN_ID = "10"; // Optimism @@ -156,7 +180,25 @@ describe("Across Action Provider", () => { beforeEach(() => { jest.clearAllMocks(); // Reset to default implementation - mockCreateAcrossClient.mockImplementation(defaultClientImplementation); + mockCreateAcrossClient.mockImplementation(() => { + const client = defaultClientImplementation(); + // Add the chains property to match what the code expects + return { + ...client, + chains: [ + { + id: 1, + name: "Ethereum", + network: "mainnet", + }, + { + id: 10, + name: "Optimism", + network: "optimism", + }, + ], + }; + }); mockPublicClient = { getBalance: jest.fn().mockResolvedValue(BigInt("2000000000000000000")), // 2 ETH @@ -168,13 +210,19 @@ describe("Across Action Provider", () => { mockWallet = { getAddress: jest.fn().mockReturnValue(MOCK_RECIPIENT), - sendTransaction: jest.fn(), + sendTransaction: jest.fn().mockResolvedValue("0xmocktxhash"), waitForTransactionReceipt: jest.fn(), + getNetwork: jest.fn().mockReturnValue({ + chainId: "1", // Ethereum mainnet + networkId: "ethereum-mainnet", + protocolFamily: "evm", + }), + getBalance: jest.fn().mockResolvedValue(BigInt("2000000000000000000")), // 2 ETH + readContract: jest.fn().mockResolvedValue(BigInt("2000000000000000000")), // 2 ETH/USDC } as unknown as jest.Mocked; actionProvider = acrossActionProvider({ privateKey: MOCK_PRIVATE_KEY, - networkId: MOCK_NETWORK_ID, }); }); @@ -193,7 +241,7 @@ describe("Across Action Provider", () => { // Verify the SDK interactions and response expect(response).toContain("Successfully deposited tokens"); expect(response).toContain(`Token: ${MOCK_INPUT_TOKEN_SYMBOL}`); - expect(response).toContain("Transaction Hash for deposit: 0xmocktxhash"); + expect(response).toContain("Transaction Hash for deposit: 0xdepositTxHash"); }); it("should successfully bridge ERC20 tokens", async () => { @@ -205,13 +253,10 @@ describe("Across Action Provider", () => { maxSplippage: MOCK_MAX_SLIPPAGE, }; - // Mock the wallet client for the approval transaction - (createWalletClient as jest.Mock).mockReturnValue({ - writeContract: jest - .fn() - .mockResolvedValueOnce("0xapprovalTxHash") // First call for approval - .mockResolvedValueOnce("0xdepositTxHash"), // Second call for deposit - }); + // Set up mock for approval and deposit transactions + mockWallet.sendTransaction + .mockResolvedValueOnce("0xapprovalTxHash") + .mockResolvedValueOnce("0xdepositTxHash"); const response = await actionProvider.bridgeToken(mockWallet, args); @@ -285,7 +330,8 @@ describe("Across Action Provider", () => { it("should handle errors in bridging", async () => { const error = new Error("Insufficient balance"); - (mockPublicClient.getBalance as jest.Mock).mockRejectedValue(error); + mockWallet.getBalance.mockRejectedValueOnce(error); + mockWallet.sendTransaction.mockRejectedValueOnce(error); const args = { inputTokenSymbol: MOCK_INPUT_TOKEN_SYMBOL, diff --git a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts index 7818514a1..6a4de7e5f 100644 --- a/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts +++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts @@ -1,20 +1,11 @@ import { z } from "zod"; -import { - parseUnits, - Hex, - createWalletClient, - http, - Chain, - formatUnits, - PublicClient, - createPublicClient, -} from "viem"; +import { parseUnits, Hex, createWalletClient, http, formatUnits, encodeFunctionData } from "viem"; import { ActionProvider } from "../actionProvider"; import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN, getChain } from "../../network"; import { CreateAction } from "../actionDecorator"; import { BridgeTokenSchema, CheckDepositStatusSchema } from "./schemas"; import { EvmWalletProvider } from "../../wallet-providers"; -import { isTestnet } from "./utils"; +import { isAcrossSupportedTestnet } from "./utils"; import { privateKeyToAccount } from "viem/accounts"; import { abi as ERC20_ABI } from "../erc20/constants"; /** @@ -22,13 +13,9 @@ import { abi as ERC20_ABI } from "../erc20/constants"; */ export interface AcrossActionProviderConfig { /** - * Private key of the signer that (co-)owns the Safe. + * Private key of the wallet provider */ privateKey: string; - /** - * Network ID, for example "base-sepolia" or "ethereum-mainnet". - */ - networkId?: string; } /** @@ -36,8 +23,6 @@ export interface AcrossActionProviderConfig { */ export class AcrossActionProvider extends ActionProvider { #privateKey: string; - #chain: Chain; - #publicClient: PublicClient; /** * Constructor for the AcrossActionProvider. * @@ -48,14 +33,6 @@ export class AcrossActionProvider extends ActionProvider { this.#privateKey = config.privateKey; const account = privateKeyToAccount(this.#privateKey as Hex); if (!account) throw new Error("Invalid private key"); - - this.#chain = NETWORK_ID_TO_VIEM_CHAIN[config.networkId || "base-sepolia"]; - if (!this.#chain) throw new Error(`Unsupported network: ${config.networkId}`); - - this.#publicClient = createPublicClient({ - chain: this.#chain, - transport: http(), - }); } /** @@ -69,18 +46,20 @@ export class AcrossActionProvider extends ActionProvider { name: "bridge_token", description: ` This tool will bridge tokens from the current chain to another chain using the Across Protocol. - Supports testnet to testnet (e.g. ethereum-sepolia to base-sepolia) or mainnet to mainnet (e.g. ethereum-mainnet to base-mainnet) bridging. It takes the following inputs: - - destinationChainId: The chain ID of the destination chain (e.g. 84532 for base-sepolia, 1 for ethereum-mainnet) - - inputTokenSymbol: The symbol of the token to bridge (e.g., 'ETH', 'USDC') + - destinationChainId: The chain ID of the destination chain (e.g. 8453 for base-mainnet) + - inputTokenSymbol: The symbol of the token to bridge (e.g. 'ETH', 'USDC') - amount: The amount of tokens to bridge in whole units (e.g. 1.5 WETH, 10 USDC) - recipient: (Optional) The recipient address on the destination chain (defaults to sender) - maxSplippage: (Optional) The maximum slippage percentage (defaults to 1.5%) + Important notes: - - Origin chain is the currently connected chain of the wallet provider (e.g. base-sepolia, ethereum-mainnet) - - Testnet deposits are not be refunded if not filled on destination chain + - Origin chain is the currently connected chain of the wallet provider + - Supports cross-chain transfers between EVM-compatible chains for both mainnets and test networks + - Testnet deposits are not refunded if not filled on destination chain - Ensure sufficient balance of the input token before bridging + - Returns deposit ID that can be used to check the status of the deposit `, schema: BridgeTokenSchema, }) @@ -94,7 +73,14 @@ export class AcrossActionProvider extends ActionProvider { const createAcrossClient = acrossModule.createAcrossClient; // Get recipient address if provided, otherwise use sender - const recipient = (args.recipient || walletProvider.getAddress()) as Hex; + const address = walletProvider.getAddress() as Hex; + const recipient = (args.recipient || address) as Hex; + + // Get origin chain + const originChain = getChain(walletProvider.getNetwork().chainId as string); + if (!originChain) { + throw new Error(`Unsupported origin chain: ${walletProvider.getNetwork()}`); + } // Get destination chain const destinationNetworkId = CHAIN_ID_TO_NETWORK_ID[Number(args.destinationChainId)]; @@ -103,40 +89,52 @@ export class AcrossActionProvider extends ActionProvider { throw new Error(`Unsupported destination chain: ${args.destinationChainId}`); } + // Sanity checks + if (originChain.id === destinationChain.id) { + throw new Error("Origin and destination chains cannot be the same"); + } + const useTestnet = isAcrossSupportedTestnet(originChain.id); + if (useTestnet !== isAcrossSupportedTestnet(destinationChain.id)) { + throw new Error(`Cross-chain transfers between ${originChain.name} and ${destinationChain.name} are not supported. + Origin and destination chains must either be both testnets or both mainnets.`); + } + // Create wallet client const account = privateKeyToAccount(this.#privateKey as Hex); + if (account.address !== walletProvider.getAddress()) { + throw new Error("Private key does not match wallet provider address"); + } const walletClient = createWalletClient({ account, - chain: this.#chain, + chain: originChain, transport: http(), }); // Create Across client const acrossClient = createAcrossClient({ - //integratorId: INTEGRATOR_ID, - chains: [this.#chain, destinationChain], - useTestnet: isTestnet(this.#chain.id), + chains: [originChain, destinationChain], + useTestnet, }); // Get chain details to find token information const chainDetails = await acrossClient.getSupportedChains({}); - const originChainDetails = chainDetails.find(chain => chain.chainId === this.#chain.id); + const originChainDetails = chainDetails.find(chain => chain.chainId === originChain.id); if (!originChainDetails) { - throw new Error(`Origin chain ${this.#chain.id} not supported by Across Protocol`); + throw new Error(`Origin chain ${originChain.id} not supported by Across Protocol`); } // Find token by symbol on the origin chain const inputTokens = originChainDetails.inputTokens; if (!inputTokens || inputTokens.length === 0) { - throw new Error(`No input tokens available on chain ${this.#chain.id}`); + throw new Error(`No input tokens available on chain ${originChain.id}`); } const tokenInfo = inputTokens.find( token => token.symbol.toUpperCase() === args.inputTokenSymbol.toUpperCase(), ); if (!tokenInfo) { throw new Error( - `Token ${args.inputTokenSymbol} not found on chain ${this.#chain.id}. Available tokens: ${inputTokens.map(t => t.symbol).join(", ")}`, + `Token ${args.inputTokenSymbol} not found on chain ${originChain.id}. Available tokens: ${inputTokens.map(t => t.symbol).join(", ")}`, ); } @@ -149,9 +147,7 @@ export class AcrossActionProvider extends ActionProvider { const isNative = args.inputTokenSymbol.toUpperCase() === "ETH"; if (isNative) { // Check native ETH balance - const ethBalance = await this.#publicClient.getBalance({ - address: account.address, - }); + const ethBalance = await walletProvider.getBalance(); if (ethBalance < inputAmount) { throw new Error( `Insufficient balance. Requested to bridge ${formatUnits(inputAmount, decimals)} ${args.inputTokenSymbol} but balance is only ${formatUnits(ethBalance, decimals)} ${args.inputTokenSymbol}`, @@ -159,11 +155,11 @@ export class AcrossActionProvider extends ActionProvider { } } else { // Check ERC20 token balance - const tokenBalance = (await this.#publicClient.readContract({ + const tokenBalance = (await walletProvider.readContract({ address: inputToken, abi: ERC20_ABI, functionName: "balanceOf", - args: [account.address], + args: [address], })) as bigint; if (tokenBalance < inputAmount) { throw new Error( @@ -174,30 +170,23 @@ export class AcrossActionProvider extends ActionProvider { // Get available routes const routeInfo = await acrossClient.getAvailableRoutes({ - originChainId: this.#chain.id, + originChainId: originChain.id, destinationChainId: destinationChain.id, originToken: inputToken, }); // Select the appropriate route for native ETH or ERC20 token - let route; - for (let i = 0; i < routeInfo.length; i++) { - if (routeInfo[i].isNative === isNative) { - route = routeInfo[i]; - break; - } - } - + const route = routeInfo.find(route => route.isNative === isNative); if (!route) { throw new Error( - `No routes available from chain ${this.#chain.name} to chain ${destinationChain.name} for token ${args.inputTokenSymbol}`, + `No routes available from chain ${originChain.name} to chain ${destinationChain.name} for token ${args.inputTokenSymbol}`, ); } // Get quote const quote = await acrossClient.getQuote({ route, - inputAmount: inputAmount, + inputAmount, recipient, }); @@ -236,14 +225,15 @@ export class AcrossActionProvider extends ActionProvider { //Approve ERC20 token if needed let approvalTxHash; if (!isNative) { - const approvalAmount = quote.deposit.inputAmount; - approvalTxHash = await walletClient.writeContract({ - address: inputToken, - abi: ERC20_ABI, - functionName: "approve", - args: [quote.deposit.spokePoolAddress, approvalAmount], + approvalTxHash = await walletProvider.sendTransaction({ + to: inputToken, + data: encodeFunctionData({ + abi: ERC20_ABI, + functionName: "approve", + args: [quote.deposit.spokePoolAddress, quote.deposit.inputAmount], + }), }); - await this.#publicClient.waitForTransactionReceipt({ hash: approvalTxHash }); + await walletProvider.waitForTransactionReceipt(approvalTxHash); } // Simulate the deposit transaction @@ -258,12 +248,12 @@ export class AcrossActionProvider extends ActionProvider { // Wait for tx to be mined const { depositId } = await acrossClient.waitForDepositTx({ transactionHash, - originChainId: this.#chain.id, + originChainId: originChain.id, }); return ` Successfully deposited tokens: -- From: Chain ${this.#chain.id} (${this.#chain.name}) +- From: Chain ${originChain.id} (${originChain.name}) - To: Chain ${destinationChain.id} (${destinationChain.name}) - Token: ${args.inputTokenSymbol} (${inputToken}) - Input Amount: ${formattedInfo.inputAmount} ${args.inputTokenSymbol} @@ -300,9 +290,9 @@ ${!isNative ? `- Transaction Hash for approval: ${approvalTxHash}\n` : ""} walletProvider: EvmWalletProvider, args: z.infer, ): Promise { - const originChainId = Number(args.originChainId) || this.#chain.id; + const originChainId = Number(args.originChainId) || Number(walletProvider.getNetwork().chainId); - if (isTestnet(originChainId)) { + if (isAcrossSupportedTestnet(originChainId)) { throw new Error( "Checking deposit status on testnets is currently not supported by the Across API", ); diff --git a/typescript/agentkit/src/action-providers/across/utils.ts b/typescript/agentkit/src/action-providers/across/utils.ts index 08d7fbd1c..f02fe2a91 100644 --- a/typescript/agentkit/src/action-providers/across/utils.ts +++ b/typescript/agentkit/src/action-providers/across/utils.ts @@ -1,10 +1,10 @@ /** - * Checks if a chain ID corresponds to a testnet network + * Checks if a chain ID corresponds to a testnet network supported by Across * * @param chainId - The blockchain network chain ID - * @returns true if the chain ID corresponds to a testnet, false otherwise + * @returns true if the chain ID corresponds to a testnet network supported by Across, false otherwise */ -export function isTestnet(chainId: number): boolean { +export function isAcrossSupportedTestnet(chainId: number): boolean { // List of testnet chain IDs const testnetChainIds = [ 11155111, // Sepolia diff --git a/typescript/examples/langchain-cdp-chatbot/chatbot.ts b/typescript/examples/langchain-cdp-chatbot/chatbot.ts index aa571951f..182c82517 100644 --- a/typescript/examples/langchain-cdp-chatbot/chatbot.ts +++ b/typescript/examples/langchain-cdp-chatbot/chatbot.ts @@ -10,7 +10,6 @@ import { pythActionProvider, openseaActionProvider, alloraActionProvider, - acrossActionProvider, } from "@coinbase/agentkit"; import { getLangChainTools } from "@coinbase/agentkit-langchain"; import { HumanMessage } from "@langchain/core/messages"; @@ -123,10 +122,6 @@ async function initializeAgent() { ] : []), alloraActionProvider(), - acrossActionProvider({ - networkId: walletProvider.getNetwork().networkId, - privateKey: await (await walletProvider.getWallet().getDefaultAddress()).export(), - }), ], }); From 53b2dbaab7fbd287695c1f8d27216701b9bcc2ff Mon Sep 17 00:00:00 2001 From: Philippe d'Argent Date: Tue, 25 Mar 2025 19:18:37 +0100 Subject: [PATCH 6/6] improved across readme/changelog --- typescript/.changeset/neat-emus-exist.md | 4 +++- typescript/agentkit/src/action-providers/across/README.md | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/typescript/.changeset/neat-emus-exist.md b/typescript/.changeset/neat-emus-exist.md index 61fcc8957..8ab082b5f 100644 --- a/typescript/.changeset/neat-emus-exist.md +++ b/typescript/.changeset/neat-emus-exist.md @@ -2,4 +2,6 @@ "@coinbase/agentkit": minor --- -Added a new action provider to interact with Across protocol +Added AcrossActionProvider to allow bridging tokens using the Across protocol +- `bridge_token` action to bridge native and ERC20 tokens +- `check_deposit_status` action to check the status of bridge deposits diff --git a/typescript/agentkit/src/action-providers/across/README.md b/typescript/agentkit/src/action-providers/across/README.md index a7b2f1a55..3ca9ffb9f 100644 --- a/typescript/agentkit/src/action-providers/across/README.md +++ b/typescript/agentkit/src/action-providers/across/README.md @@ -34,6 +34,13 @@ The Across provider supports cross-chain transfers between EVM-compatible chains - Base Sepolia to Ethereum Sepolia The status of bridge deposit can only be checked on Mainnets. +## ⚠️ Warning + +Before briding funds, always make sure that you have access to the destination address on the destination chain! + +Note that when using a CDP server wallet with CdpWalletProvider, a new wallet address is generated for each chain. This means that if you bridge tokens to the sender's address on one chain, you may not be able to access those funds on the destination chain within AgentKit since a different wallet address will be used. +While you can export the private key to access funds in external wallets, it's recommended to either use ViemWalletProvider for consistent addresses across chains or ensure the destination address is different from the sender's address. + ## Configuration The provider requires the following configuration: