Repository: coinbase/agentkit
Pull request #566: feat: add across action provider (ts)
Author: login: phdargen
Labels: name: documentation
description: Improvements or additions to documentation
name: action provider
description: New action provider
name: apple
name: typescript
Created: 2025-03-14T14:59:48Z | Merged: 2025-03-25T22:05:07Z

<!--
Thanks for contributing to AgentKit!
Please fill out the information below to help reviewers understand your changes.

Note: We require commit signing.
See here for instructions: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification
-->

## Description

Adds new action provider to perform cross-chain interactions via the Across protocol. 
Across SDK is added as new dependency.

Implements:
- bridge_token action to bridge from connected chain to another evm chain
-> supports native ETH and ERC-20 transfers (as long as supported by Across)
- check_deposit_status action to check fill status on destination chain (mainnets only)


## Tests

- [x] Added unit tests
- [x] Tested prompts

```
Chatbot: typescript/examples/langchain-cdp-chatbot/chatbot.ts
Network: Sepolia
Setup: Fauceted with 1 USDC and ETH

Prompt: bridge to chain 84532, amount 0.001 ETH, receipienct 0xb7f17bF94DBa50077be037CDc279a868f82D86C2

-------------------
Successfully deposited tokens:
- From: Chain 11155111 (Sepolia)
- To: Chain 84532 (Base Sepolia)
- Token: ETH (0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14)
- Input Amount: 0.001 ETH
- Output Amount: 0.000990746891309023 ETH
- Recipient: 0xb7f17bF94DBa50077be037CDc279a868f82D86C2
- Transaction Hash for deposit: 0x0f2c064c36991d9af1f0d81d347d5e16f61b6ea1302f430c1a57b07bec743cea
- Deposit ID: 1002457
-------------------

Prompt: bridge to chain 84532, amount 0.1 usdc, receipienct 0xb7f17bF94DBa50077be037CDc279a868f82D86C2, max slippage 3%
The bridging transaction was successful! Here are the details:

- **From Chain:** 11155111 (Sepolia)
- **To Chain:** 84532 (Base Sepolia)
- **Token:** USDC
- **Input Amount:** 0.1 USDC
- **Output Amount:** 0.098182 USDC
- **Recipient:** 0xb7f17bF94DBa50077be037CDc279a868f82D86C2
- **Transaction Hash for Approval:** 0x33575547e8031d3223e63dbc2e82fbc307b1391bd9c6379c8a91952b47024e92
- **Transaction Hash for Deposit:** 0x797f7d27e820f1832f994b6a6218118803fbc500f18caa70179811b4be21b2aa

Prompt: check status of deposit 1349975 with originChainId 137
-------------------
{
  "status": "filled",
  "depositTxInfo": {
    "txHash": "0x784f3cf234ffc960d087c5c02b166d838f7a170b337a349a49b54be837fd8152",
    "chainId": 137,
    "chainName": "Polygon"
  },
  "fillTxInfo": {
    "txHash": "0x788835d45d1ad5bc339990b23d2e09756ca1b4c98a6246be3505fb1baaf573e6",
    "chainId": 8453,
    "chainName": "Base"
  },
  "depositRefundTxInfo": null
}
-------------------
The status of the deposit with ID **1349975** and originChainId **137** is **filled**. Here are the transaction details:

- **Deposit Transaction Hash (Polygon)**: `0x784f3cf234ffc960d087c5c02b166d838f7a170b337a349a49b54be837fd8152`
  
- **Fill Transaction Hash (Base)**: `0x788835d45d1ad5bc339990b23d2e09756ca1b4c98a6246be3505fb1baaf573e6`

No refund transaction is related to this deposit.
-------------------

```

## Checklist

- [x] Added documentation to all relevant README.md files
- [x] Added a changelog entry

<!--
For instructions on adding documentation:
See here for TypeScript: https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING-TYPESCRIPT.md#documentation
and here for Python: https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING-PYTHON.md#documentation
-->

<!--
For instructions on adding changelog entries:
See here for TypeScript: https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING-TYPESCRIPT.md#changelog
and here for Python: https://github.com/coinbase/agentkit/blob/main/CONTRIBUTING-PYTHON.md#changelog
-->



Diff:
diff --git a/typescript/.changeset/neat-emus-exist.md b/typescript/.changeset/neat-emus-exist.md
new file mode 100644
index 000000000..8ab082b5f
--- /dev/null
+++ b/typescript/.changeset/neat-emus-exist.md
@@ -0,0 +1,7 @@
+---
+"@coinbase/agentkit": minor
+---
+
+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/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
 <details>
+<summary><strong>Across</strong></summary>
+<table width="100%">
+<tr>
+    <td width="200"><code>bridge_token</code></td>
+    <td width="768">Bridges tokens between supported chains using Across Protocol.</td>
+</tr>
+<tr>
+    <td width="200"><code>check_deposit_status</code></td>
+    <td width="768">Checks the status of a cross-chain bridge deposit on the Across Protocol (mainnet networks only).</td>
+</tr>
+</table>
+</details>
+<details>
 <summary><strong>Basename</strong></summary>
 <table width="100%">
 <tr>
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/README.md b/typescript/agentkit/src/action-providers/across/README.md
new file mode 100644
index 000000000..3ca9ffb9f
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/across/README.md
@@ -0,0 +1,51 @@
+# 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
+- `check_deposit_status`: Check the status of a cross-chain bridge deposit on 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 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.
+
+## ⚠️ 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:
+- `privateKey`: Private key of the wallet provider
+
+## 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..61374d0a3
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.test.ts
@@ -0,0 +1,436 @@
+import { acrossActionProvider } from "./acrossActionProvider";
+import { EvmWalletProvider } from "../../wallet-providers";
+import { Network } from "../../network";
+import { createPublicClient, 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("0xdepositTxHash"),
+    })),
+    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",
+        network: "mainnet",
+      },
+      optimism: {
+        id: 10,
+        name: "Optimism",
+        network: "optimism",
+      },
+      "base-sepolia": {
+        id: 84532,
+        name: "Base Sepolia",
+        network: "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
+      name: "Ethereum",
+      network: "mainnet",
+      inputTokens: [
+        {
+          symbol: "ETH",
+          address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
+          decimals: 18,
+        },
+        {
+          symbol: "USDC",
+          address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+          decimals: 6,
+        },
+      ],
+    },
+    {
+      chainId: 10, // Optimism
+      name: "Optimism",
+      network: "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: [],
+    },
+  }),
+  waitForDepositTx: jest.fn().mockResolvedValue({
+    depositId: "123456",
+  }),
+});
+
+// Set the default implementation
+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", () => ({
+  isAcrossSupportedTestnet: jest.fn().mockReturnValue(false),
+}));
+
+describe("Across Action Provider", () => {
+  const MOCK_PRIVATE_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
+  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<EvmWalletProvider>;
+  let actionProvider: ReturnType<typeof acrossActionProvider>;
+  let mockPublicClient: PublicClient;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    // Reset to default implementation
+    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
+      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().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<EvmWalletProvider>;
+
+    actionProvider = acrossActionProvider({
+      privateKey: MOCK_PRIVATE_KEY,
+    });
+  });
+
+  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: 0xdepositTxHash");
+    });
+
+    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,
+      };
+
+      // Set up mock for approval and deposit transactions
+      mockWallet.sendTransaction
+        .mockResolvedValueOnce("0xapprovalTxHash")
+        .mockResolvedValueOnce("0xdepositTxHash");
+
+      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: [],
+          },
+        }),
+        waitForDepositTx: jest.fn().mockResolvedValue({
+          depositId: "123456",
+        }),
+      }));
+
+      // 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");
+      mockWallet.getBalance.mockRejectedValueOnce(error);
+      mockWallet.sendTransaction.mockRejectedValueOnce(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("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 = {
+        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
new file mode 100644
index 000000000..6a4de7e5f
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
@@ -0,0 +1,365 @@
+import { z } from "zod";
+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 { isAcrossSupportedTestnet } 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 wallet provider
+   */
+  privateKey: string;
+}
+
+/**
+ * AcrossActionProvider provides actions for cross-chain bridging via Across Protocol.
+ */
+export class AcrossActionProvider extends ActionProvider<EvmWalletProvider> {
+  #privateKey: string;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  constructor(config: AcrossActionProviderConfig) {
+    super("across", []);
+    this.#privateKey = config.privateKey;
+    const account = privateKeyToAccount(this.#privateKey as Hex);
+    if (!account) throw new Error("Invalid private key");
+  }
+
+  /**
+   * 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 the current chain to another chain using the Across Protocol.
+
+    It takes the following inputs:
+    - 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 
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // Get recipient address if provided, otherwise use sender
+      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)];
+      const destinationChain = NETWORK_ID_TO_VIEM_CHAIN[destinationNetworkId];
+      if (!destinationChain) {
+        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: originChain,
+        transport: http(),
+      });
+
+      // Create Across client
+      const acrossClient = createAcrossClient({
+        chains: [originChain, destinationChain],
+        useTestnet,
+      });
+
+      // Get chain details to find token information
+      const chainDetails = await acrossClient.getSupportedChains({});
+      const originChainDetails = chainDetails.find(chain => chain.chainId === originChain.id);
+
+      if (!originChainDetails) {
+        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 ${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 ${originChain.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 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}`,
+          );
+        }
+      } else {
+        // Check ERC20 token balance
+        const tokenBalance = (await walletProvider.readContract({
+          address: inputToken,
+          abi: ERC20_ABI,
+          functionName: "balanceOf",
+          args: [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: originChain.id,
+        destinationChainId: destinationChain.id,
+        originToken: inputToken,
+      });
+
+      // Select the appropriate route for native ETH or ERC20 token
+      const route = routeInfo.find(route => route.isNative === isNative);
+      if (!route) {
+        throw new Error(
+          `No routes available from chain ${originChain.name} to chain ${destinationChain.name} for token ${args.inputTokenSymbol}`,
+        );
+      }
+
+      // Get quote
+      const quote = await acrossClient.getQuote({
+        route,
+        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) {
+        approvalTxHash = await walletProvider.sendTransaction({
+          to: inputToken,
+          data: encodeFunctionData({
+            abi: ERC20_ABI,
+            functionName: "approve",
+            args: [quote.deposit.spokePoolAddress, quote.deposit.inputAmount],
+          }),
+        });
+        await walletProvider.waitForTransactionReceipt(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);
+
+      // Wait for tx to be mined
+      const { depositId } = await acrossClient.waitForDepositTx({
+        transactionHash,
+        originChainId: originChain.id,
+      });
+
+      return `
+Successfully deposited tokens:
+- From: Chain ${originChain.id} (${originChain.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}
+- Deposit ID: ${depositId}
+        `;
+    } catch (error) {
+      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<typeof CheckDepositStatusSchema>,
+  ): Promise<string> {
+    const originChainId = Number(args.originChainId) || Number(walletProvider.getNetwork().chainId);
+
+    if (isAcrossSupportedTestnet(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.
+   *
+   * @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 = (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
new file mode 100644
index 000000000..e69de29bb
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..4801f6f28
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/across/index.ts
@@ -0,0 +1 @@
+export * from "./acrossActionProvider";
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..4a6dd228b
--- /dev/null
+++ b/typescript/agentkit/src/action-providers/across/schemas.ts
@@ -0,0 +1,45 @@
+import { z } from "zod";
+
+/**
+ * Input schema for bridge token action.
+ */
+export const BridgeTokenSchema = z
+  .object({
+    destinationChainId: z
+      .string()
+      .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')")
+      .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");
+
+/**
+ * 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");
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..f02fe2a91
--- /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 supported by Across
+ *
+ * @param chainId - The blockchain network chain ID
+ * @returns true if the chain ID corresponds to a testnet network supported by Across, false otherwise
+ */
+export function isAcrossSupportedTestnet(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/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/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",


Reviews:
user: login: KanishkKhurana
body: left one comment around manual txn simulation and executing the deposit txn. 

overall, really good job from an Across Protocol integration POV
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-17T11:27:05Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T19:26:37Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T19:57:38Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T19:58:15Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T20:05:55Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T20:48:31Z
user: login: CarsonRoscoe
body: Please add a entry into `typescript/agentkit/README.md`'s `## Action Providers` section, so this action is discoverable via readmes

Note: entries are in alphabetical order
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:03:41Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:11:30Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:18:24Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:29:08Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:31:25Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:31:44Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:32:05Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:34:09Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:07:53Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:29:09Z
user: login: CarsonRoscoe
body: re: Wallet question

Yes, the current version of the CDP server wallet exports to 1 wallet per chain. The same API keys work on all the networks, however it will generate a different address/private key per network.

We understand the current wallet persistence approach is lacking. We're exploring other approaches, for example in the python side, if you run `pipx run create-onchain-agent --beginnner` to generate a chatbot, [you can see here](https://github.com/coinbase/agentkit/blob/main/python/create-onchain-agent/templates/beginner/wallet.py) that we handle the wallets on a per-network basis now.

We will be refactoring all the examples shortly to use a similar approach in order to make sure that its a different wallet generated per network.

The temporary solution to the question regarding networks is to rename your file when you change networks. i.e. rename to`wallet_data_base_sepolia.txt` when you switch networks and generate a new wallet, and then rename back when you go back. After the refactor, this process will happen automatically for you.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:35:11Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T14:50:36Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T16:09:56Z
user: login: phdargen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T18:23:25Z
user: login: CarsonRoscoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T21:38:21Z
user: login: CarsonRoscoe
body: LGTM!

Nicely done
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T22:03:46Z

Review comments:
diff_hunk: @@ -0,0 +1,285 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} from "viem";
+import { ActionProvider } from "../actionProvider";
+import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN } from "../../network";
+import { CreateAction } from "../actionDecorator";
+import { BridgeTokenSchema } from "./schemas";
+import { EvmWalletProvider } from "../../wallet-providers";
+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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    name: "bridge_token",
+    description: `
+    This tool will bridge tokens from the current chain to another chain using the Across Protocol.
+    
+    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')
+    - 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
+    `,
+    schema: BridgeTokenSchema,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // 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,
+        });
+        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],
+        });
+        await this.#publicClient.waitForTransactionReceipt({ hash: approvalTxHash });
+      }
+
+      // Simulate the deposit transaction
+      const { request } = await acrossClient.simulateDepositTx({
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: KanishkKhurana
body: Diving deep into the logic behind this, are we simulating txn and executing the deposit fn manually because there's no way to trigger a wallet extension ? 
I ask because we have the executeQuote function which will be able to wrap complexities around txn simulation and executing the deposit txn. 

overall this is a stellar job. thank you for putting in the effort here!
created_at: 2025-03-17T11:26:06Z
author_association: NONE
diff_hunk: @@ -0,0 +1,285 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} from "viem";
+import { ActionProvider } from "../actionProvider";
+import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN } from "../../network";
+import { CreateAction } from "../actionDecorator";
+import { BridgeTokenSchema } from "./schemas";
+import { EvmWalletProvider } from "../../wallet-providers";
+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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    name: "bridge_token",
+    description: `
+    This tool will bridge tokens from the current chain to another chain using the Across Protocol.
+    
+    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')
+    - 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
+    `,
+    schema: BridgeTokenSchema,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // 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,
+        });
+        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],
+        });
+        await this.#publicClient.waitForTransactionReceipt({ hash: approvalTxHash });
+      }
+
+      // Simulate the deposit transaction
+      const { request } = await acrossClient.simulateDepositTx({
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: phdargen
body: Hi @KanishkKhurana , thank you for the feedback!

I initially used executeQuote which would indeed simplify the process. This works fine until progress.step === "deposit" is succesful but then throws errors in the progress.step === "fill" step. I suspect this is because no RPC was available for the destination chain.

Now thinking about it again, it might work when we just add the destination chain to the viem client. It would certainly be nice to also return the fill txHash. A bit busy atm but will give it a try in the next days and get back to you
created_at: 2025-03-17T19:26:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,285 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} from "viem";
+import { ActionProvider } from "../actionProvider";
+import { CHAIN_ID_TO_NETWORK_ID, Network, NETWORK_ID_TO_VIEM_CHAIN } from "../../network";
+import { CreateAction } from "../actionDecorator";
+import { BridgeTokenSchema } from "./schemas";
+import { EvmWalletProvider } from "../../wallet-providers";
+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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    name: "bridge_token",
+    description: `
+    This tool will bridge tokens from the current chain to another chain using the Across Protocol.
+    
+    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')
+    - 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
+    `,
+    schema: BridgeTokenSchema,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // 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,
+        });
+        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],
+        });
+        await this.#publicClient.waitForTransactionReceipt({ hash: approvalTxHash });
+      }
+
+      // Simulate the deposit transaction
+      const { request } = await acrossClient.simulateDepositTx({
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: phdargen
body: So executeQuote would work if provided with an additional  publicClient for the destinationChain. 

However, I think it is best to stick to the current manual tx implementation to avoid some edge cases where the fill transaction is delayed significantly. The agent would then be stuck and not return any response for an extended period of time. 

In my last commit, I added the depositID to the 'bridge_token' tool output 
and implemented a 'check_deposit_status' tool to check the status for a given depositID
created_at: 2025-03-24T19:57:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
+        });
+        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++) {
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: CarsonRoscoe
body: nit: Can be rewritten as `const route = routeInfo.find(route => route.isNative === isNative);`
created_at: 2025-03-24T19:58:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
+        });
+        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({
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: CarsonRoscoe
body: I'd prefer to minimize our reliance on `walletClient` and `publicClient` to only uses them for cases `walletProvider` cannot yet handle. Those cases are a signal to us on where there are gaps in our offering that need improvement

For things such as a writeContract call or waiting for a transaction's receipt, we do currently support those features out of the box, so please use them here when appropriate. I believe the following will get the job done.

```ts
approvalTxHash = await walletProvider.sendTransaction({
  to: inputToken,
  data: encodeFunctionData({
    abi: ERC20_ABI,
    functionName: "approve",
    args: [quote.deposit.spokePoolAddress, approvalAmount],
  }),
});
await walletProvider.waitForTransactionReceipt(approvalTxHash);
```
created_at: 2025-03-24T20:05:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: CarsonRoscoe
body: nit: capture `walletProvider.getAddress() as Address` as a variable above, and use that in place of `account.address` in both places it is referenced
created_at: 2025-03-24T20:48:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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 {
path: typescript/agentkit/src/action-providers/across/utils.ts
user: login: CarsonRoscoe
body: I have concerns about this being conflated with AgentKit supported networks, however it is valid that Across has it's own list of supported chains that will differ from ours. 

As long as the distinction is clear, I am willing to keep it. Can you rename this to something that will make that distinction clear, such as `isAcrossSupportedTestnet`?
created_at: 2025-03-24T21:11:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +123,10 @@ async function initializeAgent() {
             ]
           : []),
         alloraActionProvider(),
+        acrossActionProvider({
path: typescript/examples/langchain-cdp-chatbot/chatbot.ts
user: login: CarsonRoscoe
body: Please remove
created_at: 2025-03-24T21:18:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
+        });
+        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++) {
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: phdargen
body: changed 
created_at: 2025-03-25T10:29:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
+        });
+        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({
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: phdargen
body: Ok I removed publicClient entirely and use walletProvider where possible.
The walletClient is still needed to simulate and execute the depositTX
created_at: 2025-03-25T10:31:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: phdargen
body: done
created_at: 2025-03-25T10:31:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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 {
path: typescript/agentkit/src/action-providers/across/utils.ts
user: login: phdargen
body: changed to isAcrossSupportedTestnet
created_at: 2025-03-25T10:32:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +123,10 @@ async function initializeAgent() {
             ]
           : []),
         alloraActionProvider(),
+        acrossActionProvider({
path: typescript/examples/langchain-cdp-chatbot/chatbot.ts
user: login: phdargen
body: ok removed. Any particular reason not to add it to the example script?
created_at: 2025-03-25T10:34:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,375 @@
+import { z } from "zod";
+import {
+  parseUnits,
+  Hex,
+  createWalletClient,
+  http,
+  Chain,
+  formatUnits,
+  PublicClient,
+  createPublicClient,
+} 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 { 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<EvmWalletProvider> {
+  #privateKey: string;
+  #chain: Chain;
+  #publicClient: PublicClient;
+  /**
+   * Constructor for the AcrossActionProvider.
+   *
+   * @param config - The configuration options for the AcrossActionProvider.
+   */
+  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.
+   */
+  @CreateAction({
+    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')
+    - 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,
+  })
+  async bridgeToken(
+    walletProvider: EvmWalletProvider,
+    args: z.infer<typeof BridgeTokenSchema>,
+  ): Promise<string> {
+    try {
+      // Use dynamic import to get the Across SDK
+      const acrossModule = await import("@across-protocol/app-sdk");
+      const createAcrossClient = acrossModule.createAcrossClient;
+
+      // 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 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,
+        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,
+        });
+        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({
path: typescript/agentkit/src/action-providers/across/acrossActionProvider.ts
user: login: CarsonRoscoe
body: Understood, thank you
created_at: 2025-03-25T13:07:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +123,10 @@ async function initializeAgent() {
             ]
           : []),
         alloraActionProvider(),
+        acrossActionProvider({
path: typescript/examples/langchain-cdp-chatbot/chatbot.ts
user: login: CarsonRoscoe
body: Thanks for removing it! The main reason is that the example script is meant to stay minimal for new users. It should focus on basic, common actions like wallet, faucet, and ERC20. Actions which are understood by the majority of web2 AI builders and web3 builders exploring AI. Adding more advanced or third-party actions (like bridging) adds a level of domain knowledge that we don't want in the example showcases.

Also, if we accepted every new action in the example, it’d quickly get bloated and hard to maintain. We are currently reviewing which should be the defaults and will be updating the examples to our list in the near future.

That said, what we do want is for builders who want bridging to easily discover your action, and we understand this is an area that we can improve. I'm open to discussions on where you feel we should be highlighting all the actions
created_at: 2025-03-25T13:29:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +123,10 @@ async function initializeAgent() {
             ]
           : []),
         alloraActionProvider(),
+        acrossActionProvider({
path: typescript/examples/langchain-cdp-chatbot/chatbot.ts
user: login: phdargen
body: Ok fair enough. 

Maybe some info how to add particular actions could then be added to agentkit or chatbot README as sometimes additional configs are needed. Something like this that can be easily copied and pasted if someone wants to add this action:
```
        acrossActionProvider({
          privateKey: await (await walletProvider.getWallet().getDefaultAddress()).export(),
        }),
```
created_at: 2025-03-25T14:50:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+---
+"@coinbase/agentkit": minor
+---
+
+Added a new action provider to interact with Across protocol
path: typescript/.changeset/neat-emus-exist.md
user: login: CarsonRoscoe
body: nit: Maybe we should add a little more info so people going through the protocol have more context.

e.g. something like:
```
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
```
created_at: 2025-03-25T16:09:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+---
+"@coinbase/agentkit": minor
+---
+
+Added a new action provider to interact with Across protocol
path: typescript/.changeset/neat-emus-exist.md
user: login: phdargen
body: Ok changed it
created_at: 2025-03-25T18:23:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,6 +123,10 @@ async function initializeAgent() {
             ]
           : []),
         alloraActionProvider(),
+        acrossActionProvider({
path: typescript/examples/langchain-cdp-chatbot/chatbot.ts
user: login: CarsonRoscoe
body: Good point. We'll consider the best ways to standardize this
created_at: 2025-03-25T21:38:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: cb-heimdall
created_at: 2025-03-14T14:59:59Z
body: <h1>✅ Heimdall Review Status</h1>
<table>
<tr>
<th>Requirement</th>
<th>Status</th>
<th>More Info</th>
</tr>
<tr>
<td>Reviews</td>
<td>
✅
<code>1/1</code>
</td>
<td>
<details>
<summary>Denominator calculation</summary>
<table>
<tr>
<td>
<details>
<summary>Show calculation</summary>
<table>
<tr>
<td><code>1</code> if user is bot</td>
<td>0</td>
</tr>
<tr>
<td><code>1</code> if user is external</td>
<td>0</td>
</tr>
<tr>
<td>
From <code>.codeflow.yml</code>
</td>
<td>1</td>
</tr>
<tr>
<td>
Additional review requirements
<details>
<summary>Show calculation</summary>
<table>
<tr>
<td><strong>Max</strong></td>
<td><strong>0</strong></td>
</tr>
</table>
</details>
</td>
<td>0</td>
</tr>
<tr>
<td>
From <code>CODEOWNERS</code>
</td>
<td>0</td>
</tr>
<tr>
<td>Global minimum</td>
<td>0</td>
</tr>
<tr>
<td><strong>Max</strong></td>
<td>
<strong>
1
</strong>
</td>
</tr>
</table>
</details>
</td>
<td>1</td>
</tr>
<tr>
<td><code>1</code> if commit is unverified</td>
<td>0</td>
</tr>
<tr>
<td><strong>Sum</strong></td>
<td><strong>1</strong></td>
</tr>
</table>
</details>
</td>
</tr>
</table>
<!-- CODEOWNERS & Bar Raisers -->
<!-- Power Rangers -->
<!-- Links -->

author_association: NONE
user: login: phdargen
created_at: 2025-03-25T10:34:44Z
body: > Please add a entry into `typescript/agentkit/README.md`'s `## Action Providers` section, so this action is discoverable via readmes
> 
> Note: entries are in alphabetical order

Added entry to README
author_association: CONTRIBUTOR
user: login: phdargen
created_at: 2025-03-25T10:48:39Z
body: Hi @CarsonRoscoe, thanks for the feedback, I implemented the changes.

I have one question about the wallets created by CdpWalletProvider and saved in wallet_data.txt when using the chatbot.ts example script. Is there an easy way to use this wallet on a different chain? It seems editing the networkId in wallet_data.txt and reimporting has no effect. You can export the private key of the cdpWallet but then importing only supports mnemonicPhrase.

If the chain cannot be easily switched, do we need a warning for wallets created with CdpWalletProvider or do not default the recipient address to sender? 
author_association: CONTRIBUTOR
user: login: phdargen
created_at: 2025-03-25T14:38:29Z
body: > re: Wallet question
> 
> Yes, the current version of the CDP server wallet exports to 1 wallet per chain. The same API keys work on all the networks, however it will generate a different address/private key per network.
> 
> We understand the current wallet persistence approach is lacking. We're exploring other approaches, for example in the python side, if you run `pipx run create-onchain-agent --beginnner` to generate a chatbot, [you can see here](https://github.com/coinbase/agentkit/blob/main/python/create-onchain-agent/templates/beginner/wallet.py) that we handle the wallets on a per-network basis now.
> 
> We will be refactoring all the examples shortly to use a similar approach in order to make sure that its a different wallet generated per network.
> 
> The temporary solution to the question regarding networks is to rename your file when you change networks. i.e. rename to`wallet_data_base_sepolia.txt` when you switch networks and generate a new wallet, and then rename back when you go back. After the refactor, this process will happen automatically for you.

OK I see, thanks for the info. Imo it would be better to have the same address on all chains.

My concern in relation to this cross-chain action is that a user sends eg ETH from Base to Ethereum with destinationAdress = senderAdress. But then after switching to Ethereum a new wallet is created and the funds deposited at destinationAdress are not accessible (within AgentKit).

But even if that happens, I suppose its possible to export the private key and then import it in MM or another wallet, so the funds would at least not be lost. 
author_association: CONTRIBUTOR
user: login: CarsonRoscoe
created_at: 2025-03-25T16:01:46Z
body: > OK I see, thanks for the info. Imo it would be better to have the same address on all chains.
> 
> My concern in relation to this cross-chain action is that a user sends eg ETH from Base to Ethereum with destinationAdress = senderAdress. But then after switching to Ethereum a new wallet is created and the funds deposited at destinationAdress are not accessible (within AgentKit).
> 
> But even if that happens, I suppose its possible to export the private key and then import it in MM or another wallet, so the funds would at least not be lost.

Agreed. Fyi, multi-network wallet API support is coming soon, but in the meantime, you are right that funds are safe and can be accessed through exporting the private key. You can also swap out the `CdpWalletProvider` for a `ViemWalletProvider` in the meantime to ensure your account is the same on each network.
author_association: CONTRIBUTOR
user: login: phdargen
created_at: 2025-03-25T18:22:58Z
body: > > OK I see, thanks for the info. Imo it would be better to have the same address on all chains.
> > My concern in relation to this cross-chain action is that a user sends eg ETH from Base to Ethereum with destinationAdress = senderAdress. But then after switching to Ethereum a new wallet is created and the funds deposited at destinationAdress are not accessible (within AgentKit).
> > But even if that happens, I suppose its possible to export the private key and then import it in MM or another wallet, so the funds would at least not be lost.
> 
> Agreed. Fyi, multi-network wallet API support is coming soon, but in the meantime, you are right that funds are safe and can be accessed through exporting the private key. You can also swap out the `CdpWalletProvider` for a `ViemWalletProvider` in the meantime to ensure your account is the same on each network.

Added warning about this and recommendation to use ViemWalletProvider to across readme
author_association: CONTRIBUTOR